001/* 002 * Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.agentsflex.core.util; 017 018import java.util.concurrent.Callable; 019import java.util.concurrent.TimeUnit; 020import java.util.function.Predicate; 021 022/** 023 * 重试执行器。 024 * <p> 025 * 设计原则: 026 * - Builder 仅用于构造配置 027 * - Retryer 实例是线程安全的(无状态),可全局复用 028 */ 029public final class Retryer { 030 031 private final int maxRetries; 032 private final long initialDelayMs; 033 private final long maxDelayMs; 034 private final boolean exponentialBackoff; 035 private final long totalTimeoutMs; 036 private final Predicate<Exception> retryOnException; 037 private final Predicate<Object> retryOnResult; 038 private final String operationName; 039 040 private Retryer(Builder builder) { 041 this.maxRetries = builder.maxRetries; 042 this.initialDelayMs = builder.initialDelayMs; 043 this.maxDelayMs = builder.maxDelayMs; 044 this.exponentialBackoff = builder.exponentialBackoff; 045 this.totalTimeoutMs = builder.totalTimeoutMs; 046 this.retryOnException = builder.retryOnException; 047 this.retryOnResult = builder.retryOnResult; 048 this.operationName = builder.operationName; 049 } 050 051 public static Builder builder() { 052 return new Builder(); 053 } 054 055 056 public static <T> T retry(Callable<T> task, int maxRetries, long initialDelayMs) { 057 return builder() 058 .maxRetries(maxRetries) 059 .initialDelayMs(initialDelayMs) 060 .build() 061 .execute(task); 062 } 063 064 public static void retry(Runnable task, int maxRetries, long initialDelayMs) { 065 builder() 066 .maxRetries(maxRetries) 067 .initialDelayMs(initialDelayMs) 068 .build() 069 .execute(task); 070 } 071 072 073 public <T> T execute(Callable<T> task) { 074 if (task == null) { 075 throw new IllegalArgumentException("Task must not be null"); 076 } 077 078 long deadline = totalTimeoutMs > 0 ? System.currentTimeMillis() + totalTimeoutMs : Long.MAX_VALUE; 079 long currentDelay = initialDelayMs; 080 Exception lastException = null; 081 082 for (int attempt = 0; attempt <= maxRetries; attempt++) { 083 if (Thread.interrupted()) { 084 throw new RetryException( 085 "Retry interrupted at attempt " + attempt + " for: " + operationName, 086 new InterruptedException()); 087 } 088 089 if (System.currentTimeMillis() > deadline) { 090 throw new RetryException( 091 "Retry deadline exceeded after " + attempt + " attempts for: " + operationName, 092 new java.util.concurrent.TimeoutException()); 093 } 094 095 try { 096 T result = task.call(); 097 if (attempt < maxRetries && retryOnResult.test(result)) { 098 lastException = new RuntimeException("Retry triggered by result predicate"); 099 sleepSafely(Math.min(currentDelay, deadline - System.currentTimeMillis())); 100 if (exponentialBackoff) { 101 currentDelay = Math.min(currentDelay * 2, maxDelayMs); 102 } 103 continue; 104 } 105 return result; 106 } catch (Exception e) { 107 lastException = e; 108 if (attempt < maxRetries && retryOnException.test(e)) { 109 sleepSafely(Math.min(currentDelay, deadline - System.currentTimeMillis())); 110 if (exponentialBackoff) { 111 currentDelay = Math.min(currentDelay * 2, maxDelayMs); 112 } 113 } else { 114 break; 115 } 116 } 117 } 118 119 if (lastException != null) { 120 throw new RetryException( 121 "Retry failed for: " + operationName + " after " + (maxRetries + 1) + " attempts", 122 lastException); 123 } 124 125 throw new IllegalStateException("Retry loop exited without result or exception"); 126 } 127 128 public void execute(Runnable task) { 129 try { 130 execute(() -> { 131 task.run(); 132 return null; 133 }); 134 } catch (Exception e) { 135 if (e instanceof RuntimeException) { 136 throw (RuntimeException) e; 137 } 138 throw new RuntimeException("Error in retryable Runnable", e); 139 } 140 } 141 142 private void sleepSafely(long sleepMs) { 143 if (sleepMs <= 0) return; 144 try { 145 TimeUnit.MILLISECONDS.sleep(sleepMs); 146 } catch (InterruptedException e) { 147 Thread.currentThread().interrupt(); 148 throw new RuntimeException("Interrupted during retry backoff", e); 149 } 150 } 151 152 153 public static class Builder { 154 private int maxRetries = 2; 155 private long initialDelayMs = 100; 156 private long maxDelayMs = 5000; 157 private boolean exponentialBackoff = false; 158 private long totalTimeoutMs = 0; 159 private Predicate<Exception> retryOnException = DEFAULT_RETRYABLE_EXCEPTION; 160 private Predicate<Object> retryOnResult = r -> false; 161 private String operationName = "retryable_operation"; 162 163 public Builder maxRetries(int maxRetries) { 164 this.maxRetries = Math.max(0, maxRetries); 165 return this; 166 } 167 168 public Builder initialDelayMs(long delayMs) { 169 this.initialDelayMs = Math.max(0, delayMs); 170 return this; 171 } 172 173 public Builder maxDelayMs(long maxDelayMs) { 174 this.maxDelayMs = Math.max(this.initialDelayMs, maxDelayMs); 175 return this; 176 } 177 178 public Builder exponentialBackoff() { 179 this.exponentialBackoff = true; 180 return this; 181 } 182 183 public Builder totalTimeoutMs(long totalTimeoutMs) { 184 this.totalTimeoutMs = Math.max(0, totalTimeoutMs); 185 return this; 186 } 187 188 public Builder retryOnException(Predicate<Exception> predicate) { 189 this.retryOnException = predicate != null ? predicate : DEFAULT_RETRYABLE_EXCEPTION; 190 return this; 191 } 192 193 public Builder retryOnResult(Predicate<Object> predicate) { 194 this.retryOnResult = predicate != null ? predicate : (r -> false); 195 return this; 196 } 197 198 public Builder operationName(String name) { 199 this.operationName = name != null ? name : "retryable_operation"; 200 return this; 201 } 202 203 public Retryer build() { 204 return new Retryer(this); 205 } 206 } 207 208 209 private static final Predicate<Exception> DEFAULT_RETRYABLE_EXCEPTION = e -> 210 e instanceof java.net.SocketTimeoutException || 211 e instanceof java.net.ConnectException || 212 e instanceof java.net.UnknownHostException || 213 e instanceof java.io.IOException || 214 (e.getCause() instanceof java.net.SocketTimeoutException) || 215 (e.getCause() instanceof java.net.ConnectException); 216}