perf: Schedule rate-limit notifications on shared executor (JAVA-653)#5814
perf: Schedule rate-limit notifications on shared executor (JAVA-653)#5814runningcode wants to merge 2 commits into
Conversation
🚨 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:
|
1 similar comment
🚨 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:
|
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| eb95ded | 317.51 ms | 369.08 ms | 51.57 ms |
| 2124a46 | 319.19 ms | 415.04 ms | 95.85 ms |
| c3ee041 | 310.64 ms | 361.90 ms | 51.26 ms |
| dcc6bbf | 382.58 ms | 462.13 ms | 79.54 ms |
| 7a19fee | 315.46 ms | 368.62 ms | 53.16 ms |
| 9054d65 | 330.94 ms | 403.24 ms | 72.30 ms |
| d500866 | 326.13 ms | 378.70 ms | 52.58 ms |
| ad8da22 | 365.86 ms | 427.00 ms | 61.14 ms |
| ed33deb | 337.52 ms | 484.06 ms | 146.54 ms |
| d15471f | 310.26 ms | 377.04 ms | 66.78 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| eb95ded | 0 B | 0 B | 0 B |
| 2124a46 | 1.58 MiB | 2.12 MiB | 551.51 KiB |
| c3ee041 | 0 B | 0 B | 0 B |
| dcc6bbf | 1.58 MiB | 2.12 MiB | 553.10 KiB |
| 7a19fee | 0 B | 0 B | 0 B |
| 9054d65 | 1.58 MiB | 2.29 MiB | 723.38 KiB |
| d500866 | 0 B | 0 B | 0 B |
| ad8da22 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| ed33deb | 1.58 MiB | 2.13 MiB | 559.52 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
81bc25c to
00ed5c3
Compare
🚨 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:
|
00ed5c3 to
4f33cf7
Compare
🚨 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:
|
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>
4f33cf7 to
b040430
Compare
🚨 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:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b040430. Configure here.
| notifyObserversFutures.add( | ||
| options | ||
| .getTimerExecutorService() | ||
| .schedule(() -> notifyRateLimitObservers(), delayMillis)); |
There was a problem hiding this comment.
Relative delay desyncs lift notification
Medium Severity
The old Timer.schedule(task, date) fired on an absolute wall-clock time matching the stored limit date. The new path schedules a relative delayMillis on the shared executor (monotonic/nanoTime-based), while isActiveForCategory still decides activity from wall-clock via currentDateProvider. If the clock moves backward during the wait, the task can run while the limit still looks active, and because nothing reschedules, observers such as session replay may never get a later “lifted” callback and can stay paused.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b040430. Configure here.
| 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)) |
There was a problem hiding this comment.
do we need to close it in teardown perhaps, so it doesn't leak across tests?
| 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()) { |
There was a problem hiding this comment.
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.


📜 Description
RateLimitercreated ajava.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.getCurrentTimeMillis()call the already-computedretryAfterMillisis passed through as the delay.close(), preserving the oldTimer#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, plusAsyncHttpTransportTest.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Related PRs in this effort: LifecycleWatcher (#5819), performance collector (#5816), HostnameCache (#5817), batch processors (#5818).
🤖 Generated with Claude Code