@@ -69,6 +69,13 @@ class CancellationTest {
6969 .setTotalTimeoutDuration (java .time .Duration .ofMillis (20L ))
7070 .build ();
7171
72+ // Disable jitter so ExponentialRetryAlgorithm.createNextAttempt always schedules the full
73+ // 3000ms retry delay instead of picking a random delay in [0, 3000) ms via
74+ // ThreadLocalRandom.current().nextLong(bound), which can otherwise expire before the
75+ // cancellation thread calls resultFuture.cancel(true).
76+ // totalTimeoutDuration must exceed initialRetryDelayDuration (10000ms > 3000ms) because
77+ // ExponentialRetryAlgorithm.shouldRetry checks (totalTimeout - timeElapsed - retryDelay) > 0
78+ // before scheduling the next attempt.
7279 private static final RetrySettings SLOW_RETRY_SETTINGS =
7380 RetrySettings .newBuilder ()
7481 .setInitialRetryDelayDuration (java .time .Duration .ofMillis (3000L ))
@@ -77,7 +84,8 @@ class CancellationTest {
7784 .setInitialRpcTimeoutDuration (java .time .Duration .ofMillis (3000L ))
7885 .setRpcTimeoutMultiplier (1 )
7986 .setMaxRpcTimeoutDuration (java .time .Duration .ofMillis (3000L ))
80- .setTotalTimeoutDuration (java .time .Duration .ofMillis (3000L ))
87+ .setTotalTimeoutDuration (java .time .Duration .ofMillis (10000L ))
88+ .setJittered (false )
8189 .build ();
8290
8391 private FakeApiClock fakeClock ;
@@ -197,6 +205,10 @@ void cancellationDuringRetryDelay() throws Exception {
197205 Throwable throwable =
198206 new UnavailableException (null , FakeStatusCode .of (StatusCode .Code .UNAVAILABLE ), true );
199207 CancellationTrackingFuture <Integer > innerFuture = CancellationTrackingFuture .create ();
208+ // Attempt 1 fails immediately with UNAVAILABLE, causing CallbackChainRetryingFuture to
209+ // schedule Attempt 2 on the executor with SLOW_RETRY_SETTINGS's retry delay.
210+ // innerFuture is queued in Mockito for Attempt 2 and is only returned if the retry delay
211+ // expires and AttemptCallable.call() invokes callInt.futureCall() a second time.
200212 Mockito .when (callInt .futureCall ((Integer ) Mockito .any (), (ApiCallContext ) Mockito .any ()))
201213 .thenReturn (RetryingTest .<Integer >immediateFailedFuture (throwable ))
202214 .thenReturn (innerFuture );
@@ -220,6 +232,12 @@ void cancellationDuringRetryDelay() throws Exception {
220232 Truth .assertThat (gotException ).isNotNull ();
221233 Truth .assertThat (resultFuture .isDone ()).isTrue ();
222234 Truth .assertThat (resultFuture .isCancelled ()).isTrue ();
235+ // CallbackChainRetryingFuture replaces its active attemptFuture at each step of the retry
236+ // loop (Attempt 1 future -> scheduled retry-delay timer future -> Attempt 2 future via
237+ // AttemptCallable.call()). While waiting in the retry delay, attemptFuture points to the
238+ // scheduled timer future from ScheduledRetryingExecutor.submit(). Cancelling resultFuture
239+ // during this delay cancels that timer task before AttemptCallable.call() runs Attempt 2,
240+ // so callInt.futureCall() is never called a second time and innerFuture is never cancelled.
223241 Truth .assertThat (innerFuture .isCancelled ()).isFalse ();
224242
225243 scheduler .shutdownNow ();
0 commit comments