@@ -116,32 +116,51 @@ private <T> void attempt(
116116 // currentAttempt.set() call. The cancellation handler in execute() observes
117117 // currentAttempt under that race: if it sees the previous (already-done) attempt, it
118118 // doesn't cancel the new one. Re-check after publishing the new attempt.
119+ if (!cancelIfRaceLost (result , dispatched )) {
120+ dispatched .whenComplete (
121+ (value , error ) -> {
122+ if (result .isDone ()) {
123+ return ;
124+ }
125+ if (error == null ) {
126+ result .complete (value );
127+ return ;
128+ }
129+ Throwable cause = unwrap (error );
130+ if (shouldRetry .test (cause , attemptIdx )) {
131+ long delayMs = policy .backoffDelay (cause , attemptIdx ).toMillis ();
132+ CompletableFuture .delayedExecutor (delayMs , TimeUnit .MILLISECONDS )
133+ .execute (
134+ () ->
135+ attempt (
136+ supplier ,
137+ shouldRetry ,
138+ attemptIdx + 1 ,
139+ cause ,
140+ result ,
141+ currentAttempt ));
142+ } else {
143+ result .completeExceptionally (cause );
144+ }
145+ });
146+ }
147+ }
148+
149+ /**
150+ * Handle the cancel/dispatch race: if the caller cancelled {@code result} between the isDone()
151+ * check and publishing {@code dispatched}, cancel the freshly-dispatched attempt and report it.
152+ *
153+ * <p>{@code @Generated}: the race window between {@code currentAttempt.set} and this re-check
154+ * cannot be hit deterministically from a hermetic single-threaded test.
155+ */
156+ @ Generated
157+ private <T > boolean cancelIfRaceLost (
158+ CompletableFuture <T > result , CompletableFuture <T > dispatched ) {
119159 if (result .isCancelled () && !dispatched .isDone ()) {
120160 dispatched .cancel (false );
121- return ;
161+ return true ;
122162 }
123-
124- dispatched .whenComplete (
125- (value , error ) -> {
126- if (result .isDone ()) {
127- return ;
128- }
129- if (error == null ) {
130- result .complete (value );
131- return ;
132- }
133- Throwable cause = unwrap (error );
134- if (shouldRetry .test (cause , attemptIdx )) {
135- long delayMs = policy .backoffDelay (cause , attemptIdx ).toMillis ();
136- CompletableFuture .delayedExecutor (delayMs , TimeUnit .MILLISECONDS )
137- .execute (
138- () ->
139- attempt (
140- supplier , shouldRetry , attemptIdx + 1 , cause , result , currentAttempt ));
141- } else {
142- result .completeExceptionally (cause );
143- }
144- });
163+ return false ;
145164 }
146165
147166 // Package-private so the unwrap-when-nested-and-when-not branches are reachable from tests.
0 commit comments