From 9c5510f05d5f0479014d0e05f4a4843aa1c6610c Mon Sep 17 00:00:00 2001 From: Ben McIlwain Date: Thu, 2 Oct 2025 18:15:19 -0400 Subject: [PATCH] Add a rate limiter to remove all domain contacts action (#2838) The maximum QPS defaults to 10, but can also be specified at runtime through use of a query-string parameter. BUG = http://b/439636188 --- .../java/google/registry/batch/BatchModule.java | 16 ++++++++++++++++ .../batch/RemoveAllDomainContactsAction.java | 10 +++++++++- .../batch/RemoveAllDomainContactsActionTest.java | 5 ++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/google/registry/batch/BatchModule.java b/core/src/main/java/google/registry/batch/BatchModule.java index 252e974d3..ada004c57 100644 --- a/core/src/main/java/google/registry/batch/BatchModule.java +++ b/core/src/main/java/google/registry/batch/BatchModule.java @@ -29,9 +29,11 @@ import static google.registry.request.RequestParameters.extractRequiredParameter import static google.registry.request.RequestParameters.extractSetOfDatetimeParameters; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.RateLimiter; import dagger.Module; import dagger.Provides; import google.registry.request.Parameter; +import jakarta.inject.Named; import jakarta.servlet.http.HttpServletRequest; import java.util.Optional; import org.joda.time.DateTime; @@ -137,4 +139,18 @@ public class BatchModule { static boolean provideIsFast(HttpServletRequest req) { return extractBooleanParameter(req, PARAM_FAST); } + + private static final int DEFAULT_MAX_QPS = 10; + + @Provides + @Parameter("maxQps") + static int provideMaxQps(HttpServletRequest req) { + return extractOptionalIntParameter(req, "maxQps").orElse(DEFAULT_MAX_QPS); + } + + @Provides + @Named("removeAllDomainContacts") + static RateLimiter provideRemoveAllDomainContactsRateLimiter(@Parameter("maxQps") int maxQps) { + return RateLimiter.create(maxQps); + } } diff --git a/core/src/main/java/google/registry/batch/RemoveAllDomainContactsAction.java b/core/src/main/java/google/registry/batch/RemoveAllDomainContactsAction.java index 713645e52..0ddc5f242 100644 --- a/core/src/main/java/google/registry/batch/RemoveAllDomainContactsAction.java +++ b/core/src/main/java/google/registry/batch/RemoveAllDomainContactsAction.java @@ -30,6 +30,7 @@ import com.google.common.base.Ascii; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; +import com.google.common.util.concurrent.RateLimiter; import google.registry.config.RegistryConfig.Config; import google.registry.flows.EppController; import google.registry.flows.EppRequestSource; @@ -48,6 +49,7 @@ import google.registry.request.Response; import google.registry.request.auth.Auth; import google.registry.request.lock.LockHandler; import jakarta.inject.Inject; +import jakarta.inject.Named; import java.util.List; import java.util.concurrent.Callable; import java.util.logging.Level; @@ -79,6 +81,7 @@ public class RemoveAllDomainContactsAction implements Runnable { private final EppController eppController; private final String registryAdminClientId; private final LockHandler lockHandler; + private final RateLimiter rateLimiter; private final Response response; private final String updateDomainXml; private int successes = 0; @@ -91,10 +94,12 @@ public class RemoveAllDomainContactsAction implements Runnable { EppController eppController, @Config("registryAdminClientId") String registryAdminClientId, LockHandler lockHandler, + @Named("removeAllDomainContacts") RateLimiter rateLimiter, Response response) { this.eppController = eppController; this.registryAdminClientId = registryAdminClientId; this.lockHandler = lockHandler; + this.rateLimiter = rateLimiter; this.response = response; this.updateDomainXml = readResourceUtf8(RemoveAllDomainContactsAction.class, "domain_remove_contacts.xml"); @@ -146,7 +151,10 @@ public class RemoveAllDomainContactsAction implements Runnable { .setMaxResults(BATCH_SIZE) .getResultList()); - domainRepoIdsBatch.forEach(this::runDomainUpdateFlow); + for (String domainRepoId : domainRepoIdsBatch) { + rateLimiter.acquire(); + runDomainUpdateFlow(domainRepoId); + } } while (!domainRepoIdsBatch.isEmpty()); String msg = String.format( diff --git a/core/src/test/java/google/registry/batch/RemoveAllDomainContactsActionTest.java b/core/src/test/java/google/registry/batch/RemoveAllDomainContactsActionTest.java index 6f7cbbaa7..2d358c9e7 100644 --- a/core/src/test/java/google/registry/batch/RemoveAllDomainContactsActionTest.java +++ b/core/src/test/java/google/registry/batch/RemoveAllDomainContactsActionTest.java @@ -23,9 +23,11 @@ import static google.registry.testing.DatabaseHelper.newDomain; import static google.registry.testing.DatabaseHelper.persistActiveContact; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static org.mockito.Mockito.mock; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.util.concurrent.RateLimiter; import google.registry.flows.DaggerEppTestComponent; import google.registry.flows.EppController; import google.registry.flows.EppTestComponent.FakesAndMocksModule; @@ -51,6 +53,7 @@ class RemoveAllDomainContactsActionTest { new JpaTestExtensions.Builder().buildIntegrationTestExtension(); private final FakeResponse response = new FakeResponse(); + private final RateLimiter rateLimiter = mock(RateLimiter.class); private RemoveAllDomainContactsAction action; @BeforeEach @@ -69,7 +72,7 @@ class RemoveAllDomainContactsActionTest { .eppController(); action = new RemoveAllDomainContactsAction( - eppController, "NewRegistrar", new FakeLockHandler(true), response); + eppController, "NewRegistrar", new FakeLockHandler(true), rateLimiter, response); } @Test