3131
3232import com .google .api .core .InternalApi ;
3333import com .google .common .collect .ImmutableList ;
34- import java .util .ArrayList ;
3534import java .util .List ;
3635import java .util .Map ;
3736import org .jspecify .annotations .NullMarked ;
4544@ NullMarked
4645@ InternalApi
4746class 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
0 commit comments