From e86602b00925fd67807b5ac423805b12a7882421 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 29 Jul 2026 11:34:22 +0200 Subject: [PATCH 1/3] 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 e20c0835637..e50cb032d41 100644 --- a/sentry/src/main/java/io/sentry/SentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/SentryExecutorService.java @@ -55,6 +55,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 08e41f217c91d614abc96c2e36413776e6754487 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 29 Jul 2026 11:35:10 +0200 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ced1fee970..70c9f04dee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Performance +- 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)) - Use `RGB_565` instead of `ARGB_8888` for screenshot and replay capture bitmaps, halving per-frame memory usage ([#5821](https://github.com/getsentry/sentry-java/pull/5821)) - Remove an unused lock from `SentryPerformanceProvider`, which was allocated on every cold start in `ContentProvider.onCreate` without ever being acquired ([#5871](https://github.com/getsentry/sentry-java/pull/5871)) - Parse the app start profiling config with only the deserializer it needs instead of building a full `JsonSerializer` and `SentryOptions`, cutting 188 of 221 allocations on the main thread before `Application.onCreate` ([#5867](https://github.com/getsentry/sentry-java/pull/5867)) From b5d9bea0a830837ccc747dad2a47dda6676b198b Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 4 Aug 2026 17:27:55 +0200 Subject: [PATCH 3/3] test: Drop SentryExecutorService shutdown policy tests (JAVA-653) One test only asserted the setter took effect and the other timed close() against a wall-clock bound, which is flaky on loaded CI. The comment on setExecuteExistingDelayedTasksAfterShutdownPolicy explains why the policy is set, which is enough to prevent someone removing it. Co-Authored-By: Claude Opus 5 (1M context) --- .../io/sentry/SentryExecutorServiceTest.kt | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index e9e68a29799..153feecb4a4 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -8,7 +8,6 @@ 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 @@ -111,27 +110,6 @@ 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()