package io.github.bucket4j; import io.github.bucket4j.grid.BucketNotFoundException; import io.github.bucket4j.grid.ProxyManager; import io.github.bucket4j.grid.RecoveryStrategy; import io.github.bucket4j.util.ConsumptionScenario; import org.junit.Test; import java.time.Duration; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; import static io.github.bucket4j.grid.RecoveryStrategy.RECONSTRUCT; import static io.github.bucket4j.grid.RecoveryStrategy.THROW_BUCKET_NOT_FOUND_EXCEPTION; import static org.junit.Assert.*; public abstract class AbstractDistributedBucketTest<B extends AbstractBucketBuilder<B>, E extends Extension<B>> { private final String key = UUID.randomUUID().toString(); private final String anotherKey = UUID.randomUUID().toString(); private final Class<E> extensionClass = getExtensionClass(); private B builder = Bucket4j.extension(getExtensionClass()).builder() .addLimit(Bandwidth.simple(1_000, Duration.ofMinutes(1)).withInitialTokens(0)) .addLimit(Bandwidth.simple(200, Duration.ofSeconds(10)).withInitialTokens(0)); private double permittedRatePerSecond = Math.min(1_000d / 60, 200.0 / 10); protected abstract Class<E> getExtensionClass(); protected abstract Bucket build(B builder, String key, RecoveryStrategy recoveryStrategy); protected abstract ProxyManager<String> newProxyManager(); protected abstract void removeBucketFromBackingStorage(String key); @Test public abstract void testThatImpossibleToPassNullCacheToProxyManagerConstructor(); @Test public void testReconstructRecoveryStrategy() { B builder = Bucket4j.extension(extensionClass).builder() .addLimit(Bandwidth.simple(1_000, Duration.ofMinutes(1))) .addLimit(Bandwidth.simple(200, Duration.ofSeconds(10))); Bucket bucket = build(builder, key, RECONSTRUCT); assertTrue(bucket.tryConsume(1)); // simulate crash removeBucketFromBackingStorage(key); assertTrue(bucket.tryConsume(1)); } @Test public void testThrowExceptionRecoveryStrategy() { B builder = Bucket4j.extension(extensionClass).builder() .addLimit(Bandwidth.simple(1_000, Duration.ofMinutes(1))) .addLimit(Bandwidth.simple(200, Duration.ofSeconds(10))); Bucket bucket = build(builder, key, THROW_BUCKET_NOT_FOUND_EXCEPTION); assertTrue(bucket.tryConsume(1)); // simulate crash removeBucketFromBackingStorage(key); try { bucket.tryConsume(1); fail(); } catch (BucketNotFoundException e) { // ok } } @Test public void testLocateConfigurationThroughProxyManager() { ProxyManager<String> proxyManager = newProxyManager(); // should return empty optional if bucket is not stored Optional<BucketConfiguration> remoteConfiguration = proxyManager.getProxyConfiguration(key); assertFalse(remoteConfiguration.isPresent()); // should return not empty options if bucket is stored B builder = Bucket4j.extension(extensionClass).builder() .addLimit(Bandwidth.simple(1_000, Duration.ofMinutes(1))) .addLimit(Bandwidth.simple(200, Duration.ofSeconds(10))); build(builder, key, THROW_BUCKET_NOT_FOUND_EXCEPTION); remoteConfiguration = proxyManager.getProxyConfiguration(key); assertTrue(remoteConfiguration.isPresent()); // should return empty optional if bucket is removed removeBucketFromBackingStorage(key); remoteConfiguration = proxyManager.getProxyConfiguration(key); assertFalse(remoteConfiguration.isPresent()); } @Test public void testLocateBucketThroughProxyManager() { ProxyManager<String> proxyManager = newProxyManager(); // should return empty optional if bucket is not stored Optional<Bucket> remoteBucket = proxyManager.getProxy(key); assertFalse(remoteBucket.isPresent()); // should return not empty options if bucket is stored B builder = Bucket4j.extension(extensionClass).builder() .addLimit(Bandwidth.simple(1_000, Duration.ofMinutes(1))) .addLimit(Bandwidth.simple(200, Duration.ofSeconds(10))); build(builder, key, THROW_BUCKET_NOT_FOUND_EXCEPTION); remoteBucket = proxyManager.getProxy(key); assertTrue(remoteBucket.isPresent()); // should return empty optional if bucket is removed removeBucketFromBackingStorage(key); remoteBucket = proxyManager.getProxy(key); assertFalse(remoteBucket.isPresent()); } @Test public void testUnconditionalConsume() throws Exception { BucketConfiguration configuration = Bucket4j.extension(extensionClass).builder() .addLimit(Bandwidth.simple(1_000, Duration.ofMinutes(1))) .buildConfiguration(); Bucket bucket = newProxyManager().getProxy(key, () -> configuration); long overdraftNanos = bucket.consumeIgnoringRateLimits(121_000); assertEquals(overdraftNanos, TimeUnit.MINUTES.toNanos(120)); } @Test public void testUnconditionalConsumeVerbose() throws Exception { BucketConfiguration configuration = Bucket4j.extension(extensionClass).builder() .addLimit(Bandwidth.simple(1_000, Duration.ofMinutes(1))) .buildConfiguration(); Bucket bucket = newProxyManager().getProxy(key, () -> configuration); VerboseResult<Long> result = bucket.asVerbose().consumeIgnoringRateLimits(121_000); long overdraftNanos = result.getValue(); assertEquals(overdraftNanos, TimeUnit.MINUTES.toNanos(120)); assertEquals(configuration, result.getConfiguration()); } @Test public void testTryConsume() throws Exception { Function<Bucket, Long> action = bucket -> bucket.tryConsume(1)? 1L : 0L; Supplier<Bucket> bucketSupplier = () -> build(builder, key, THROW_BUCKET_NOT_FOUND_EXCEPTION); ConsumptionScenario scenario = new ConsumptionScenario(4, TimeUnit.SECONDS.toNanos(5), bucketSupplier, action, permittedRatePerSecond); scenario.executeAndValidateRate(); } @Test public void testTryConsumeWithLimit() throws Exception { Function<Bucket, Long> action = bucket -> bucket.asScheduler().tryConsumeUninterruptibly(1, TimeUnit.MILLISECONDS.toNanos(50), UninterruptibleBlockingStrategy.PARKING) ? 1L : 0L; Supplier<Bucket> bucketSupplier = () -> build(builder, key, THROW_BUCKET_NOT_FOUND_EXCEPTION); ConsumptionScenario scenario = new ConsumptionScenario(4, TimeUnit.SECONDS.toNanos(5), bucketSupplier, action, permittedRatePerSecond); scenario.executeAndValidateRate(); } @Test public void testTryConsumeAsync() throws Exception { Bucket testBucket = build(builder, anotherKey, RECONSTRUCT); if (!testBucket.isAsyncModeSupported()) { return; } Function<Bucket, Long> action = bucket -> { try { return bucket.asAsync().tryConsume(1).get() ? 1L : 0L; } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } }; Supplier<Bucket> bucketSupplier = () -> build(builder, key, THROW_BUCKET_NOT_FOUND_EXCEPTION); ConsumptionScenario scenario = new ConsumptionScenario(4, TimeUnit.SECONDS.toNanos(5), bucketSupplier, action, permittedRatePerSecond); scenario.executeAndValidateRate(); } @Test public void testTryConsumeAsyncWithLimit() throws Exception { Bucket testBucket = build(builder, anotherKey, RECONSTRUCT); if (!testBucket.isAsyncModeSupported()) { return; } ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Function<Bucket, Long> action = bucket -> { try { return bucket.asAsyncScheduler().tryConsume(1, TimeUnit.MILLISECONDS.toNanos(50), scheduler).get() ? 1L :0L; } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } }; Supplier<Bucket> bucketSupplier = () -> build(builder, key, THROW_BUCKET_NOT_FOUND_EXCEPTION); ConsumptionScenario scenario = new ConsumptionScenario(4, TimeUnit.SECONDS.toNanos(5), bucketSupplier, action, permittedRatePerSecond); scenario.executeAndValidateRate(); } @Test public void testBucketRegistryWithKeyIndependentConfiguration() { BucketConfiguration configuration = Bucket4j.configurationBuilder() .addLimit(Bandwidth.simple(10, Duration.ofDays(1))) .build(); ProxyManager<String> registry = newProxyManager(); Bucket bucket1 = registry.getProxy(key, () -> configuration); assertTrue(bucket1.tryConsume(10)); assertFalse(bucket1.tryConsume(1)); Bucket bucket2 = registry.getProxy(anotherKey, () -> configuration); assertTrue(bucket2.tryConsume(10)); assertFalse(bucket2.tryConsume(1)); } @Test public void testBucketWithNotLazyConfiguration() { BucketConfiguration configuration = Bucket4j.configurationBuilder() .addLimit(Bandwidth.simple(10, Duration.ofDays(1))) .build(); ProxyManager<String> registry = newProxyManager(); Bucket bucket = registry.getProxy(key, configuration); assertTrue(bucket.tryConsume(10)); assertFalse(bucket.tryConsume(1)); } }