6262import com .google .common .util .concurrent .UncheckedExecutionException ;
6363import com .google .protobuf .TypeRegistry ;
6464import java .util .Set ;
65+ import java .util .concurrent .atomic .AtomicBoolean ;
6566import java .util .concurrent .atomic .AtomicInteger ;
6667import org .junit .jupiter .api .AfterEach ;
6768import org .junit .jupiter .api .BeforeEach ;
@@ -93,6 +94,9 @@ class RetryingTest {
9394 .setTypeRegistry (TypeRegistry .newBuilder ().build ())
9495 .build ();
9596
97+ private AtomicInteger tracerAttemptsFailed = new AtomicInteger ();
98+ private AtomicInteger tracerAttempts = new AtomicInteger ();
99+ private AtomicBoolean tracerOperationFailed = new AtomicBoolean (false );
96100 private RecordingScheduler executor ;
97101 private FakeApiClock fakeClock ;
98102 private ClientContext clientContext ;
@@ -122,6 +126,8 @@ void resetClock() {
122126 executor = RecordingScheduler .create (fakeClock );
123127 clientContext =
124128 ClientContext .newBuilder ()
129+ // we use a custom tracer to confirm whether the retrials are being recorded.
130+ .setTracerFactory (getApiTracerFactory ())
125131 .setExecutor (executor )
126132 .setClock (fakeClock )
127133 .setDefaultCallContext (HttpJsonCallContext .createDefault ())
@@ -135,12 +141,6 @@ void teardown() {
135141
136142 @ Test
137143 void retry () {
138- // we use a custom tracer to confirm whether the retrials are being recorded.
139- AtomicInteger tracerFailedAttempts = new AtomicInteger ();
140- AtomicInteger tracerAttempts = new AtomicInteger ();
141- ApiTracerFactory tracerFactory = getApiTracerFactory (tracerFailedAttempts , tracerAttempts );
142- clientContext = clientContext .toBuilder ().setTracerFactory (tracerFactory ).build ();
143-
144144 // set a retriable that will fail 3 times before returning "2"
145145 ImmutableSet <StatusCode .Code > retryable = ImmutableSet .of (Code .UNAVAILABLE );
146146 Mockito .when (callInt .futureCall ((Integer ) any (), (ApiCallContext ) any ()))
@@ -155,8 +155,9 @@ void retry() {
155155 HttpJsonCallableFactory .createUnaryCallable (
156156 callInt , callSettings , httpJsonCallSettings , clientContext );
157157 assertThat (callable .call (initialRequest )).isEqualTo (2 );
158- assertThat (tracerFailedAttempts .get ()).isEqualTo (3 );
158+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (3 );
159159 assertThat (tracerAttempts .get ()).isEqualTo (4 );
160+ assertThat (tracerOperationFailed .get ()).isEqualTo (false );
160161
161162 // Capture the argument passed to futureCall
162163 ArgumentCaptor <Integer > argumentCaptor = ArgumentCaptor .forClass (Integer .class );
@@ -194,6 +195,9 @@ void retryTotalTimeoutExceeded() {
194195 HttpJsonCallableFactory .createUnaryCallable (
195196 callInt , callSettings , httpJsonCallSettings , clientContext );
196197 assertThrows (ApiException .class , () -> callable .call (initialRequest ));
198+ assertThat (tracerAttempts .get ()).isEqualTo (1 );
199+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (0 );
200+ assertThat (tracerOperationFailed .get ()).isEqualTo (true );
197201 // Capture the argument passed to futureCall
198202 ArgumentCaptor <Integer > argumentCaptor = ArgumentCaptor .forClass (Integer .class );
199203 verify (callInt , atLeastOnce ()).futureCall (argumentCaptor .capture (), any (ApiCallContext .class ));
@@ -214,6 +218,9 @@ void retryMaxAttemptsExceeded() {
214218 HttpJsonCallableFactory .createUnaryCallable (
215219 callInt , callSettings , httpJsonCallSettings , clientContext );
216220 assertThrows (ApiException .class , () -> callable .call (initialRequest ));
221+ assertThat (tracerAttempts .get ()).isEqualTo (2 );
222+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (1 );
223+ assertThat (tracerOperationFailed .get ()).isEqualTo (true );
217224 // Capture the argument passed to futureCall
218225 ArgumentCaptor <Integer > argumentCaptor = ArgumentCaptor .forClass (Integer .class );
219226 verify (callInt , atLeastOnce ()).futureCall (argumentCaptor .capture (), any (ApiCallContext .class ));
@@ -234,6 +241,9 @@ void retryWithinMaxAttempts() {
234241 HttpJsonCallableFactory .createUnaryCallable (
235242 callInt , callSettings , httpJsonCallSettings , clientContext );
236243 assertThat (callable .call (initialRequest )).isEqualTo (2 );
244+ assertThat (tracerAttempts .get ()).isEqualTo (3 );
245+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (2 );
246+ assertThat (tracerOperationFailed .get ()).isEqualTo (false );
237247 // Capture the argument passed to futureCall
238248 ArgumentCaptor <Integer > argumentCaptor = ArgumentCaptor .forClass (Integer .class );
239249 verify (callInt , atLeastOnce ()).futureCall (argumentCaptor .capture (), any (ApiCallContext .class ));
@@ -260,6 +270,9 @@ void retryOnStatusUnknown() {
260270 HttpJsonCallableFactory .createUnaryCallable (
261271 callInt , callSettings , httpJsonCallSettings , clientContext );
262272 assertThat (callable .call (initialRequest )).isEqualTo (2 );
273+ assertThat (tracerAttempts .get ()).isEqualTo (4 );
274+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (3 );
275+ assertThat (tracerOperationFailed .get ()).isEqualTo (false );
263276 // Capture the argument passed to futureCall
264277 ArgumentCaptor <Integer > argumentCaptor = ArgumentCaptor .forClass (Integer .class );
265278 verify (callInt , atLeastOnce ()).futureCall (argumentCaptor .capture (), any (ApiCallContext .class ));
@@ -277,8 +290,12 @@ void retryOnUnexpectedException() {
277290 UnaryCallable <Integer , Integer > callable =
278291 HttpJsonCallableFactory .createUnaryCallable (
279292 callInt , callSettings , httpJsonCallSettings , clientContext );
293+ // Should an unexpected RuntimeException be received as an UnknownException?
280294 UnknownException exception =
281295 assertThrows (UnknownException .class , () -> callable .call (initialRequest ));
296+ assertThat (tracerAttempts .get ()).isEqualTo (1 );
297+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (0 );
298+ assertThat (tracerOperationFailed .get ()).isEqualTo (true );
282299 assertThat (exception ).hasCauseThat ().isSameInstanceAs (throwable );
283300 // Capture the argument passed to futureCall
284301 ArgumentCaptor <Integer > argumentCaptor = ArgumentCaptor .forClass (Integer .class );
@@ -308,6 +325,9 @@ void retryNoRecover() {
308325 HttpJsonCallableFactory .createUnaryCallable (
309326 callInt , callSettings , httpJsonCallSettings , clientContext );
310327 ApiException exception = assertThrows (ApiException .class , () -> callable .call (initialRequest ));
328+ assertThat (tracerAttempts .get ()).isEqualTo (1 );
329+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (0 );
330+ assertThat (tracerOperationFailed .get ()).isEqualTo (true );
311331 assertThat (exception ).isSameInstanceAs (apiException );
312332 // Capture the argument passed to futureCall
313333 ArgumentCaptor <Integer > argumentCaptor = ArgumentCaptor .forClass (Integer .class );
@@ -334,6 +354,12 @@ void retryKeepFailing() {
334354
335355 UncheckedExecutionException exception =
336356 assertThrows (UncheckedExecutionException .class , () -> Futures .getUnchecked (future ));
357+ // the number of attempts varies. Here we just make sure that all of them except the last are
358+ // considered as failed
359+ // attempts and that the operation was considered as failed.
360+ assertThat (tracerAttemptsFailed .get ()).isGreaterThan (0 );
361+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (tracerAttempts .get () - 1 );
362+ assertThat (tracerOperationFailed .get ()).isEqualTo (true );
337363 assertThat (exception ).hasCauseThat ().isInstanceOf (ApiException .class );
338364 assertThat (exception ).hasCauseThat ().hasMessageThat ().contains ("Unavailable" );
339365 // Capture the argument passed to futureCall
@@ -374,6 +400,9 @@ void testKnownStatusCode() {
374400 callInt , callSettings , httpJsonCallSettings , clientContext );
375401 ApiException exception =
376402 assertThrows (FailedPreconditionException .class , () -> callable .call (initialRequest ));
403+ assertThat (tracerAttempts .get ()).isEqualTo (1 );
404+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (0 );
405+ assertThat (tracerOperationFailed .get ()).isEqualTo (true );
377406 assertThat (exception .getStatusCode ().getTransportCode ())
378407 .isEqualTo (HTTP_CODE_PRECONDITION_FAILED );
379408 assertThat (exception ).hasMessageThat ().contains ("precondition failed" );
@@ -398,6 +427,9 @@ void testUnknownStatusCode() {
398427 UnknownException exception =
399428 assertThrows (UnknownException .class , () -> callable .call (initialRequest ));
400429 assertThat (exception ).hasMessageThat ().isEqualTo ("java.lang.RuntimeException: unknown" );
430+ assertThat (tracerAttempts .get ()).isEqualTo (1 );
431+ assertThat (tracerAttemptsFailed .get ()).isEqualTo (0 );
432+ assertThat (tracerOperationFailed .get ()).isEqualTo (true );
401433 // Capture the argument passed to futureCall
402434 ArgumentCaptor <Integer > argumentCaptor = ArgumentCaptor .forClass (Integer .class );
403435 verify (callInt , atLeastOnce ()).futureCall (argumentCaptor .capture (), any (ApiCallContext .class ));
@@ -412,13 +444,12 @@ public static UnaryCallSettings<Integer, Integer> createSettings(
412444 .build ();
413445 }
414446
415- private static ApiTracerFactory getApiTracerFactory (
416- AtomicInteger tracerFailedAttempts , AtomicInteger tracerAttempts ) {
447+ private ApiTracerFactory getApiTracerFactory () {
417448 ApiTracer tracer =
418449 new BaseApiTracer () {
419450 @ Override
420451 public void attemptFailed (Throwable error , Duration delay ) {
421- tracerFailedAttempts .incrementAndGet ();
452+ tracerAttemptsFailed .incrementAndGet ();
422453 super .attemptFailed (error , delay );
423454 }
424455
@@ -427,6 +458,12 @@ public void attemptStarted(int attemptNumber) {
427458 tracerAttempts .incrementAndGet ();
428459 super .attemptStarted (attemptNumber );
429460 }
461+
462+ @ Override
463+ public void operationFailed (Throwable error ) {
464+ tracerOperationFailed .set (true );
465+ super .operationFailed (error );
466+ }
430467 };
431468 ApiTracerFactory tracerFactory = (parent , spanName , operationType ) -> tracer ;
432469 return tracerFactory ;
0 commit comments