mirror of
https://github.com/google/nomulus
synced 2026-08-26 00:56:23 +00:00
Refactor into a QuotaManager interface to have a noop version (#3214)
This means that we don't need to worry about checking if the Valkey connection is active
This commit is contained in:
@@ -27,6 +27,8 @@ import google.registry.eppserver.handler.EppServiceHandler;
|
||||
import google.registry.eppserver.quota.EppServerQuotaManager;
|
||||
import google.registry.networking.handler.SslServerInitializer;
|
||||
import google.registry.quota.GenericValkeyQuotaManager;
|
||||
import google.registry.quota.NoopQuotaManager;
|
||||
import google.registry.quota.QuotaManager;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
|
||||
@@ -140,7 +142,10 @@ public final class EppProtocolModule {
|
||||
@CommandQuota
|
||||
static EppServerQuotaManager provideCommandQuotaManager(
|
||||
@Config("eppServerQuota") RegistryConfigSettings.Quota quota, Optional<UnifiedJedis> jedis) {
|
||||
return new EppServerQuotaManager(
|
||||
quota, new GenericValkeyQuotaManager(jedis.orElse(null), "command"));
|
||||
QuotaManager quotaManager =
|
||||
jedis.isPresent()
|
||||
? GenericValkeyQuotaManager.create(jedis.get(), "command")
|
||||
: new NoopQuotaManager();
|
||||
return new EppServerQuotaManager(quota, quotaManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package google.registry.eppserver.quota;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.config.RegistryConfigSettings.Quota;
|
||||
import google.registry.config.RegistryConfigSettings.Quota.QuotaGroup;
|
||||
import google.registry.quota.GenericValkeyQuotaManager;
|
||||
import google.registry.quota.QuotaManager;
|
||||
import java.time.Duration;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@@ -31,11 +31,11 @@ public class EppServerQuotaManager {
|
||||
|
||||
private static final Duration DEFAULT_TTL = Duration.ofHours(1);
|
||||
|
||||
private final GenericValkeyQuotaManager quotaManager;
|
||||
private final QuotaManager quotaManager;
|
||||
private final QuotaGroup defaultQuota;
|
||||
private final ImmutableMap<String, QuotaGroup> customQuotas;
|
||||
|
||||
public EppServerQuotaManager(Quota quota, GenericValkeyQuotaManager quotaManager) {
|
||||
public EppServerQuotaManager(Quota quota, QuotaManager quotaManager) {
|
||||
this.quotaManager = quotaManager;
|
||||
this.defaultQuota = quota.defaultQuota;
|
||||
|
||||
|
||||
@@ -15,18 +15,18 @@
|
||||
package google.registry.quota;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.net.URLEncoder;
|
||||
import java.time.Duration;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import redis.clients.jedis.UnifiedJedis;
|
||||
|
||||
/** Generic quota manager that uses Redis/Valkey as the backing store. */
|
||||
@ThreadSafe
|
||||
public class GenericValkeyQuotaManager {
|
||||
public class GenericValkeyQuotaManager implements QuotaManager {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@@ -63,16 +63,21 @@ public class GenericValkeyQuotaManager {
|
||||
private final UnifiedJedis jedis;
|
||||
private final String namespace;
|
||||
|
||||
public GenericValkeyQuotaManager(@Nullable UnifiedJedis jedis, String namespace) {
|
||||
public static GenericValkeyQuotaManager create(UnifiedJedis jedis, String namespace) {
|
||||
// TODO(gbrodman): upload the scripts to Valkey so we can reference them by hash rather than
|
||||
// uploading the whole script each time
|
||||
return new GenericValkeyQuotaManager(jedis, namespace);
|
||||
}
|
||||
|
||||
private GenericValkeyQuotaManager(UnifiedJedis jedis, String namespace) {
|
||||
checkNotNull(jedis, "UnifiedJedis cannot be null");
|
||||
this.jedis = jedis;
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
/** Attempts to acquire a quota token from Valkey. */
|
||||
@Override
|
||||
public boolean acquireQuota(String id, int maxTokenAmount, Duration expirationDuration) {
|
||||
if (jedis == null) {
|
||||
return true; // Fail open if no Valkey configured
|
||||
}
|
||||
checkArgument(expirationDuration.isPositive(), "Duration must be positive");
|
||||
checkArgument(maxTokenAmount >= 0, "Max token amount must be non-negative");
|
||||
|
||||
@@ -95,10 +100,8 @@ public class GenericValkeyQuotaManager {
|
||||
}
|
||||
|
||||
/** Refreshes the TTL of an existing quota token. */
|
||||
@Override
|
||||
public void refreshQuota(String id, Duration expirationDuration) {
|
||||
if (jedis == null) {
|
||||
return;
|
||||
}
|
||||
checkArgument(expirationDuration.isPositive(), "Duration must be positive");
|
||||
|
||||
String key = createValkeyKey(id);
|
||||
@@ -111,10 +114,8 @@ public class GenericValkeyQuotaManager {
|
||||
}
|
||||
|
||||
/** Returns a token to the pool (used for connection throttling). */
|
||||
@Override
|
||||
public void releaseQuota(String id, int maxTokenAmount) {
|
||||
if (jedis == null) {
|
||||
return;
|
||||
}
|
||||
checkArgument(maxTokenAmount >= 0, "Max token amount must be non-negative");
|
||||
|
||||
String key = createValkeyKey(id);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2026 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.quota;
|
||||
|
||||
import java.time.Duration;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* A {@link QuotaManager} that always returns true and performs no actions.
|
||||
*
|
||||
* <p>Used when Valkey/Jedis are not available.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public class NoopQuotaManager implements QuotaManager {
|
||||
|
||||
@Override
|
||||
public boolean acquireQuota(String id, int maxTokenAmount, Duration expirationDuration) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshQuota(String id, Duration expirationDuration) {}
|
||||
|
||||
@Override
|
||||
public void releaseQuota(String id, int maxTokenAmount) {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2026 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.quota;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/** Interface for managing quota. */
|
||||
public interface QuotaManager {
|
||||
/** Attempts to acquire a token (out of the given max amount) with the given TTL. */
|
||||
boolean acquireQuota(String id, int maxTokenAmount, Duration expirationDuration);
|
||||
|
||||
/** Refreshes the TTL of an existing token. */
|
||||
void refreshQuota(String id, Duration expirationDuration);
|
||||
|
||||
/** Returns a token to the pool (possibly useful for connection throttling). */
|
||||
void releaseQuota(String id, int maxTokenAmount);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import static org.mockito.Mockito.when;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.config.RegistryConfigSettings.Quota;
|
||||
import google.registry.config.RegistryConfigSettings.Quota.QuotaGroup;
|
||||
import google.registry.quota.GenericValkeyQuotaManager;
|
||||
import google.registry.quota.QuotaManager;
|
||||
import java.time.Duration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -34,7 +34,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EppServerQuotaManagerTest {
|
||||
|
||||
@Mock private GenericValkeyQuotaManager quotaManager;
|
||||
@Mock private QuotaManager quotaManager;
|
||||
|
||||
private Quota quotaConfig;
|
||||
private EppServerQuotaManager manager;
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.quota;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import io.github.ss_bhatt.testcontainers.valkey.ValkeyContainer;
|
||||
import java.time.Duration;
|
||||
@@ -41,7 +42,7 @@ class GenericValkeyQuotaManagerTest {
|
||||
.hostAndPort(new HostAndPort(valkey.getHost(), valkey.getFirstMappedPort()))
|
||||
.build();
|
||||
jedis.flushAll();
|
||||
quotaManager = new GenericValkeyQuotaManager(jedis, "testQuota");
|
||||
quotaManager = GenericValkeyQuotaManager.create(jedis, "testQuota");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -76,7 +77,7 @@ class GenericValkeyQuotaManagerTest {
|
||||
@Test
|
||||
void testAcquireQuota_isolatedByNamespaceAndId() {
|
||||
GenericValkeyQuotaManager otherQuotaManager =
|
||||
new GenericValkeyQuotaManager(jedis, "otherQuota");
|
||||
GenericValkeyQuotaManager.create(jedis, "otherQuota");
|
||||
|
||||
assertThat(quotaManager.acquireQuota("user1", 1, Duration.ofMinutes(1))).isTrue();
|
||||
assertThat(quotaManager.acquireQuota("user1", 1, Duration.ofMinutes(1))).isFalse();
|
||||
@@ -89,9 +90,9 @@ class GenericValkeyQuotaManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcquireQuota_nullJedis_failsOpen() {
|
||||
GenericValkeyQuotaManager nullJedisManager = new GenericValkeyQuotaManager(null, "testQuota");
|
||||
assertThat(nullJedisManager.acquireQuota("user2", 10, Duration.ofMinutes(1))).isTrue();
|
||||
void testCreate_nullJedis_throwsNpe() {
|
||||
assertThrows(
|
||||
NullPointerException.class, () -> GenericValkeyQuotaManager.create(null, "testQuota"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -114,12 +115,6 @@ class GenericValkeyQuotaManagerTest {
|
||||
assertThat(jedis.exists("testQuota:nonexistent")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRefreshQuota_nullJedis_noop() {
|
||||
GenericValkeyQuotaManager nullJedisManager = new GenericValkeyQuotaManager(null, "testQuota");
|
||||
assertDoesNotThrow(() -> nullJedisManager.refreshQuota("user2", Duration.ofMinutes(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRefreshQuota_jedisException_handled() {
|
||||
jedis.close();
|
||||
@@ -156,12 +151,6 @@ class GenericValkeyQuotaManagerTest {
|
||||
assertThat(jedis.exists("testQuota:nonexistent")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReleaseQuota_nullJedis_noop() {
|
||||
GenericValkeyQuotaManager nullJedisManager = new GenericValkeyQuotaManager(null, "testQuota");
|
||||
assertDoesNotThrow(() -> nullJedisManager.releaseQuota("user2", 10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReleaseQuota_jedisException_handled() {
|
||||
jedis.close();
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2026 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.quota;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class NoopQuotaManagerTest {
|
||||
|
||||
private final NoopQuotaManager quotaManager = new NoopQuotaManager();
|
||||
|
||||
@Test
|
||||
void testAcquireQuota_returnsTrue() {
|
||||
assertThat(quotaManager.acquireQuota("user1", 10, Duration.ofMinutes(1))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRefreshQuota_noop() {
|
||||
assertDoesNotThrow(() -> quotaManager.refreshQuota("user1", Duration.ofMinutes(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReleaseQuota_noop() {
|
||||
assertDoesNotThrow(() -> quotaManager.releaseQuota("user1", 10));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user