1414import io .sentry .hints .DiskFlushNotification ;
1515import io .sentry .hints .Retryable ;
1616import io .sentry .hints .SubmissionResult ;
17+ import io .sentry .time .Deadline ;
18+ import io .sentry .time .ElapsedRealtimeClock ;
1719import io .sentry .util .AutoClosableReentrantLock ;
1820import io .sentry .util .HintUtils ;
1921import io .sentry .util .StringUtils ;
2224import java .util .ArrayList ;
2325import java .util .Arrays ;
2426import java .util .Collections ;
25- import java .util .Date ;
2627import java .util .Iterator ;
2728import java .util .List ;
2829import java .util .Map ;
2930import java .util .concurrent .ConcurrentHashMap ;
3031import java .util .concurrent .CopyOnWriteArrayList ;
3132import java .util .concurrent .Future ;
3233import java .util .concurrent .RejectedExecutionException ;
34+ import java .util .concurrent .TimeUnit ;
3335import org .jetbrains .annotations .NotNull ;
3436import org .jetbrains .annotations .Nullable ;
3537
3638/** Controls retry limits on different category types sent to Sentry. */
3739public final class RateLimiter implements Closeable {
3840
39- private static final int HTTP_RETRY_AFTER_DEFAULT_DELAY_MILLIS = 60000 ;
41+ private static final long HTTP_RETRY_AFTER_DEFAULT_DELAY_MILLIS = 60_000 ;
4042
41- private final @ NotNull ICurrentDateProvider currentDateProvider ;
42- private final @ NotNull SentryOptions options ;
43- private final @ NotNull Map <DataCategory , @ NotNull Date > sentryRetryAfterLimit =
43+ private final @ NotNull ElapsedRealtimeClock clock ;
44+ private final @ NotNull RateLimiterConfig config ;
45+ private final @ NotNull Map <DataCategory , @ NotNull Deadline > sentryRetryAfterLimit =
4446 new ConcurrentHashMap <>();
4547 private final @ NotNull List <IRateLimitObserver > rateLimitObservers = new CopyOnWriteArrayList <>();
4648 private final @ NotNull List <Future <?>> notifyObserversFutures = new ArrayList <>();
4749 private final @ NotNull AutoClosableReentrantLock notifyFuturesLock =
4850 new AutoClosableReentrantLock ();
4951
5052 public RateLimiter (
51- final @ NotNull ICurrentDateProvider currentDateProvider ,
52- final @ NotNull SentryOptions options ) {
53- this .currentDateProvider = currentDateProvider ;
54- this .options = options ;
53+ final @ NotNull ElapsedRealtimeClock clock , final @ NotNull RateLimiterConfig config ) {
54+ this .clock = clock ;
55+ this .config = config ;
5556 }
5657
5758 public RateLimiter (final @ NotNull SentryOptions options ) {
58- this (CurrentDateProvider .getInstance (), options );
59+ this (options .getElapsedRealtimeClock (), options );
60+ }
61+
62+ /**
63+ * @deprecated backoff is measured on {@link SentryOptions#getElapsedRealtimeClock()}; use {@link
64+ * #RateLimiter(SentryOptions)}. An injected wall clock is adapted so that an existing custom
65+ * transport keeps the behaviour it has today, but it is not monotonic and can step.
66+ */
67+ @ Deprecated
68+ public RateLimiter (
69+ final @ NotNull ICurrentDateProvider currentDateProvider ,
70+ final @ NotNull SentryOptions options ) {
71+ // ICurrentDateProvider is itself a `long ()` interface, so an unadorned lambda matches this
72+ // constructor as readily as the intended one. The cast is what makes the call non-recursive.
73+ this (
74+ (ElapsedRealtimeClock )
75+ () -> TimeUnit .MILLISECONDS .toNanos (currentDateProvider .getCurrentTimeMillis ()),
76+ options );
5977 }
6078
6179 public @ Nullable SentryEnvelope filter (
@@ -70,14 +88,14 @@ public RateLimiter(final @NotNull SentryOptions options) {
7088 }
7189
7290 dropItems .add (item );
73- options
91+ config
7492 .getClientReportRecorder ()
7593 .recordLostEnvelopeItem (DiscardReason .RATELIMIT_BACKOFF , item );
7694 }
7795 }
7896
7997 if (dropItems != null ) {
80- options
98+ config
8199 .getLogger ()
82100 .log (
83101 SentryLevel .WARNING ,
@@ -94,7 +112,7 @@ public RateLimiter(final @NotNull SentryOptions options) {
94112
95113 // no reason to continue
96114 if (toSend .isEmpty ()) {
97- options
115+ config
98116 .getLogger ()
99117 .log (SentryLevel .WARNING , "Envelope discarded due all items rate limited." );
100118
@@ -107,16 +125,11 @@ public RateLimiter(final @NotNull SentryOptions options) {
107125 return envelope ;
108126 }
109127
110- @ SuppressWarnings ({"JdkObsolete" , "JavaUtilDate" })
111128 public boolean isActiveForCategory (final @ NotNull DataCategory dataCategory ) {
112- final Date currentDate = new Date (currentDateProvider .getCurrentTimeMillis ());
113-
114129 // check all categories
115- final Date dateAllCategories = sentryRetryAfterLimit .get (DataCategory .All );
116- if (dateAllCategories != null ) {
117- if (!currentDate .after (dateAllCategories )) {
118- return true ;
119- }
130+ final @ Nullable Deadline allCategories = sentryRetryAfterLimit .get (DataCategory .All );
131+ if (allCategories != null && !allCategories .hasPassed ()) {
132+ return true ;
120133 }
121134
122135 // Unknown should not be rate limited
@@ -125,24 +138,15 @@ public boolean isActiveForCategory(final @NotNull DataCategory dataCategory) {
125138 }
126139
127140 // check for specific dataCategory
128- final Date dateCategory = sentryRetryAfterLimit .get (dataCategory );
129- if (dateCategory != null ) {
130- return !currentDate .after (dateCategory );
131- }
132-
133- return false ;
141+ final @ Nullable Deadline categoryLimit = sentryRetryAfterLimit .get (dataCategory );
142+ return categoryLimit != null && !categoryLimit .hasPassed ();
134143 }
135144
136145 @ SuppressWarnings ({"JdkObsolete" , "JavaUtilDate" })
137146 public boolean isAnyRateLimitActive () {
138- final Date currentDate = new Date (currentDateProvider .getCurrentTimeMillis ());
139-
140- for (DataCategory dataCategory : sentryRetryAfterLimit .keySet ()) {
141- final Date dateCategory = sentryRetryAfterLimit .get (dataCategory );
142- if (dateCategory != null ) {
143- if (!currentDate .after (dateCategory )) {
144- return true ;
145- }
147+ for (final @ NotNull Deadline limit : sentryRetryAfterLimit .values ()) {
148+ if (!limit .hasPassed ()) {
149+ return true ;
146150 }
147151 }
148152
@@ -163,7 +167,7 @@ private void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean r
163167 DiskFlushNotification .class ,
164168 (diskFlushNotification ) -> {
165169 diskFlushNotification .markFlushed ();
166- options .getLogger ().log (SentryLevel .DEBUG , "Disk flush envelope fired due to rate limit" );
170+ config .getLogger ().log (SentryLevel .DEBUG , "Disk flush envelope fired due to rate limit" );
167171 });
168172 }
169173
@@ -251,15 +255,11 @@ public void updateRetryAfterLimits(
251255
252256 if (rateLimit .length > 0 ) {
253257 final String retryAfter = rateLimit [0 ];
254- long retryAfterMillis = parseRetryAfterOrDefault (retryAfter );
258+ final @ NotNull Deadline deadline = parseRetryAfterOrDefault (retryAfter );
255259
256260 if (rateLimit .length > 1 ) {
257261 final String allCategories = rateLimit [1 ];
258262
259- // we dont care if Date is UTC as we just add the relative seconds
260- final Date date =
261- new Date (currentDateProvider .getCurrentTimeMillis () + retryAfterMillis );
262-
263263 if (allCategories != null && !allCategories .isEmpty ()) {
264264 final String [] categories = allCategories .split (";" , -1 );
265265
@@ -270,48 +270,43 @@ public void updateRetryAfterLimits(
270270 if (catItemCapitalized != null ) {
271271 dataCategory = DataCategory .valueOf (catItemCapitalized );
272272 } else {
273- options .getLogger ().log (ERROR , "Couldn't capitalize: %s" , catItem );
273+ config .getLogger ().log (ERROR , "Couldn't capitalize: %s" , catItem );
274274 }
275275 } catch (IllegalArgumentException e ) {
276- options .getLogger ().log (INFO , e , "Unknown category: %s" , catItem );
276+ config .getLogger ().log (INFO , e , "Unknown category: %s" , catItem );
277277 }
278278 // we dont apply rate limiting for unknown categories
279279 if (DataCategory .Unknown .equals (dataCategory )) {
280280 continue ;
281281 }
282282
283- applyRetryAfterOnlyIfLonger (dataCategory , date , retryAfterMillis );
283+ applyRetryAfterOnlyIfLonger (dataCategory , deadline );
284284 }
285285 } else {
286286 // if categories are empty, we should apply to "all" categories.
287- applyRetryAfterOnlyIfLonger (DataCategory .All , date , retryAfterMillis );
287+ applyRetryAfterOnlyIfLonger (DataCategory .All , deadline );
288288 }
289289 }
290290 }
291291 }
292292 } else if (errorCode == 429 ) {
293- final long retryAfterMillis = parseRetryAfterOrDefault (retryAfterHeader );
294- // we dont care if Date is UTC as we just add the relative seconds
295- final Date date = new Date (currentDateProvider .getCurrentTimeMillis () + retryAfterMillis );
296- applyRetryAfterOnlyIfLonger (DataCategory .All , date , retryAfterMillis );
293+ applyRetryAfterOnlyIfLonger (DataCategory .All , parseRetryAfterOrDefault (retryAfterHeader ));
297294 }
298295 }
299296
300297 /**
301- * apply new timestamp for rate limiting only if its longer than the previous one
298+ * apply the new deadline for rate limiting only if it is longer than the previous one
302299 *
303300 * @param dataCategory the DataCategory
304- * @param date the Date to be applied
305- * @param delayMillis the millis until the rate limit is lifted
301+ * @param deadline when the rate limit is lifted
306302 */
307- @ SuppressWarnings ({"JdkObsolete" , "JavaUtilDate" })
308303 private void applyRetryAfterOnlyIfLonger (
309- final @ NotNull DataCategory dataCategory , final @ NotNull Date date , final long delayMillis ) {
310- final Date oldDate = sentryRetryAfterLimit .get (dataCategory );
304+ final @ NotNull DataCategory dataCategory , final @ NotNull Deadline deadline ) {
305+ final @ Nullable Deadline oldLimit = sentryRetryAfterLimit .get (dataCategory );
311306
312- // only overwrite its previous date if the limit is even longer
313- if (oldDate == null || date . after ( oldDate )) {
314- sentryRetryAfterLimit .put (dataCategory , date );
307+ // only overwrite the previous deadline if the limit is even longer
308+ if (oldLimit == null || deadline . isAfter ( oldLimit )) {
309+ sentryRetryAfterLimit .put (dataCategory , deadline );
315310
316311 notifyRateLimitObservers ();
317312
@@ -326,11 +321,12 @@ private void applyRetryAfterOnlyIfLonger(
326321 }
327322 try {
328323 notifyObserversFutures .add (
329- options
324+ config
330325 .getTimerExecutorService ()
331- .schedule (this ::notifyRateLimitObservers , delayMillis ));
326+ .schedule (
327+ this ::notifyRateLimitObservers , deadline .remaining (TimeUnit .MILLISECONDS )));
332328 } catch (RejectedExecutionException e ) {
333- options
329+ config
334330 .getLogger ()
335331 .log (SentryLevel .WARNING , "Failed to schedule rate limit lifted notification." , e );
336332 }
@@ -344,7 +340,7 @@ private void applyRetryAfterOnlyIfLonger(
344340 * @param retryAfterHeader the header
345341 * @return the millis in seconds or the default seconds value
346342 */
347- private long parseRetryAfterOrDefault (final @ Nullable String retryAfterHeader ) {
343+ private @ NotNull Deadline parseRetryAfterOrDefault (final @ Nullable String retryAfterHeader ) {
348344 long retryAfterMillis = HTTP_RETRY_AFTER_DEFAULT_DELAY_MILLIS ;
349345 if (retryAfterHeader != null ) {
350346 try {
@@ -354,7 +350,7 @@ private long parseRetryAfterOrDefault(final @Nullable String retryAfterHeader) {
354350 // let's use the default then
355351 }
356352 }
357- return retryAfterMillis ;
353+ return Deadline . after ( clock , retryAfterMillis , TimeUnit . MILLISECONDS ) ;
358354 }
359355
360356 private void notifyRateLimitObservers () {
0 commit comments