### Question:
Limiters { public Consumer<Runnable> rateLimiter( Object handle, Duration minInterval ) { return caps.computeIfAbsent( handle, ignored -> Limiters.rateLimiter( minInterval, clock ) ); } Limiters();  Limiters( Clock clock ); Consumer<Runnable> rateLimiter( Object handle, Duration minInterval ); static Consumer<Runnable> rateLimiter( Duration minInterval ); static Consumer<Runnable> rateLimiter( Duration minInterval, Clock clock );  }### Answer:
@Test public void shouldRateLimitCalls() { int intervalMillis = 10; FakeClock clock = Clocks.fakeClock(); Consumer<Runnable> cap = rateLimiter( Duration.ofMillis( intervalMillis ), clock ); AtomicInteger cnt = new AtomicInteger(); Runnable op = cnt::incrementAndGet; cap.accept( op ); cap.accept( op ); cap.accept( op ); assertThat( cnt.get(), equalTo( 1 ) ); clock.forward( intervalMillis, MILLISECONDS ); cap.accept( op ); cap.accept( op ); cap.accept( op ); assertThat( cnt.get(), equalTo( 2 ) ); clock.forward( 1000 * intervalMillis, MILLISECONDS ); cap.accept( op ); cap.accept( op ); cap.accept( op ); assertThat( cnt.get(), equalTo( 3 ) ); }
@Test public void shouldOnlyAllowOneThreadPerInterval() throws Exception { int intervalMillis = 10; int nThreads = 10; int iterations = 100; FakeClock clock = Clocks.fakeClock(); Consumer<Runnable> cap = rateLimiter( Duration.ofMillis( intervalMillis ), clock ); AtomicInteger cnt = new AtomicInteger(); Runnable op = cnt::incrementAndGet; for ( int iteration = 1; iteration <= iterations; iteration++ ) { clock.forward( intervalMillis, MILLISECONDS ); CountDownLatch latch = new CountDownLatch( 1 ); ExecutorService es = Executors.newCachedThreadPool(); for ( int j = 0; j < nThreads; j++ ) { es.submit( () -> { try { latch.await(); } catch ( InterruptedException e ) { e.printStackTrace(); } cap.accept( op ); } ); } latch.countDown(); es.shutdown(); es.awaitTermination( 10, SECONDS ); assertThat( cnt.get(), equalTo( iteration ) ); } }
@Test public void distinctRateLimitersOperateIndependently() throws Exception { Limiters limiters = new Limiters( Clocks.fakeClock() ); AtomicInteger cnt = new AtomicInteger(); Consumer<Runnable> rateLimiterA = limiters.rateLimiter( "A", ETERNITY ); Consumer<Runnable> rateLimiterB = limiters.rateLimiter( "B", ETERNITY ); rateLimiterA.accept( cnt::incrementAndGet ); rateLimiterA.accept( cnt::incrementAndGet ); rateLimiterA.accept( cnt::incrementAndGet ); rateLimiterB.accept( cnt::incrementAndGet ); rateLimiterB.accept( cnt::incrementAndGet ); rateLimiterB.accept( cnt::incrementAndGet ); assertEquals( 2, cnt.get() ); }
@Test public void shouldReturnSameRateLimiterForSameHandle() throws Exception { Limiters limiters = new Limiters( Clocks.fakeClock() ); AtomicInteger cnt = new AtomicInteger(); Consumer<Runnable> rateLimiterA = limiters.rateLimiter( "SAME", ETERNITY ); Consumer<Runnable> rateLimiterB = limiters.rateLimiter( "SAME", ETERNITY ); rateLimiterA.accept( cnt::incrementAndGet ); rateLimiterA.accept( cnt::incrementAndGet ); rateLimiterB.accept( cnt::incrementAndGet ); rateLimiterB.accept( cnt::incrementAndGet ); assertEquals( 1, cnt.get() ); }