diff --git a/CHANGELOG.md b/CHANGELOG.md index ce5e378b9f2..2a6f64ea2fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes - Always remove the first-draw listener after the first frame, even if no layout pass follows it ([#6152](https://github.com/getsentry/sentry-java/pull/6152)) +- Prevent Reactor pool maintenance tasks from retaining unbounded Sentry scope chains ([#6283](https://github.com/getsentry/sentry-java/pull/6283)) ## 8.61.1 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..f20d3622669 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,19 +20,31 @@ 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$$Lambda", + "reactor.netty.http.client.Http2Pool$$Lambda", + "reactor.netty.resources.PooledConnectionProvider$$Lambda" }; - @Override - public Runnable apply(final @NotNull Runnable runnable) { - final String runnableClassName = runnable.getClass().getName(); + // The standalone reactor.pool.SimpleDequePool is intentionally not excluded because it also + // schedules acquisition-delivery tasks that require scope propagation. + static boolean isExcluded(final @NotNull String runnableClassName) { for (final String excludedPrefix : EXCLUDED_RUNNABLE_PREFIXES) { if (runnableClassName.startsWith(excludedPrefix)) { - return runnable; + return true; } } + return false; + } + + @Override + public Runnable apply(final @NotNull Runnable runnable) { + if (isExcluded(runnable.getClass().getName())) { + return runnable; + } final IScopes newScopes = Sentry.getCurrentScopes().forkedCurrentScope("spring.scheduleHook"); diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryScheduleHookTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryScheduleHookTest.kt index 32b56e2e27c..189acf89d52 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryScheduleHookTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryScheduleHookTest.kt @@ -8,7 +8,9 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertTrue class SentryScheduleHookTest { private val dsn = "http://key@localhost/proj" @@ -55,4 +57,37 @@ class SentryScheduleHookTest { } .get() } + + @Test + fun `known maintenance runnable class names are excluded`() { + val classNames = + listOf( + "reactor.kafka.receiver.internals.ConsumerEventLoop\$PollEvent", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$\$Lambda\$42/0x00000001", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$\$Lambda/0x00000001", + "reactor.netty.http.client.Http2Pool\$\$Lambda\$42/0x00000001", + "reactor.netty.http.client.Http2Pool\$\$Lambda/0x00000001", + "reactor.netty.resources.PooledConnectionProvider\$\$Lambda\$42/0x00000001", + "reactor.netty.resources.PooledConnectionProvider\$\$Lambda/0x00000001", + ) + + classNames.forEach { className -> + assertTrue(SentryScheduleHook.isExcluded(className), className) + } + } + + @Test + fun `request carrying runnable class names are not excluded`() { + val classNames = + listOf( + "reactor.netty.http.client.Http2Pool\$Borrower", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$QueueBorrowerMono", + "reactor.pool.SimpleDequePool\$\$Lambda\$42/0x00000001", + "com.example.RequestTask", + ) + + classNames.forEach { className -> + assertFalse(SentryScheduleHook.isExcluded(className), className) + } + } } 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..66eee33c3e6 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,19 +20,31 @@ 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$$Lambda", + "reactor.netty.http.client.Http2Pool$$Lambda", + "reactor.netty.resources.PooledConnectionProvider$$Lambda" }; - @Override - public Runnable apply(final @NotNull Runnable runnable) { - final String runnableClassName = runnable.getClass().getName(); + // The standalone reactor.pool.SimpleDequePool is intentionally not excluded because it also + // schedules acquisition-delivery tasks that require scope propagation. + static boolean isExcluded(final @NotNull String runnableClassName) { for (final String excludedPrefix : EXCLUDED_RUNNABLE_PREFIXES) { if (runnableClassName.startsWith(excludedPrefix)) { - return runnable; + return true; } } + return false; + } + + @Override + public Runnable apply(final @NotNull Runnable runnable) { + if (isExcluded(runnable.getClass().getName())) { + return runnable; + } final IScopes newScopes = Sentry.getCurrentScopes().forkedCurrentScope("spring.scheduleHook"); diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt index a5c16182fa7..07249866a7e 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt @@ -8,7 +8,9 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertTrue class SentryScheduleHookTest { private val dsn = "http://key@localhost/proj" @@ -55,4 +57,37 @@ class SentryScheduleHookTest { } .get() } + + @Test + fun `known maintenance runnable class names are excluded`() { + val classNames = + listOf( + "reactor.kafka.receiver.internals.ConsumerEventLoop\$PollEvent", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$\$Lambda\$42/0x00000001", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$\$Lambda/0x00000001", + "reactor.netty.http.client.Http2Pool\$\$Lambda\$42/0x00000001", + "reactor.netty.http.client.Http2Pool\$\$Lambda/0x00000001", + "reactor.netty.resources.PooledConnectionProvider\$\$Lambda\$42/0x00000001", + "reactor.netty.resources.PooledConnectionProvider\$\$Lambda/0x00000001", + ) + + classNames.forEach { className -> + assertTrue(SentryScheduleHook.isExcluded(className), className) + } + } + + @Test + fun `request carrying runnable class names are not excluded`() { + val classNames = + listOf( + "reactor.netty.http.client.Http2Pool\$Borrower", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$QueueBorrowerMono", + "reactor.pool.SimpleDequePool\$\$Lambda\$42/0x00000001", + "com.example.RequestTask", + ) + + classNames.forEach { className -> + assertFalse(SentryScheduleHook.isExcluded(className), className) + } + } } 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..303dc9a20a8 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,19 +20,31 @@ 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$$Lambda", + "reactor.netty.http.client.Http2Pool$$Lambda", + "reactor.netty.resources.PooledConnectionProvider$$Lambda" }; - @Override - public Runnable apply(final @NotNull Runnable runnable) { - final String runnableClassName = runnable.getClass().getName(); + // The standalone reactor.pool.SimpleDequePool is intentionally not excluded because it also + // schedules acquisition-delivery tasks that require scope propagation. + static boolean isExcluded(final @NotNull String runnableClassName) { for (final String excludedPrefix : EXCLUDED_RUNNABLE_PREFIXES) { if (runnableClassName.startsWith(excludedPrefix)) { - return runnable; + return true; } } + return false; + } + + @Override + public Runnable apply(final @NotNull Runnable runnable) { + if (isExcluded(runnable.getClass().getName())) { + return runnable; + } final IScopes newScopes = Sentry.getCurrentScopes().forkedCurrentScope("spring.scheduleHook"); diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryScheduleHookTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryScheduleHookTest.kt index f6c37e176e3..2c45bf1daa8 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryScheduleHookTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryScheduleHookTest.kt @@ -8,7 +8,9 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertTrue class SentryScheduleHookTest { private val dsn = "http://key@localhost/proj" @@ -55,4 +57,37 @@ class SentryScheduleHookTest { } .get() } + + @Test + fun `known maintenance runnable class names are excluded`() { + val classNames = + listOf( + "reactor.kafka.receiver.internals.ConsumerEventLoop\$PollEvent", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$\$Lambda\$42/0x00000001", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$\$Lambda/0x00000001", + "reactor.netty.http.client.Http2Pool\$\$Lambda\$42/0x00000001", + "reactor.netty.http.client.Http2Pool\$\$Lambda/0x00000001", + "reactor.netty.resources.PooledConnectionProvider\$\$Lambda\$42/0x00000001", + "reactor.netty.resources.PooledConnectionProvider\$\$Lambda/0x00000001", + ) + + classNames.forEach { className -> + assertTrue(SentryScheduleHook.isExcluded(className), className) + } + } + + @Test + fun `request carrying runnable class names are not excluded`() { + val classNames = + listOf( + "reactor.netty.http.client.Http2Pool\$Borrower", + "reactor.netty.internal.shaded.reactor.pool.SimpleDequePool\$QueueBorrowerMono", + "reactor.pool.SimpleDequePool\$\$Lambda\$42/0x00000001", + "com.example.RequestTask", + ) + + classNames.forEach { className -> + assertFalse(SentryScheduleHook.isExcluded(className), className) + } + } }