Skip to content

Commit 15a64ee

Browse files
authoredNov 13, 2024··
fix: httpjson callables to trace attempts (started, failed) (#3300)
Fixes #2503 ### Approach This PR uses the approach suggested by the same bug. > We should move creating the TracedUnaryCallable to the last step for HttpJson. This would require refactoring this file: https://github.com/googleapis/sdk-platform-java/blob/7902a41c87240d607179d07c28cce462ea135c5f/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallableFactory.java ### Confirmation that it works The confirmation that it works is in the modified tests [(e.g. `retry()`)](https://github.com/googleapis/sdk-platform-java/blob/1edf55754d1f602a3bf70b3a44d1c42689ae961b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/RetryingTest.java#L158-L159). When using the proposed test version against the version of `HttpJsonCallableFactory` from the main branch, it will fail because it will not record attempts started or failed whatsoever: ![image](https://github.com/user-attachments/assets/1cb89d0c-7f34-4b1c-93a5-25f2112040f7) The image above shows all tests having failed when using the production files from `main`, implying that no tracer attempts are recorded for any of the tests.
1 parent 4c5a43c commit 15a64ee

File tree

5 files changed

+178
-17
lines changed

5 files changed

+178
-17
lines changed
 

‎gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallableFactory.java

+5-6
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ static <RequestT, ResponseT> UnaryCallable<RequestT, ResponseT> createUnaryCalla
8484
callable =
8585
Callables.retrying(
8686
callable, callSettings, clientContext, httpJsonCallSettings.getRequestMutator());
87+
callable =
88+
new TracedUnaryCallable<>(
89+
callable,
90+
clientContext.getTracerFactory(),
91+
getSpanName(httpJsonCallSettings.getMethodDescriptor()));
8792
return callable.withDefaultCallContext(clientContext.getDefaultCallContext());
8893
}
8994

@@ -136,12 +141,6 @@ public static <RequestT, ResponseT> UnaryCallable<RequestT, ResponseT> createUna
136141
UnaryCallable<RequestT, ResponseT> innerCallable =
137142
createDirectUnaryCallable(httpJsonCallSettings);
138143

139-
innerCallable =
140-
new TracedUnaryCallable<>(
141-
innerCallable,
142-
clientContext.getTracerFactory(),
143-
getSpanName(httpJsonCallSettings.getMethodDescriptor()));
144-
145144
return createUnaryCallable(innerCallable, callSettings, httpJsonCallSettings, clientContext);
146145
}
147146

‎gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/RetryingTest.java

+37
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import com.google.api.core.ApiFutures;
4444
import com.google.api.gax.core.FakeApiClock;
4545
import com.google.api.gax.core.RecordingScheduler;
46+
import com.google.api.gax.httpjson.testing.TestApiTracerFactory;
4647
import com.google.api.gax.retrying.RetrySettings;
4748
import com.google.api.gax.rpc.ApiCallContext;
4849
import com.google.api.gax.rpc.ApiException;
@@ -80,6 +81,7 @@ class RetryingTest {
8081

8182
private final Integer initialRequest = 1;
8283
private final Integer modifiedRequest = 0;
84+
private TestApiTracerFactory tracerFactory;
8385

8486
private final HttpJsonCallSettings<Integer, Integer> httpJsonCallSettings =
8587
HttpJsonCallSettings.<Integer, Integer>newBuilder()
@@ -115,8 +117,11 @@ class RetryingTest {
115117
void resetClock() {
116118
fakeClock = new FakeApiClock(System.nanoTime());
117119
executor = RecordingScheduler.create(fakeClock);
120+
tracerFactory = new TestApiTracerFactory();
118121
clientContext =
119122
ClientContext.newBuilder()
123+
// we use a custom tracer to confirm whether the retrials are being recorded.
124+
.setTracerFactory(tracerFactory)
120125
.setExecutor(executor)
121126
.setClock(fakeClock)
122127
.setDefaultCallContext(HttpJsonCallContext.createDefault())
@@ -130,6 +135,7 @@ void teardown() {
130135

131136
@Test
132137
void retry() {
138+
// set a retriable that will fail 3 times before returning "2"
133139
ImmutableSet<StatusCode.Code> retryable = ImmutableSet.of(Code.UNAVAILABLE);
134140
Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any()))
135141
.thenReturn(ApiFutures.<Integer>immediateFailedFuture(HTTP_SERVICE_UNAVAILABLE_EXCEPTION))
@@ -143,6 +149,9 @@ void retry() {
143149
HttpJsonCallableFactory.createUnaryCallable(
144150
callInt, callSettings, httpJsonCallSettings, clientContext);
145151
assertThat(callable.call(initialRequest)).isEqualTo(2);
152+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(3);
153+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(4);
154+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isFalse();
146155

147156
// Capture the argument passed to futureCall
148157
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -180,6 +189,9 @@ void retryTotalTimeoutExceeded() {
180189
HttpJsonCallableFactory.createUnaryCallable(
181190
callInt, callSettings, httpJsonCallSettings, clientContext);
182191
assertThrows(ApiException.class, () -> callable.call(initialRequest));
192+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(1);
193+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(0);
194+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isFalse();
183195
// Capture the argument passed to futureCall
184196
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
185197
verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class));
@@ -200,6 +212,9 @@ void retryMaxAttemptsExceeded() {
200212
HttpJsonCallableFactory.createUnaryCallable(
201213
callInt, callSettings, httpJsonCallSettings, clientContext);
202214
assertThrows(ApiException.class, () -> callable.call(initialRequest));
215+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(2);
216+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(2);
217+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isTrue();
203218
// Capture the argument passed to futureCall
204219
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
205220
verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class));
@@ -220,6 +235,9 @@ void retryWithinMaxAttempts() {
220235
HttpJsonCallableFactory.createUnaryCallable(
221236
callInt, callSettings, httpJsonCallSettings, clientContext);
222237
assertThat(callable.call(initialRequest)).isEqualTo(2);
238+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(3);
239+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(2);
240+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isFalse();
223241
// Capture the argument passed to futureCall
224242
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
225243
verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class));
@@ -246,6 +264,9 @@ void retryOnStatusUnknown() {
246264
HttpJsonCallableFactory.createUnaryCallable(
247265
callInt, callSettings, httpJsonCallSettings, clientContext);
248266
assertThat(callable.call(initialRequest)).isEqualTo(2);
267+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(4);
268+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(3);
269+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isFalse();
249270
// Capture the argument passed to futureCall
250271
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
251272
verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class));
@@ -264,6 +285,9 @@ void retryOnUnexpectedException() {
264285
HttpJsonCallableFactory.createUnaryCallable(
265286
callInt, callSettings, httpJsonCallSettings, clientContext);
266287
ApiException exception = assertThrows(ApiException.class, () -> callable.call(initialRequest));
288+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(1);
289+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(0);
290+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isFalse();
267291
assertThat(exception).hasCauseThat().isSameInstanceAs(throwable);
268292
// Capture the argument passed to futureCall
269293
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -293,6 +317,9 @@ void retryNoRecover() {
293317
HttpJsonCallableFactory.createUnaryCallable(
294318
callInt, callSettings, httpJsonCallSettings, clientContext);
295319
ApiException exception = assertThrows(ApiException.class, () -> callable.call(initialRequest));
320+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(1);
321+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(0);
322+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isFalse();
296323
assertThat(exception).isSameInstanceAs(apiException);
297324
// Capture the argument passed to futureCall
298325
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -319,6 +346,10 @@ void retryKeepFailing() {
319346

320347
UncheckedExecutionException exception =
321348
assertThrows(UncheckedExecutionException.class, () -> Futures.getUnchecked(future));
349+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isGreaterThan(0);
350+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get())
351+
.isEqualTo(tracerFactory.getInstance().getAttemptsStarted().get());
352+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isTrue();
322353
assertThat(exception).hasCauseThat().isInstanceOf(ApiException.class);
323354
assertThat(exception).hasCauseThat().hasMessageThat().contains("Unavailable");
324355
// Capture the argument passed to futureCall
@@ -359,6 +390,9 @@ void testKnownStatusCode() {
359390
callInt, callSettings, httpJsonCallSettings, clientContext);
360391
ApiException exception =
361392
assertThrows(FailedPreconditionException.class, () -> callable.call(initialRequest));
393+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(1);
394+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(0);
395+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isFalse();
362396
assertThat(exception.getStatusCode().getTransportCode())
363397
.isEqualTo(HTTP_CODE_PRECONDITION_FAILED);
364398
assertThat(exception).hasMessageThat().contains("precondition failed");
@@ -383,6 +417,9 @@ void testUnknownStatusCode() {
383417
UnknownException exception =
384418
assertThrows(UnknownException.class, () -> callable.call(initialRequest));
385419
assertThat(exception).hasMessageThat().isEqualTo("java.lang.RuntimeException: unknown");
420+
assertThat(tracerFactory.getInstance().getAttemptsStarted().get()).isEqualTo(1);
421+
assertThat(tracerFactory.getInstance().getAttemptsFailed().get()).isEqualTo(0);
422+
assertThat(tracerFactory.getInstance().getRetriesExhausted().get()).isFalse();
386423
// Capture the argument passed to futureCall
387424
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
388425
verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.httpjson.testing;
31+
32+
import com.google.api.gax.tracing.ApiTracer;
33+
import java.util.concurrent.atomic.AtomicBoolean;
34+
import java.util.concurrent.atomic.AtomicInteger;
35+
import org.threeten.bp.Duration;
36+
37+
/**
38+
* Test tracer that keeps count of different events. See {@link TestApiTracerFactory} for more
39+
* details.
40+
*/
41+
public class TestApiTracer implements ApiTracer {
42+
43+
private final AtomicInteger attemptsStarted = new AtomicInteger();
44+
private final AtomicInteger attemptsFailed = new AtomicInteger();
45+
private final AtomicBoolean retriesExhausted = new AtomicBoolean(false);
46+
47+
public TestApiTracer() {}
48+
49+
public AtomicInteger getAttemptsStarted() {
50+
return attemptsStarted;
51+
}
52+
53+
public AtomicInteger getAttemptsFailed() {
54+
return attemptsFailed;
55+
}
56+
57+
public AtomicBoolean getRetriesExhausted() {
58+
return retriesExhausted;
59+
}
60+
61+
@Override
62+
public void attemptStarted(int attemptNumber) {
63+
attemptsStarted.incrementAndGet();
64+
}
65+
66+
@Override
67+
public void attemptStarted(Object request, int attemptNumber) {
68+
attemptsStarted.incrementAndGet();
69+
}
70+
71+
@Override
72+
public void attemptFailed(Throwable error, Duration delay) {
73+
attemptsFailed.incrementAndGet();
74+
}
75+
76+
@Override
77+
public void attemptFailedRetriesExhausted(Throwable error) {
78+
attemptsFailed.incrementAndGet();
79+
retriesExhausted.set(true);
80+
}
81+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.httpjson.testing;
31+
32+
import com.google.api.gax.tracing.ApiTracer;
33+
import com.google.api.gax.tracing.ApiTracerFactory;
34+
import com.google.api.gax.tracing.SpanName;
35+
36+
/**
37+
* Produces a {@link TestApiTracer}, which keeps count of the attempts made and attempts
38+
* made-and-failed. It also keeps count of the operations failed and when the retries have been
39+
* exhausted.
40+
*/
41+
public class TestApiTracerFactory implements ApiTracerFactory {
42+
private final TestApiTracer instance = new TestApiTracer();
43+
44+
public TestApiTracer getInstance() {
45+
return instance;
46+
}
47+
48+
@Override
49+
public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) {
50+
return instance;
51+
}
52+
}

‎showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java

+3-11
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282
import org.junit.jupiter.api.AfterEach;
8383
import org.junit.jupiter.api.Assertions;
8484
import org.junit.jupiter.api.BeforeEach;
85-
import org.junit.jupiter.api.Disabled;
8685
import org.junit.jupiter.api.Test;
8786

8887
/**
@@ -320,12 +319,10 @@ void testGrpc_operationSucceeded_recordsMetrics() throws InterruptedException {
320319
verifyStatusAttribute(actualMetricDataList, statusCountList);
321320
}
322321

323-
@Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503")
324322
@Test
325323
void testHttpJson_operationSucceeded_recordsMetrics() throws InterruptedException {
326324
int attemptCount = 1;
327-
EchoRequest echoRequest =
328-
EchoRequest.newBuilder().setContent("test_http_operation_succeeded").build();
325+
EchoRequest echoRequest = EchoRequest.newBuilder().setContent("content").build();
329326
httpClient.echo(echoRequest);
330327

331328
List<MetricData> actualMetricDataList = getMetricDataList();
@@ -373,7 +370,6 @@ void testGrpc_operationCancelled_recordsMetrics() throws Exception {
373370
verifyStatusAttribute(actualMetricDataList, statusCountList);
374371
}
375372

376-
@Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503")
377373
@Test
378374
void testHttpJson_operationCancelled_recordsMetrics() throws Exception {
379375
int attemptCount = 1;
@@ -430,7 +426,6 @@ void testGrpc_operationFailed_recordsMetrics() throws InterruptedException {
430426
verifyStatusAttribute(actualMetricDataList, statusCountList);
431427
}
432428

433-
@Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503")
434429
@Test
435430
void testHttpJson_operationFailed_recordsMetrics() throws InterruptedException {
436431
int attemptCount = 1;
@@ -526,7 +521,6 @@ void testGrpc_attemptFailedRetriesExhausted_recordsMetrics() throws Exception {
526521
grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
527522
}
528523

529-
@Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503")
530524
@Test
531525
void testHttpJson_attemptFailedRetriesExhausted_recordsMetrics() throws Exception {
532526
int attemptCount = 3;
@@ -621,7 +615,6 @@ void testGrpc_attemptPermanentFailure_recordsMetrics() throws InterruptedExcepti
621615
verifyStatusAttribute(actualMetricDataList, statusCountList);
622616
}
623617

624-
@Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503")
625618
@Test
626619
void testHttpJson_attemptPermanentFailure_recordsMetrics() throws InterruptedException {
627620
int attemptCount = 1;
@@ -722,7 +715,6 @@ void testGrpc_multipleFailedAttempts_successfulOperation() throws Exception {
722715
grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
723716
}
724717

725-
@Disabled("https://github.com/googleapis/sdk-platform-java/issues/2503")
726718
@Test
727719
void testHttpJson_multipleFailedAttempts_successfulOperation() throws Exception {
728720
int attemptCount = 3;
@@ -737,7 +729,7 @@ void testHttpJson_multipleFailedAttempts_successfulOperation() throws Exception
737729

738730
EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder();
739731
httpJsonEchoSettingsBuilder
740-
.echoSettings()
732+
.blockSettings()
741733
.setRetrySettings(retrySettings)
742734
.setRetryableCodes(ImmutableSet.of(Code.DEADLINE_EXCEEDED));
743735
EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build());
@@ -771,7 +763,7 @@ void testHttpJson_multipleFailedAttempts_successfulOperation() throws Exception
771763
.setSuccess(BlockResponse.newBuilder().setContent("httpjsonBlockResponse"))
772764
.build();
773765

774-
grpcClient.block(blockRequest);
766+
httpClient.block(blockRequest);
775767

776768
List<MetricData> actualMetricDataList = getMetricDataList();
777769
verifyPointDataSum(actualMetricDataList, attemptCount);

0 commit comments

Comments
 (0)
Please sign in to comment.