ref(transport): Measure rate-limit backoff on a monotonic clock (JAVA-574) - #6030
Draft
runningcode wants to merge 2 commits into
Draft
ref(transport): Measure rate-limit backoff on a monotonic clock (JAVA-574)#6030runningcode wants to merge 2 commits into
runningcode wants to merge 2 commits into
Conversation
…-574) Retry-after limits were java.util.Date values derived from System.currentTimeMillis(). A wall clock is the wrong instrument for a backoff window: it steps when the device syncs time, so an NTP correction could lift a 60 second rate limit early or extend it by however far the clock jumped. The limits now live on the elapsed-real-time clock, which counts forward at a steady rate and keeps counting while the device sleeps, which is what a server-dictated wait means. Storing Deadline rather than a timestamp also removes the duplicated parameter on applyRetryAfterOnlyIfLonger, which took both an absolute deadline and the delay needed to reach it, and lets three JdkObsolete and JavaUtilDate suppressions go with the Dates. RateLimiter also took the whole SentryOptions while reading exactly three methods from it. It now depends on RateLimiterConfig, declared next to its consumer, so what a rate limiter touches is three lines to read rather than three hundred. SentryOptions implements it with no new methods, so every existing caller compiles unchanged. Both existing constructors stay, so the .api diff is additions only. The ICurrentDateProvider one is deprecated and adapts the injected provider rather than ignoring it, since a custom ITransportFactory may be passing one. One boundary moves by a nanosecond: a limit used to be active while `now <= deadline` and is now active while `now < deadline`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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:
|
1 similar comment
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:
|
📲 Install BuildsAndroid
|
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📜 Description
RateLimiterstored retry-after limits asjava.util.Datevalues built fromSystem.currentTimeMillis(). They are nowDeadlines on the elapsed-real-time clock.Stacked on #6029, which is stacked on #6028.
💡 Motivation and Context
A wall clock is the wrong instrument for a backoff window. It steps when the device syncs time, so an NTP correction could lift a 60-second rate limit early, or extend it by however far the clock jumped. The elapsed-real-time clock counts forward at a steady rate and keeps counting while the device is suspended — which is what a wait the server asked for means.
These limits are in-memory only. They live in a
ConcurrentHashMap, are never serialized and never compared across processes, so nothing about this reaches a payload and it is safe to land before v9.Two things fall out of storing a
Deadlineinstead of a timestamp:applyRetryAfterOnlyIfLongertook the same information twice — an absolute deadline and the delay needed to reach it — because the scheduled "limit lifted" notification needed the delay. It now asks the deadline:deadline.remaining(MILLISECONDS).JdkObsolete/JavaUtilDatesuppressions go with theDates.RateLimiteralso took the wholeSentryOptionswhile reading exactly three methods from it. It now depends onRateLimiterConfig, declared next to its consumer, so what a rate limiter touches is three lines to read rather than three hundred.SentryOptionsimplements it with no new methods, so every existing caller compiles unchanged.RateLimiterConfigis@ApiStatus.Internal, which is what makes adding it as a supertype of a class with four public subclasses safe — if it ever becomes public API, this is worth revisiting.💚 How did you test it?
One boundary moves by a nanosecond. A limit used to be active while
now <= deadline(!currentDate.after(limit)) and is now active whilenow < deadline(!deadline.hasPassed()). A new test pins both sides: active at 999 ms into a one-second limit, not active at exactly 1000 ms.The existing tests dropped their positionally-stubbed
ICurrentDateProviderforTestTicker.Deadlinereads its clock lazily and a different number of times than the old code, sothenReturn(0, 0, 1001)sequences would break on any change in call count. Four of them were also asserting against a third stub value that was never consumed —When both retry headers are not present, default delay is setnever actually advanced past anything — and now genuinely exercise expiry.sentrymodule: 3519 tests, 0 failures.sentry-apache-http-client-5,sentry-spring-jakartaandsentry-android-corecompile unchanged..apiis additions only:Both existing constructors stay.
RateLimiter(ICurrentDateProvider, SentryOptions)is deprecated and adapts the injected provider rather than ignoring it — a customITransportFactorymay be passing one, and silently dropping it would pass CI while breaking someone's suite. Note thatICurrentDateProvideris itself along ()functional interface, so the adapting lambda needs an explicit cast or it resolves back to the deprecated constructor; there is a comment at the call site saying so.📝 Checklist
sendDefaultPIIis enabled.