Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import dev.voidframework.bucket4j.exception.BucketTokenException;
import dev.voidframework.core.utils.ConfigurationUtils;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.BandwidthBuilder;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import io.github.bucket4j.local.LocalBucketBuilder;
import io.github.bucket4j.local.SynchronizationStrategy;

Expand Down Expand Up @@ -155,31 +155,49 @@ private Bucket createBucket(final String configurationPath) {
*/
private Bandwidth createBandwidth(final Config bandwidthConfiguration) {

// Create refill object
final long tokens = bandwidthConfiguration.getLong("refill.tokens");
final Duration period = bandwidthConfiguration.getDuration("refill.period");
final String id = bandwidthConfiguration.getString("id");
final String refillStrategy = bandwidthConfiguration.getString("refill.strategy").toUpperCase(Locale.ENGLISH);

final Refill refill = switch (refillStrategy) {
case "GREEDY" -> Refill.greedy(tokens, period);
case "INTERVALLY" -> Refill.intervally(tokens, period);
case "INTERVALLY_ALIGNED" -> Refill.intervallyAligned(
tokens,
period,
Instant.now().plusMillis(bandwidthConfiguration.getDuration("refill.timeOfFirstRefill", TimeUnit.MILLISECONDS)),
bandwidthConfiguration.getBoolean("refill.useAdaptiveInitialTokens"));
final Duration period = bandwidthConfiguration.getDuration("refill.period");
final long capacity = bandwidthConfiguration.getInt("capacity");
final long refillTokens = bandwidthConfiguration.getLong("refill.tokens");
final long refillInitialTokens = ConfigurationUtils.getLongOrDefault(bandwidthConfiguration, "refill.initialTokens", capacity);

return switch (refillStrategy) {
case "GREEDY" -> BandwidthBuilder.builder()
.capacity(capacity)
.refillGreedy(refillTokens, period)
.initialTokens(refillInitialTokens)
.id(id)
.build();
case "INTERVALLY" -> BandwidthBuilder.builder()
.capacity(capacity)
.refillIntervally(refillTokens, period)
.initialTokens(refillInitialTokens)
.id(id)
.build();
case "INTERVALLY_ALIGNED" -> {
if (bandwidthConfiguration.getBoolean("refill.useAdaptiveInitialTokens")) {
yield BandwidthBuilder.builder()
.capacity(capacity)
.refillIntervallyAlignedWithAdaptiveInitialTokens(
refillTokens,
period,
Instant.now().plusMillis(bandwidthConfiguration.getDuration("refill.timeOfFirstRefill", TimeUnit.MILLISECONDS)))
.id(id)
.build();
} else {
yield BandwidthBuilder.builder()
.capacity(capacity)
.refillIntervallyAligned(
refillTokens,
period,
Instant.now().plusMillis(bandwidthConfiguration.getDuration("refill.timeOfFirstRefill", TimeUnit.MILLISECONDS)))
.initialTokens(refillInitialTokens)
.id(id)
.build();
}
}
default -> throw new BucketTokenException.UnknownRefillStrategy(refillStrategy);
};

// Create bandwidth
final Bandwidth bandwidth = Bandwidth.classic(bandwidthConfiguration.getInt("capacity"), refill);
if (bandwidth.isIntervallyAligned() && bandwidth.isUseAdaptiveInitialTokens()) {
return bandwidth
.withId(bandwidthConfiguration.getString("id"));
} else {
return bandwidth
.withInitialTokens(ConfigurationUtils.getLongOrDefault(bandwidthConfiguration, "refill.initialTokens", bandwidth.getCapacity()))
.withId(bandwidthConfiguration.getString("id"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ void bucketConfigurationPathIsNull() {
}

@Test
void bucketOrDie() {
void bucketOrDie_die() {

// Arrange
final Config configuration = ConfigFactory.parseString("");
Expand All @@ -103,6 +103,40 @@ void bucketOrDie() {
Assertions.assertEquals("Bucket 'bucketAPI1' does not exist", exception.getMessage());
}

@Test
void bucketOrDie_success() {

// Arrange
final Config configuration = ConfigFactory.parseString("""
voidframework.bucket4j.bucketAPI1.synchronizationStrategy = "LOCK_FREE"

voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.id = "one"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.capacity = 60
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.strategy = "GREEDY"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.tokens = 60
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.period = "1 minutes"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.initialTokens = 24
""");
final BucketTokenRegistry bucketTokenRegistry = new BucketTokenRegistry(configuration);

// Create the bucket
bucketTokenRegistry.bucket("bucketAPI1");

// Act
final Bucket bucket = bucketTokenRegistry.bucketOrDie("bucketAPI1");

// Assert
final LocalBucket localBucket1 = (LocalBucket) bucket;
Assertions.assertNotNull(localBucket1);
Assertions.assertEquals(1, localBucket1.getConfiguration().getBandwidths().length);
Assertions.assertEquals("one", localBucket1.getConfiguration().getBandwidths()[0].getId());
Assertions.assertEquals(60, localBucket1.getConfiguration().getBandwidths()[0].getCapacity());
Assertions.assertEquals(24, localBucket1.getConfiguration().getBandwidths()[0].getInitialTokens());
Assertions.assertEquals(60, localBucket1.getConfiguration().getBandwidths()[0].getRefillTokens());
Assertions.assertEquals(60000000000L, localBucket1.getConfiguration().getBandwidths()[0].getRefillPeriodNanos());
Assertions.assertFalse(localBucket1.getConfiguration().getBandwidths()[0].isRefillIntervally());
}

@Test
void getAllBuckets() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ protected void configure() {
}

@Test
void bucket4JModuleWithIntervallyAlignedBucket() {
void bucket4JModuleWithIntervallyAlignedBucket_withAdaptiveInitialTokens() {

// Arrange
final Config configuration = ConfigFactory.parseString("""
Expand Down Expand Up @@ -154,6 +154,48 @@ protected void configure() {
Assertions.assertTrue(localBucket.getConfiguration().getBandwidths()[0].isIntervallyAligned());
}

@Test
void bucket4JModuleWithIntervallyAlignedBucket_withoutAdaptiveInitialTokens() {

// Arrange
final Config configuration = ConfigFactory.parseString("""
voidframework.bucket4j.bucketAPI1.synchronizationStrategy = "LOCK_FREE"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.id = "limit-bucket-1"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.capacity = 120
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.strategy = "INTERVALLY_ALIGNED"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.tokens = 1
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.period = "1 hours"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.timeOfFirstRefill = "5 minutes"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.useAdaptiveInitialTokens = false
""");

// Act
final Injector injector = Guice.createInjector(Stage.PRODUCTION, new AbstractModule() {

@Override
protected void configure() {
bind(Config.class).toInstance(configuration);
install(new Bucket4JModule(configuration));
}
});

final BucketTokenRegistry bucketTokenRegistry = injector.getInstance(BucketTokenRegistry.class);

// Assert
final LocalBucket localBucket = (LocalBucket) bucketTokenRegistry.bucket("bucketAPI1");
Assertions.assertNotNull(localBucket);
Assertions.assertEquals(1, localBucket.getConfiguration().getBandwidths().length);

Assertions.assertEquals("limit-bucket-1", localBucket.getConfiguration().getBandwidths()[0].getId());
Assertions.assertEquals(120, localBucket.getConfiguration().getBandwidths()[0].getCapacity());
Assertions.assertEquals(120, localBucket.getConfiguration().getBandwidths()[0].getInitialTokens());
Assertions.assertEquals(1, localBucket.getConfiguration().getBandwidths()[0].getRefillTokens());
Assertions.assertEquals(3600000000000L, localBucket.getConfiguration().getBandwidths()[0].getRefillPeriodNanos());
Assertions.assertTrue(localBucket.getConfiguration().getBandwidths()[0].isRefillIntervally());
Assertions.assertFalse(localBucket.getConfiguration().getBandwidths()[0].isUseAdaptiveInitialTokens());
Assertions.assertTrue(localBucket.getConfiguration().getBandwidths()[0].isIntervallyAligned());
}

@Test
void bucket4JModuleWithUnknowRefillType() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package dev.voidframework.bucket4j.module;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import dev.voidframework.bucket4j.BucketTokenRegistry;
import dev.voidframework.bucket4j.annotation.BucketToken;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;

@TestMethodOrder(MethodOrderer.MethodName.class)
final class BucketTokenInterceptorTest {

@Test
void invoke_primaryMethod() throws Throwable {

// Arrange
final Config configuration = ConfigFactory.parseString("""
voidframework.bucket4j.bucketAPI1.synchronizationStrategy = "LOCK_FREE"

voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.id = "one"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.capacity = 60
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.strategy = "GREEDY"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.tokens = 60
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.period = "1 minutes"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.initialTokens = 24
""");
final BucketTokenRegistry bucketTokenRegistry = new BucketTokenRegistry(configuration);
final BucketTokenInterceptor interceptor = new BucketTokenInterceptor(bucketTokenRegistry);

final DummService dummService = new DummService();
final Method methodPrimary = DummService.class.getDeclaredMethod("primary");
final MethodInvocation methodInvocation = new FakeMethodInvocation(dummService, methodPrimary);

// Act
final Object value = interceptor.invoke(methodInvocation);

// Assert
Assertions.assertNotNull(value);
Assertions.assertEquals(value, "PRIMARY");
}

@Test
void invoke_fallbackMethod() throws Throwable {

// Arrange
final Config configuration = ConfigFactory.parseString("""
voidframework.bucket4j.bucketAPI1.synchronizationStrategy = "LOCK_FREE"

voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.id = "one"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.capacity = 60
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.strategy = "GREEDY"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.tokens = 60
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.period = "24 hours"
voidframework.bucket4j.bucketAPI1.bandwidthLimits.0.refill.initialTokens = 0
""");
final BucketTokenRegistry bucketTokenRegistry = new BucketTokenRegistry(configuration);
final BucketTokenInterceptor interceptor = new BucketTokenInterceptor(bucketTokenRegistry);

final DummService dummService = new DummService();
final Method methodPrimary = DummService.class.getDeclaredMethod("primary");
final MethodInvocation methodInvocation = new FakeMethodInvocation(dummService, methodPrimary);

// Act
final Object value = interceptor.invoke(methodInvocation);

// Assert
Assertions.assertNotNull(value);
Assertions.assertEquals(value, "FALLBACK");
}

/**
* A fake method invocation.
*/
private static final class FakeMethodInvocation implements MethodInvocation {

private final Object owner;
private final Method method;
private final Object[] arguments;

/**
* Build a new instance
*
* @param owner Instance of the object containing the method
* @param method The called method
*/
public FakeMethodInvocation(final Object owner,
final Method method) {

this(owner, method, new Object[0]);
}

/**
* Build a new instance
*
* @param owner Instance of the object containing the method
* @param method The called method
* @param arguments The method arguments
*/
public FakeMethodInvocation(final Object owner,
final Method method,
final Object[] arguments) {

this.owner = owner;
this.method = method;
this.arguments = arguments;
}

@Override
public Method getMethod() {

return method;
}

@Override
public Object[] getArguments() {

return arguments;
}

@Override
public Object proceed() throws Throwable {

return method.invoke(owner, arguments);
}

@Override
public Object getThis() {

return owner;
}

@Override
public AccessibleObject getStaticPart() {

throw new UnsupportedOperationException();
}
}

/**
* Dummy service.
*/
private static final class DummService {

public String accept() {

// This method is here to simulate the fact that fallback
// method is not found at the first foreach turn
return "ACCEPT";
}

public String done() {

// This method is here to simulate the fact that fallback
// method is not found at the first foreach turn
return "DONE";
}

public String fallback() {

return "FALLBACK";
}

@BucketToken(value = "bucketAPI1", fallbackMethod = "fallback")
public String primary() {

return "PRIMARY";
}
}
}