From 8ee8aa927efe0823803894ee21e581f740854271 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 19 Oct 2026 12:35:56 +0200 Subject: [PATCH 1/3] fix(spring): Prevent Reactor pool scope chain leaks Skip scope propagation for Reactor pool maintenance tasks that reschedule themselves indefinitely. This prevents each run from retaining the previous run through the parent scope chain. Add a Reactor Netty connection pool regression test that verifies background eviction keeps scope depth bounded. Fixes GH-6248 Refs JAVA-773 Co-Authored-By: Claude --- .../spring7/webflux/SentryScheduleHook.java | 6 +- sentry-spring-jakarta/build.gradle.kts | 1 + .../jakarta/webflux/SentryScheduleHook.java | 6 +- .../SentryScheduleHookNettyPoolTest.kt | 72 +++++++++++++++++++ .../spring/webflux/SentryScheduleHook.java | 6 +- 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookNettyPoolTest.kt diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryScheduleHook.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryScheduleHook.java index 89b79fc3e27..57eb86cece6 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryScheduleHook.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryScheduleHook.java @@ -20,9 +20,13 @@ public final class SentryScheduleHook implements Function { * scope parent chain buildup. * * @see GitHub Issue #5051 + * @see GitHub Issue #6248 */ private static final String[] EXCLUDED_RUNNABLE_PREFIXES = { - "reactor.kafka.receiver.internals.ConsumerEventLoop" + "reactor.kafka.receiver.internals.ConsumerEventLoop", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool", + "reactor.pool.SimpleDequePool", + "reactor.netty.resources.PooledConnectionProvider" }; @Override diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index f103bfcbe0a..d1d688d8f7f 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { testImplementation(libs.awaitility.kotlin) testImplementation(libs.context.propagation) testImplementation(libs.graphql.java24) + testImplementation(libs.google.truth) testImplementation(libs.kotlin.test.junit) testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.inline) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java index 61f48538722..04629fbe7be 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java @@ -20,9 +20,13 @@ public final class SentryScheduleHook implements Function { * scope parent chain buildup. * * @see GitHub Issue #5051 + * @see GitHub Issue #6248 */ private static final String[] EXCLUDED_RUNNABLE_PREFIXES = { - "reactor.kafka.receiver.internals.ConsumerEventLoop" + "reactor.kafka.receiver.internals.ConsumerEventLoop", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool", + "reactor.pool.SimpleDequePool", + "reactor.netty.resources.PooledConnectionProvider" }; @Override diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookNettyPoolTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookNettyPoolTest.kt new file mode 100644 index 00000000000..0c983428e11 --- /dev/null +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookNettyPoolTest.kt @@ -0,0 +1,72 @@ +package io.sentry.spring.jakarta.webflux + +import com.google.common.truth.Truth.assertThat +import io.sentry.IScopes +import io.sentry.Sentry +import java.time.Duration +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.Test +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import reactor.netty.http.client.HttpClient +import reactor.netty.http.server.HttpServer +import reactor.netty.resources.ConnectionProvider + +class SentryScheduleHookNettyPoolTest { + private fun depth(scopes: IScopes?): Int = generateSequence(scopes) { it.parentScopes }.count() + + @Test + fun `reactor netty pool eviction keeps the scopes chain bounded`() { + Sentry.init { it.dsn = "https://key@localhost/proj" } + + val maxDepth = AtomicInteger() + val evictionSchedules = AtomicInteger() + val enoughEvictions = CountDownLatch(1) + Schedulers.onScheduleHook("probe") { runnable -> + if ( + runnable.javaClass.name.startsWith( + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool" + ) + ) { + maxDepth.accumulateAndGet(depth(Sentry.getCurrentScopes()), ::maxOf) + if (evictionSchedules.incrementAndGet() == 100) { + enoughEvictions.countDown() + } + } + runnable + } + Schedulers.onScheduleHook("sentry", SentryScheduleHook()) + + val server = + HttpServer.create() + .port(0) + .handle { _, response -> response.sendString(Mono.just("ok")) } + .bindNow() + val pool = + ConnectionProvider.builder("scope-chain-regression") + .maxIdleTime(Duration.ofMillis(1)) + .evictInBackground(Duration.ofMillis(1)) + .build() + + try { + HttpClient.create(pool) + .get() + .uri("http://localhost:${server.port()}/") + .responseContent() + .aggregate() + .asString() + .block() + + assertThat(enoughEvictions.await(10, TimeUnit.SECONDS)).isTrue() + assertThat(maxDepth.get()).isLessThan(10) + } finally { + Schedulers.resetOnScheduleHook("sentry") + Schedulers.resetOnScheduleHook("probe") + pool.dispose() + server.disposeNow() + Sentry.close() + } + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryScheduleHook.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryScheduleHook.java index 96622200ca6..dc3e6d1fbca 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryScheduleHook.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryScheduleHook.java @@ -20,9 +20,13 @@ public final class SentryScheduleHook implements Function { * scope parent chain buildup. * * @see GitHub Issue #5051 + * @see GitHub Issue #6248 */ private static final String[] EXCLUDED_RUNNABLE_PREFIXES = { - "reactor.kafka.receiver.internals.ConsumerEventLoop" + "reactor.kafka.receiver.internals.ConsumerEventLoop", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool", + "reactor.pool.SimpleDequePool", + "reactor.netty.resources.PooledConnectionProvider" }; @Override From 5c5023af00263bf0ecdd697b36ae6d6635268049 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 19 Oct 2026 12:38:50 +0200 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6bd32c9e3b..d7664a95150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Prevent Reactor pool maintenance tasks from retaining unbounded Sentry scope chains ([#6283](https://github.com/getsentry/sentry-java/pull/6283)) - Measure Session Replay's 1h recording cap and its touch-move debounce on a monotonic clock, so a device time change no longer stops a healthy recording early, keeps a finished one alive, or suppresses gesture events ([#6090](https://github.com/getsentry/sentry-java/pull/6090)) ## 8.61.0 From 76642799d0f5a5452dc9c48088c7a4e3f45c45ef Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 19 Oct 2026 12:40:30 +0200 Subject: [PATCH 3/3] test(spring): Drop Reactor pool regression test Remove the Reactor Netty integration test and its test-only Truth dependency. Co-Authored-By: Claude --- sentry-spring-jakarta/build.gradle.kts | 1 - .../SentryScheduleHookNettyPoolTest.kt | 72 ------------------- 2 files changed, 73 deletions(-) delete mode 100644 sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookNettyPoolTest.kt diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index d1d688d8f7f..f103bfcbe0a 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -64,7 +64,6 @@ dependencies { testImplementation(libs.awaitility.kotlin) testImplementation(libs.context.propagation) testImplementation(libs.graphql.java24) - testImplementation(libs.google.truth) testImplementation(libs.kotlin.test.junit) testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.inline) diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookNettyPoolTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookNettyPoolTest.kt deleted file mode 100644 index 0c983428e11..00000000000 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookNettyPoolTest.kt +++ /dev/null @@ -1,72 +0,0 @@ -package io.sentry.spring.jakarta.webflux - -import com.google.common.truth.Truth.assertThat -import io.sentry.IScopes -import io.sentry.Sentry -import java.time.Duration -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -import kotlin.test.Test -import reactor.core.publisher.Mono -import reactor.core.scheduler.Schedulers -import reactor.netty.http.client.HttpClient -import reactor.netty.http.server.HttpServer -import reactor.netty.resources.ConnectionProvider - -class SentryScheduleHookNettyPoolTest { - private fun depth(scopes: IScopes?): Int = generateSequence(scopes) { it.parentScopes }.count() - - @Test - fun `reactor netty pool eviction keeps the scopes chain bounded`() { - Sentry.init { it.dsn = "https://key@localhost/proj" } - - val maxDepth = AtomicInteger() - val evictionSchedules = AtomicInteger() - val enoughEvictions = CountDownLatch(1) - Schedulers.onScheduleHook("probe") { runnable -> - if ( - runnable.javaClass.name.startsWith( - "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool" - ) - ) { - maxDepth.accumulateAndGet(depth(Sentry.getCurrentScopes()), ::maxOf) - if (evictionSchedules.incrementAndGet() == 100) { - enoughEvictions.countDown() - } - } - runnable - } - Schedulers.onScheduleHook("sentry", SentryScheduleHook()) - - val server = - HttpServer.create() - .port(0) - .handle { _, response -> response.sendString(Mono.just("ok")) } - .bindNow() - val pool = - ConnectionProvider.builder("scope-chain-regression") - .maxIdleTime(Duration.ofMillis(1)) - .evictInBackground(Duration.ofMillis(1)) - .build() - - try { - HttpClient.create(pool) - .get() - .uri("http://localhost:${server.port()}/") - .responseContent() - .aggregate() - .asString() - .block() - - assertThat(enoughEvictions.await(10, TimeUnit.SECONDS)).isTrue() - assertThat(maxDepth.get()).isLessThan(10) - } finally { - Schedulers.resetOnScheduleHook("sentry") - Schedulers.resetOnScheduleHook("probe") - pool.dispose() - server.disposeNow() - Sentry.close() - } - } -}