Utility class for executing operations with retry logic and exponential backoff. Only retries transient errors (network issues, 5xx errors, S3 throttling). Does not retry client errors (4xx) or application-level failures.
| 31 | * Does not retry client errors (4xx) or application-level failures. |
| 32 | */ |
| 33 | public class RetryUtil { |
| 34 | private static final Logger log = LoggerFactory.getLogger(RetryUtil.class); |
| 35 | |
| 36 | /** |
| 37 | * Executes the given operation with retry logic and exponential backoff. |
| 38 | * |
| 39 | * @param operation The operation to execute |
| 40 | * @param maxAttempts Maximum number of attempts (e.g., 3) |
| 41 | * @param initialDelayMs Initial delay in milliseconds (e.g., 1000 for 1 second) |
| 42 | * @param operationName Name of the operation for logging purposes |
| 43 | * @param <T> Return type of the operation |
| 44 | * @return The result from the operation |
| 45 | * @throws Exception if all retry attempts are exhausted or a non-retryable error occurs |
| 46 | */ |
| 47 | public static <T> T executeWithRetry( |
| 48 | Callable<T> operation, |
| 49 | int maxAttempts, |
| 50 | long initialDelayMs, |
| 51 | String operationName) throws Exception { |
| 52 | |
| 53 | Exception lastException = null; |
| 54 | |
| 55 | for (int attempt = 1; attempt <= maxAttempts; attempt++) { |
| 56 | try { |
| 57 | return operation.call(); |
| 58 | } catch (Exception e) { |
| 59 | lastException = e; |
| 60 | |
| 61 | // Check if we've exhausted all attempts |
| 62 | if (attempt == maxAttempts) { |
| 63 | log.error("{} failed after {} attempts", operationName, maxAttempts, e); |
| 64 | throw e; |
| 65 | } |
| 66 | |
| 67 | // Check if error is retryable |
| 68 | if (!isRetryable(e)) { |
| 69 | log.error("{} failed with non-retryable error: {}", |
| 70 | operationName, e.getClass().getSimpleName(), e); |
| 71 | throw e; |
| 72 | } |
| 73 | |
| 74 | // Calculate exponential backoff delay: 1s, 2s, 4s, 8s, ... |
| 75 | long delayMs = initialDelayMs * (1L << (attempt - 1)); |
| 76 | |
| 77 | log.warn("{} attempt {}/{} failed, retrying in {}ms: {}", |
| 78 | operationName, attempt, maxAttempts, delayMs, e.getMessage()); |
| 79 | |
| 80 | // Wait before retrying |
| 81 | try { |
| 82 | Thread.sleep(delayMs); |
| 83 | } catch (InterruptedException ie) { |
| 84 | Thread.currentThread().interrupt(); |
| 85 | throw new RuntimeException("Retry interrupted", ie); |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | // Should not reach here, but throw last exception just in case |
nothing calls this directly
no outgoing calls
no test coverage detected