/*
*
* 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 static com.jayway.awaitility.Awaitility.await;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.BDDAssertions.then;
import static org.hamcrest.CoreMatchers.equalTo;
import com.jayway.awaitility.core.ConditionFactory;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.event.RateLimiterEvent;
import io.reactivex.Flowable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@RunWith(PowerMockRunner.class)
@PrepareForTest(AtomicRateLimiter.class)
public class AtomicRateLimiterTest {
private static final String LIMITER_NAME = "test";
private static final long CYCLE_IN_NANOS = 500_000_000L;
private static final long POLL_INTERVAL_IN_NANOS = 2_000_000L;
private static final int PERMISSIONS_RER_CYCLE = 1;
private RateLimiterConfig rateLimiterConfig;
private AtomicRateLimiter rateLimiter;
private AtomicRateLimiter.AtomicRateLimiterMetrics metrics;
private Flowable<RateLimiterEvent> eventStream;
private static ConditionFactory awaitImpatiently() {
return await()
.pollDelay(1, TimeUnit.MICROSECONDS)
.pollInterval(POLL_INTERVAL_IN_NANOS, TimeUnit.NANOSECONDS);
}
private void setTimeOnNanos(long nanoTime) throws Exception {
PowerMockito.doReturn(nanoTime)
.when(rateLimiter, "currentNanoTime");
}
@Before
public void setup() {
rateLimiterConfig = RateLimiterConfig.custom()
.limitForPeriod(PERMISSIONS_RER_CYCLE)
.limitRefreshPeriod(Duration.ofNanos(CYCLE_IN_NANOS))
.timeoutDuration(Duration.ZERO)
.build();
AtomicRateLimiter testLimiter = new AtomicRateLimiter(LIMITER_NAME, rateLimiterConfig);
rateLimiter = PowerMockito.spy(testLimiter);
metrics = rateLimiter.getDetailedMetrics();
eventStream = rateLimiter.getEventStream();
}
@Test
public void notSpyRawTest() {
AtomicRateLimiter rawLimiter = new AtomicRateLimiter("rawLimiter", rateLimiterConfig);
AtomicRateLimiter.AtomicRateLimiterMetrics rawDetailedMetrics = rawLimiter.getDetailedMetrics();
long firstCycle = rawDetailedMetrics.getCycle();
while (firstCycle == rawDetailedMetrics.getCycle()) {
System.out.print('.'); // wait for current cycle to pass
}
boolean firstPermission = rawLimiter.getPermission(Duration.ZERO);
long nanosToWait = rawDetailedMetrics.getNanosToWait();
long startTime = System.nanoTime();
while(System.nanoTime() - startTime < nanosToWait) {
System.out.print('*'); // wait for permission renewal
}
boolean secondPermission = rawLimiter.getPermission(Duration.ZERO);
long secondCycle = rawDetailedMetrics.getCycle();
then(secondCycle - firstCycle).isEqualTo(2);
then(firstPermission).isTrue();
then(secondPermission).isTrue();
}
@Test
public void permissionsInFirstCycle() throws Exception {
setTimeOnNanos(CYCLE_IN_NANOS - 10);
RateLimiter.Metrics metrics = rateLimiter.getMetrics();
int availablePermissions = metrics.getAvailablePermissions();
then(availablePermissions).isEqualTo(PERMISSIONS_RER_CYCLE);
}
@Test
public void acquireAndRefreshWithEventPublishing() throws Exception {
CompletableFuture<ArrayList<String>> events = subscribeOnAllEventsDescriptions(4);
setTimeOnNanos(CYCLE_IN_NANOS);
boolean permission = rateLimiter.getPermission(Duration.ZERO);
then(permission).isTrue();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
boolean secondPermission = rateLimiter.getPermission(Duration.ZERO);
then(secondPermission).isFalse();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
setTimeOnNanos(CYCLE_IN_NANOS * 2);
boolean thirdPermission = rateLimiter.getPermission(Duration.ZERO);
then(thirdPermission).isTrue();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
boolean fourthPermission = rateLimiter.getPermission(Duration.ZERO);
then(fourthPermission).isFalse();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
ArrayList<String> eventStrings = events.get();
then(eventStrings.get(0)).contains("type=SUCCESSFUL_ACQUIRE");
then(eventStrings.get(1)).contains("type=FAILED_ACQUIRE");
then(eventStrings.get(2)).contains("type=SUCCESSFUL_ACQUIRE");
then(eventStrings.get(3)).contains("type=FAILED_ACQUIRE");
}
@Test
public void reserveAndRefresh() throws Exception {
setTimeOnNanos(CYCLE_IN_NANOS);
boolean permission = rateLimiter.getPermission(Duration.ZERO);
then(permission).isTrue();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
AtomicReference<Boolean> reservedPermission = new AtomicReference<>(null);
Thread caller = new Thread(
() -> reservedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS))));
caller.setDaemon(true);
caller.start();
awaitImpatiently()
.atMost(5, SECONDS)
.until(caller::getState, equalTo(Thread.State.TIMED_WAITING));
then(metrics.getAvailablePermissions()).isEqualTo(-1);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS + CYCLE_IN_NANOS);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(1);
setTimeOnNanos(CYCLE_IN_NANOS * 2 + 10);
awaitImpatiently()
.atMost(5, SECONDS)
.until(reservedPermission::get, equalTo(true));
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS - 10);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
}
@Test
public void reserveFewThenSkipCyclesBeforeRefresh() throws Exception {
setTimeOnNanos(CYCLE_IN_NANOS);
boolean permission = rateLimiter.getPermission(Duration.ZERO);
then(permission).isTrue();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
AtomicReference<Boolean> firstReservedPermission = new AtomicReference<>(null);
Thread firstCaller = new Thread(
() -> firstReservedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS))));
firstCaller.setDaemon(true);
firstCaller.start();
awaitImpatiently()
.atMost(5, SECONDS)
.until(firstCaller::getState, equalTo(Thread.State.TIMED_WAITING));
then(metrics.getAvailablePermissions()).isEqualTo(-1);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS * 2);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(1);
AtomicReference<Boolean> secondReservedPermission = new AtomicReference<>(null);
Thread secondCaller = new Thread(
() -> secondReservedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS * 2))));
secondCaller.setDaemon(true);
secondCaller.start();
awaitImpatiently()
.atMost(5, SECONDS)
.until(secondCaller::getState, equalTo(Thread.State.TIMED_WAITING));
then(metrics.getAvailablePermissions()).isEqualTo(-2);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS * 3);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(2);
setTimeOnNanos(CYCLE_IN_NANOS * 6 + 10);
awaitImpatiently()
.atMost(5, SECONDS)
.until(firstReservedPermission::get, equalTo(true));
awaitImpatiently()
.atMost(5, SECONDS)
.until(secondReservedPermission::get, equalTo(true));
then(metrics.getAvailablePermissions()).isEqualTo(1);
then(metrics.getNanosToWait()).isEqualTo(0L);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
}
@Test
public void rejectedByTimeout() throws Exception {
setTimeOnNanos(CYCLE_IN_NANOS);
boolean permission = rateLimiter.getPermission(Duration.ZERO);
then(permission).isTrue();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
AtomicReference<Boolean> declinedPermission = new AtomicReference<>(null);
Thread caller = new Thread(
() -> declinedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS - 1))));
caller.setDaemon(true);
caller.start();
awaitImpatiently()
.atMost(5, SECONDS)
.until(caller::getState, equalTo(Thread.State.TIMED_WAITING));
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(1);
setTimeOnNanos(CYCLE_IN_NANOS * 2 - 1);
awaitImpatiently()
.atMost(5, SECONDS)
.until(declinedPermission::get, equalTo(false));
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(1L);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
}
@Test
public void waitingThreadIsInterrupted() throws Exception {
setTimeOnNanos(CYCLE_IN_NANOS);
boolean permission = rateLimiter.getPermission(Duration.ZERO);
then(permission).isTrue();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
AtomicReference<Boolean> declinedPermission = new AtomicReference<>(null);
AtomicBoolean wasInterrupted = new AtomicBoolean(false);
Thread caller = new Thread(
() -> {
declinedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS - 1)));
wasInterrupted.set(Thread.currentThread().isInterrupted());
}
);
caller.isDaemon();
caller.start();
awaitImpatiently()
.atMost(5, SECONDS)
.until(caller::getState, equalTo(Thread.State.TIMED_WAITING));
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(1);
caller.interrupt();
awaitImpatiently()
.atMost(5, SECONDS)
.until(declinedPermission::get, equalTo(false));
then(wasInterrupted.get()).isTrue();
then(metrics.getAvailablePermissions()).isEqualTo(0);
then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS);
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
}
@Test
public void metricsTest() {
RateLimiter.Metrics metrics = rateLimiter.getMetrics();
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
then(metrics.getAvailablePermissions()).isEqualTo(1);
AtomicRateLimiter.AtomicRateLimiterMetrics detailedMetrics = rateLimiter.getDetailedMetrics();
then(detailedMetrics.getNumberOfWaitingThreads()).isEqualTo(0);
then(detailedMetrics.getAvailablePermissions()).isEqualTo(1);
then(detailedMetrics.getNanosToWait()).isEqualTo(0);
then(detailedMetrics.getCycle()).isGreaterThan(0);
}
@Test
public void namePropagation() {
then(rateLimiter.getName()).isEqualTo(LIMITER_NAME);
}
@Test
public void configPropagation() {
then(rateLimiter.getRateLimiterConfig()).isEqualTo(rateLimiterConfig);
}
@Test
public void metrics() {
then(rateLimiter.getMetrics().getNumberOfWaitingThreads()).isEqualTo(0);
}
private CompletableFuture<ArrayList<String>> subscribeOnAllEventsDescriptions(final int capacity) {
CompletableFuture<ArrayList<String>> future = new CompletableFuture<>();
eventStream
.take(capacity)
.map(Object::toString)
.collectInto(new ArrayList<String>(capacity), ArrayList::add)
.subscribe(future::complete);
return future;
}
}