/*
*
* 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 com.jayway.awaitility.core.ConditionFactory;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.jayway.awaitility.Awaitility.await;
import static com.jayway.awaitility.Duration.FIVE_HUNDRED_MILLISECONDS;
import static java.lang.Thread.State.*;
import static java.time.Duration.ZERO;
import static io.vavr.control.Try.run;
import static org.assertj.core.api.BDDAssertions.then;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
public class SemaphoreBasedRateLimiterImplTest {
private static final int LIMIT = 2;
private static final Duration TIMEOUT = Duration.ofSeconds(5);
private static final Duration REFRESH_PERIOD = Duration.ofMillis(100);
private static final String CONFIG_MUST_NOT_BE_NULL = "RateLimiterConfig must not be null";
private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null";
private static final Object O = new Object();
@Rule
public ExpectedException exception = ExpectedException.none();
private RateLimiterConfig config;
private static ConditionFactory awaitImpatiently() {
return await()
.pollDelay(1, TimeUnit.MICROSECONDS)
.pollInterval(2, TimeUnit.MILLISECONDS);
}
@Before
public void init() {
config = RateLimiterConfig.custom()
.timeoutDuration(TIMEOUT)
.limitRefreshPeriod(REFRESH_PERIOD)
.limitForPeriod(LIMIT)
.build();
}
@Test
public void rateLimiterCreationWithProvidedScheduler() throws Exception {
ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class);
RateLimiterConfig configSpy = spy(config);
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", configSpy, scheduledExecutorService);
ArgumentCaptor<Runnable> refreshLimitRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(scheduledExecutorService)
.scheduleAtFixedRate(
refreshLimitRunnableCaptor.capture(),
eq(config.getLimitRefreshPeriod().toNanos()),
eq(config.getLimitRefreshPeriod().toNanos()),
eq(TimeUnit.NANOSECONDS)
);
Runnable refreshLimitRunnable = refreshLimitRunnableCaptor.getValue();
then(limit.getPermission(ZERO)).isTrue();
then(limit.getPermission(ZERO)).isTrue();
then(limit.getPermission(ZERO)).isFalse();
Thread.sleep(REFRESH_PERIOD.toMillis() * 2);
verify(configSpy, times(1)).getLimitForPeriod();
refreshLimitRunnable.run();
verify(configSpy, times(2)).getLimitForPeriod();
then(limit.getPermission(ZERO)).isTrue();
then(limit.getPermission(ZERO)).isTrue();
then(limit.getPermission(ZERO)).isFalse();
}
@Test
public void rateLimiterCreationWithDefaultScheduler() throws Exception {
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config);
awaitImpatiently().atMost(FIVE_HUNDRED_MILLISECONDS)
.until(() -> limit.getPermission(ZERO), equalTo(false));
awaitImpatiently().atMost(110, TimeUnit.MILLISECONDS)
.until(() -> limit.getPermission(ZERO), equalTo(true));
}
@Test
public void getPermissionAndMetrics() throws Exception {
ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class);
RateLimiterConfig configSpy = spy(config);
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", configSpy, scheduledExecutorService);
RateLimiter.Metrics detailedMetrics = limit.getMetrics();
CompletableFuture<ArrayList<String>> events = subscribeOnAllEventsDescriptions(limit, LIMIT + 1);
SynchronousQueue<Object> synchronousQueue = new SynchronousQueue<>();
Thread thread = new Thread(() -> {
run(() -> {
for (int i = 0; i < LIMIT; i++) {
synchronousQueue.put(O);
limit.getPermission(TIMEOUT);
}
limit.getPermission(TIMEOUT);
});
});
thread.setDaemon(true);
thread.start();
for (int i = 0; i < LIMIT; i++) {
synchronousQueue.take();
}
awaitImpatiently()
.atMost(100, TimeUnit.MILLISECONDS).until(detailedMetrics::getAvailablePermissions, equalTo(0));
awaitImpatiently()
.atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TIMED_WAITING));
then(detailedMetrics.getAvailablePermissions()).isEqualTo(0);
limit.refreshLimit();
awaitImpatiently()
.atMost(100, TimeUnit.MILLISECONDS).until(detailedMetrics::getAvailablePermissions, equalTo(1));
awaitImpatiently()
.atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TERMINATED));
then(detailedMetrics.getAvailablePermissions()).isEqualTo(1);
ArrayList<String> eventStrings = events.get();
eventStrings.forEach(eventString -> {
then(eventString).contains("type=SUCCESSFUL_ACQUIRE");
});
}
@Test
public void getPermissionInterruption() throws Exception {
ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class);
RateLimiterConfig configSpy = spy(config);
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", configSpy, scheduledExecutorService);
CompletableFuture<ArrayList<String>> events = subscribeOnAllEventsDescriptions(limit, LIMIT + 1);
limit.getPermission(ZERO);
limit.getPermission(ZERO);
Thread thread = new Thread(() -> {
limit.getPermission(TIMEOUT);
while (true) {
Function.identity().apply(1);
}
});
thread.setDaemon(true);
thread.start();
awaitImpatiently()
.atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TIMED_WAITING));
thread.interrupt();
awaitImpatiently()
.atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(RUNNABLE));
awaitImpatiently()
.atMost(100, TimeUnit.MILLISECONDS).until(thread::isInterrupted);
ArrayList<String> eventStrings = events.get();
then(eventStrings.get(0)).contains("type=SUCCESSFUL_ACQUIRE");
then(eventStrings.get(1)).contains("type=SUCCESSFUL_ACQUIRE");
then(eventStrings.get(2)).contains("type=FAILED_ACQUIRE");
}
@Test
public void getName() throws Exception {
ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler);
then(limit.getName()).isEqualTo("test");
}
@Test
public void getMetrics() throws Exception {
ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler);
RateLimiter.Metrics metrics = limit.getMetrics();
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
}
@Test
public void getRateLimiterConfig() throws Exception {
ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler);
then(limit.getRateLimiterConfig()).isEqualTo(config);
}
@Test
public void isUpperLimitedForPermissions() throws Exception {
ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler);
RateLimiter.Metrics metrics = limit.getMetrics();
then(metrics.getAvailablePermissions()).isEqualTo(2);
limit.refreshLimit();
then(metrics.getAvailablePermissions()).isEqualTo(2);
}
@Test
public void getDetailedMetrics() throws Exception {
ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler);
RateLimiter.Metrics metrics = limit.getMetrics();
then(metrics.getNumberOfWaitingThreads()).isEqualTo(0);
then(metrics.getAvailablePermissions()).isEqualTo(2);
}
@Test
public void constructionWithNullName() throws Exception {
exception.expect(NullPointerException.class);
exception.expectMessage(NAME_MUST_NOT_BE_NULL);
new SemaphoreBasedRateLimiter(null, config, null);
}
@Test
public void constructionWithNullConfig() throws Exception {
exception.expect(NullPointerException.class);
exception.expectMessage(CONFIG_MUST_NOT_BE_NULL);
new SemaphoreBasedRateLimiter("test", null, null);
}
private CompletableFuture<ArrayList<String>> subscribeOnAllEventsDescriptions(final SemaphoreBasedRateLimiter limiter, final int capacity) {
CompletableFuture<ArrayList<String>> future = new CompletableFuture<>();
limiter.getEventStream()
.take(capacity)
.map(Object::toString)
.collectInto(new ArrayList<String>(capacity), ArrayList::add)
.subscribe(future::complete);
return future;
}
}