Skip to content

perf: Schedule rate-limit notifications on shared executor (JAVA-653) - #5814

Open
runningcode wants to merge 2 commits into
mainfrom
no/perf/reuse-timer-executor
Open

perf: Schedule rate-limit notifications on shared executor (JAVA-653)#5814
runningcode wants to merge 2 commits into
mainfrom
no/perf/reuse-timer-executor

Conversation

@runningcode

@runningcode runningcode commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

📜 Description

RateLimiter created a java.util.Timer (a dedicated thread) whose thread stayed alive for the rest of the process once the SDK got rate limited. The "rate limit lifted" observer notification now runs on the shared timer executor (SentryOptions#getTimerExecutorService), already used for transaction timeouts, whose single worker thread is reused and self-terminates when idle.

  • The notification is scheduled with the delay until the limit expires; to avoid an extra getCurrentTimeMillis() call the already-computed retryAfterMillis is passed through as the delay.
  • Pending notification futures are tracked (completed ones pruned on each schedule) and cancelled on close(), preserving the old Timer#cancel() semantics.

💡 Motivation and Context

Part of reducing the number of threads created by the SDK: JAVA-653.

Once an app got rate limited, this timer thread lived forever. The shared timer executor's worker is reused and idles out.

💚 How did you test it?

Existing RateLimiterTest, adapted from the Timer-mock verification to the executor/future model, plus AsyncHttpTransportTest.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

Related PRs in this effort: LifecycleWatcher (#5819), performance collector (#5816), HostnameCache (#5817), batch processors (#5818).

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Jul 22, 2026

Copy link
Copy Markdown

JAVA-653

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry/src/main/java/io/sentry/transport/RateLimiter.java

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry/src/main/java/io/sentry/transport/RateLimiter.java

@sentry

sentry Bot commented Jul 22, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.50.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 470.48 ms 557.22 ms 86.75 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
05aa61d 326.06 ms 385.46 ms 59.40 ms
bb0ff41 315.84 ms 350.76 ms 34.92 ms
806307f 357.85 ms 424.64 ms 66.79 ms
d501a7e 307.33 ms 341.94 ms 34.61 ms
0ee65e9 321.06 ms 361.24 ms 40.18 ms
ed33deb 334.19 ms 362.30 ms 28.11 ms
9fbb112 401.87 ms 515.87 ms 114.00 ms
b8bd880 314.56 ms 336.50 ms 21.94 ms
5b1a06b 315.40 ms 353.33 ms 37.94 ms
6edfca2 316.43 ms 398.90 ms 82.46 ms

App size

Revision Plain With Sentry Diff
05aa61d 0 B 0 B 0 B
bb0ff41 0 B 0 B 0 B
806307f 1.58 MiB 2.10 MiB 533.42 KiB
d501a7e 0 B 0 B 0 B
0ee65e9 0 B 0 B 0 B
ed33deb 1.58 MiB 2.13 MiB 559.52 KiB
9fbb112 1.58 MiB 2.11 MiB 539.18 KiB
b8bd880 1.58 MiB 2.29 MiB 722.92 KiB
5b1a06b 0 B 0 B 0 B
6edfca2 1.58 MiB 2.13 MiB 559.07 KiB

Previous results on branch: no/perf/reuse-timer-executor

Startup times

Revision Plain With Sentry Diff
91c5190 392.32 ms 460.78 ms 68.46 ms
50f90d3 319.27 ms 373.26 ms 53.99 ms
a70f7a7 324.94 ms 379.86 ms 54.92 ms

App size

Revision Plain With Sentry Diff
91c5190 0 B 0 B 0 B
50f90d3 0 B 0 B 0 B
a70f7a7 0 B 0 B 0 B

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry/src/main/java/io/sentry/transport/RateLimiter.java

@runningcode runningcode changed the title perf: Replace Timer threads with shared timer executor (JAVA-653) perf: Schedule rate-limit notifications on shared executor (JAVA-653) Jul 22, 2026
@runningcode
runningcode force-pushed the no/perf/reuse-timer-executor branch from 00ed5c3 to 4f33cf7 Compare July 22, 2026 15:52
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry/src/main/java/io/sentry/transport/RateLimiter.java

@runningcode
runningcode force-pushed the no/perf/reuse-timer-executor branch from 4f33cf7 to b040430 Compare July 23, 2026 09:48
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry/src/main/java/io/sentry/transport/RateLimiter.java

@runningcode
runningcode marked this pull request as ready for review July 23, 2026 09:53
Comment thread sentry/src/main/java/io/sentry/transport/RateLimiter.java Outdated
fun getSUT(): RateLimiter {
val options = SentryOptions().apply { setLogger(NoOpLogger.getInstance()) }
// a real executor so scheduled rate-limit-lifted notifications actually run
options.setTimerExecutorService(SentryExecutorService(options))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to close it in teardown perhaps, so it doesn't leak across tests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

timer = new Timer(true);
// notify observers again once the rate limit is lifted, using the shared timer executor
// instead of a dedicated Timer thread
try (final @NotNull ISentryLifecycleToken ignored = notifyFuturesLock.acquire()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: unsure if we need to protect this with checking for instanceof NoOpSentryExecutorService to avoid allocating FutureTasks for nothing (isDone also returns false when no-op, so pruning wouldn't do anything), but we're not really doing that anywhere else I believe, so I'm fine with keeping it as-is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it is a good point but also quite an edge case. Since we aren't doing it anywhere else, I don't think we should do it here.

@romtsn romtsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple of nits, but LGTM 🎉

@runningcode
runningcode force-pushed the no/perf/reuse-timer-executor branch from b040430 to b202421 Compare July 28, 2026 15:31
runningcode and others added 2 commits July 28, 2026 17:35
RateLimiter created a java.util.Timer whose thread stayed alive forever
once the SDK got rate limited. Schedule the "rate limit lifted"
observer notification on the shared timer executor instead, whose
single worker thread is reused across all timeouts and self-terminates
when idle. Pending notifications are cancelled on close().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@runningcode
runningcode force-pushed the no/perf/reuse-timer-executor branch from b202421 to 472e0fb Compare July 28, 2026 15:37
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry/src/main/java/io/sentry/transport/RateLimiter.java

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 472e0fb. Configure here.

notifyObserversFutures.add(
options
.getTimerExecutorService()
.schedule(this::notifyRateLimitObservers, delayMillis));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shutdown stalls on pending rate-limit tasks

Medium Severity

Rate-limit lift notifications are now scheduled on the shared timer executor, often with long delays. Scopes.close() shuts that executor down before RateLimiter.close() can cancel those futures, so awaitTermination waits out the full shutdown timeout whenever a limit is still pending. The old dedicated Timer was cancelled independently and did not block SDK shutdown.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 472e0fb. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants