diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ccd002c3dd..4dbf4ee6eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +<<<<<<< HEAD ## Version 1.64.0 (2026-07-10) ### API @@ -93,6 +94,10 @@ ([#8469](https://github.com/open-telemetry/opentelemetry-java/pull/8469)) * Make benchmark path configurable ([#8557](https://github.com/open-telemetry/opentelemetry-java/pull/8557)) +======= +* Add shutdown lifecycle method to `Sampler` to allow resource cleanup for stateful samplers. + ([#8574](https://github.com/open-telemetry/opentelemetry-java/pull/8574)) +>>>>>>> 83b70e5ee (Shutdown JaegerRemoteSampler resources) ## Version 1.63.0 (2026-06-05) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-jaeger-remote-sampler.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-jaeger-remote-sampler.txt index 0d80e31c1f9..8052854dd9d 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-jaeger-remote-sampler.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-jaeger-remote-sampler.txt @@ -1,2 +1,6 @@ -Comparing source compatibility of opentelemetry-sdk-extension-jaeger-remote-sampler-1.65.0-SNAPSHOT.jar against opentelemetry-sdk-extension-jaeger-remote-sampler-1.63.0.jar -No changes. \ No newline at end of file +Comparing source compatibility of opentelemetry-sdk-extension-jaeger-remote-sampler-1.65.0-SNAPSHOT.jar against opentelemetry-sdk-extension-jaeger-remote-sampler-1.64.0.jar +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.extension.trace.jaeger.sampler.JaegerRemoteSampler (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + === UNCHANGED METHOD: PUBLIC void close() + +++ NEW ANNOTATION: java.lang.Deprecated + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.common.CompletableResultCode shutdown() diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt index 583ee29da68..b5b3a82f5bd 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt @@ -1,4 +1,7 @@ -Comparing source compatibility of opentelemetry-sdk-trace-1.65.0-SNAPSHOT.jar against opentelemetry-sdk-trace-1.63.0.jar -*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.trace.export.BatchSpanProcessor (not serializable) +Comparing source compatibility of opentelemetry-sdk-trace-1.65.0-SNAPSHOT.jar against opentelemetry-sdk-trace-1.64.0.jar +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.sdk.trace.samplers.Sampler (not serializable) === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.trace.export.BatchSpanProcessor create(io.opentelemetry.sdk.trace.export.SpanExporter) + +++ NEW INTERFACE: java.io.Closeable + +++ NEW INTERFACE: java.lang.AutoCloseable + +++ NEW METHOD: PUBLIC(+) void close() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.common.CompletableResultCode shutdown() diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/SamplerFactoryTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/SamplerFactoryTest.java index 110845bea71..9a6a1f599bf 100644 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/SamplerFactoryTest.java +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/SamplerFactoryTest.java @@ -58,10 +58,8 @@ class SamplerFactoryTest { @ParameterizedTest @MethodSource("createArguments") void create(@Nullable SamplerModel model, Sampler expectedSampler) { - // Some samplers like JaegerRemoteSampler are Closeable - ensure these get cleaned up - if (expectedSampler instanceof Closeable) { - cleanup.addCloseable((Closeable) expectedSampler); - } + // // All samplers are Closeable - ensure these get cleaned up + cleanup.addCloseable(expectedSampler); List closeables = new ArrayList<>(); Sampler sampler = SamplerFactory.getInstance().create(model, context); diff --git a/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSampler.java b/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSampler.java index 2e32f566273..52a8b350e7c 100644 --- a/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSampler.java +++ b/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSampler.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.export.GrpcResponse; import io.opentelemetry.sdk.common.export.GrpcSender; import io.opentelemetry.sdk.common.export.GrpcStatusCode; @@ -16,19 +17,19 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.sdk.trace.samplers.SamplingResult; -import java.io.Closeable; import java.io.IOException; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; /** Remote sampler that gets sampling configuration from remote Jaeger server. */ -public final class JaegerRemoteSampler implements Sampler, Closeable { +public final class JaegerRemoteSampler implements Sampler { private static final Logger logger = Logger.getLogger(JaegerRemoteSampler.class.getName()); private static final String WORKER_THREAD_NAME = @@ -41,6 +42,8 @@ public final class JaegerRemoteSampler implements Sampler, Closeable { private volatile Sampler sampler; + private final AtomicBoolean isShutdown = new AtomicBoolean(); + private final GrpcSender grpcSender; JaegerRemoteSampler( @@ -177,9 +180,19 @@ public static JaegerRemoteSamplerBuilder builder() { @Override @SuppressWarnings("Interruption") - public void close() { + public CompletableResultCode shutdown() { + if (isShutdown.getAndSet(true)) { + return CompletableResultCode.ofSuccess(); + } + pollFuture.cancel(true); pollExecutor.shutdownNow(); - grpcSender.shutdown().join(10, TimeUnit.SECONDS); + return grpcSender.shutdown(); + } + + @Override + @Deprecated + public void close() { + shutdown(); } } diff --git a/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerTest.java b/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerTest.java index e646a0d4921..88047b9f92d 100644 --- a/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerTest.java +++ b/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerTest.java @@ -24,6 +24,7 @@ import io.opentelemetry.exporter.internal.TlsUtil; import io.opentelemetry.exporter.sender.okhttp.internal.OkHttpGrpcSender; import io.opentelemetry.internal.testing.slf4j.SuppressLogger; +import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.export.GrpcStatusCode; import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling; import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling.RateLimitingSamplingStrategy; @@ -160,6 +161,22 @@ void connectionWorks() { } } + @Test + void shutdown() { + JaegerRemoteSampler sampler = + JaegerRemoteSampler.builder() + .setEndpoint(server.httpUri().toString()) + .setPollingInterval(1, TimeUnit.SECONDS) + .setServiceName(SERVICE_NAME) + .build(); + + CompletableResultCode first = sampler.shutdown(); + CompletableResultCode second = sampler.shutdown(); + + assertThat(first.join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + assertThat(second.isSuccess()).isTrue(); + } + @Test void tlsConnectionWorks() throws IOException { try (JaegerRemoteSampler sampler = diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java index 254d78fd254..790933215c2 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java @@ -10,6 +10,7 @@ import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.Arrays; import java.util.List; import java.util.function.Supplier; import javax.annotation.Nullable; @@ -106,18 +107,22 @@ SdkTracerInstrumentation getTracerInstrumentation() { } /** - * Stops tracing, including shutting down processors and set to {@code true} {@link + * Stops tracing, including shutting down processors and sampler and set to {@code true} {@link * #hasBeenShutdown()}. * - * @return a {@link CompletableResultCode} that will be completed when the span processor is shut - * down. + * @return a {@link CompletableResultCode} that will be completed when the span processor and + * sampler are shut down. */ CompletableResultCode shutdown() { synchronized (lock) { if (shutdownResult != null) { return shutdownResult; } - shutdownResult = activeSpanProcessor.shutdown(); + + shutdownResult = + CompletableResultCode.ofAll( + Arrays.asList(activeSpanProcessor.shutdown(), sampler.shutdown())); + return shutdownResult; } } diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java index 73edba3b6f1..17837cab691 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java @@ -11,13 +11,16 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.TraceId; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.data.LinkData; +import java.io.Closeable; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.annotation.concurrent.ThreadSafe; /** A Sampler is used to make decisions on {@link Span} sampling. */ @ThreadSafe -public interface Sampler { +public interface Sampler extends Closeable { /** * Returns a {@link Sampler} that always makes a "yes" {@link SamplingResult} for {@link Span} @@ -119,4 +122,20 @@ SamplingResult shouldSample( * @return the description of this {@code Sampler}. */ String getDescription(); + + /** + * Shuts down the sampler, releasing any resources held by it. + * + *

This method is called when the {@code SdkTracerProvider} is shut down. + * + * @return a {@link CompletableResultCode} which is completed when shutdown is finished. + */ + default CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + default void close() { + shutdown().join(10, TimeUnit.SECONDS); + } } diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java index b007fe06b22..4ba416c4f88 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java @@ -237,6 +237,19 @@ void shutdown() { Mockito.verify(spanProcessor, Mockito.times(1)).shutdown(); } + @Test + void shutdown_ShutsDownSampler() { + Sampler sampler = mock(Sampler.class); + + when(sampler.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + + SdkTracerProvider tracerProvider = SdkTracerProvider.builder().setSampler(sampler).build(); + + tracerProvider.shutdown(); + + verify(sampler).shutdown(); + } + @Test void close() { tracerProvider.close(); diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorTest.java index b83e48efad8..5e9bc38ff78 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorTest.java @@ -72,6 +72,8 @@ class BatchSpanProcessorTest { void setUp() { when(mockSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); when(mockSpanExporter.export(anyList())).thenReturn(CompletableResultCode.ofSuccess()); + + when(mockSampler.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); } @AfterEach diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessorTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessorTest.java index 62b1d59a84f..e5c860db4a7 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessorTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessorTest.java @@ -73,6 +73,8 @@ void setUp() { simpleSampledSpansProcessor = SimpleSpanProcessor.create(spanExporter); when(spanExporter.export(anyCollection())).thenReturn(CompletableResultCode.ofSuccess()); when(spanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + + when(mockSampler.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); } @Test