From 9e11ee11b2ddbb860cad63c6c64fdbc57480fed7 Mon Sep 17 00:00:00 2001 From: "jaipilot[bot]" <273169020+jaipilot[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:22:42 +0000 Subject: [PATCH] Skip scheduling a redundant timeout task for synchronously-completed metric exports --- .../metrics/export/PeriodicMetricReader.java | 7 ++++++ .../export/PeriodicMetricReaderTest.java | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java index d44a95226e1..6e2efd69f8f 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java @@ -263,6 +263,13 @@ private CompletableResultCode applyTimeout(CompletableResultCode result) { if (exporterTimeoutNanos == Long.MAX_VALUE) { return result; } + if (result.isDone()) { + // Already complete (e.g. a synchronous exporter) - scheduling a timeout task would be + // wasted work, since it would immediately be cancelled. + return result.isSuccess() + ? CompletableResultCode.ofSuccess() + : CompletableResultCode.ofFailure(); + } try { CompletableResultCode timeoutResult = new CompletableResultCode(); diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderTest.java index 64797cbaf8e..d7023e4f9ad 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderTest.java @@ -10,6 +10,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -860,6 +861,29 @@ void timeoutEnforcement_failsSlowExporter() throws Exception { } } + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void applyTimeout_skipsSchedulingForSynchronouslyCompletedExport() { + // A timeout is only useful for exports that have not already finished. When the exporter + // completes synchronously, no timeout task should be scheduled since it would be immediately + // cancelled. + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + when(scheduler.scheduleAtFixedRate(any(), anyLong(), anyLong(), any())) + .thenReturn(mock(ScheduledFuture.class)); + when(scheduler.schedule(any(Runnable.class), anyLong(), any(TimeUnit.class))) + .thenReturn(mock(ScheduledFuture.class)); + when(metricExporter.export(any())).thenReturn(CompletableResultCode.ofSuccess()); + when(metricExporter.flush()).thenReturn(CompletableResultCode.ofSuccess()); + + PeriodicMetricReader reader = + PeriodicMetricReader.builder(metricExporter).setExecutor(scheduler).build(); + reader.register(collectionRegistration); + + assertThat(reader.forceFlush().join(5, TimeUnit.SECONDS).isSuccess()).isTrue(); + + verify(scheduler, never()).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class)); + } + // Helper test classes for timeout testing private static class DelayingMetricExporter implements MetricExporter { private final long delayMs;