diff --git a/.gitignore b/.gitignore index 55dfa75..50beca6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.settings +.classpath +.project +bin/ # Package Files # *.jar diff --git a/ratelimitj-core/src/main/java/es/moki/ratelimitj/core/limiter/request/ReactiveRequestRateLimiter.java b/ratelimitj-core/src/main/java/es/moki/ratelimitj/core/limiter/request/ReactiveRequestRateLimiter.java index f6a9f2a..43af62e 100644 --- a/ratelimitj-core/src/main/java/es/moki/ratelimitj/core/limiter/request/ReactiveRequestRateLimiter.java +++ b/ratelimitj-core/src/main/java/es/moki/ratelimitj/core/limiter/request/ReactiveRequestRateLimiter.java @@ -11,6 +11,8 @@ public interface ReactiveRequestRateLimiter { Mono overLimitWhenIncrementedReactive(String key); Mono overLimitWhenIncrementedReactive(String key, int weight); + + Mono incrementRegardlessReactive(String key, int weight); Mono geLimitWhenIncrementedReactive(String key); diff --git a/ratelimitj-core/src/main/java/es/moki/ratelimitj/core/limiter/request/RequestRateLimiter.java b/ratelimitj-core/src/main/java/es/moki/ratelimitj/core/limiter/request/RequestRateLimiter.java index 53e93b5..bab2482 100644 --- a/ratelimitj-core/src/main/java/es/moki/ratelimitj/core/limiter/request/RequestRateLimiter.java +++ b/ratelimitj-core/src/main/java/es/moki/ratelimitj/core/limiter/request/RequestRateLimiter.java @@ -11,6 +11,15 @@ public interface RequestRateLimiter { * @return {@code true} if the key is over the limit, otherwise {@code false} */ boolean overLimitWhenIncremented(String key); + + /** + * Regardless of being over limit, we still want to track the count, this is for a client that is going to act + * regardless of a limit, but, still needs to be counted towareds said limit. + * @param key + * @param weight + * @return {@code true} if the key is over the limit, otherwise {@code false} + */ + boolean incrementRegardless(String key, int weight); /** * Determine if the given key, after incrementing by the given weight, has exceeded the configured rate limit. @@ -55,4 +64,5 @@ public interface RequestRateLimiter { * @return {@code true} if the key existed, otherwise {@code false} . */ boolean resetLimit(String key); + } diff --git a/ratelimitj-hazelcast/src/main/java/es/moki/ratelimitj/hazelcast/HazelcastSlidingWindowRequestRateLimiter.java b/ratelimitj-hazelcast/src/main/java/es/moki/ratelimitj/hazelcast/HazelcastSlidingWindowRequestRateLimiter.java index 27a03b3..ebad1da 100644 --- a/ratelimitj-hazelcast/src/main/java/es/moki/ratelimitj/hazelcast/HazelcastSlidingWindowRequestRateLimiter.java +++ b/ratelimitj-hazelcast/src/main/java/es/moki/ratelimitj/hazelcast/HazelcastSlidingWindowRequestRateLimiter.java @@ -50,7 +50,7 @@ public boolean overLimitWhenIncremented(String key) { // TODO support muli keys @Override public boolean overLimitWhenIncremented(String key, int weight) { - return eqOrGeLimit(key, weight, true); + return eqOrGeLimit(key, weight, true, false); } @Override @@ -60,20 +60,19 @@ public boolean geLimitWhenIncremented(String key) { @Override public boolean geLimitWhenIncremented(String key, int weight) { - return eqOrGeLimit(key, weight, false); + return eqOrGeLimit(key, weight, false, false); } - -// @Override -// public boolean isOverLimit(String key) { -// return overLimitWhenIncremented(key, 0); -// } -// -// @Override -// public boolean isGeLimit(String key) { -// return geLimitWhenIncremented(key, 0); -// } - + + + @Override + public boolean incrementRegardless(String key, int weight) { + return eqOrGeLimit(key, weight, false, true); + } + + + + @Override public boolean resetLimit(String key) { IMap map = hz.getMap(key); if (map == null || map.isEmpty()) { @@ -92,7 +91,7 @@ private IMap getMap(String key, int longestDuration) { return hz.getMap(key); } - private boolean eqOrGeLimit(String key, int weight, boolean strictlyGreater) { + private boolean eqOrGeLimit(String key, int weight, boolean strictlyGreater, boolean increment_anyway) { requireNonNull(key, "key cannot be null"); requireNonNull(rules, "rules cannot be null"); @@ -152,7 +151,10 @@ private boolean eqOrGeLimit(String key, int weight, boolean strictlyGreater) { // check our limits long count = coalesce(cur, 0L) + weight; if (count > rule.getLimit()) { - return true; // over limit, don't record request + geLimit = true; + if (!increment_anyway) { + return true; // over limit, don't record request + } } else if (!strictlyGreater && count == rule.getLimit()) { geLimit = true; // at limit, do record request } diff --git a/ratelimitj-hazelcast/src/test/java/es/moki/ratelimitj/hazelcast/HazelcastRequestRateLimiterInternalTest.java b/ratelimitj-hazelcast/src/test/java/es/moki/ratelimitj/hazelcast/HazelcastRequestRateLimiterInternalTest.java index 6886c05..5389bfb 100644 --- a/ratelimitj-hazelcast/src/test/java/es/moki/ratelimitj/hazelcast/HazelcastRequestRateLimiterInternalTest.java +++ b/ratelimitj-hazelcast/src/test/java/es/moki/ratelimitj/hazelcast/HazelcastRequestRateLimiterInternalTest.java @@ -59,10 +59,41 @@ void shouldEventuallyCleanUpExpiredKeys() throws Exception { }); IMap map = hz.getMap(key); + while (map.size() != 0) { Thread.sleep(10); } assertThat(map.size()).isZero(); } + + @Test + void shouldCheckIncrementAnyway() throws Exception { + + ImmutableSet rules = ImmutableSet.of(RequestLimitRule.of(20, TimeUnit.SECONDS, 5)); + RequestRateLimiter requestRateLimiter = getRateLimiter(rules, timeBandit); + + String key = "ip:127.0.0.5"; + + timeBandit.addUnixTimeMilliSeconds(100L); + // Counter should be 0 now + assertThat(requestRateLimiter.overLimitWhenIncremented(key, 1)).isFalse(); + // Counter should be 1 now and 'false', meaning not over limit + + timeBandit.addUnixTimeMilliSeconds(100L); + assertThat(requestRateLimiter.incrementRegardless(key, 10)).isTrue(); + // Counter should be 11 now and 'true', meaning over limit + + timeBandit.addUnixTimeMilliSeconds(100L); + assertThat(requestRateLimiter.overLimitWhenIncremented(key, 1)).isTrue(); + // Counter should be 11 now and 'true', meaning over limit + // If the increment regardless worked, then the value would have been 2 + // which would not be under the limit. + + + + + } + + } diff --git a/ratelimitj-inmemory/src/main/java/es/moki/ratelimitj/inmemory/request/InMemorySlidingWindowRequestRateLimiter.java b/ratelimitj-inmemory/src/main/java/es/moki/ratelimitj/inmemory/request/InMemorySlidingWindowRequestRateLimiter.java index 32cfeb4..987f7bc 100644 --- a/ratelimitj-inmemory/src/main/java/es/moki/ratelimitj/inmemory/request/InMemorySlidingWindowRequestRateLimiter.java +++ b/ratelimitj-inmemory/src/main/java/es/moki/ratelimitj/inmemory/request/InMemorySlidingWindowRequestRateLimiter.java @@ -59,9 +59,14 @@ public boolean overLimitWhenIncremented(String key) { @Override public boolean overLimitWhenIncremented(String key, int weight) { - return eqOrGeLimit(key, weight, true); + return eqOrGeLimit(key, weight, true, false); } + @Override + public boolean incrementRegardless(String key, int weight) { + return eqOrGeLimit(key, weight, true, true); + } + @Override public boolean geLimitWhenIncremented(String key) { return geLimitWhenIncremented(key, 1); @@ -69,8 +74,9 @@ public boolean geLimitWhenIncremented(String key) { @Override public boolean geLimitWhenIncremented(String key, int weight) { - return eqOrGeLimit(key, weight, false); + return eqOrGeLimit(key, weight, false, false); } + // @Override // public boolean isOverLimit(String key) { @@ -100,7 +106,9 @@ private ConcurrentMap getMap(String key, int longestDuration) { }); } - private boolean eqOrGeLimit(String key, int weight, boolean strictlyGreater) { + // Chris Fauerbach, adding a way to override the increment even if over limit + // @chrisfauerbach , https://fauie.com , github.com/chrisfauerbach + private boolean eqOrGeLimit(String key, int weight, boolean strictlyGreater, boolean increment_regardless) { requireNonNull(key, "key cannot be null"); requireNonNull(rules, "rules cannot be null"); @@ -157,7 +165,11 @@ private boolean eqOrGeLimit(String key, int weight, boolean strictlyGreater) { // check our limits long count = coalesce(cur, 0L) + weight; if (count > rule.getLimit()) { - return true; // over limit, don't record request + if (increment_regardless) { + geLimit = true; + }else { + return true; // over limit, don't record request + } } else if (!strictlyGreater && count == rule.getLimit()) { geLimit = true; // at limit, do record request } diff --git a/ratelimitj-inmemory/src/test/java/es/moki/ratelimitj/inmemory/request/InMemoryRequestRateLimiterInternalTest.java b/ratelimitj-inmemory/src/test/java/es/moki/ratelimitj/inmemory/request/InMemoryRequestRateLimiterInternalTest.java index bbd27d5..31e2362 100644 --- a/ratelimitj-inmemory/src/test/java/es/moki/ratelimitj/inmemory/request/InMemoryRequestRateLimiterInternalTest.java +++ b/ratelimitj-inmemory/src/test/java/es/moki/ratelimitj/inmemory/request/InMemoryRequestRateLimiterInternalTest.java @@ -59,4 +59,33 @@ void shouldEventuallyCleanUpExpiredKeys() throws Exception { assertThat(expiryingKeyMap.size()).isZero(); } + + @Test + void shouldCheckIncrementAnyway() throws Exception { + + ImmutableSet rules = ImmutableSet.of(RequestLimitRule.of(20, TimeUnit.SECONDS, 5)); + RequestRateLimiter requestRateLimiter = getRateLimiter(rules, timeBandit); + + String key = "ip:127.0.0.5"; + + timeBandit.addUnixTimeMilliSeconds(100L); + // Counter should be 0 now + assertThat(requestRateLimiter.overLimitWhenIncremented(key, 1)).isFalse(); + // Counter should be 1 now and 'false', meaning not over limit + + timeBandit.addUnixTimeMilliSeconds(100L); + assertThat(requestRateLimiter.incrementRegardless(key, 10)).isTrue(); + // Counter should be 11 now and 'true', meaning over limit + + timeBandit.addUnixTimeMilliSeconds(100L); + assertThat(requestRateLimiter.overLimitWhenIncremented(key, 1)).isTrue(); + // Counter should be 11 now and 'true', meaning over limit + // If the increment regardless worked, then the value would have been 2 + // which would not be under the limit. + + + + + } + } diff --git a/ratelimitj-redis/src/main/java/es/moki/ratelimitj/redis/request/RedisSlidingWindowRequestRateLimiter.java b/ratelimitj-redis/src/main/java/es/moki/ratelimitj/redis/request/RedisSlidingWindowRequestRateLimiter.java index 04a4575..e737d83 100644 --- a/ratelimitj-redis/src/main/java/es/moki/ratelimitj/redis/request/RedisSlidingWindowRequestRateLimiter.java +++ b/ratelimitj-redis/src/main/java/es/moki/ratelimitj/redis/request/RedisSlidingWindowRequestRateLimiter.java @@ -82,7 +82,7 @@ public boolean overLimitWhenIncremented(String key) { @Override public boolean overLimitWhenIncremented(String key, int weight) { - return throwOnTimeout(eqOrGeLimitReactive(key, weight, true)); + return throwOnTimeout(eqOrGeLimitReactive(key, weight, true, false)); } @Override @@ -92,7 +92,12 @@ public boolean geLimitWhenIncremented(String key) { @Override public boolean geLimitWhenIncremented(String key, int weight) { - return throwOnTimeout(eqOrGeLimitReactive(key, weight, false)); + return throwOnTimeout(eqOrGeLimitReactive(key, weight, false, false)); + } + + @Override + public boolean incrementRegardless(String key, int weight) { + return throwOnTimeout(eqOrGeLimitReactive(key, weight, false, true)); } // @Override @@ -117,7 +122,7 @@ public Mono overLimitWhenIncrementedReactive(String key) { @Override public Mono overLimitWhenIncrementedReactive(String key, int weight) { - return eqOrGeLimitReactive(key, weight, true); + return eqOrGeLimitReactive(key, weight, true, false); } @Override @@ -127,7 +132,12 @@ public Mono geLimitWhenIncrementedReactive(String key) { @Override public Mono geLimitWhenIncrementedReactive(String key, int weight) { - return eqOrGeLimitReactive(key, weight, false); + return eqOrGeLimitReactive(key, weight, false, false); + } + + @Override + public Mono incrementRegardlessReactive(String key, int weight) { + return eqOrGeLimitReactive(key, weight, true, true); } @Override @@ -136,18 +146,19 @@ public Mono resetLimitReactive(String key) { } private CompletionStage eqOrGeLimitAsync(String key, int weight, boolean strictlyGreater) { - return eqOrGeLimitReactive(key, weight, strictlyGreater).toFuture(); + return eqOrGeLimitReactive(key, weight, strictlyGreater, false).toFuture(); } - private Mono eqOrGeLimitReactive(String key, int weight, boolean strictlyGreater) { + + + private Mono eqOrGeLimitReactive(String key, int weight, boolean strictlyGreater, boolean incremement_regardless) { requireNonNull(key); // TODO script load can be reactive // TODO handle scenario where script is not loaded, flush scripts and test scenario String sha = scriptLoader.scriptSha(); - return timeSupplier.getReactive().flatMapMany(time -> - connection.reactive().evalsha(sha, VALUE, new String[]{key}, rulesJson, Long.toString(time), Integer.toString(weight), toStringOneZero(strictlyGreater))) + connection.reactive().evalsha(sha, VALUE, new String[]{key}, rulesJson, Long.toString(time), Integer.toString(weight), toStringOneZero(strictlyGreater), toStringOneZero(incremement_regardless))) .next() .map("1"::equals) .doOnSuccess(over -> { diff --git a/ratelimitj-redis/src/main/resources/sliding-window-ratelimit.lua b/ratelimitj-redis/src/main/resources/sliding-window-ratelimit.lua index f768653..0d26ae4 100644 --- a/ratelimitj-redis/src/main/resources/sliding-window-ratelimit.lua +++ b/ratelimitj-redis/src/main/resources/sliding-window-ratelimit.lua @@ -4,6 +4,11 @@ local limits = cjson.decode(ARGV[1]) local now = tonumber(ARGV[2]) local weight = tonumber(ARGV[3] or '1') local strictly_greater = tonumber(ARGV[4] or '1') == 1 + +--Chris Fauerbach, adding a way to override the increment even if over limit +--@chrisfauerbach , https://fauie.com , github.com/chrisfauerbach +local incr_regardless = tonumber(ARGV[5] or '1') == 1 + local longest_duration = limits[1][1] or 0 local saved_keys = {} local ge_limit = '0' @@ -55,7 +60,12 @@ for i, limit in ipairs(limits) do -- check our limits local count = tonumber(cur or '0') + weight if count > limit[2] then - return '1' -- over limit, don't record request + -- Checking the overlimit, but allowing the increment + if incr_regardless then + ge_limit= '1' + else + return '1' -- over limit, don't record request + end elseif count == limit[2] and not strictly_greater then ge_limit = '1' -- at limit, do record request end diff --git a/ratelimitj-redis/src/test/java/es/moki/ratelimitj/redis/request/RedisSlidingWindowSyncRequestRateLimiterTest.java b/ratelimitj-redis/src/test/java/es/moki/ratelimitj/redis/request/RedisSlidingWindowSyncRequestRateLimiterTest.java index fecc083..2434e32 100644 --- a/ratelimitj-redis/src/test/java/es/moki/ratelimitj/redis/request/RedisSlidingWindowSyncRequestRateLimiterTest.java +++ b/ratelimitj-redis/src/test/java/es/moki/ratelimitj/redis/request/RedisSlidingWindowSyncRequestRateLimiterTest.java @@ -4,20 +4,30 @@ import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter; import es.moki.ratelimitj.core.time.TimeSupplier; import es.moki.ratelimitj.test.limiter.request.AbstractSyncRequestRateLimiterTest; +import es.moki.ratelimitj.test.time.TimeBanditSupplier; import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableSet; + +import static org.assertj.core.api.Assertions.assertThat; import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RedisSlidingWindowSyncRequestRateLimiterTest extends AbstractSyncRequestRateLimiterTest { private static RedisClient client; private static StatefulRedisConnection connect; + private static Logger LOG = LoggerFactory.getLogger(RedisSlidingWindowSyncRequestRateLimiterTest.class); @BeforeAll static void beforeAll() { @@ -42,4 +52,33 @@ void afterEach() { protected RequestRateLimiter getRateLimiter(Set rules, TimeSupplier timeSupplier) { return new RedisSlidingWindowRequestRateLimiter(connect, rules, timeSupplier); } + + + @Test + void shouldCheckIncrementAnyway() throws Exception { + + ImmutableSet rules = ImmutableSet.of(RequestLimitRule.of(20, TimeUnit.SECONDS, 5)); + TimeBanditSupplier timeBandit = new TimeBanditSupplier(); + + RequestRateLimiter requestRateLimiter = getRateLimiter(rules, timeBandit); + + String key = "ip:127.0.0.5"; + + timeBandit.addUnixTimeMilliSeconds(100L); + // Counter should be 0 now + assertThat(requestRateLimiter.overLimitWhenIncremented(key, 1)).isFalse(); + // Counter should be 1 now and 'false', meaning not over limit + LOG.debug("About to print add 10 to go over."); + timeBandit.addUnixTimeMilliSeconds(100L); + assertThat(requestRateLimiter.incrementRegardless(key, 10)).isTrue(); + // Counter should be 11 now and 'true', meaning over limit + + LOG.debug("About to print add 1 that should be over.."); + timeBandit.addUnixTimeMilliSeconds(100L); + assertThat(requestRateLimiter.overLimitWhenIncremented(key, 1)).isTrue(); + // Counter should be 11 now and 'true', meaning over limit + // If the increment regardless worked, then the value would have been 2 + // which would not be under the limit. + } + }