package org.threadly.concurrent.wrapper.limiter;
import static org.junit.Assert.*;
import static org.threadly.TestConstants.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.threadly.concurrent.DoNothingRunnable;
import org.threadly.concurrent.PriorityScheduler;
import org.threadly.concurrent.PrioritySchedulerTest.PrioritySchedulerFactory;
import org.threadly.concurrent.StrictPriorityScheduler;
import org.threadly.concurrent.SubmitterExecutor;
import org.threadly.concurrent.SubmitterExecutorInterfaceTest;
import org.threadly.concurrent.TestCallable;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.test.concurrent.TestableScheduler;
import org.threadly.util.Clock;
@SuppressWarnings("javadoc")
public class RateLimiterExecutorTest extends SubmitterExecutorInterfaceTest {
private RateLimiterExecutor limiter;
private TestableScheduler scheduler;
@Before
public void setup() {
scheduler = new TestableScheduler();
limiter = new RateLimiterExecutor(scheduler, 1);
}
@After
public void cleanupDown() {
scheduler = null;
limiter = null;
}
@Override
protected SubmitterExecutorFactory getSubmitterExecutorFactory() {
return new RateLimiterFactory();
}
@Test
@SuppressWarnings("unused")
public void constructorFail() {
try {
new RateLimiterExecutor(null, 10);
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
new RateLimiterExecutor(scheduler, 0);
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void getCurrentMinimumDelayTest() {
assertEquals(0, limiter.getMinimumDelay());
limiter.execute(10, DoNothingRunnable.instance());
int delay = limiter.getMinimumDelay();
assertEquals(10000, delay, 1000);
limiter.execute(10, DoNothingRunnable.instance());
delay = limiter.getMinimumDelay();
assertEquals(20000, delay, 1000);
}
@Test
public void getFutureTillDelayTest() {
// verify that an empty limiter returns a finished future
ListenableFuture<?> f = limiter.getFutureTillDelay(0);
assertTrue(f.isDone());
// verify a it works if the limiter has waiting tasks
limiter.execute(1, DoNothingRunnable.instance());
f = limiter.getFutureTillDelay(0);
assertFalse(f.isDone());
scheduler.advance(1000);
assertTrue(f.isDone());
}
@Test
public void executeWithPermitsReturnedDelayTest() {
long firstDelay = limiter.execute(10, DoNothingRunnable.instance());
assertEquals(0, firstDelay);
long secondDelay = limiter.execute(1, DoNothingRunnable.instance());
// should be incremented from first delay
assertTrue(secondDelay > 8000);
}
@Test
public void limitTest() throws InterruptedException, ExecutionException {
int rateLimit = 100;
final AtomicInteger ranPermits = new AtomicInteger();
PriorityScheduler pse = new StrictPriorityScheduler(32);
try {
RateLimiterExecutor rls = new RateLimiterExecutor(pse, rateLimit);
ListenableFuture<?> lastFuture = null;
double startTime = Clock.accurateForwardProgressingMillis();
boolean flip = true;
for (int i = 0; i < TEST_QTY * 2; i++) {
final int permit = 5;
if (flip) {
lastFuture = rls.submit(permit, new Runnable() {
@Override
public void run() {
ranPermits.addAndGet(permit);
}
});
flip = false;
} else {
lastFuture = rls.submit(permit, new Callable<Void>() {
@Override
public Void call() {
ranPermits.addAndGet(permit);
return null;
}
});
flip = true;
}
}
lastFuture.get();
long endTime = Clock.accurateForwardProgressingMillis();
double actualLimit = ranPermits.get() / ((endTime - startTime) / 1000);
assertEquals(rateLimit, actualLimit, SLOW_MACHINE ? 75 : 50);
} finally {
pse.shutdownNow();
}
}
@Test
public void executeWithPermitsFail() {
try {
limiter.execute(-1, DoNothingRunnable.instance());
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
limiter.execute(1, null);
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void submitRunnableWithPermitsFail() {
try {
limiter.submit(-1, DoNothingRunnable.instance());
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
limiter.submit(1, (Runnable)null);
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void submitCallableWithPermitsFail() {
try {
limiter.submit(-1, new TestCallable());
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
limiter.submit(1, (Callable<?>)null);
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test (expected = RejectedExecutionException.class)
public void rejectDueToScheduleDelay() {
limiter = new RateLimiterExecutor(scheduler, 1, 1000);
limiter.execute(2000, DoNothingRunnable.instance());
limiter.execute(DoNothingRunnable.instance());
}
private static class RateLimiterFactory implements SubmitterExecutorFactory {
private final PrioritySchedulerFactory schedulerFactory = new PrioritySchedulerFactory();
private final int rateLimit = TEST_PROFILE == TestLoad.Stress ? 50 : 1000;
@Override
public SubmitterExecutor makeSubmitterExecutor(int poolSize, boolean prestartIfAvailable) {
return new RateLimiterExecutor(schedulerFactory.makeSchedulerService(poolSize,
prestartIfAvailable),
rateLimit);
}
@Override
public void shutdown() {
schedulerFactory.shutdown();
}
}
}