Skip to content

Commit 25caadf

Browse files
committed
feat(gax): correlate M3 client duration metrics with T3 span exemplars
1 parent 2f08818 commit 25caadf

2 files changed

Lines changed: 175 additions & 40 deletions

File tree

‎sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java‎

Lines changed: 154 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131

3232
import com.google.api.core.InternalApi;
3333
import com.google.common.collect.ImmutableList;
34-
import java.util.ArrayList;
3534
import java.util.List;
3635
import java.util.Map;
3736
import org.jspecify.annotations.NullMarked;
@@ -45,6 +44,8 @@
4544
@NullMarked
4645
@InternalApi
4746
class CompositeTracer extends BaseApiTracer {
47+
private static final Scope NO_OP_SCOPE = () -> {};
48+
4849
private final List<ApiTracer> children;
4950

5051
public CompositeTracer(List<ApiTracer> children) {
@@ -53,60 +54,155 @@ public CompositeTracer(List<ApiTracer> children) {
5354

5455
@Override
5556
public Scope inScope() {
56-
final List<Scope> childScopes = new ArrayList<>(children.size());
57+
// Fast paths for 0 or 1 child tracer to avoid allocating an array or CompositeScope
58+
if (children.isEmpty()) {
59+
return NO_OP_SCOPE;
60+
}
61+
if (children.size() == 1) {
62+
Scope scope = children.get(0).inScope();
63+
return scope != null ? scope : NO_OP_SCOPE;
64+
}
65+
66+
Scope[] childScopes = new Scope[children.size()];
67+
int scopeCount = 0;
5768

5869
try {
70+
// Enter each child scope in forward order, tracking only non-null scopes
5971
for (ApiTracer child : children) {
60-
childScopes.add(child.inScope());
72+
Scope scope = child.inScope();
73+
if (scope != null) {
74+
childScopes[scopeCount++] = scope;
75+
}
6176
}
62-
} catch (RuntimeException e) {
63-
for (int i = childScopes.size() - 1; i >= 0; i--) {
77+
if (scopeCount == 0) {
78+
return NO_OP_SCOPE;
79+
}
80+
if (scopeCount == 1) {
81+
return childScopes[0];
82+
}
83+
return new CompositeScope(childScopes, scopeCount);
84+
} catch (Throwable t) {
85+
// Roll back already-opened child scopes in reverse (LIFO) order if any child fails
86+
for (int i = scopeCount - 1; i >= 0; i--) {
6487
try {
65-
childScopes.get(i).close();
66-
} catch (RuntimeException suppressed) {
67-
e.addSuppressed(suppressed);
88+
childScopes[i].close();
89+
} catch (Throwable suppressed) {
90+
if (t != suppressed) {
91+
t.addSuppressed(suppressed);
92+
}
6893
}
6994
}
70-
throw e;
95+
throw throwException(t);
7196
}
97+
}
7298

73-
return () -> {
74-
RuntimeException exception = null;
75-
for (int i = childScopes.size() - 1; i >= 0; i--) {
76-
try {
77-
childScopes.get(i).close();
78-
} catch (RuntimeException e) {
79-
if (exception == null) {
80-
exception = e;
81-
} else {
82-
exception.addSuppressed(e);
99+
/**
100+
* An aggregate {@link Scope} that encapsulates and closes multiple child scopes in reverse order.
101+
*/
102+
private static class CompositeScope implements Scope {
103+
private final Scope[] scopes;
104+
private final int count;
105+
106+
/**
107+
* Constructs a {@code CompositeScope} managing the given array of active child scopes.
108+
*
109+
* @param scopes the array containing active child scopes
110+
* @param count the number of valid scopes in the array
111+
*/
112+
CompositeScope(Scope[] scopes, int count) {
113+
this.scopes = scopes;
114+
this.count = count;
115+
}
116+
117+
/** Closes all managed child scopes in reverse order, suppressing secondary exceptions. */
118+
@Override
119+
public void close() {
120+
Throwable firstException = null;
121+
// Close child scopes in reverse (LIFO) order to mirror the order they were entered
122+
for (int i = count - 1; i >= 0; i--) {
123+
Scope scope = scopes[i];
124+
if (scope != null) {
125+
// Clear reference to make CompositeScope.close() idempotent
126+
scopes[i] = null;
127+
try {
128+
scope.close();
129+
} catch (Throwable t) {
130+
// Capture the first failure and attach subsequent failures as suppressed exceptions
131+
if (firstException == null) {
132+
firstException = t;
133+
} else if (firstException != t) {
134+
firstException.addSuppressed(t);
135+
}
83136
}
84137
}
85138
}
86-
if (exception != null) {
87-
throw exception;
139+
if (firstException != null) {
140+
throw throwException(firstException);
88141
}
89-
};
142+
}
143+
}
144+
145+
/**
146+
* Rethrows or wraps a {@link Throwable} without losing runtime exception or error fidelity.
147+
*
148+
* @param t the throwable to rethrow or wrap
149+
* @return a {@link RuntimeException} wrapping {@code t} if {@code t} is a checked exception
150+
*/
151+
private static RuntimeException throwException(Throwable t) {
152+
if (t instanceof RuntimeException) {
153+
return (RuntimeException) t;
154+
}
155+
if (t instanceof Error) {
156+
throw (Error) t;
157+
}
158+
return new RuntimeException(t);
159+
}
160+
161+
/**
162+
* Enters the tracer's ambient scope safely, returning a no-op {@link Scope} if entering fails.
163+
* This avoids allocating runnables, throwing exceptions, or returning null during lifecycle
164+
* notifications.
165+
*
166+
* <p>Entering the ambient scope ensures active trace span context is present on the thread so
167+
* that OpenTelemetry metric measurements recorded in lifecycle callbacks attach exemplars
168+
* pointing to the active span.
169+
*/
170+
private Scope enterScope() {
171+
try {
172+
return inScope();
173+
} catch (RuntimeException e) {
174+
// Ignore to prevent disrupting the lifecycle notification
175+
return NO_OP_SCOPE;
176+
}
90177
}
91178

92179
@Override
93180
public void operationSucceeded() {
94-
for (int i = children.size() - 1; i >= 0; i--) {
95-
children.get(i).operationSucceeded();
181+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
182+
try (Scope s = enterScope()) {
183+
for (int i = children.size() - 1; i >= 0; i--) {
184+
children.get(i).operationSucceeded();
185+
}
96186
}
97187
}
98188

99189
@Override
100190
public void operationCancelled() {
101-
for (int i = children.size() - 1; i >= 0; i--) {
102-
children.get(i).operationCancelled();
191+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
192+
try (Scope s = enterScope()) {
193+
for (int i = children.size() - 1; i >= 0; i--) {
194+
children.get(i).operationCancelled();
195+
}
103196
}
104197
}
105198

106199
@Override
107200
public void operationFailed(Throwable error) {
108-
for (int i = children.size() - 1; i >= 0; i--) {
109-
children.get(i).operationFailed(error);
201+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
202+
try (Scope s = enterScope()) {
203+
for (int i = children.size() - 1; i >= 0; i--) {
204+
children.get(i).operationFailed(error);
205+
}
110206
}
111207
}
112208

@@ -134,43 +230,61 @@ public void attemptStarted(Object request, int attemptNumber) {
134230

135231
@Override
136232
public void attemptSucceeded() {
137-
for (int i = children.size() - 1; i >= 0; i--) {
138-
children.get(i).attemptSucceeded();
233+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
234+
try (Scope s = enterScope()) {
235+
for (int i = children.size() - 1; i >= 0; i--) {
236+
children.get(i).attemptSucceeded();
237+
}
139238
}
140239
}
141240

142241
@Override
143242
public void attemptCancelled() {
144-
for (int i = children.size() - 1; i >= 0; i--) {
145-
children.get(i).attemptCancelled();
243+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
244+
try (Scope s = enterScope()) {
245+
for (int i = children.size() - 1; i >= 0; i--) {
246+
children.get(i).attemptCancelled();
247+
}
146248
}
147249
}
148250

149251
@Override
150252
public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) {
151-
for (int i = children.size() - 1; i >= 0; i--) {
152-
children.get(i).attemptFailed(error, delay);
253+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
254+
try (Scope s = enterScope()) {
255+
for (int i = children.size() - 1; i >= 0; i--) {
256+
children.get(i).attemptFailed(error, delay);
257+
}
153258
}
154259
}
155260

156261
@Override
157262
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
158-
for (int i = children.size() - 1; i >= 0; i--) {
159-
children.get(i).attemptFailedDuration(error, delay);
263+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
264+
try (Scope s = enterScope()) {
265+
for (int i = children.size() - 1; i >= 0; i--) {
266+
children.get(i).attemptFailedDuration(error, delay);
267+
}
160268
}
161269
}
162270

163271
@Override
164272
public void attemptFailedRetriesExhausted(Throwable error) {
165-
for (int i = children.size() - 1; i >= 0; i--) {
166-
children.get(i).attemptFailedRetriesExhausted(error);
273+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
274+
try (Scope s = enterScope()) {
275+
for (int i = children.size() - 1; i >= 0; i--) {
276+
children.get(i).attemptFailedRetriesExhausted(error);
277+
}
167278
}
168279
}
169280

170281
@Override
171282
public void attemptPermanentFailure(Throwable error) {
172-
for (int i = children.size() - 1; i >= 0; i--) {
173-
children.get(i).attemptPermanentFailure(error);
283+
// Activate ambient span context so child tracers (e.g. metrics) can sample trace exemplars
284+
try (Scope s = enterScope()) {
285+
for (int i = children.size() - 1; i >= 0; i--) {
286+
children.get(i).attemptPermanentFailure(error);
287+
}
174288
}
175289
}
176290

‎sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import static org.mockito.Mockito.doThrow;
3636
import static org.mockito.Mockito.inOrder;
3737
import static org.mockito.Mockito.mock;
38+
import static org.mockito.Mockito.times;
3839
import static org.mockito.Mockito.verify;
3940
import static org.mockito.Mockito.when;
4041

@@ -125,6 +126,26 @@ void testInScope_childScopeCloseThrows() {
125126
inOrder.verify(scope1).close();
126127
}
127128

129+
@Test
130+
void testInScope_compositeScopeClose_isIdempotent() {
131+
ApiTracer.Scope scope1 =
132+
mock(ApiTracer.Scope.class, Mockito.withSettings().withoutAnnotations());
133+
ApiTracer.Scope scope2 =
134+
mock(ApiTracer.Scope.class, Mockito.withSettings().withoutAnnotations());
135+
136+
when(child1.inScope()).thenReturn(scope1);
137+
when(child2.inScope()).thenReturn(scope2);
138+
139+
ApiTracer.Scope compositeScope = compositeTracer.inScope();
140+
141+
compositeScope.close();
142+
// Subsequent close should be idempotent and not invoke underlying scopes again
143+
compositeScope.close();
144+
145+
verify(scope2, times(1)).close();
146+
verify(scope1, times(1)).close();
147+
}
148+
128149
@Test
129150
void testOperationSucceeded() {
130151
compositeTracer.operationSucceeded();

0 commit comments

Comments
 (0)