From 1611d476a7c4235435a3d066139ec6e1b3a50989 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 29 Oct 2025 11:41:17 +0100 Subject: [PATCH 01/10] removed SentryExecutorService max queue limit --- .../java/io/sentry/SentryExecutorService.java | 64 +--------- .../io/sentry/SentryExecutorServiceTest.kt | 120 ++---------------- 2 files changed, 17 insertions(+), 167 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryExecutorService.java b/sentry/src/main/java/io/sentry/SentryExecutorService.java index 873e4744e3c..3668c632d75 100644 --- a/sentry/src/main/java/io/sentry/SentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/SentryExecutorService.java @@ -2,7 +2,6 @@ import io.sentry.util.AutoClosableReentrantLock; import java.util.concurrent.Callable; -import java.util.concurrent.CancellationException; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -23,12 +22,6 @@ public final class SentryExecutorService implements ISentryExecutorService { */ private static final int INITIAL_QUEUE_SIZE = 40; - /** - * By default, the work queue is unbounded so it can grow as much as the memory allows. We want to - * limit it by 271 which would be x8 times growth from the default initial capacity. - */ - private static final int MAX_QUEUE_SIZE = 271; - private final @NotNull ScheduledThreadPoolExecutor executorService; private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); @@ -56,43 +49,19 @@ public SentryExecutorService() { @Override public @NotNull Future submit(final @NotNull Runnable runnable) throws RejectedExecutionException { - if (executorService.getQueue().size() < MAX_QUEUE_SIZE) { - return executorService.submit(runnable); - } - if (options != null) { - options - .getLogger() - .log(SentryLevel.WARNING, "Task " + runnable + " rejected from " + executorService); - } - return new CancelledFuture<>(); + return executorService.submit(runnable); } @Override public @NotNull Future submit(final @NotNull Callable callable) throws RejectedExecutionException { - if (executorService.getQueue().size() < MAX_QUEUE_SIZE) { - return executorService.submit(callable); - } - if (options != null) { - options - .getLogger() - .log(SentryLevel.WARNING, "Task " + callable + " rejected from " + executorService); - } - return new CancelledFuture<>(); + return executorService.submit(callable); } @Override public @NotNull Future schedule(final @NotNull Runnable runnable, final long delayMillis) throws RejectedExecutionException { - if (executorService.getQueue().size() < MAX_QUEUE_SIZE) { - return executorService.schedule(runnable, delayMillis, TimeUnit.MILLISECONDS); - } - if (options != null) { - options - .getLogger() - .log(SentryLevel.WARNING, "Task " + runnable + " rejected from " + executorService); - } - return new CancelledFuture<>(); + return executorService.schedule(runnable, delayMillis, TimeUnit.MILLISECONDS); } @Override @@ -158,31 +127,4 @@ private static final class SentryExecutorServiceThreadFactory implements ThreadF return ret; } } - - private static final class CancelledFuture implements Future { - @Override - public boolean cancel(final boolean mayInterruptIfRunning) { - return true; - } - - @Override - public boolean isCancelled() { - return true; - } - - @Override - public boolean isDone() { - return true; - } - - @Override - public T get() { - throw new CancellationException(); - } - - @Override - public T get(final long timeout, final @NotNull TimeUnit unit) { - throw new CancellationException(); - } - } } diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index 19dd60469cc..5609bbb39be 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -1,15 +1,11 @@ package io.sentry import io.sentry.test.getProperty -import java.util.concurrent.BlockingQueue -import java.util.concurrent.Callable -import java.util.concurrent.CancellationException import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue import org.awaitility.kotlin.await @@ -19,6 +15,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit class SentryExecutorServiceTest { @Test @@ -109,108 +106,6 @@ class SentryExecutorServiceTest { assertFalse(sentryExecutor.isClosed) } - @Test - fun `SentryExecutorService submit runnable returns cancelled future when queue size exceeds limit`() { - val queue = mock>() - whenever(queue.size).thenReturn(272) // Above MAX_QUEUE_SIZE (271) - - val executor = mock { on { getQueue() } doReturn queue } - - val options = mock() - val logger = mock() - whenever(options.logger).thenReturn(logger) - - val sentryExecutor = SentryExecutorService(executor, options) - val future = sentryExecutor.submit {} - - assertTrue(future.isCancelled) - assertTrue(future.isDone) - assertFailsWith { future.get() } - verify(executor, never()).submit(any()) - verify(logger).log(any(), any()) - } - - @Test - fun `SentryExecutorService submit runnable accepts when queue size is within limit`() { - val queue = mock>() - whenever(queue.size).thenReturn(270) // Below MAX_QUEUE_SIZE (271) - - val executor = mock { on { getQueue() } doReturn queue } - - val sentryExecutor = SentryExecutorService(executor, null) - sentryExecutor.submit {} - - verify(executor).submit(any()) - } - - @Test - fun `SentryExecutorService submit callable returns cancelled future when queue size exceeds limit`() { - val queue = mock>() - whenever(queue.size).thenReturn(272) // Above MAX_QUEUE_SIZE (271) - - val executor = mock { on { getQueue() } doReturn queue } - - val options = mock() - val logger = mock() - whenever(options.logger).thenReturn(logger) - - val sentryExecutor = SentryExecutorService(executor, options) - val future = sentryExecutor.submit(Callable { "result" }) - - assertTrue(future.isCancelled) - assertTrue(future.isDone) - assertFailsWith { future.get() } - verify(executor, never()).submit(any>()) - verify(logger).log(any(), any()) - } - - @Test - fun `SentryExecutorService submit callable accepts when queue size is within limit`() { - val queue = mock>() - whenever(queue.size).thenReturn(270) // Below MAX_QUEUE_SIZE (271) - - val executor = mock { on { getQueue() } doReturn queue } - - val sentryExecutor = SentryExecutorService(executor, null) - sentryExecutor.submit(Callable { "result" }) - - verify(executor).submit(any>()) - } - - @Test - fun `SentryExecutorService schedule returns cancelled future when queue size exceeds limit`() { - val queue = mock>() - whenever(queue.size).thenReturn(272) // Above MAX_QUEUE_SIZE (271) - - val executor = mock { on { getQueue() } doReturn queue } - - val options = mock() - val logger = mock() - whenever(options.logger).thenReturn(logger) - - val sentryExecutor = SentryExecutorService(executor, options) - val future = sentryExecutor.schedule({}, 1000L) - - assertTrue(future.isCancelled) - assertTrue(future.isDone) - assertFailsWith { future.get() } - verify(executor, never()).schedule(any(), any(), any()) - verify(logger).log(any(), any()) - } - - @Test - fun `SentryExecutorService schedule accepts when queue size is within limit`() { - val queue = mock>() - whenever(queue.size).thenReturn(270) // Below MAX_QUEUE_SIZE (271) - - val executor = mock { on { getQueue() } doReturn queue } - - val sentryExecutor = SentryExecutorService(executor, null) - sentryExecutor.schedule({}, 1000L) - - verify(executor).schedule(any(), any(), any()) - } - @Test fun `SentryExecutorService prewarm schedules dummy tasks and clears queue`() { val executor = ScheduledThreadPoolExecutor(1) @@ -225,4 +120,17 @@ class SentryExecutorServiceTest { // the queue should be empty assertEquals(0, executor.queue.size) } + + @Test + fun `SentryExecutorService runs any number of job`() { + val sentryExecutor = SentryExecutorService() + var called = false + // Post 1k jobs after 1 day, to test new jobs are accepted + repeat(1000) { + sentryExecutor.schedule({}, TimeUnit.DAYS.toMillis(1)) + } + sentryExecutor.submit { called = true } + await.until { called } + assertTrue(called) + } } From ba30c5fc90292d53c94b4b2ab7b7f1f437c81eb6 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 29 Oct 2025 11:43:58 +0100 Subject: [PATCH 02/10] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23335431abc..16156cee4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Removed SentryExecutorService limit ([#4846](https://github.com/getsentry/sentry-java/pull/4846)) - [ANR] Removed AndroidTransactionProfiler lock ([#4817](https://github.com/getsentry/sentry-java/pull/4817)) - Fix wrong .super() call in SentryTimberTree ([#4844](https://github.com/getsentry/sentry-java/pull/4844)) From 80d60f7d41eceb88b957732f1639153c9b4478aa Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 29 Oct 2025 10:46:45 +0000 Subject: [PATCH 03/10] Format code --- sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index 5609bbb39be..7fb0f80f216 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -3,6 +3,7 @@ package io.sentry import io.sentry.test.getProperty import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test import kotlin.test.assertEquals @@ -15,7 +16,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.util.concurrent.TimeUnit class SentryExecutorServiceTest { @Test @@ -126,9 +126,7 @@ class SentryExecutorServiceTest { val sentryExecutor = SentryExecutorService() var called = false // Post 1k jobs after 1 day, to test new jobs are accepted - repeat(1000) { - sentryExecutor.schedule({}, TimeUnit.DAYS.toMillis(1)) - } + repeat(1000) { sentryExecutor.schedule({}, TimeUnit.DAYS.toMillis(1)) } sentryExecutor.submit { called = true } await.until { called } assertTrue(called) From df1de9bb0edb166e448fb5f466c50d601dd49431 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 29 Oct 2025 12:19:55 +0100 Subject: [PATCH 04/10] format code --- sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index 5609bbb39be..7fb0f80f216 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -3,6 +3,7 @@ package io.sentry import io.sentry.test.getProperty import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test import kotlin.test.assertEquals @@ -15,7 +16,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.util.concurrent.TimeUnit class SentryExecutorServiceTest { @Test @@ -126,9 +126,7 @@ class SentryExecutorServiceTest { val sentryExecutor = SentryExecutorService() var called = false // Post 1k jobs after 1 day, to test new jobs are accepted - repeat(1000) { - sentryExecutor.schedule({}, TimeUnit.DAYS.toMillis(1)) - } + repeat(1000) { sentryExecutor.schedule({}, TimeUnit.DAYS.toMillis(1)) } sentryExecutor.submit { called = true } await.until { called } assertTrue(called) From fcd859500c0a88f1681e779b926a25238274e35f Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 29 Oct 2025 16:30:37 +0100 Subject: [PATCH 05/10] readded executor queue limit for submit() submit() now tries to purge if it reaches the limit --- .../java/io/sentry/SentryExecutorService.java | 62 +++++++++++- .../io/sentry/SentryExecutorServiceTest.kt | 97 +++++++++++++++++-- 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryExecutorService.java b/sentry/src/main/java/io/sentry/SentryExecutorService.java index 3668c632d75..b96a0e27db3 100644 --- a/sentry/src/main/java/io/sentry/SentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/SentryExecutorService.java @@ -2,6 +2,7 @@ import io.sentry.util.AutoClosableReentrantLock; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -22,6 +23,12 @@ public final class SentryExecutorService implements ISentryExecutorService { */ private static final int INITIAL_QUEUE_SIZE = 40; + /** + * By default, the work queue is unbounded so it can grow as much as the memory allows. We want to + * limit it by 271 which would be x8 times growth from the default initial capacity. + */ + private static final int MAX_QUEUE_SIZE = 271; + private final @NotNull ScheduledThreadPoolExecutor executorService; private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); @@ -46,16 +53,41 @@ public SentryExecutorService() { this(new ScheduledThreadPoolExecutor(1, new SentryExecutorServiceThreadFactory()), null); } + private boolean isQueueAvailable() { + // If limit is reached, purge cancelled tasks from the queue + if (executorService.getQueue().size() >= MAX_QUEUE_SIZE) { + executorService.purge(); + } + // Check limit again after purge + return executorService.getQueue().size() < MAX_QUEUE_SIZE; + } + @Override public @NotNull Future submit(final @NotNull Runnable runnable) throws RejectedExecutionException { - return executorService.submit(runnable); + if (isQueueAvailable()) { + return executorService.submit(runnable); + } + if (options != null) { + options + .getLogger() + .log(SentryLevel.WARNING, "Task " + runnable + " rejected from " + executorService); + } + return new CancelledFuture<>(); } @Override public @NotNull Future submit(final @NotNull Callable callable) throws RejectedExecutionException { - return executorService.submit(callable); + if (isQueueAvailable()) { + return executorService.submit(callable); + } + if (options != null) { + options + .getLogger() + .log(SentryLevel.WARNING, "Task " + callable + " rejected from " + executorService); + } + return new CancelledFuture<>(); } @Override @@ -127,4 +159,30 @@ private static final class SentryExecutorServiceThreadFactory implements ThreadF return ret; } } + private static final class CancelledFuture implements Future { + @Override + public boolean cancel(final boolean mayInterruptIfRunning) { + return true; + } + + @Override + public boolean isCancelled() { + return true; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public T get() { + throw new CancellationException(); + } + + @Override + public T get(final long timeout, final @NotNull TimeUnit unit) { + throw new CancellationException(); + } + } } diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index 7fb0f80f216..c56703ac627 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -14,8 +14,13 @@ import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.concurrent.BlockingQueue +import java.util.concurrent.Callable +import java.util.concurrent.CancellationException +import kotlin.test.assertFailsWith class SentryExecutorServiceTest { @Test @@ -105,6 +110,86 @@ class SentryExecutorServiceTest { whenever(executor.isShutdown).thenReturn(false) assertFalse(sentryExecutor.isClosed) } + @Test + fun `SentryExecutorService submit runnable returns cancelled future when queue size exceeds limit`() { + val queue = mock>() + whenever(queue.size).thenReturn(272) // Above MAX_QUEUE_SIZE (271) + + val executor = mock { on { getQueue() } doReturn queue } + + val options = mock() + val logger = mock() + whenever(options.logger).thenReturn(logger) + + val sentryExecutor = SentryExecutorService(executor, options) + val future = sentryExecutor.submit {} + + assertTrue(future.isCancelled) + assertTrue(future.isDone) + assertFailsWith { future.get() } + verify(executor, never()).submit(any()) + verify(logger).log(any(), any()) + } + + @Test + fun `SentryExecutorService submit runnable accepts when queue size is within limit`() { + val queue = mock>() + whenever(queue.size).thenReturn(270) // Below MAX_QUEUE_SIZE (271) + + val executor = mock { on { getQueue() } doReturn queue } + + val sentryExecutor = SentryExecutorService(executor, null) + sentryExecutor.submit {} + + verify(executor).submit(any()) + } + + @Test + fun `SentryExecutorService submit callable returns cancelled future when queue size exceeds limit`() { + val queue = mock>() + whenever(queue.size).thenReturn(272) // Above MAX_QUEUE_SIZE (271) + + val executor = mock { on { getQueue() } doReturn queue } + + val options = mock() + val logger = mock() + whenever(options.logger).thenReturn(logger) + + val sentryExecutor = SentryExecutorService(executor, options) + val future = sentryExecutor.submit(Callable { "result" }) + + assertTrue(future.isCancelled) + assertTrue(future.isDone) + assertFailsWith { future.get() } + verify(executor, never()).submit(any>()) + verify(logger).log(any(), any()) + } + + @Test + fun `SentryExecutorService submit callable accepts when queue size is within limit`() { + val queue = mock>() + whenever(queue.size).thenReturn(270) // Below MAX_QUEUE_SIZE (271) + + val executor = mock { on { getQueue() } doReturn queue } + + val sentryExecutor = SentryExecutorService(executor, null) + sentryExecutor.submit(Callable { "result" }) + + verify(executor).submit(any>()) + } + + @Test + fun `SentryExecutorService schedule accepts when queue size is within limit`() { + val queue = mock>() + whenever(queue.size).thenReturn(270) // Below MAX_QUEUE_SIZE (271) + + val executor = mock { on { getQueue() } doReturn queue } + + val sentryExecutor = SentryExecutorService(executor, null) + sentryExecutor.schedule({}, 1000L) + + verify(executor).schedule(any(), any(), any()) + } @Test fun `SentryExecutorService prewarm schedules dummy tasks and clears queue`() { @@ -122,13 +207,11 @@ class SentryExecutorServiceTest { } @Test - fun `SentryExecutorService runs any number of job`() { - val sentryExecutor = SentryExecutorService() - var called = false - // Post 1k jobs after 1 day, to test new jobs are accepted + fun `SentryExecutorService schedules any number of job`() { + val executor = ScheduledThreadPoolExecutor(1) + val sentryExecutor = SentryExecutorService(executor, null) + // Post 1k jobs after 1 day, to test they are all accepted repeat(1000) { sentryExecutor.schedule({}, TimeUnit.DAYS.toMillis(1)) } - sentryExecutor.submit { called = true } - await.until { called } - assertTrue(called) + assertEquals(1000, executor.queue.size) } } From 7dbd75b7d63e0d4260512ea5e962863ed7a45483 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 29 Oct 2025 16:31:07 +0100 Subject: [PATCH 06/10] updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16156cee4a1..2afdedb600b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixes -- Removed SentryExecutorService limit ([#4846](https://github.com/getsentry/sentry-java/pull/4846)) +- Removed SentryExecutorService limit for delayed scheduled tasks ([#4846](https://github.com/getsentry/sentry-java/pull/4846)) - [ANR] Removed AndroidTransactionProfiler lock ([#4817](https://github.com/getsentry/sentry-java/pull/4817)) - Fix wrong .super() call in SentryTimberTree ([#4844](https://github.com/getsentry/sentry-java/pull/4844)) From c3dc300b812afaed2a74bd7bbe370064e83578a6 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 29 Oct 2025 15:34:10 +0000 Subject: [PATCH 07/10] Format code --- .../src/main/java/io/sentry/SentryExecutorService.java | 9 +++++---- .../test/java/io/sentry/SentryExecutorServiceTest.kt | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryExecutorService.java b/sentry/src/main/java/io/sentry/SentryExecutorService.java index b96a0e27db3..adb50b232e9 100644 --- a/sentry/src/main/java/io/sentry/SentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/SentryExecutorService.java @@ -70,8 +70,8 @@ private boolean isQueueAvailable() { } if (options != null) { options - .getLogger() - .log(SentryLevel.WARNING, "Task " + runnable + " rejected from " + executorService); + .getLogger() + .log(SentryLevel.WARNING, "Task " + runnable + " rejected from " + executorService); } return new CancelledFuture<>(); } @@ -84,8 +84,8 @@ private boolean isQueueAvailable() { } if (options != null) { options - .getLogger() - .log(SentryLevel.WARNING, "Task " + callable + " rejected from " + executorService); + .getLogger() + .log(SentryLevel.WARNING, "Task " + callable + " rejected from " + executorService); } return new CancelledFuture<>(); } @@ -159,6 +159,7 @@ private static final class SentryExecutorServiceThreadFactory implements ThreadF return ret; } } + private static final class CancelledFuture implements Future { @Override public boolean cancel(final boolean mayInterruptIfRunning) { diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index c56703ac627..87dc530f8d3 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -1,12 +1,16 @@ package io.sentry import io.sentry.test.getProperty +import java.util.concurrent.BlockingQueue +import java.util.concurrent.Callable +import java.util.concurrent.CancellationException import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue import org.awaitility.kotlin.await @@ -14,13 +18,8 @@ import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.never -import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.util.concurrent.BlockingQueue -import java.util.concurrent.Callable -import java.util.concurrent.CancellationException -import kotlin.test.assertFailsWith class SentryExecutorServiceTest { @Test @@ -110,6 +109,7 @@ class SentryExecutorServiceTest { whenever(executor.isShutdown).thenReturn(false) assertFalse(sentryExecutor.isClosed) } + @Test fun `SentryExecutorService submit runnable returns cancelled future when queue size exceeds limit`() { val queue = mock>() From a7be60dcd80beec6a2a8cc57c10e806e9ed844d6 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 29 Oct 2025 16:34:44 +0100 Subject: [PATCH 08/10] added purge test --- .../java/io/sentry/SentryExecutorServiceTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index c56703ac627..96a216ae45d 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -214,4 +214,18 @@ class SentryExecutorServiceTest { repeat(1000) { sentryExecutor.schedule({}, TimeUnit.DAYS.toMillis(1)) } assertEquals(1000, executor.queue.size) } + + @Test + fun `SentryExecutorService purges cancelled jobs when limit is reached`() { + val executor = ScheduledThreadPoolExecutor(1) + val sentryExecutor = SentryExecutorService(executor, null) + // Post 1k jobs after 1 day, to test they are all accepted + repeat(1000) { + val future = sentryExecutor.schedule({}, TimeUnit.DAYS.toMillis(1)) + future.cancel(true) + } + assertEquals(1000, executor.queue.size) + sentryExecutor.submit { } + assertEquals(1, executor.queue.size) + } } From f0067e778100063086d040d572600106ab100336 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 29 Oct 2025 15:38:28 +0000 Subject: [PATCH 09/10] Format code --- sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index f7235b8f554..a736042f7a8 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -225,7 +225,7 @@ class SentryExecutorServiceTest { future.cancel(true) } assertEquals(1000, executor.queue.size) - sentryExecutor.submit { } + sentryExecutor.submit {} assertEquals(1, executor.queue.size) } } From 636113d03e555891ebcedf84ef9964b226634ed5 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 29 Oct 2025 16:50:24 +0100 Subject: [PATCH 10/10] added purge test --- sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index a736042f7a8..00ebe2fae4b 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -225,6 +225,7 @@ class SentryExecutorServiceTest { future.cancel(true) } assertEquals(1000, executor.queue.size) + // Submit should purge cancelled scheduled jobs sentryExecutor.submit {} assertEquals(1, executor.queue.size) }