All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
Retry.withMaxDelay(Duration)- caps every backoff delay (applied after the multiplier and jitter). Without a cap the delay grows unboundedly and, for a large multiplier or many attempts,multiplier^noverflows to an endless wait; the cap keeps waits bounded.Retry.onRetry(RetryListener)and the newRetryListenerfunctional interface - called right before each retry with the 1-based failed attempt number, its failure and the delay about to be waited. Works for bothexecuteandexecuteAsync; not called after the final attempt or for non-retryable failures. A listener that throws aborts retrying (propagated byexecute, completes the future exceptionally forexecuteAsync).
Retry.executeAsync(Supplier<CompletableFuture<T>>): an async counterpart toexecute(Callable<T>)for tasks that are already asynchronous.- Applies the exact same retry policy as the sync path: it reuses the
existing
isRetryable(exception-type matching viaretryOn) andcomputeDelayMillis(backoff multiplier + jitter) logic rather than duplicating it. - Never blocks a thread while waiting between attempts: retries are
scheduled via
CompletableFuture.delayedExecutor(...)instead ofThread.sleep. - A synchronous throw from the supplier itself (before it returns a future) is treated the same as an exceptionally-completed future.
- Failure causes are unwrapped from
CompletionExceptionbefore the retryability check, soretryOn-configured types match the same way they would on the synchronous path. - Returns a
CompletableFuture<T>that completes with the eventual result, or completes exceptionally once retries are exhausted or the failure isn't retryable.
- Applies the exact same retry policy as the sync path: it reuses the
existing
- README: new "## Async Retry" section with a runnable example, and an additional realistic example (a reused policy across multiple calls) in "## Usage".
- Tests (
RetryTest) covering the new async path: eventual success after N failures with the exact invocation count asserted, immediate failure on a non-retryable exception, exhaustion aftermaxAttempts, success on the first try, and a supplier that throws synchronously before returning a future.
- Test coverage for documented-but-untested
Retryedge cases:- The constructor rejects
maxAttempts < 1, anullinitialDelay, and a negativeinitialDelay. withBackoffMultiplier()rejects a negative multiplier.withJitter()rejects a fraction outside[0, 1].- Explicitly calling
retryOn()with zero arguments retries any exception, the same as never calling it. retryOn()matches a subtype of a configured exception class (assignability viaisInstance), not just an exact type match.maxAttempts = 1means no retries at all: the first failure propagates immediately and the task is called exactly once.- An
Error(notException) thrown by the task propagates as the sameErrorinstance once retries are exhausted, rather than being wrapped.
- The constructor rejects
No behavioral changes were needed — all new edge-case tests passed against the existing implementation.