All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.launchdarkly.sdk.android.Throttler Maven / Gradle / Ivy

package com.launchdarkly.sdk.android;

import androidx.annotation.NonNull;

import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Throttler class used to rate-limit invocations of a {@link Runnable}.
 * Uses exponential backoff with random jitter to determine delay between multiple calls.
 */
class Throttler {

    @NonNull
    private final Runnable taskRunnable;
    private final long retryTimeMs;
    private final long maxRetryTimeMs;

    private final Random jitter = new Random();
    private final AtomicInteger attempts = new AtomicInteger(-1);
    private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    private ScheduledFuture taskFuture;

    Throttler(@NonNull final Runnable runnable, long retryTimeMs, long maxRetryTimeMs) {
        this.taskRunnable = runnable;
        this.retryTimeMs = retryTimeMs;
        this.maxRetryTimeMs = maxRetryTimeMs;
    }

    private synchronized void run() {
        taskRunnable.run();
    }

    synchronized void attemptRun() {
        int attempt = attempts.getAndIncrement();

        // Grace first run instant for client initialization
        if (attempt < 0) {
            taskRunnable.run();
            return;
        }

        // First invocation is instant, as is the first invocation after throttling has ended
        if (attempt == 0) {
            taskRunnable.run();
            executorService.schedule(attempts::decrementAndGet, retryTimeMs, TimeUnit.MILLISECONDS);
            return;
        }

        long jitterVal = calculateJitterVal(attempt);
        executorService.schedule(attempts::decrementAndGet, jitterVal, TimeUnit.MILLISECONDS);
        if (taskFuture == null || taskFuture.isDone()) {
            taskFuture = executorService.schedule(this::run, backoffWithJitter(jitterVal), TimeUnit.MILLISECONDS);
        }
    }

    synchronized void cancel() {
        if (taskFuture != null) {
            taskFuture.cancel(false);
        }
    }

    long calculateJitterVal(int reconnectAttempts) {
        return Math.min(maxRetryTimeMs, retryTimeMs * pow2(reconnectAttempts));
    }

    long backoffWithJitter(long jitterVal) {
        return jitterVal / 2 + nextLong(jitter, jitterVal) / 2;
    }

    // Returns 2**k, or Integer.MAX_VALUE if 2**k would overflow
    private int pow2(int k) {
        return (k < Integer.SIZE - 1) ? (1 << k) : Integer.MAX_VALUE;
    }

    // Adapted from http://stackoverflow.com/questions/2546078/java-random-long-number-in-0-x-n-range
    // Since ThreadLocalRandom.current().nextLong(n) requires Android 5
    private long nextLong(Random rand, long bound) {
        if (bound <= 0) {
            throw new IllegalArgumentException("bound must be positive");
        }

        long r = rand.nextLong() & Long.MAX_VALUE;
        long m = bound - 1L;
        if ((bound & m) == 0) { // i.e., bound is a power of 2
            r = (bound * r) >> (Long.SIZE - 1);
        } else {
            //noinspection StatementWithEmptyBody
            for (long u = r; u - (r = u % bound) + m < 0L; u = rand.nextLong() & Long.MAX_VALUE) ;
        }
        return r;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy