Configure fail-open policy on individual rate limiters

This commit is contained in:
Jon Chambers
2025-03-28 16:45:05 -04:00
committed by Jon Chambers
parent e9bd5da2c3
commit 771a700acd
10 changed files with 93 additions and 113 deletions

View File

@@ -8,7 +8,6 @@ package org.whispersystems.textsecuregcm.limits;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@@ -16,15 +15,15 @@ class RateLimiterConfigTest {
@Test
void leakRatePerMillis() {
assertEquals(0.001, new RateLimiterConfig(1, Duration.ofSeconds(1)).leakRatePerMillis());
assertEquals(1e6, new RateLimiterConfig(1, Duration.ofNanos(1)).leakRatePerMillis());
assertEquals(0.001, new RateLimiterConfig(1, Duration.ofSeconds(1), false).leakRatePerMillis());
assertEquals(1e6, new RateLimiterConfig(1, Duration.ofNanos(1), false).leakRatePerMillis());
}
@Test
void isRegenerationRatePositive() {
assertTrue(new RateLimiterConfig(1, Duration.ofSeconds(1)).hasPositiveRegenerationRate());
assertTrue(new RateLimiterConfig(1, Duration.ofNanos(1)).hasPositiveRegenerationRate());
assertFalse(new RateLimiterConfig(1, Duration.ZERO).hasPositiveRegenerationRate());
assertFalse(new RateLimiterConfig(1, Duration.ofSeconds(-1)).hasPositiveRegenerationRate());
assertTrue(new RateLimiterConfig(1, Duration.ofSeconds(1), false).hasPositiveRegenerationRate());
assertTrue(new RateLimiterConfig(1, Duration.ofNanos(1), false).hasPositiveRegenerationRate());
assertFalse(new RateLimiterConfig(1, Duration.ZERO, false).hasPositiveRegenerationRate());
assertFalse(new RateLimiterConfig(1, Duration.ofSeconds(-1), false).hasPositiveRegenerationRate());
}
}

View File

@@ -5,6 +5,7 @@
package org.whispersystems.textsecuregcm.limits;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -22,8 +23,9 @@ import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitPolicy;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient;
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
@@ -57,7 +59,7 @@ public class RateLimitersLuaScriptTest {
final RateLimiters.For descriptor = RateLimiters.For.REGISTRATION;
final FaultTolerantRedisClusterClient redisCluster = REDIS_CLUSTER_EXTENSION.getRedisCluster();
final RateLimiters limiters = new RateLimiters(
Map.of(descriptor.id(), new RateLimiterConfig(60, Duration.ofSeconds(1))),
Map.of(descriptor.id(), new RateLimiterConfig(60, Duration.ofSeconds(1), false)),
dynamicConfig,
RateLimiters.defaultScript(redisCluster),
redisCluster,
@@ -74,7 +76,7 @@ public class RateLimitersLuaScriptTest {
final RateLimiters.For descriptor = RateLimiters.For.REGISTRATION;
final FaultTolerantRedisClusterClient redisCluster = REDIS_CLUSTER_EXTENSION.getRedisCluster();
final RateLimiters limiters = new RateLimiters(
Map.of(descriptor.id(), new RateLimiterConfig(1000, Duration.ofSeconds(1))),
Map.of(descriptor.id(), new RateLimiterConfig(1000, Duration.ofSeconds(1), false)),
dynamicConfig,
RateLimiters.defaultScript(redisCluster),
redisCluster,
@@ -119,20 +121,25 @@ public class RateLimitersLuaScriptTest {
assertEquals(750L, decodeBucket(key).orElseThrow().tokensRemaining);
}
@Test
public void testFailOpen() throws Exception {
when(configuration.getRateLimitPolicy()).thenReturn(new DynamicRateLimitPolicy(true));
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testFailOpen(final boolean failOpen) {
final RateLimiters.For descriptor = RateLimiters.For.REGISTRATION;
final FaultTolerantRedisClusterClient redisCluster = mock(FaultTolerantRedisClusterClient.class);
final RateLimiters limiters = new RateLimiters(
Map.of(descriptor.id(), new RateLimiterConfig(1000, Duration.ofSeconds(1))),
Map.of(descriptor.id(), new RateLimiterConfig(1000, Duration.ofSeconds(1), failOpen)),
dynamicConfig,
RateLimiters.defaultScript(redisCluster),
redisCluster,
Clock.systemUTC());
when(redisCluster.withCluster(any())).thenThrow(new RedisException("fail"));
final RateLimiter rateLimiter = limiters.forDescriptor(descriptor);
rateLimiter.validate("test", 200);
if (failOpen) {
assertDoesNotThrow(() -> rateLimiter.validate("test", 200));
} else {
assertThrows(RedisException.class, () -> rateLimiter.validate("test", 200));
}
}
private String serializeToOldBucketValueFormat(

View File

@@ -5,9 +5,9 @@
package org.whispersystems.textsecuregcm.limits;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -20,7 +20,6 @@ import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitPolicy;
import org.whispersystems.textsecuregcm.redis.ClusterLuaScript;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
@@ -56,30 +55,31 @@ public class RateLimitersTest {
prekeys:
bucketSize: 150
permitRegenerationDuration: PT6S
failOpen: true
attachmentCreate:
bucketSize: 4
permitRegenerationDuration: PT30S
rateLimitPolicy:
failOpen: true
failOpen: true
""";
public record GenericHolder(
@Valid @NotNull @JsonProperty Map<String, RateLimiterConfig> limits,
@Valid @JsonProperty DynamicRateLimitPolicy rateLimitPolicy) {
public record SimpleDynamicConfiguration(@Valid @NotNull @JsonProperty Map<String, RateLimiterConfig> limits) {
}
@Test
public void testValidateConfigs() throws Exception {
assertThrows(IllegalArgumentException.class, () -> {
final GenericHolder cfg = DynamicConfigurationManager.parseConfiguration(BAD_YAML, GenericHolder.class).orElseThrow();
final RateLimiters rateLimiters = new RateLimiters(cfg.limits(), dynamicConfig, validateScript, redisCluster, clock);
final SimpleDynamicConfiguration dynamicConfiguration =
DynamicConfigurationManager.parseConfiguration(BAD_YAML, SimpleDynamicConfiguration.class).orElseThrow();
final RateLimiters rateLimiters = new RateLimiters(dynamicConfiguration.limits(), dynamicConfig, validateScript, redisCluster, clock);
rateLimiters.validateValuesAndConfigs();
});
final GenericHolder cfg = DynamicConfigurationManager.parseConfiguration(GOOD_YAML, GenericHolder.class).orElseThrow();
assertTrue(cfg.rateLimitPolicy.failOpen());
final RateLimiters rateLimiters = new RateLimiters(cfg.limits(), dynamicConfig, validateScript, redisCluster, clock);
rateLimiters.validateValuesAndConfigs();
final SimpleDynamicConfiguration dynamicConfiguration =
DynamicConfigurationManager.parseConfiguration(GOOD_YAML, SimpleDynamicConfiguration.class).orElseThrow();
final RateLimiters rateLimiters = new RateLimiters(dynamicConfiguration.limits(), dynamicConfig, validateScript, redisCluster, clock);
assertDoesNotThrow(rateLimiters::validateValuesAndConfigs);
}
@Test
@@ -116,9 +116,9 @@ public class RateLimitersTest {
@Test
void testChangingConfiguration() {
final RateLimiterConfig initialRateLimiterConfig = new RateLimiterConfig(4, Duration.ofMinutes(1));
final RateLimiterConfig updatedRateLimiterCongig = new RateLimiterConfig(17, Duration.ofSeconds(3));
final RateLimiterConfig baseConfig = new RateLimiterConfig(1, Duration.ofMinutes(1));
final RateLimiterConfig initialRateLimiterConfig = new RateLimiterConfig(4, Duration.ofMinutes(1), false);
final RateLimiterConfig updatedRateLimiterCongig = new RateLimiterConfig(17, Duration.ofSeconds(3), false);
final RateLimiterConfig baseConfig = new RateLimiterConfig(1, Duration.ofMinutes(1), false);
final Map<String, RateLimiterConfig> limitsConfigMap = new HashMap<>();
@@ -146,8 +146,8 @@ public class RateLimitersTest {
@Test
public void testRateLimiterHasItsPrioritiesStraight() throws Exception {
final RateLimiters.For descriptor = RateLimiters.For.CAPTCHA_CHALLENGE_ATTEMPT;
final RateLimiterConfig configForDynamic = new RateLimiterConfig(1, Duration.ofMinutes(1));
final RateLimiterConfig configForStatic = new RateLimiterConfig(2, Duration.ofSeconds(30));
final RateLimiterConfig configForDynamic = new RateLimiterConfig(1, Duration.ofMinutes(1), false);
final RateLimiterConfig configForStatic = new RateLimiterConfig(2, Duration.ofSeconds(30), false);
final RateLimiterConfig defaultConfig = descriptor.defaultConfig();
final Map<String, RateLimiterConfig> mapForDynamic = new HashMap<>();
@@ -188,7 +188,7 @@ public class RateLimitersTest {
@Override
public RateLimiterConfig defaultConfig() {
return new RateLimiterConfig(1, Duration.ofMinutes(1));
return new RateLimiterConfig(1, Duration.ofMinutes(1), false);
}
}