Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

### SDK

#### Metrics

* Add `PeriodicMetricReaderBuilder.setShutdownTimeout` to configure the previously hardcoded
shutdown wait time. Default is kept as 5s.
([#8756](https://github.com/open-telemetry/opentelemetry-java/pull/8756))

## Version 1.65.0 (2026-08-07)

**NOTE:** The `opentelemetry-exporter-zipkin` artifact has stopped being published. It was
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ Comparing source compatibility of opentelemetry-sdk-metrics-1.66.0-SNAPSHOT.jar
*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.metrics.export.PeriodicMetricReaderBuilder (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.metrics.export.PeriodicMetricReaderBuilder setInternalTelemetryVersion(io.opentelemetry.sdk.common.InternalTelemetryVersion)
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.metrics.export.PeriodicMetricReaderBuilder setShutdownTimeout(long, java.util.concurrent.TimeUnit)
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.metrics.export.PeriodicMetricReaderBuilder setShutdownTimeout(java.time.Duration)
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public final class PeriodicMetricReader implements MetricReader {
private final long intervalNanos;
private final ScheduledExecutorService scheduler;
private final Scheduled scheduled;
private final long shutdownTimeoutMillis;
private final Object lock = new Object();
private final InternalTelemetryVersion internalTelemetryVersion;

Expand All @@ -74,13 +75,15 @@ public static PeriodicMetricReaderBuilder builder(MetricExporter exporter) {
long intervalNanos,
ScheduledExecutorService scheduler,
int maxExportBatchSize,
InternalTelemetryVersion internalTelemetryVersion) {
InternalTelemetryVersion internalTelemetryVersion,
long shutdownTimeoutMillis) {
this.exporter = exporter;
this.intervalNanos = intervalNanos;
this.scheduler = scheduler;
this.maxExportBatchSize = maxExportBatchSize;
this.scheduled = new Scheduled();
this.internalTelemetryVersion = internalTelemetryVersion;
this.shutdownTimeoutMillis = shutdownTimeoutMillis;
}

@Override
Expand Down Expand Up @@ -133,12 +136,12 @@ public CompletableResultCode shutdown() {
}
scheduler.shutdown();
try {
scheduler.awaitTermination(5, TimeUnit.SECONDS);
scheduler.awaitTermination(shutdownTimeoutMillis, TimeUnit.MILLISECONDS);
// Wait for any in-flight export to complete before performing the final collection.
// Without this, doRun() sees exportAvailable=false and drops the final metrics.
scheduled.flushInProgress.join(5, TimeUnit.SECONDS);
scheduled.flushInProgress.join(shutdownTimeoutMillis, TimeUnit.MILLISECONDS);
CompletableResultCode flushResult = scheduled.doRun();
flushResult.join(5, TimeUnit.SECONDS);
flushResult.join(shutdownTimeoutMillis, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// force a shutdown if the export hasn't finished.
scheduler.shutdownNow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
public final class PeriodicMetricReaderBuilder {

static final long DEFAULT_SCHEDULE_DELAY_MINUTES = 1;
static final long DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 5000;

private final MetricExporter metricExporter;

Expand All @@ -35,6 +36,8 @@ public final class PeriodicMetricReaderBuilder {

private int maxExportBatchSize;

private long shutdownTimeoutMillis = DEFAULT_SHUTDOWN_TIMEOUT_MILLIS;

PeriodicMetricReaderBuilder(MetricExporter metricExporter) {
this.metricExporter = metricExporter;
}
Expand Down Expand Up @@ -78,6 +81,30 @@ PeriodicMetricReaderBuilder setMaxExportBatchSize(int maxExportBatchSize) {
return this;
}

/**
* Sets the maximum time to wait for shutdown to complete. This timeout is applied independently
* to each phase of the shutdown sequence (awaiting executor termination, joining any in-flight
* export, and joining the final export), so shutdown may take up to three times this value. If
* unset, defaults to {@value DEFAULT_SHUTDOWN_TIMEOUT_MILLIS}ms per phase.
*/
public PeriodicMetricReaderBuilder setShutdownTimeout(long timeout, TimeUnit unit) {
requireNonNull(unit, "unit");
checkArgument(timeout > 0, "timeout must be positive");
shutdownTimeoutMillis = unit.toMillis(timeout);
return this;
}

/**
* Sets the maximum time to wait for shutdown to complete. This timeout is applied independently
* to each phase of the shutdown sequence (awaiting executor termination, joining any in-flight
* export, and joining the final export), so shutdown may take up to three times this value. If
* unset, defaults to {@value DEFAULT_SHUTDOWN_TIMEOUT_MILLIS}ms per phase.
*/
public PeriodicMetricReaderBuilder setShutdownTimeout(Duration timeout) {
requireNonNull(timeout, "timeout");
return setShutdownTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS);
}

/** Build a {@link PeriodicMetricReader} with the configuration of this builder. */
public PeriodicMetricReader build() {
ScheduledExecutorService executor = this.executor;
Expand All @@ -86,7 +113,12 @@ public PeriodicMetricReader build() {
Executors.newScheduledThreadPool(1, new DaemonThreadFactory("PeriodicMetricReader"));
}
return new PeriodicMetricReader(
metricExporter, intervalNanos, executor, maxExportBatchSize, internalTelemetryVersion);
metricExporter,
intervalNanos,
executor,
maxExportBatchSize,
internalTelemetryVersion,
shutdownTimeoutMillis);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,79 @@ void invalidConfig() {
assertThatThrownBy(() -> PeriodicMetricReader.builder(metricExporter).setExecutor(null))
.isInstanceOf(NullPointerException.class)
.hasMessage("executor");
assertThatThrownBy(
() -> PeriodicMetricReader.builder(metricExporter).setShutdownTimeout(1, null))
.isInstanceOf(NullPointerException.class)
.hasMessage("unit");
assertThatThrownBy(
() ->
PeriodicMetricReader.builder(metricExporter)
.setShutdownTimeout(-1, TimeUnit.MILLISECONDS))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("timeout must be positive");
assertThatThrownBy(() -> PeriodicMetricReader.builder(metricExporter).setShutdownTimeout(null))
.isInstanceOf(NullPointerException.class)
.hasMessage("timeout");
}

@Test
@Timeout(10)
@SuppressLogger(PeriodicMetricReader.class)
void shutdown_respectsConfiguredWaitTime() throws Exception {
CompletableResultCode neverCompletes = new CompletableResultCode();
CountDownLatch exportStarted = new CountDownLatch(1);

MetricExporter blockingExporter =
new MetricExporter() {
@Override
public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) {
return AggregationTemporality.CUMULATIVE;
}

@Override
public CompletableResultCode export(Collection<MetricData> metrics) {
exportStarted.countDown();
return neverCompletes;
}

@Override
public CompletableResultCode flush() {
return CompletableResultCode.ofSuccess();
}

@Override
public CompletableResultCode shutdown() {
return CompletableResultCode.ofSuccess();
}
};

PeriodicMetricReader reader =
PeriodicMetricReader.builder(blockingExporter)
.setInterval(Duration.ofSeconds(Integer.MAX_VALUE))
.setShutdownTimeout(Duration.ofMillis(100))
.build();
reader.register(collectionRegistration);

// Start an export that never completes so shutdown() has to wait on it.
reader.forceFlush();
assertThat(exportStarted.await(5, TimeUnit.SECONDS)).isTrue();

// shutdown() must give up after the configured 100ms per phase, well before the
// 5s-per-phase default would elapse.
CountDownLatch shutdownDone = new CountDownLatch(1);
Thread shutdownThread =
new Thread(
() -> {
reader.shutdown();
shutdownDone.countDown();
});
shutdownThread.setDaemon(true);
shutdownThread.start();

assertThat(shutdownDone.await(3, TimeUnit.SECONDS)).isTrue();

// Release the hanging export so the test thread does not leak a pending result.
neverCompletes.succeed();
}

@Test
Expand Down
Loading