package org.threadly.concurrent.wrapper.limiter; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.threadly.TestConstants.SLOW_MACHINE; import static org.threadly.TestConstants.TEST_PROFILE; import static org.threadly.TestConstants.TEST_QTY; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; import org.threadly.TestConstants.TestLoad; import org.threadly.concurrent.DoNothingRunnable; import org.threadly.concurrent.PriorityScheduler; import org.threadly.concurrent.StrictPriorityScheduler; import org.threadly.concurrent.SubmitterExecutor; import org.threadly.concurrent.SubmitterExecutorInterfaceTest; import org.threadly.concurrent.TestCallable; import org.threadly.concurrent.PrioritySchedulerTest.PrioritySchedulerFactory; import org.threadly.concurrent.future.ListenableFuture; import org.threadly.test.concurrent.TestUtils; import org.threadly.test.concurrent.TestableScheduler; import org.threadly.util.Clock; @SuppressWarnings("javadoc") public class KeyedRateLimiterExecutorTest extends SubmitterExecutorInterfaceTest { private TestableScheduler scheduler; private KeyedRateLimiterExecutor limiter; @Before public void setup() { scheduler = new TestableScheduler(); limiter = new KeyedRateLimiterExecutor(scheduler, 1); } @Override protected SubmitterExecutorFactory getSubmitterExecutorFactory() { return new KeyedRateLimiterFactory(); } @SuppressWarnings("unused") @Test public void constructorFail() { try { new KeyedRateLimiterExecutor(null, 10); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } try { new KeyedRateLimiterExecutor(scheduler, 0); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } } @Test public void getCurrentMinimumDelayTest() { Object key = new Object(); assertEquals(0, limiter.getMinimumDelay(key)); limiter.execute(10, key, DoNothingRunnable.instance()); int delay = limiter.getMinimumDelay(key); assertEquals(10000, delay, 1000); limiter.execute(10, key, DoNothingRunnable.instance()); delay = limiter.getMinimumDelay(key); assertEquals(20000, delay, 1000); } @Test public void getFutureTillDelayTest() { Object key = new Object(); // verify that an empty limiter returns a finished future ListenableFuture<?> f = limiter.getFutureTillDelay(key, 0); assertTrue(f.isDone()); // verify it works if the limiter has waiting tasks limiter.execute(key, DoNothingRunnable.instance()); f = limiter.getFutureTillDelay(key, 0); assertFalse(f.isDone()); scheduler.advance(1000); assertTrue(f.isDone()); } @Test public void executeWithPermitsReturnedDelayTest() { long firstDelay = limiter.execute(10, "foo", DoNothingRunnable.instance()); assertEquals(0, firstDelay); long secondDelay = limiter.execute(1, "foo", DoNothingRunnable.instance()); // should be incremented from first delay assertTrue(secondDelay > 8000); } @Test public void limitTest() throws InterruptedException, ExecutionException { Object key = new Object(); int rateLimit = 100; final AtomicInteger ranPermits = new AtomicInteger(); PriorityScheduler pse = new StrictPriorityScheduler(32); try { KeyedRateLimiterExecutor krls = new KeyedRateLimiterExecutor(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 = krls.submit(permit, key, new Runnable() { @Override public void run() { ranPermits.addAndGet(permit); } }); flip = false; } else { lastFuture = krls.submit(permit, key, 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, new Object(), DoNothingRunnable.instance()); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } try { limiter.execute(1, new Object(), null); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } } @Test public void submitRunnableWithPermitsFail() { try { limiter.submit(-1, new Object(), DoNothingRunnable.instance()); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } try { limiter.submit(1, new Object(), (Runnable)null); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } } @Test public void submitCallableWithPermitsFail() { try { limiter.submit(-1, new Object(), new TestCallable()); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } try { limiter.submit(1, new Object(), (Callable<?>)null); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } } @Test public void submitWithNoKeyFail() { try { limiter.submit(1, null, DoNothingRunnable.instance()); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } try { limiter.submit(1, null, new TestCallable()); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } } @Test public void verifyCleanupTaskTest() { Object key = new Object(); limiter.execute(.1, key, DoNothingRunnable.instance()); assertEquals(2, scheduler.advance(1000)); assertEquals(1, limiter.getTrackedKeyCount()); assertFalse(limiter.currentLimiters.isEmpty()); // should have item for 100 millis TestUtils.sleep(100); TestUtils.blockTillClockAdvances(); assertEquals(1, scheduler.advance(1000)); assertTrue(limiter.currentLimiters.isEmpty()); assertEquals(0, limiter.getTrackedKeyCount()); } @Test (expected = RejectedExecutionException.class) public void rejectDueToScheduleDelay() { limiter = new KeyedRateLimiterExecutor(scheduler, 1, 1000); limiter.execute(2000, "foo", DoNothingRunnable.instance()); limiter.execute("foo", DoNothingRunnable.instance()); } private static class KeyedRateLimiterFactory 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 KeyedRateLimiterExecutor(schedulerFactory.makeSchedulerService(poolSize, prestartIfAvailable), rateLimit).getSubmitterExecutorForKey(new Object()); } @Override public void shutdown() { schedulerFactory.shutdown(); } } }