mirror of
https://github.com/google/nomulus
synced 2025-12-23 06:15:42 +00:00
Add cache for deletion times of existing domains (#2840)
This should help in instances of popular domains dropping, since we won't need to do an additional two database loads every time (assuming the deletion time is in the future).
This commit is contained in:
@@ -18,7 +18,6 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
|
||||
import static google.registry.flows.FlowUtils.persistEntityChanges;
|
||||
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
|
||||
import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.COLLISION_MESSAGE;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.checkHasBillingAccount;
|
||||
@@ -224,6 +223,7 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
@Inject DomainCreateFlowCustomLogic flowCustomLogic;
|
||||
@Inject DomainFlowTmchUtils tmchUtils;
|
||||
@Inject DomainPricingLogic pricingLogic;
|
||||
@Inject DomainDeletionTimeCache domainDeletionTimeCache;
|
||||
|
||||
@Inject DomainCreateFlow() {}
|
||||
|
||||
@@ -239,13 +239,13 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
validateRegistrarIsLoggedIn(registrarId);
|
||||
verifyRegistrarIsActive(registrarId);
|
||||
extensionManager.validate();
|
||||
verifyDomainDoesNotExist();
|
||||
DateTime now = tm().getTransactionTime();
|
||||
DomainCommand.Create command = cloneAndLinkReferences((Create) resourceCommand, now);
|
||||
Period period = command.getPeriod();
|
||||
verifyUnitIsYears(period);
|
||||
int years = period.getValue();
|
||||
validateRegistrationPeriod(years);
|
||||
verifyResourceDoesNotExist(Domain.class, targetId, now, registrarId);
|
||||
// Validate that this is actually a legal domain name on a TLD that the registrar has access to.
|
||||
InternetDomainName domainName = validateDomainName(command.getDomainName());
|
||||
String domainLabel = domainName.parts().getFirst();
|
||||
@@ -649,6 +649,15 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
.build();
|
||||
}
|
||||
|
||||
private void verifyDomainDoesNotExist() throws ResourceCreateContentionException {
|
||||
Optional<DateTime> previousDeletionTime =
|
||||
domainDeletionTimeCache.getDeletionTimeForDomain(targetId);
|
||||
if (previousDeletionTime.isPresent()
|
||||
&& !tm().getTransactionTime().isAfter(previousDeletionTime.get())) {
|
||||
throw new ResourceCreateContentionException(targetId);
|
||||
}
|
||||
}
|
||||
|
||||
private static BillingEvent createEapBillingEvent(
|
||||
FeesAndCredits feesAndCredits, BillingEvent createBillingEvent) {
|
||||
return new BillingEvent.Builder()
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2025 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.flows.domain;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.CacheLoader;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.Expiry;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Functionally-static loading cache that keeps track of deletion (AKA drop) times for domains.
|
||||
*
|
||||
* <p>Some domain names may have many create requests issued shortly before (and directly after) the
|
||||
* name is released due to a previous registrant deleting it. In those cases, caching the deletion
|
||||
* time of the existing domain allows us to short-circuit the request and avoid any load on the
|
||||
* database checking the existing domain (at least, in cases where the request hits a particular
|
||||
* node more than once).
|
||||
*
|
||||
* <p>The cache is fairly short-lived (as we're concerned about many requests at basically the same
|
||||
* time), and entries also expire when the drop actually happens. If the domain is re-created after
|
||||
* a drop, the next load attempt will populate the cache with a deletion time of END_OF_TIME, which
|
||||
* will be read from the cache by subsequent attempts.
|
||||
*
|
||||
* <p>We take advantage of the fact that Caffeine caches don't store nulls returned from the
|
||||
* CacheLoader, so a null result (meaning the domain doesn't exist) won't affect future calls (this
|
||||
* avoids a stale-cache situation where the cache "thinks" the domain doesn't exist, but it does).
|
||||
* Put another way, if a domain really doesn't exist, we'll re-attempt the database load every time.
|
||||
*
|
||||
* <p>We don't explicitly set the cache inside domain create/delete flows, in case the transaction
|
||||
* fails at commit time. It's better to have stale data, or to require an additional database load,
|
||||
* than to have incorrect data.
|
||||
*
|
||||
* <p>Note: this should be injected as a singleton -- it's essentially static, but we have it as a
|
||||
* non-static object for concurrent testing purposes.
|
||||
*/
|
||||
public class DomainDeletionTimeCache {
|
||||
|
||||
// Max expiry time is ten minutes
|
||||
private static final int MAX_EXPIRY_MILLIS = 10 * 60 * 1000;
|
||||
private static final int MAX_ENTRIES = 500;
|
||||
private static final int NANOS_IN_ONE_MILLISECOND = 100000;
|
||||
|
||||
/**
|
||||
* Expire after the max duration, or after the domain is set to drop (whichever comes first).
|
||||
*
|
||||
* <p>If the domain has already been deleted (the deletion time is <= now), the entry will
|
||||
* immediately be expired/removed.
|
||||
*
|
||||
* <p>NB: the Expiry class requires the return value in <b>nanoseconds</b>, not milliseconds
|
||||
*/
|
||||
private static final Expiry<String, DateTime> EXPIRY_POLICY =
|
||||
new Expiry<>() {
|
||||
@Override
|
||||
public long expireAfterCreate(String key, DateTime value, long currentTime) {
|
||||
long millisUntilDeletion = value.getMillis() - tm().getTransactionTime().getMillis();
|
||||
return NANOS_IN_ONE_MILLISECOND
|
||||
* Math.max(0L, Math.min(MAX_EXPIRY_MILLIS, millisUntilDeletion));
|
||||
}
|
||||
|
||||
/** Reset the time entirely on update, as if we were creating the entry anew. */
|
||||
@Override
|
||||
public long expireAfterUpdate(
|
||||
String key, DateTime value, long currentTime, long currentDuration) {
|
||||
return expireAfterCreate(key, value, currentTime);
|
||||
}
|
||||
|
||||
/** Reads do not change the expiry duration. */
|
||||
@Override
|
||||
public long expireAfterRead(
|
||||
String key, DateTime value, long currentTime, long currentDuration) {
|
||||
return currentDuration;
|
||||
}
|
||||
};
|
||||
|
||||
/** Attempt to load the domain's deletion time if the domain exists. */
|
||||
private static final CacheLoader<String, DateTime> CACHE_LOADER =
|
||||
(domainName) -> {
|
||||
ForeignKeyUtils.MostRecentResource mostRecentResource =
|
||||
ForeignKeyUtils.loadMostRecentResources(
|
||||
Domain.class, ImmutableSet.of(domainName), false)
|
||||
.get(domainName);
|
||||
return mostRecentResource == null ? null : mostRecentResource.deletionTime();
|
||||
};
|
||||
|
||||
public static DomainDeletionTimeCache create() {
|
||||
return new DomainDeletionTimeCache(
|
||||
Caffeine.newBuilder()
|
||||
.expireAfter(EXPIRY_POLICY)
|
||||
.maximumSize(MAX_ENTRIES)
|
||||
.build(CACHE_LOADER));
|
||||
}
|
||||
|
||||
private final LoadingCache<String, DateTime> cache;
|
||||
|
||||
private DomainDeletionTimeCache(LoadingCache<String, DateTime> cache) {
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
/** Returns the domain's deletion time, or null if it doesn't currently exist. */
|
||||
public Optional<DateTime> getDeletionTimeForDomain(String domainName) {
|
||||
return Optional.ofNullable(cache.get(domainName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2025 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.flows.domain;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
/** Dagger module to provide the {@link DomainDeletionTimeCache}. */
|
||||
@Module
|
||||
public class DomainDeletionTimeCacheModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static DomainDeletionTimeCache provideDomainDeletionTimeCache() {
|
||||
return DomainDeletionTimeCache.create();
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public final class ForeignKeyUtils {
|
||||
*/
|
||||
public static <E extends EppResource> ImmutableMap<String, VKey<E>> load(
|
||||
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
|
||||
return load(clazz, foreignKeys, false).entrySet().stream()
|
||||
return loadMostRecentResources(clazz, foreignKeys, false).entrySet().stream()
|
||||
.filter(e -> now.isBefore(e.getValue().deletionTime()))
|
||||
.collect(toImmutableMap(Entry::getKey, e -> VKey.create(clazz, e.getValue().repoId())));
|
||||
}
|
||||
@@ -104,7 +104,8 @@ public final class ForeignKeyUtils {
|
||||
* same max {@code deleteTime}, usually {@code END_OF_TIME}, lest this method throws an error due
|
||||
* to duplicate keys.
|
||||
*/
|
||||
private static <E extends EppResource> ImmutableMap<String, MostRecentResource> load(
|
||||
public static <E extends EppResource>
|
||||
ImmutableMap<String, MostRecentResource> loadMostRecentResources(
|
||||
Class<E> clazz, Collection<String> foreignKeys, boolean useReplicaTm) {
|
||||
String fkProperty = RESOURCE_TYPE_TO_FK_PROPERTY.get(clazz);
|
||||
JpaTransactionManager tmToUse = useReplicaTm ? replicaTm() : tm();
|
||||
@@ -148,7 +149,7 @@ public final class ForeignKeyUtils {
|
||||
ImmutableList<String> foreignKeys =
|
||||
keys.stream().map(key -> (String) key.getKey()).collect(toImmutableList());
|
||||
ImmutableMap<String, MostRecentResource> existingKeys =
|
||||
ForeignKeyUtils.load(clazz, foreignKeys, true);
|
||||
ForeignKeyUtils.loadMostRecentResources(clazz, foreignKeys, true);
|
||||
// The above map only contains keys that exist in the database, so we re-add the
|
||||
// missing ones with Optional.empty() values for caching.
|
||||
return Maps.asMap(
|
||||
@@ -234,7 +235,7 @@ public final class ForeignKeyUtils {
|
||||
e -> VKey.create(clazz, e.getValue().get().repoId())));
|
||||
}
|
||||
|
||||
record MostRecentResource(String repoId, DateTime deletionTime) {
|
||||
public record MostRecentResource(String repoId, DateTime deletionTime) {
|
||||
|
||||
static MostRecentResource create(String repoId, DateTime deletionTime) {
|
||||
return new MostRecentResource(repoId, deletionTime);
|
||||
|
||||
@@ -30,6 +30,7 @@ import google.registry.export.DriveModule;
|
||||
import google.registry.export.sheet.SheetsServiceModule;
|
||||
import google.registry.flows.ServerTridProviderModule;
|
||||
import google.registry.flows.custom.CustomLogicFactoryModule;
|
||||
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
|
||||
import google.registry.groups.DirectoryModule;
|
||||
import google.registry.groups.GmailModule;
|
||||
import google.registry.groups.GroupsModule;
|
||||
@@ -66,6 +67,7 @@ import jakarta.inject.Singleton;
|
||||
CredentialModule.class,
|
||||
CustomLogicFactoryModule.class,
|
||||
DirectoryModule.class,
|
||||
DomainDeletionTimeCacheModule.class,
|
||||
DriveModule.class,
|
||||
GmailModule.class,
|
||||
GroupsModule.class,
|
||||
|
||||
@@ -26,6 +26,7 @@ import google.registry.export.DriveModule;
|
||||
import google.registry.export.sheet.SheetsServiceModule;
|
||||
import google.registry.flows.ServerTridProviderModule;
|
||||
import google.registry.flows.custom.CustomLogicFactoryModule;
|
||||
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
|
||||
import google.registry.groups.DirectoryModule;
|
||||
import google.registry.groups.GmailModule;
|
||||
import google.registry.groups.GroupsModule;
|
||||
@@ -56,6 +57,7 @@ import jakarta.inject.Singleton;
|
||||
CloudTasksUtilsModule.class,
|
||||
CredentialModule.class,
|
||||
CustomLogicFactoryModule.class,
|
||||
DomainDeletionTimeCacheModule.class,
|
||||
DirectoryModule.class,
|
||||
DriveModule.class,
|
||||
GmailModule.class,
|
||||
|
||||
@@ -22,6 +22,7 @@ import google.registry.config.CredentialModule;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.flows.ServerTridProviderModule;
|
||||
import google.registry.flows.custom.CustomLogicFactoryModule;
|
||||
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
|
||||
import google.registry.groups.DirectoryModule;
|
||||
import google.registry.groups.GmailModule;
|
||||
import google.registry.groups.GroupsModule;
|
||||
@@ -50,6 +51,7 @@ import jakarta.inject.Singleton;
|
||||
CustomLogicFactoryModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
DirectoryModule.class,
|
||||
DomainDeletionTimeCacheModule.class,
|
||||
FrontendRequestComponentModule.class,
|
||||
GmailModule.class,
|
||||
GroupsModule.class,
|
||||
|
||||
@@ -23,6 +23,7 @@ import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.export.DriveModule;
|
||||
import google.registry.flows.ServerTridProviderModule;
|
||||
import google.registry.flows.custom.CustomLogicFactoryModule;
|
||||
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
|
||||
import google.registry.groups.DirectoryModule;
|
||||
import google.registry.groups.GroupsModule;
|
||||
import google.registry.groups.GroupssettingsModule;
|
||||
@@ -47,6 +48,7 @@ import jakarta.inject.Singleton;
|
||||
CustomLogicFactoryModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
DirectoryModule.class,
|
||||
DomainDeletionTimeCacheModule.class,
|
||||
DriveModule.class,
|
||||
GroupsModule.class,
|
||||
GroupssettingsModule.class,
|
||||
|
||||
@@ -25,6 +25,7 @@ import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode;
|
||||
import google.registry.flows.custom.CustomLogicFactory;
|
||||
import google.registry.flows.custom.TestCustomLogicFactory;
|
||||
import google.registry.flows.domain.DomainDeletionTimeCache;
|
||||
import google.registry.flows.domain.DomainFlowTmchUtils;
|
||||
import google.registry.monitoring.whitebox.EppMetric;
|
||||
import google.registry.request.RequestScope;
|
||||
@@ -126,6 +127,11 @@ public interface EppTestComponent {
|
||||
ServerTridProvider provideServerTridProvider() {
|
||||
return new FakeServerTridProvider();
|
||||
}
|
||||
|
||||
@Provides
|
||||
DomainDeletionTimeCache provideDomainDeletionTimeCache() {
|
||||
return DomainDeletionTimeCache.create();
|
||||
}
|
||||
}
|
||||
|
||||
class FakeServerTridProvider implements ServerTridProvider {
|
||||
|
||||
@@ -149,7 +149,6 @@ import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeem
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
|
||||
import google.registry.flows.exceptions.ContactsProhibitedException;
|
||||
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
|
||||
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
|
||||
import google.registry.flows.exceptions.ResourceCreateContentionException;
|
||||
import google.registry.model.billing.BillingBase;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
@@ -1238,8 +1237,8 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
void testFailure_alreadyExists() throws Exception {
|
||||
persistContactsAndHosts();
|
||||
persistActiveDomain(getUniqueIdFromCommand());
|
||||
ResourceAlreadyExistsForThisClientException thrown =
|
||||
assertThrows(ResourceAlreadyExistsForThisClientException.class, this::runFlow);
|
||||
ResourceCreateContentionException thrown =
|
||||
assertThrows(ResourceCreateContentionException.class, this::runFlow);
|
||||
assertAboutEppExceptions()
|
||||
.that(thrown)
|
||||
.marshalsToXml()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2025 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.flows.domain;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistDomainAsDeleted;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Tests for {@link DomainDeletionTimeCache}. */
|
||||
public class DomainDeletionTimeCacheTest {
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2025-10-01T00:00:00.000Z"));
|
||||
private final DomainDeletionTimeCache cache = DomainDeletionTimeCache.create();
|
||||
|
||||
@RegisterExtension
|
||||
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
DatabaseHelper.createTld("tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDomainAvailable_null() {
|
||||
assertThat(getDeletionTimeFromCache("nonexistent.tld")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDomainNotAvailable_notDeleted() {
|
||||
persistActiveDomain("active.tld");
|
||||
assertThat(getDeletionTimeFromCache("active.tld")).hasValue(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDomainAvailable_deletedInFuture() {
|
||||
persistDomainAsDeleted(persistActiveDomain("domain.tld"), clock.nowUtc().plusDays(1));
|
||||
assertThat(getDeletionTimeFromCache("domain.tld")).hasValue(clock.nowUtc().plusDays(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCache_returnsOldData() {
|
||||
Domain domain = persistActiveDomain("domain.tld");
|
||||
assertThat(getDeletionTimeFromCache("domain.tld")).hasValue(END_OF_TIME);
|
||||
persistDomainAsDeleted(domain, clock.nowUtc().plusDays(1));
|
||||
// Without intervention, the cache should have the old data
|
||||
assertThat(getDeletionTimeFromCache("domain.tld")).hasValue(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCache_returnsNewDataAfterDomainCreate() {
|
||||
// Null deletion dates (meaning an avilable domain) shouldn't be cached
|
||||
assertThat(getDeletionTimeFromCache("domain.tld")).isEmpty();
|
||||
persistDomainAsDeleted(persistActiveDomain("domain.tld"), clock.nowUtc().plusDays(1));
|
||||
assertThat(getDeletionTimeFromCache("domain.tld")).hasValue(clock.nowUtc().plusDays(1));
|
||||
}
|
||||
|
||||
private Optional<DateTime> getDeletionTimeFromCache(String domainName) {
|
||||
return tm().transact(() -> cache.getDeletionTimeForDomain(domainName));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import google.registry.config.CredentialModule;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.flows.ServerTridProviderModule;
|
||||
import google.registry.flows.custom.CustomLogicFactoryModule;
|
||||
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
|
||||
import google.registry.groups.GmailModule;
|
||||
import google.registry.groups.GroupsModule;
|
||||
import google.registry.groups.GroupssettingsModule;
|
||||
@@ -43,6 +44,7 @@ import jakarta.inject.Singleton;
|
||||
CredentialModule.class,
|
||||
CustomLogicFactoryModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
DomainDeletionTimeCacheModule.class,
|
||||
FrontendRequestComponent.FrontendRequestComponentModule.class,
|
||||
GmailModule.class,
|
||||
GroupsModule.class,
|
||||
|
||||
Reference in New Issue
Block a user