perf: Schedule rate-limit notifications on shared executor (JAVA-653) by runningcode · Pull Request #5814 · getsentry/sentry-java · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
57 changes: 33 additions & 24 deletions sentry/src/main/java/io/sentry/transport/RateLimiter.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand All @@ -42,8 +43,9 @@ public final class RateLimiter implements Closeable {
private final @NotNull Map<DataCategory, @NotNull Date> sentryRetryAfterLimit =
new ConcurrentHashMap<>();
private final @NotNull List<IRateLimitObserver> rateLimitObservers = new CopyOnWriteArrayList<>();
private @Nullable Timer timer = null;
private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock();
private final @NotNull List<Future<?>> notifyObserversFutures = new ArrayList<>();
private final @NotNull AutoClosableReentrantLock notifyFuturesLock =
new AutoClosableReentrantLock();

public RateLimiter(
final @NotNull ICurrentDateProvider currentDateProvider,
Expand Down Expand Up @@ -278,11 +280,11 @@ public void updateRetryAfterLimits(
continue;
}

applyRetryAfterOnlyIfLonger(dataCategory, date);
applyRetryAfterOnlyIfLonger(dataCategory, date, retryAfterMillis);
}
} else {
// if categories are empty, we should apply to "all" categories.
applyRetryAfterOnlyIfLonger(DataCategory.All, date);
applyRetryAfterOnlyIfLonger(DataCategory.All, date, retryAfterMillis);
}
}
}
Expand All @@ -291,7 +293,7 @@ public void updateRetryAfterLimits(
final long retryAfterMillis = parseRetryAfterOrDefault(retryAfterHeader);
// we dont care if Date is UTC as we just add the relative seconds
final Date date = new Date(currentDateProvider.getCurrentTimeMillis() + retryAfterMillis);
applyRetryAfterOnlyIfLonger(DataCategory.All, date);
applyRetryAfterOnlyIfLonger(DataCategory.All, date, retryAfterMillis);
}
}

Expand All @@ -300,10 +302,11 @@ public void updateRetryAfterLimits(
*
* @param dataCategory the DataCategory
* @param date the Date to be applied
* @param delayMillis the millis until the rate limit is lifted
*/
@SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
private void applyRetryAfterOnlyIfLonger(
final @NotNull DataCategory dataCategory, final @NotNull Date date) {
final @NotNull DataCategory dataCategory, final @NotNull Date date, final long delayMillis) {
final Date oldDate = sentryRetryAfterLimit.get(dataCategory);

// only overwrite its previous date if the limit is even longer
Expand All @@ -312,19 +315,25 @@ private void applyRetryAfterOnlyIfLonger(

notifyRateLimitObservers();

try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) {
if (timer == null) {
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()) {
Comment thread
runningcode marked this conversation as resolved.
final @NotNull Iterator<Future<?>> iterator = notifyObserversFutures.iterator();
while (iterator.hasNext()) {
if (iterator.next().isDone()) {
iterator.remove();
}
}
try {
notifyObserversFutures.add(
options
.getTimerExecutorService()
.schedule(this::notifyRateLimitObservers, delayMillis));
Comment thread
runningcode marked this conversation as resolved.
} catch (RejectedExecutionException e) {
options
.getLogger()
.log(SentryLevel.WARNING, "Failed to schedule rate limit lifted notification.", e);
}

timer.schedule(
new TimerTask() {
@Override
public void run() {
notifyRateLimitObservers();
}
},
date);
}
}
}
Expand Down Expand Up @@ -364,11 +373,11 @@ public void removeRateLimitObserver(@NotNull final IRateLimitObserver observer)

@Override
public void close() throws IOException {
try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) {
if (timer != null) {
timer.cancel();
timer = null;
try (final @NotNull ISentryLifecycleToken ignored = notifyFuturesLock.acquire()) {
for (Future<?> future : notifyObserversFutures) {
future.cancel(false);
}
notifyObserversFutures.clear();
}
rateLimitObservers.clear();
}
Expand Down
35 changes: 24 additions & 11 deletions sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt
Loading