From 01456dd309ac465d10da9817ef0dc909993f6dcd Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 29 Jul 2026 11:34:22 +0200 Subject: [PATCH 1/2] perf: Drop pending timer tasks on shutdown to unblock close (JAVA-653) The timer executor is shut down by Scopes.close() via shutdown() followed by awaitTermination(). ScheduledThreadPoolExecutor keeps queued delayed tasks across shutdown() by default, so awaitTermination() blocks for the full shutdown timeout (2s by default) whenever a long timeout is still pending: an unfinished transaction's idle/deadline timer, the 30s LifecycleWatcher end-session task, or a rate limit lifted notification. Those tasks are discarded by the subsequent shutdownNow() anyway, so dropping them upfront is behaviour preserving and only saves the wait. This only affects shutdown(), so the SDK restart path that intentionally leaves the timer executor running is unaffected. Co-Authored-By: Claude Opus 5 (1M context) --- .../java/io/sentry/SentryExecutorService.java | 4 ++++ .../io/sentry/SentryExecutorServiceTest.kt | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/sentry/src/main/java/io/sentry/SentryExecutorService.java b/sentry/src/main/java/io/sentry/SentryExecutorService.java index a469dca5853..fc6a7c4f818 100644 --- a/sentry/src/main/java/io/sentry/SentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/SentryExecutorService.java @@ -56,6 +56,10 @@ public SentryExecutorService(final @Nullable SentryOptions options) { executorService.setRemoveOnCancelPolicy(removeOnCancelPolicy); executorService.setKeepAliveTime(keepAliveTime, keepAliveTimeUnit); executorService.allowCoreThreadTimeOut(true); + // by default shutdown() keeps queued delayed tasks, so awaitTermination blocks for the full + // shutdown timeout whenever a long timeout is still pending. Those tasks are discarded by the + // subsequent shutdownNow() anyway, so dropping them upfront only saves the wait. + executorService.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); } public SentryExecutorService() { diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index 153feecb4a4..e9e68a29799 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -8,6 +8,7 @@ import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.system.measureTimeMillis import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -110,6 +111,27 @@ class SentryExecutorServiceTest { sentryExecutor.close(15000) } + @Test + fun `SentryExecutorService discards pending delayed tasks on shutdown when requested`() { + val sentryExecutor = SentryExecutorService(null, true, 30, TimeUnit.SECONDS) + val executor = sentryExecutor.getProperty("executorService") + assertFalse(executor.executeExistingDelayedTasksAfterShutdownPolicy) + sentryExecutor.close(15000) + } + + @Test + fun `SentryExecutorService close does not wait for a pending delayed task`() { + val sentryExecutor = SentryExecutorService(null, true, 30, TimeUnit.SECONDS) + val ran = AtomicBoolean(false) + sentryExecutor.schedule({ ran.set(true) }, 30000) + + val elapsed = measureTimeMillis { sentryExecutor.close(5000) } + + assertTrue(elapsed < 5000, "close blocked for ${elapsed}ms waiting on the pending task") + assertTrue(sentryExecutor.isClosed) + assertFalse(ran.get()) + } + @Test fun `SentryExecutorService isClosed returns true if executor is shutdown`() { val executor = mock() From c9c256ec40b9c38de6b05d405e19849b0294d78b Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 29 Jul 2026 11:35:10 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aee94a20b4..f3bd4c18d54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Performance - Reduce the number of SDK threads: `LifecycleWatcher` now schedules the session-end task on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5819](https://github.com/getsentry/sentry-java/pull/5819)) +- Avoid waiting up to `shutdownTimeoutMillis` when closing the SDK with a pending transaction timeout or session-end task ([#5851](https://github.com/getsentry/sentry-java/pull/5851)) - Speed up deserialization of arbitrary JSON objects by typing numbers without throwing exceptions ([#5783](https://github.com/getsentry/sentry-java/pull/5783)) ### Dependencies