/* * * Copyright 2016 Robert Winkler and Bohdan Storozhuk * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * */ package io.github.resilience4j.ratelimiter.internal; import io.github.resilience4j.ratelimiter.RateLimiter; import io.github.resilience4j.ratelimiter.RateLimiterConfig; import io.github.resilience4j.ratelimiter.event.RateLimiterEvent; import io.github.resilience4j.ratelimiter.event.RateLimiterOnFailureEvent; import io.github.resilience4j.ratelimiter.event.RateLimiterOnSuccessEvent; import io.reactivex.Flowable; import io.reactivex.processors.FlowableProcessor; import io.reactivex.processors.PublishProcessor; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; import static java.lang.Long.min; import static java.lang.System.nanoTime; import static java.lang.Thread.currentThread; import static java.util.concurrent.locks.LockSupport.parkNanos; /** * {@link AtomicRateLimiter} splits all nanoseconds from the start of epoch into cycles. * <p>Each cycle has duration of {@link RateLimiterConfig#limitRefreshPeriod} in nanoseconds. * <p>By contract on start of each cycle {@link AtomicRateLimiter} should * set {@link State#activePermissions} to {@link RateLimiterConfig#limitForPeriod}. * For the {@link AtomicRateLimiter} callers it is really looks so, but under the hood there is * some optimisations that will skip this refresh if {@link AtomicRateLimiter} is not used actively. * <p>All {@link AtomicRateLimiter} updates are atomic and state is encapsulated in {@link AtomicReference} to * {@link AtomicRateLimiter.State} */ public class AtomicRateLimiter implements RateLimiter { private static final long nanoTimeStart = nanoTime(); private final String name; private final RateLimiterConfig rateLimiterConfig; private final long cyclePeriodInNanos; private final int permissionsPerCycle; private final AtomicInteger waitingThreads; private final AtomicReference<State> state; private final FlowableProcessor<RateLimiterEvent> eventPublisher; public AtomicRateLimiter(String name, RateLimiterConfig rateLimiterConfig) { this.name = name; this.rateLimiterConfig = rateLimiterConfig; cyclePeriodInNanos = rateLimiterConfig.getLimitRefreshPeriod().toNanos(); permissionsPerCycle = rateLimiterConfig.getLimitForPeriod(); waitingThreads = new AtomicInteger(0); state = new AtomicReference<>(new State(0, permissionsPerCycle, 0)); PublishProcessor<RateLimiterEvent> publisher = PublishProcessor.create(); this.eventPublisher = publisher.toSerialized(); } /** * Calculates time elapsed from the class loading. */ private long currentNanoTime() { return nanoTime() - nanoTimeStart; } /** * {@inheritDoc} */ @Override public boolean getPermission(final Duration timeoutDuration) { long timeoutInNanos = timeoutDuration.toNanos(); State modifiedState = updateStateWithBackOff(timeoutInNanos); boolean result = waitForPermissionIfNecessary(timeoutInNanos, modifiedState.nanosToWait); publishRateLimiterEvent(result); return result; } /** * Atomically updates the current {@link State} with the results of * applying the {@link AtomicRateLimiter#calculateNextState}, returning the updated {@link State}. * It differs from {@link AtomicReference#updateAndGet(UnaryOperator)} by constant back off. * It means that after one try to {@link AtomicReference#compareAndSet(Object, Object)} * this method will wait for a while before try one more time. * This technique was originally described in this * <a href="https://arxiv.org/abs/1305.5800"> paper</a> * and showed great results with {@link AtomicRateLimiter} in benchmark tests. * * @param timeoutInNanos a side-effect-free function * @return the updated value */ private State updateStateWithBackOff(final long timeoutInNanos) { AtomicRateLimiter.State prev; AtomicRateLimiter.State next; do { prev = state.get(); next = calculateNextState(timeoutInNanos, prev); } while (!compareAndSet(prev, next)); return next; } /** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * It differs from {@link AtomicReference#updateAndGet(UnaryOperator)} by constant back off. * It means that after one try to {@link AtomicReference#compareAndSet(Object, Object)} * this method will wait for a while before try one more time. * This technique was originally described in this * <a href="https://arxiv.org/abs/1305.5800"> paper</a> * and showed great results with {@link AtomicRateLimiter} in benchmark tests. * * @param current the expected value * @param next the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */ private boolean compareAndSet(final State current, final State next) { if (state.compareAndSet(current, next)) { return true; } parkNanos(1); // back-off return false; } /** * A side-effect-free function that can calculate next {@link State} from current. * It determines time duration that you should wait for permission and reserves it for you, * if you'll be able to wait long enough. * * @param timeoutInNanos max time that caller can wait for permission in nanoseconds * @param activeState current state of {@link AtomicRateLimiter} * @return next {@link State} */ private State calculateNextState(final long timeoutInNanos, final State activeState) { long currentNanos = currentNanoTime(); long currentCycle = currentNanos / cyclePeriodInNanos; long nextCycle = activeState.activeCycle; int nextPermissions = activeState.activePermissions; if (nextCycle != currentCycle) { long elapsedCycles = currentCycle - nextCycle; long accumulatedPermissions = elapsedCycles * permissionsPerCycle; nextCycle = currentCycle; nextPermissions = (int) min(nextPermissions + accumulatedPermissions, permissionsPerCycle); } long nextNanosToWait = nanosToWaitForPermission(nextPermissions, currentNanos, currentCycle); State nextState = reservePermissions(timeoutInNanos, nextCycle, nextPermissions, nextNanosToWait); return nextState; } /** * Calculates time to wait for next permission as * [time to the next cycle] + [duration of full cycles until reserved permissions expire] * * @param availablePermissions currently available permissions, can be negative if some permissions have been reserved * @param currentNanos current time in nanoseconds * @param currentCycle current {@link AtomicRateLimiter} cycle * @return nanoseconds to wait for the next permission */ private long nanosToWaitForPermission(final int availablePermissions, final long currentNanos, final long currentCycle) { if (availablePermissions > 0) { return 0L; } long nextCycleTimeInNanos = (currentCycle + 1) * cyclePeriodInNanos; long nanosToNextCycle = nextCycleTimeInNanos - currentNanos; int fullCyclesToWait = (-availablePermissions) / permissionsPerCycle; return (fullCyclesToWait * cyclePeriodInNanos) + nanosToNextCycle; } /** * Determines whether caller can acquire permission before timeout or not and then creates corresponding {@link State}. * Reserves permissions only if caller can successfully wait for permission. * * @param timeoutInNanos max time that caller can wait for permission in nanoseconds * @param cycle cycle for new {@link State} * @param permissions permissions for new {@link State} * @param nanosToWait nanoseconds to wait for the next permission * @return new {@link State} with possibly reserved permissions and time to wait */ private State reservePermissions(final long timeoutInNanos, final long cycle, final int permissions, final long nanosToWait) { boolean canAcquireInTime = timeoutInNanos >= nanosToWait; int permissionsWithReservation = permissions; if (canAcquireInTime) { permissionsWithReservation--; } return new State(cycle, permissionsWithReservation, nanosToWait); } /** * If nanosToWait is bigger than 0 it tries to park {@link Thread} for nanosToWait but not longer then timeoutInNanos. * * @param timeoutInNanos max time that caller can wait * @param nanosToWait nanoseconds caller need to wait * @return true if caller was able to wait for nanosToWait without {@link Thread#interrupt} and not exceed timeout */ private boolean waitForPermissionIfNecessary(final long timeoutInNanos, final long nanosToWait) { boolean canAcquireImmediately = nanosToWait <= 0; boolean canAcquireInTime = timeoutInNanos >= nanosToWait; if (canAcquireImmediately) { return true; } if (canAcquireInTime) { return waitForPermission(nanosToWait); } waitForPermission(timeoutInNanos); return false; } /** * Parks {@link Thread} for nanosToWait. * <p>If the current thread is {@linkplain Thread#interrupted} * while waiting for a permit then it won't throw {@linkplain InterruptedException}, * but its interrupt status will be set. * * @param nanosToWait nanoseconds caller need to wait * @return true if caller was not {@link Thread#interrupted} while waiting */ private boolean waitForPermission(final long nanosToWait) { waitingThreads.incrementAndGet(); long deadline = currentNanoTime() + nanosToWait; boolean wasInterrupted = false; while (currentNanoTime() < deadline && !wasInterrupted) { long sleepBlockDuration = deadline - currentNanoTime(); parkNanos(sleepBlockDuration); wasInterrupted = Thread.interrupted(); } waitingThreads.decrementAndGet(); if (wasInterrupted) { currentThread().interrupt(); } return !wasInterrupted; } /** * {@inheritDoc} */ @Override public String getName() { return name; } /** * {@inheritDoc} */ @Override public RateLimiterConfig getRateLimiterConfig() { return rateLimiterConfig; } /** * {@inheritDoc} */ @Override public Metrics getMetrics() { return new AtomicRateLimiterMetrics(); } /** * {@inheritDoc} */ @Override public Flowable<RateLimiterEvent> getEventStream() { return eventPublisher; } @Override public String toString() { return "AtomicRateLimiter{" + "name='" + name + '\'' + ", rateLimiterConfig=" + rateLimiterConfig + '}'; } /** * Get the enhanced Metrics with some implementation specific details. * * @return the detailed metrics */ public AtomicRateLimiterMetrics getDetailedMetrics() { return new AtomicRateLimiterMetrics(); } private void publishRateLimiterEvent(boolean permissionAcquired) { if (!eventPublisher.hasSubscribers()) { return; } if (permissionAcquired) { eventPublisher.onNext(new RateLimiterOnSuccessEvent(name)); return; } eventPublisher.onNext(new RateLimiterOnFailureEvent(name)); } /** * <p>{@link AtomicRateLimiter.State} represents immutable state of {@link AtomicRateLimiter} where: * <ul> * <li>activeCycle - {@link AtomicRateLimiter} cycle number that was used * by the last {@link AtomicRateLimiter#getPermission(Duration)} call.</li> * <p> * <li>activePermissions - count of available permissions after * the last {@link AtomicRateLimiter#getPermission(Duration)} call. * Can be negative if some permissions where reserved.</li> * <p> * <li>nanosToWait - count of nanoseconds to wait for permission for * the last {@link AtomicRateLimiter#getPermission(Duration)} call.</li> * </ul> */ private static class State { private final long activeCycle; private final int activePermissions; private final long nanosToWait; private State(final long activeCycle, final int activePermissions, final long nanosToWait) { this.activeCycle = activeCycle; this.activePermissions = activePermissions; this.nanosToWait = nanosToWait; } } /** * Enhanced {@link Metrics} with some implementation specific details */ public class AtomicRateLimiterMetrics implements Metrics { private AtomicRateLimiterMetrics() { } /** * {@inheritDoc} */ @Override public int getNumberOfWaitingThreads() { return waitingThreads.get(); } /** * {@inheritDoc} */ @Override public int getAvailablePermissions() { State currentState = state.get(); State estimatedState = calculateNextState(-1, currentState); return estimatedState.activePermissions; } /** * @return estimated time duration in nanos to wait for the next permission */ public long getNanosToWait() { State currentState = state.get(); State estimatedState = calculateNextState(-1, currentState); return estimatedState.nanosToWait; } /** * @return estimated current cycle */ public long getCycle() { State currentState = state.get(); State estimatedState = calculateNextState(-1, currentState); return estimatedState.activeCycle; } } }