/*
*
* 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;
}
}
}