Skip to content

Commit 6edc786

Browse files
authored
Capture context class loader during async callback registration (open-telemetry#8091)
1 parent 3cca3e1 commit 6edc786

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/CallbackRegistration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.List;
1515
import java.util.logging.Level;
1616
import java.util.logging.Logger;
17+
import javax.annotation.Nullable;
1718

1819
/**
1920
* A registered callback.
@@ -29,11 +30,13 @@ public final class CallbackRegistration {
2930
private final Runnable callback;
3031
private final List<InstrumentDescriptor> instrumentDescriptors;
3132
private final boolean hasStorages;
33+
@Nullable private final ClassLoader contextClassLoader;
3234

3335
private CallbackRegistration(
3436
List<SdkObservableMeasurement> observableMeasurements, Runnable callback) {
3537
this.observableMeasurements = observableMeasurements;
3638
this.callback = callback;
39+
this.contextClassLoader = Thread.currentThread().getContextClassLoader();
3740
this.instrumentDescriptors =
3841
observableMeasurements.stream()
3942
.map(SdkObservableMeasurement::getInstrumentDescriptor)
@@ -80,13 +83,18 @@ public void invokeCallback(RegisteredReader reader, long startEpochNanos, long e
8083
observableMeasurements.forEach(
8184
observableMeasurement ->
8285
observableMeasurement.setActiveReader(reader, startEpochNanos, epochNanos));
86+
// Restore the context class loader that was active when the callback was registered.
87+
Thread currentThread = Thread.currentThread();
88+
ClassLoader previousContextClassLoader = currentThread.getContextClassLoader();
89+
currentThread.setContextClassLoader(contextClassLoader);
8390
try {
8491
callback.run();
8592
} catch (Throwable e) {
8693
propagateIfFatal(e);
8794
throttlingLogger.log(
8895
Level.WARNING, "An exception occurred invoking callback for " + this + ".", e);
8996
} finally {
97+
currentThread.setContextClassLoader(previousContextClassLoader);
9098
observableMeasurements.forEach(SdkObservableMeasurement::unsetActiveReader);
9199
}
92100
}

sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/CallbackRegistrationTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.Arrays;
3131
import java.util.Collections;
3232
import java.util.concurrent.atomic.AtomicLong;
33+
import java.util.concurrent.atomic.AtomicReference;
3334
import org.junit.jupiter.api.BeforeEach;
3435
import org.junit.jupiter.api.Test;
3536
import org.junit.jupiter.api.extension.ExtendWith;
@@ -219,6 +220,59 @@ void invokeCallback_NoStorage() {
219220
assertThat(counter.get()).isEqualTo(0);
220221
}
221222

223+
@Test
224+
void invokeCallback_RestoresContextClassLoader() {
225+
// Simulate the context class loader at registration time
226+
ClassLoader registrationClassLoader = new ClassLoader() {};
227+
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
228+
229+
Thread.currentThread().setContextClassLoader(registrationClassLoader);
230+
AtomicReference<ClassLoader> observedClassLoader = new AtomicReference<>();
231+
Runnable callback =
232+
() -> observedClassLoader.set(Thread.currentThread().getContextClassLoader());
233+
CallbackRegistration callbackRegistration =
234+
CallbackRegistration.create(Collections.singletonList(measurement2), callback);
235+
Thread.currentThread().setContextClassLoader(originalClassLoader);
236+
237+
// Simulate invocation on a thread with null context class loader (like DaemonThreadFactory)
238+
Thread.currentThread().setContextClassLoader(null);
239+
callbackRegistration.invokeCallback(registeredReader, 0, 1);
240+
241+
// Callback should have seen the registration-time classloader
242+
assertThat(observedClassLoader.get()).isSameAs(registrationClassLoader);
243+
244+
// After invocation, the thread's context classloader should be restored to null
245+
assertThat(Thread.currentThread().getContextClassLoader()).isNull();
246+
247+
// Clean up
248+
Thread.currentThread().setContextClassLoader(originalClassLoader);
249+
}
250+
251+
@Test
252+
void invokeCallback_RestoresContextClassLoaderOnException() {
253+
ClassLoader registrationClassLoader = new ClassLoader() {};
254+
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
255+
256+
Thread.currentThread().setContextClassLoader(registrationClassLoader);
257+
Runnable callback =
258+
() -> {
259+
throw new RuntimeException("Error!");
260+
};
261+
CallbackRegistration callbackRegistration =
262+
CallbackRegistration.create(Collections.singletonList(measurement2), callback);
263+
Thread.currentThread().setContextClassLoader(originalClassLoader);
264+
265+
// Simulate invocation on a thread with null context class loader
266+
Thread.currentThread().setContextClassLoader(null);
267+
callbackRegistration.invokeCallback(registeredReader, 0, 1);
268+
269+
// Context classloader should still be restored even after exception
270+
assertThat(Thread.currentThread().getContextClassLoader()).isNull();
271+
272+
// Clean up
273+
Thread.currentThread().setContextClassLoader(originalClassLoader);
274+
}
275+
222276
@Test
223277
void invokeCallback_MultipleMeasurements_ThrowsException() {
224278
Runnable callback =

0 commit comments

Comments
 (0)