Skip to content

ref(transport): Measure rate-limit backoff on a monotonic clock (JAVA-574) - #6030

Open
runningcode wants to merge 6 commits into
mainfrom
no/java-574-ratelimiter-deadline
Open

ref(transport): Measure rate-limit backoff on a monotonic clock (JAVA-574)#6030
runningcode wants to merge 6 commits into
mainfrom
no/java-574-ratelimiter-deadline

Conversation

@runningcode

@runningcode runningcode commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

📜 Description

I wrote all these lines by hand in case you care to read them. :)

Wall clocks are bad

So this PR started as a way to fix the rate limiter using a wall clock. That's clearly bad™️ because a wall clock can drift which means if the clock jumps backward or forward suddenly we could send a ton of requests when the ratelimiter expires or extend the rate limiter for far longer.

I think the code is a LOT easier to read and speaks for itself.

Static constructors are good

With that change, we need to change the object's constructor because we don't pass in a ICurrentDateProvider anymore. So the old constructors are kept (and deprecated) only to maintain backward compatibility with any possible existing customers. We should remove the constructors in v9. We also use a static constructor because Josh Bloch is my man. (See Effective Java item 1 "Consider static factory methods instead of constructors.")

SentryOptions is a god object

Since we are changing the constructor, I decided it would be a good idea to NOT pass in the SentryOptions god object and instead hide it behind an interface that only gives us what we need from that object. There's no functionality change but it makes it easier to test and confines the scope our SentryOptions usage to make future refactors easier.

Another route instead of the RateLimiterConfig

Another option would be to only pass in the objects from SentryOptions that we need to the RateLimiter's constructor. I really wanted to do this. The issue is that all the fields are publicly mutable so we can't pass them in to the constructor because they can change. 😢 Mutable state is very fun /s

So TL;DR on what's changed:

  1. Use monotonic clock to determine rate limit
  2. Use static constructor
  3. RateLimiterConfig interface instead of SentryOptions.

💡 Motivation and Context

A wall clock is bad for RateLimiting since it can jump backwards.

  • resolves: JAVA-574

💚 How did you test it?

Tests pass.

📝 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.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

@linear-code

linear-code Bot commented Aug 31, 2026

Copy link
Copy Markdown

JAVA-574

@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 Aug 31, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Sep 2, 2026

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

@github-actions

github-actions Bot commented Sep 2, 2026

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/java-717-connection-cache-deadline branch from 2b35532 to 76fef57 Compare September 3, 2026 15:11
@runningcode
runningcode force-pushed the no/java-574-ratelimiter-deadline branch from 781c864 to 8bb8665 Compare September 3, 2026 15:11
@github-actions

github-actions Bot commented Sep 3, 2026

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/java-717-connection-cache-deadline branch from 76fef57 to 81e7109 Compare September 3, 2026 15:41
@runningcode
runningcode force-pushed the no/java-574-ratelimiter-deadline branch from 8bb8665 to fc85c12 Compare September 3, 2026 15:41
@github-actions

github-actions Bot commented Sep 3, 2026

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/java-717-connection-cache-deadline branch from 81e7109 to 47bf886 Compare September 4, 2026 08:30
@runningcode
runningcode force-pushed the no/java-574-ratelimiter-deadline branch from fc85c12 to be5eaeb Compare September 4, 2026 08:33
@github-actions

github-actions Bot commented Sep 4, 2026

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

@github-actions

github-actions Bot commented Sep 8, 2026

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/java-717-connection-cache-deadline branch from 0154972 to 3fb8770 Compare September 8, 2026 15:10
@runningcode
runningcode force-pushed the no/java-574-ratelimiter-deadline branch from da479f2 to 7de7107 Compare September 8, 2026 15:10
@github-actions

github-actions Bot commented Sep 8, 2026

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/java-717-connection-cache-deadline branch 2 times, most recently from 240c770 to 4562a70 Compare September 9, 2026 13:29
Base automatically changed from no/java-717-connection-cache-deadline to main September 9, 2026 13:50
@runningcode
runningcode force-pushed the no/java-574-ratelimiter-deadline branch from 7de7107 to 62c2029 Compare September 9, 2026 13:58
@github-actions

github-actions Bot commented Sep 9, 2026

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 and others added 3 commits September 9, 2026 16:56
…-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 monotonic
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>
The RateLimiterConfig constructor is now private behind
RateLimiter.create, and both SentryOptions constructors are deprecated.
Handing over the whole options object hides which three of its methods a
rate limiter reads, which is the very thing the config interface exists
to name, so both transport factories call the factory method instead.

RateLimiterTest supplies its own RateLimiterConfig rather than a
SentryOptions carrying a @testonly executor setter and a reflective
client-report-recorder injection. A DeferredExecutorService replaces the
real executor, so the lifted-limit notification runs when the test says
so instead of a real second later, leaving the deadline's virtual clock
as the only clock in the test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@runningcode
runningcode force-pushed the no/java-574-ratelimiter-deadline branch from 62c2029 to 258eedb Compare September 9, 2026 14:56
@github-actions

github-actions Bot commented Sep 9, 2026

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 September 9, 2026 15:30
Comment thread sentry/src/main/java/io/sentry/transport/RateLimiter.java
@runningcode runningcode added the sanity-check PR needs a lightweight review for obvious issues label Sep 9, 2026

@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.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 258eedb. Configure here.

Comment thread sentry/src/main/java/io/sentry/transport/RateLimiter.java
A server-supplied `Retry-After: -1`, or a negative delay in
`X-Sentry-Rate-Limits`, reached `Deadline.after` unvalidated and threw
`IllegalArgumentException` out of response handling. Clamp a negative
amount to an already-passed deadline, restoring the behavior the
wall-clock implementation had, and keeping the tick arithmetic away from
wrapping on a large negative value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

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

@0xadam-brown 0xadam-brown 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.

So good! Just one potential blocker; otherwise looks fantastic 💯

A wall clock is bad for RateLimiting since it can jump backwards.

:poetry:

We also use a static constructor because Josh Bloch is my man. (See Effective Java item 1 "Consider static factory methods instead of constructors.")

🐐 🏅

Comment thread sentry/src/main/java/io/sentry/time/Deadline.java
Comment thread sentry/src/main/java/io/sentry/time/Deadline.java
}

/**
* @deprecated use the create static constructor instead

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.

nit: Could add a link to make this foolproof if you want (same below)

/**
* @deprecated use the create static constructor instead
*/
@Deprecated

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.

⚠️ Can we do this given that MonotonicClock is still internal? Some folks might fail compilation on warnings...

(Same below)

return true;
}
final @Nullable Deadline allCategories = sentryRetryAfterLimit.get(DataCategory.All);
if (allCategories != null && !allCategories.hasPassed()) {

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.

(future aside: still thinking hasNotPassed() would be nice at some point 😄... )

import org.jetbrains.annotations.NotNull;

/**
* The configuration {@link RateLimiter} reads. Declared next to its consumer rather than alongside

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: Consider moving a generalized version of the "Declared next..." comment into SentryOptions where we declare its implementation of these interfaces, since that'll be a pattern we want to continue (we could then remove it here).

* <p>Implementations are expected to delegate to live configuration rather than snapshot it, so
* that a logger or executor replaced after {@code Sentry.init} is still picked up.
*/
@ApiStatus.Internal

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.

👍

public interface RateLimiterConfig {

@NotNull
ILogger getLogger();

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.

future aside: maybe we'll want to hoist some of these want-them-everywhere deps into their own interface at some point and let RateLimiterConfig, etc. implement that.

/** Sentry SDK options */
@Open
public class SentryOptions {
public class SentryOptions implements RateLimiterConfig {

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.

The many prices paid for service locators... sigh. (But I like this move given that we have one!)

RateLimiter is public because ITransport.getRateLimiter returns it and a
custom transport has to build one. Deprecating the options constructor
pointed those callers at RateLimiter.create, whose two parameter types
are both @ApiStatus.Internal, so following the deprecation meant reaching
into internal API.

Un-deprecate the options constructor and mark the factory internal
instead, which is what it already was in practice. Both transport
factories call the constructor, dropping the getMonotonicTicker/options
double-pass and, in the apache module, an internal call from another
module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

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

…ted (JAVA-574)

Both transport factories go back to RateLimiter.create, which names the
collaborators a rate limiter reads instead of handing over the whole
options object. The constructor stays as the one path a caller outside
the SDK can take, and now says so: it is only non-deprecated because
create takes the internal MonotonicTicker, and should be deprecated once
that type is public API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

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

@0xadam-brown 0xadam-brown 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.

Thanks for the update 💯 🕐 🚀

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

Labels

sanity-check PR needs a lightweight review for obvious issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants