mirror of
https://github.com/google/nomulus
synced 2026-08-11 01:36:16 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6863f678f1 | ||
|
|
6bd90e967b | ||
|
|
5faf3d283c | ||
|
|
149fb66ac5 | ||
|
|
8c96940a27 |
+1
-1
@@ -56,7 +56,7 @@ PROPERTIES_HEADER = """\
|
||||
# nom_build), run ./nom_build --help.
|
||||
#
|
||||
# DO NOT EDIT THIS FILE BY HAND
|
||||
org.gradle.jvmargs=-Xmx1024m
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
"""
|
||||
|
||||
@@ -56,9 +56,10 @@ public final class FlowUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Persists the saves and deletes in an {@link EntityChanges} to the DB. */
|
||||
/** Persists the inserts, updates, and deletes in an {@link EntityChanges} to the DB. */
|
||||
public static void persistEntityChanges(EntityChanges entityChanges) {
|
||||
tm().putAll(entityChanges.getSaves());
|
||||
tm().insertAll(entityChanges.getInserts());
|
||||
tm().updateAll(entityChanges.getUpdates());
|
||||
tm().delete(entityChanges.getDeletes());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,25 +19,30 @@ import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.VKey;
|
||||
|
||||
/** A record that encapsulates database entities to both save and delete. */
|
||||
/** A record that encapsulates database entities to insert, update, and delete. */
|
||||
public record EntityChanges(
|
||||
ImmutableSet<ImmutableObject> saves, ImmutableSet<VKey<ImmutableObject>> deletes) {
|
||||
ImmutableSet<ImmutableObject> inserts,
|
||||
ImmutableSet<ImmutableObject> updates,
|
||||
ImmutableSet<VKey<ImmutableObject>> deletes) {
|
||||
|
||||
public ImmutableSet<ImmutableObject> getSaves() {
|
||||
return saves;
|
||||
public ImmutableSet<ImmutableObject> getInserts() {
|
||||
return inserts;
|
||||
}
|
||||
|
||||
public ImmutableSet<ImmutableObject> getUpdates() {
|
||||
return updates;
|
||||
}
|
||||
;
|
||||
|
||||
public ImmutableSet<VKey<ImmutableObject>> getDeletes() {
|
||||
return deletes;
|
||||
}
|
||||
;
|
||||
|
||||
public static Builder newBuilder() {
|
||||
// Default both entities to save and entities to delete to empty sets, so that the build()
|
||||
// method won't subsequently throw an exception if one doesn't end up being applicable.
|
||||
// Default inserts, updates, and deletes to empty sets, so that the build() method won't
|
||||
// subsequently throw an exception if one doesn't end up being applicable.
|
||||
return new AutoBuilder_EntityChanges_Builder()
|
||||
.setSaves(ImmutableSet.of())
|
||||
.setInserts(ImmutableSet.of())
|
||||
.setUpdates(ImmutableSet.of())
|
||||
.setDeletes(ImmutableSet.of());
|
||||
}
|
||||
|
||||
@@ -45,12 +50,21 @@ public record EntityChanges(
|
||||
@AutoBuilder
|
||||
public interface Builder {
|
||||
|
||||
Builder setSaves(ImmutableSet<ImmutableObject> entitiesToSave);
|
||||
Builder setInserts(ImmutableSet<ImmutableObject> entitiesToInsert);
|
||||
|
||||
ImmutableSet.Builder<ImmutableObject> savesBuilder();
|
||||
ImmutableSet.Builder<ImmutableObject> insertsBuilder();
|
||||
|
||||
default Builder addSave(ImmutableObject entityToSave) {
|
||||
savesBuilder().add(entityToSave);
|
||||
default Builder addInsert(ImmutableObject entityToInsert) {
|
||||
insertsBuilder().add(entityToInsert);
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder setUpdates(ImmutableSet<ImmutableObject> entitiesToUpdate);
|
||||
|
||||
ImmutableSet.Builder<ImmutableObject> updatesBuilder();
|
||||
|
||||
default Builder addUpdate(ImmutableObject entityToUpdate) {
|
||||
updatesBuilder().add(entityToUpdate);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -357,11 +357,11 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
domainHistoryId, registrationExpirationTime, isAnchorTenant, allocationToken);
|
||||
PollMessage.Autorenew autorenewPollMessage =
|
||||
createAutorenewPollMessage(domainHistoryId, registrationExpirationTime);
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
|
||||
entitiesToSave.add(createBillingEvent, autorenewBillingEvent, autorenewPollMessage);
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
|
||||
entitiesToInsert.add(createBillingEvent, autorenewBillingEvent, autorenewPollMessage);
|
||||
// Bill for EAP cost, if any.
|
||||
if (!feesAndCredits.getEapCost().isZero()) {
|
||||
entitiesToSave.add(createEapBillingEvent(feesAndCredits, createBillingEvent));
|
||||
entitiesToInsert.add(createEapBillingEvent(feesAndCredits, createBillingEvent));
|
||||
}
|
||||
|
||||
ImmutableSet<ReservationType> reservationTypes = getReservationTypes(domainName);
|
||||
@@ -404,12 +404,13 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
DomainHistory domainHistory =
|
||||
buildDomainHistory(domain, tld, now, period, tld.getAddGracePeriodLength());
|
||||
if (reservationTypes.contains(NAME_COLLISION)) {
|
||||
entitiesToSave.add(
|
||||
entitiesToInsert.add(
|
||||
createNameCollisionOneTimePollMessage(targetId, domainHistory, registrarId, now));
|
||||
}
|
||||
entitiesToSave.add(domain, domainHistory);
|
||||
entitiesToInsert.add(domain, domainHistory);
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
|
||||
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
|
||||
entitiesToSave.add(
|
||||
entitiesToUpdate.add(
|
||||
AllocationTokenFlowUtils.redeemToken(
|
||||
allocationToken.get(), domainHistory.getHistoryEntryId()));
|
||||
}
|
||||
@@ -422,7 +423,10 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
.setNewDomain(domain)
|
||||
.setHistoryEntry(domainHistory)
|
||||
.setEntityChanges(
|
||||
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build())
|
||||
EntityChanges.newBuilder()
|
||||
.setInserts(entitiesToInsert.build())
|
||||
.setUpdates(entitiesToUpdate.build())
|
||||
.build())
|
||||
.setYears(years)
|
||||
.build());
|
||||
persistEntityChanges(entityChanges);
|
||||
@@ -649,6 +653,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()
|
||||
|
||||
@@ -151,7 +151,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
|
||||
verifyDeleteAllowed(existingDomain, tld, now);
|
||||
flowCustomLogic.afterValidation(
|
||||
AfterValidationParameters.newBuilder().setExistingDomain(existingDomain).build());
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
|
||||
Domain.Builder builder;
|
||||
if (existingDomain.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
|
||||
builder =
|
||||
@@ -221,7 +221,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
|
||||
} else {
|
||||
PollMessage.OneTime deletePollMessage =
|
||||
createDeletePollMessage(existingDomain, domainHistoryId, deletionTime);
|
||||
entitiesToSave.add(deletePollMessage);
|
||||
entitiesToInsert.add(deletePollMessage);
|
||||
builder.setDeletePollMessage(deletePollMessage.createVKey());
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
|
||||
// registrar other than the sponsoring registrar (which will necessarily be a superuser).
|
||||
if (durationUntilDelete.isLongerThan(Duration.ZERO)
|
||||
&& !registrarId.equals(existingDomain.getPersistedCurrentSponsorRegistrarId())) {
|
||||
entitiesToSave.add(
|
||||
entitiesToInsert.add(
|
||||
createImmediateDeletePollMessage(existingDomain, domainHistoryId, now, deletionTime));
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
|
||||
for (GracePeriod gracePeriod : existingDomain.getGracePeriods()) {
|
||||
// No cancellation is written if the grace period was not for a billable event.
|
||||
if (gracePeriod.hasBillingEvent()) {
|
||||
entitiesToSave.add(
|
||||
entitiesToInsert.add(
|
||||
BillingCancellation.forGracePeriod(gracePeriod, now, domainHistoryId, targetId));
|
||||
if (gracePeriod.getBillingEvent() != null) {
|
||||
// Take the amount of registration time being refunded off the expiration time.
|
||||
@@ -271,7 +271,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
|
||||
// ResourceDeleteFlow since it's listed in serverApproveEntities.
|
||||
requestDomainDnsRefresh(existingDomain.getDomainName());
|
||||
|
||||
entitiesToSave.add(newDomain, domainHistory);
|
||||
entitiesToInsert.add(domainHistory);
|
||||
EntityChanges entityChanges =
|
||||
flowCustomLogic.beforeSave(
|
||||
BeforeSaveParameters.newBuilder()
|
||||
@@ -279,7 +279,10 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
|
||||
.setNewDomain(newDomain)
|
||||
.setHistoryEntry(domainHistory)
|
||||
.setEntityChanges(
|
||||
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build())
|
||||
EntityChanges.newBuilder()
|
||||
.setInserts(entitiesToInsert.build())
|
||||
.addUpdate(newDomain)
|
||||
.build())
|
||||
.build());
|
||||
BeforeResponseReturnData responseData =
|
||||
flowCustomLogic.beforeResponse(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -711,7 +711,7 @@ public class DomainFlowUtils {
|
||||
|
||||
BillingRecurrence newBillingRecurrence =
|
||||
existingBillingRecurrence.asBuilder().setRecurrenceEndTime(newEndTime).build();
|
||||
tm().put(newBillingRecurrence);
|
||||
tm().update(newBillingRecurrence);
|
||||
return newBillingRecurrence;
|
||||
}
|
||||
|
||||
|
||||
@@ -245,11 +245,13 @@ public final class DomainRenewFlow implements MutatingFlow {
|
||||
.build();
|
||||
DomainHistory domainHistory =
|
||||
buildDomainHistory(newDomain, now, command.getPeriod(), tld.getRenewGracePeriodLength());
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
|
||||
entitiesToSave.add(
|
||||
newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
|
||||
entitiesToInsert.add(
|
||||
domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
|
||||
entitiesToUpdate.add(newDomain);
|
||||
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
|
||||
entitiesToSave.add(
|
||||
entitiesToUpdate.add(
|
||||
AllocationTokenFlowUtils.redeemToken(
|
||||
allocationToken.get(), domainHistory.getHistoryEntryId()));
|
||||
}
|
||||
@@ -262,7 +264,10 @@ public final class DomainRenewFlow implements MutatingFlow {
|
||||
.setYears(years)
|
||||
.setHistoryEntry(domainHistory)
|
||||
.setEntityChanges(
|
||||
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build())
|
||||
EntityChanges.newBuilder()
|
||||
.setInserts(entitiesToInsert.build())
|
||||
.setUpdates(entitiesToUpdate.build())
|
||||
.build())
|
||||
.build());
|
||||
BeforeResponseReturnData responseData =
|
||||
flowCustomLogic.beforeResponse(
|
||||
|
||||
@@ -146,18 +146,18 @@ public final class DomainRestoreRequestFlow implements MutatingFlow {
|
||||
verifyRestoreAllowed(command, existingDomain, feeUpdate, feesAndCredits, now);
|
||||
HistoryEntryId domainHistoryId = createHistoryEntryId(existingDomain);
|
||||
historyBuilder.setRevisionId(domainHistoryId.getRevisionId());
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
|
||||
|
||||
DateTime newExpirationTime =
|
||||
existingDomain.getRegistrationExpirationTime().plusYears(isExpired ? 1 : 0);
|
||||
// Restore the expiration time on the deleted domain, except if that's already passed, then add
|
||||
// a year and bill for it immediately, with no grace period.
|
||||
if (isExpired) {
|
||||
entitiesToSave.add(
|
||||
entitiesToInsert.add(
|
||||
createRenewBillingEvent(domainHistoryId, feesAndCredits.getRenewCost(), now));
|
||||
}
|
||||
// Always bill for the restore itself.
|
||||
entitiesToSave.add(
|
||||
entitiesToInsert.add(
|
||||
createRestoreBillingEvent(domainHistoryId, feesAndCredits.getRestoreCost(), now));
|
||||
|
||||
BillingRecurrence autorenewEvent =
|
||||
@@ -166,12 +166,14 @@ public final class DomainRestoreRequestFlow implements MutatingFlow {
|
||||
.setRecurrenceEndTime(END_OF_TIME)
|
||||
.setDomainHistoryId(domainHistoryId)
|
||||
.build();
|
||||
entitiesToInsert.add(autorenewEvent);
|
||||
PollMessage.Autorenew autorenewPollMessage =
|
||||
newAutorenewPollMessage(existingDomain)
|
||||
.setEventTime(newExpirationTime)
|
||||
.setAutorenewEndTime(END_OF_TIME)
|
||||
.setDomainHistoryId(domainHistoryId)
|
||||
.build();
|
||||
entitiesToInsert.add(autorenewPollMessage);
|
||||
Domain newDomain =
|
||||
performRestore(
|
||||
existingDomain,
|
||||
@@ -181,8 +183,9 @@ public final class DomainRestoreRequestFlow implements MutatingFlow {
|
||||
now,
|
||||
registrarId);
|
||||
DomainHistory domainHistory = buildDomainHistory(newDomain, now);
|
||||
entitiesToSave.add(newDomain, domainHistory, autorenewEvent, autorenewPollMessage);
|
||||
tm().putAll(entitiesToSave.build());
|
||||
entitiesToInsert.add(domainHistory);
|
||||
tm().update(newDomain);
|
||||
tm().insertAll(entitiesToInsert.build());
|
||||
if (existingDomain.getDeletePollMessage() != null) {
|
||||
tm().delete(existingDomain.getDeletePollMessage());
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
|
||||
.setDomainHistoryId(domainHistoryId)
|
||||
.build());
|
||||
|
||||
ImmutableList.Builder<ImmutableObject> entitiesToSave = new ImmutableList.Builder<>();
|
||||
ImmutableList.Builder<ImmutableObject> entitiesToInsert = new ImmutableList.Builder<>();
|
||||
// If we are within an autorenew grace period, cancel the autorenew billing event and don't
|
||||
// increase the registration time, since the transfer subsumes the autorenew's extra year.
|
||||
GracePeriod autorenewGrace =
|
||||
@@ -184,7 +184,7 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
|
||||
// then the gaining registrar is not charged for the one-year renewal and the losing registrar
|
||||
// still needs to be charged for the auto-renew.
|
||||
if (billingEvent.isPresent()) {
|
||||
entitiesToSave.add(
|
||||
entitiesToInsert.add(
|
||||
BillingCancellation.forGracePeriod(autorenewGrace, now, domainHistoryId, targetId));
|
||||
}
|
||||
}
|
||||
@@ -259,14 +259,11 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
|
||||
PollMessage gainingClientPollMessage =
|
||||
createGainingTransferPollMessage(
|
||||
targetId, newDomain.getTransferData(), newExpirationTime, now, domainHistoryId);
|
||||
billingEvent.ifPresent(entitiesToSave::add);
|
||||
entitiesToSave.add(
|
||||
autorenewEvent,
|
||||
gainingClientPollMessage,
|
||||
gainingClientAutorenewPollMessage,
|
||||
newDomain,
|
||||
domainHistory);
|
||||
tm().putAll(entitiesToSave.build());
|
||||
billingEvent.ifPresent(entitiesToInsert::add);
|
||||
entitiesToInsert.add(
|
||||
autorenewEvent, gainingClientPollMessage, gainingClientAutorenewPollMessage, domainHistory);
|
||||
tm().update(newDomain);
|
||||
tm().insertAll(entitiesToInsert.build());
|
||||
// Delete the billing event and poll messages that were written in case the transfer would have
|
||||
// been implicitly server approved.
|
||||
tm().delete(existingDomain.getTransferData().getServerApproveEntities());
|
||||
|
||||
@@ -110,8 +110,8 @@ public final class DomainTransferCancelFlow implements MutatingFlow {
|
||||
Domain newDomain =
|
||||
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_CANCELLED, now, registrarId);
|
||||
DomainHistory domainHistory = buildDomainHistory(newDomain, tld, now);
|
||||
tm().putAll(
|
||||
newDomain,
|
||||
tm().update(newDomain);
|
||||
tm().insertAll(
|
||||
domainHistory,
|
||||
createLosingTransferPollMessage(
|
||||
targetId, newDomain.getTransferData(), null, domainHistoryId));
|
||||
|
||||
@@ -109,8 +109,8 @@ public final class DomainTransferRejectFlow implements MutatingFlow {
|
||||
Domain newDomain =
|
||||
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_REJECTED, now, registrarId);
|
||||
DomainHistory domainHistory = buildDomainHistory(newDomain, tld, now);
|
||||
tm().putAll(
|
||||
newDomain,
|
||||
tm().update(newDomain);
|
||||
tm().insertAll(
|
||||
domainHistory,
|
||||
createGainingTransferPollMessage(
|
||||
targetId, newDomain.getTransferData(), null, now, domainHistoryId));
|
||||
|
||||
@@ -283,11 +283,9 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
|
||||
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(
|
||||
newDomain.createVKey(), now, ImmutableSortedSet.of(automaticTransferTime));
|
||||
tm().putAll(
|
||||
new ImmutableSet.Builder<>()
|
||||
.add(newDomain, domainHistory, requestPollMessage)
|
||||
.addAll(serverApproveEntities)
|
||||
.build());
|
||||
tm().put(newDomain);
|
||||
tm().putAll(serverApproveEntities);
|
||||
tm().insertAll(domainHistory, requestPollMessage);
|
||||
return responseBuilder
|
||||
.setResultFromCode(SUCCESS_WITH_ACTION_PENDING)
|
||||
.setResData(createResponse(period, existingDomain, newDomain, now))
|
||||
|
||||
@@ -190,14 +190,16 @@ public final class DomainUpdateFlow implements MutatingFlow {
|
||||
if (requiresDnsUpdate(existingDomain, newDomain)) {
|
||||
requestDomainDnsRefresh(targetId);
|
||||
}
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
|
||||
entitiesToSave.add(newDomain, domainHistory);
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
|
||||
entitiesToUpdate.add(newDomain);
|
||||
entitiesToInsert.add(domainHistory);
|
||||
Optional<BillingEvent> statusUpdateBillingEvent =
|
||||
createBillingEventForStatusUpdates(existingDomain, newDomain, domainHistory, now);
|
||||
statusUpdateBillingEvent.ifPresent(entitiesToSave::add);
|
||||
statusUpdateBillingEvent.ifPresent(entitiesToInsert::add);
|
||||
Optional<PollMessage.OneTime> serverStatusUpdatePollMessage =
|
||||
createPollMessageForServerStatusUpdates(existingDomain, newDomain, domainHistory, now);
|
||||
serverStatusUpdatePollMessage.ifPresent(entitiesToSave::add);
|
||||
serverStatusUpdatePollMessage.ifPresent(entitiesToInsert::add);
|
||||
EntityChanges entityChanges =
|
||||
flowCustomLogic.beforeSave(
|
||||
BeforeSaveParameters.newBuilder()
|
||||
@@ -205,7 +207,10 @@ public final class DomainUpdateFlow implements MutatingFlow {
|
||||
.setNewDomain(newDomain)
|
||||
.setExistingDomain(existingDomain)
|
||||
.setEntityChanges(
|
||||
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build())
|
||||
EntityChanges.newBuilder()
|
||||
.setInserts(entitiesToInsert.build())
|
||||
.setUpdates(entitiesToUpdate.build())
|
||||
.build())
|
||||
.build());
|
||||
persistEntityChanges(entityChanges);
|
||||
return responseBuilder.build();
|
||||
|
||||
@@ -126,7 +126,8 @@ public final class HostCreateFlow implements MutatingFlow {
|
||||
.setSuperordinateDomain(superordinateDomain.map(Domain::createVKey).orElse(null))
|
||||
.build();
|
||||
historyBuilder.setType(HOST_CREATE).setHost(newHost);
|
||||
ImmutableSet<ImmutableObject> entitiesToSave = ImmutableSet.of(newHost, historyBuilder.build());
|
||||
ImmutableSet<ImmutableObject> entitiesToInsert =
|
||||
ImmutableSet.of(newHost, historyBuilder.build());
|
||||
if (superordinateDomain.isPresent()) {
|
||||
tm().update(
|
||||
superordinateDomain
|
||||
@@ -138,7 +139,7 @@ public final class HostCreateFlow implements MutatingFlow {
|
||||
// they are only written as NS records from the referencing domain.
|
||||
requestHostDnsRefresh(targetId);
|
||||
}
|
||||
tm().insertAll(entitiesToSave);
|
||||
tm().insertAll(entitiesToInsert);
|
||||
return responseBuilder.setResData(HostCreateData.create(targetId, now)).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.metadata.MetadataExtension;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
@@ -198,16 +197,12 @@ public final class HostUpdateFlow implements MutatingFlow {
|
||||
.setPersistedCurrentSponsorRegistrarId(newPersistedRegistrarId)
|
||||
.build();
|
||||
verifyHasIpsIffIsExternal(command, existingHost, newHost);
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
|
||||
entitiesToUpdate.add(newHost);
|
||||
if (isHostRename) {
|
||||
updateSuperordinateDomains(existingHost, newHost);
|
||||
}
|
||||
enqueueTasks(existingHost, newHost);
|
||||
entitiesToInsert.add(historyBuilder.setType(HOST_UPDATE).setHost(newHost).build());
|
||||
tm().updateAll(entitiesToUpdate.build());
|
||||
tm().insertAll(entitiesToInsert.build());
|
||||
tm().update(newHost);
|
||||
tm().insert(historyBuilder.setType(HOST_UPDATE).setHost(newHost).build());
|
||||
return responseBuilder.build();
|
||||
}
|
||||
|
||||
@@ -290,7 +285,7 @@ public final class HostUpdateFlow implements MutatingFlow {
|
||||
&& newHost.isSubordinate()
|
||||
&& Objects.equals(
|
||||
existingHost.getSuperordinateDomain(), newHost.getSuperordinateDomain())) {
|
||||
tm().put(
|
||||
tm().update(
|
||||
tm().loadByKey(existingHost.getSuperordinateDomain())
|
||||
.asBuilder()
|
||||
.removeSubordinateHost(existingHost.getHostName())
|
||||
@@ -299,14 +294,14 @@ public final class HostUpdateFlow implements MutatingFlow {
|
||||
return;
|
||||
}
|
||||
if (existingHost.isSubordinate()) {
|
||||
tm().put(
|
||||
tm().update(
|
||||
tm().loadByKey(existingHost.getSuperordinateDomain())
|
||||
.asBuilder()
|
||||
.removeSubordinateHost(existingHost.getHostName())
|
||||
.build());
|
||||
}
|
||||
if (newHost.isSubordinate()) {
|
||||
tm().put(
|
||||
tm().update(
|
||||
tm().loadByKey(newHost.getSuperordinateDomain())
|
||||
.asBuilder()
|
||||
.addSubordinateHost(newHost.getHostName())
|
||||
|
||||
@@ -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,8 +104,9 @@ 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(
|
||||
Class<E> clazz, Collection<String> foreignKeys, boolean useReplicaTm) {
|
||||
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();
|
||||
return tmToUse.reTransact(
|
||||
@@ -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);
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package google.registry.model.tmch;
|
||||
|
||||
import static google.registry.config.RegistryConfig.getClaimsListCacheDuration;
|
||||
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
@@ -79,14 +78,11 @@ public class ClaimsListDao {
|
||||
*/
|
||||
private static ClaimsList getUncached() {
|
||||
return tm().reTransact(
|
||||
() -> {
|
||||
Long revisionId =
|
||||
tm().query("SELECT MAX(revisionId) FROM ClaimsList", Long.class)
|
||||
.getSingleResult();
|
||||
return tm().createQueryComposer(ClaimsList.class)
|
||||
.where("revisionId", EQ, revisionId)
|
||||
.first();
|
||||
})
|
||||
() ->
|
||||
tm().query("FROM ClaimsList ORDER BY revisionId DESC", ClaimsList.class)
|
||||
.setMaxResults(1)
|
||||
.getResultStream()
|
||||
.findFirst())
|
||||
.orElse(ClaimsList.create(START_OF_TIME, ImmutableMap.of()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-2
@@ -40,8 +40,9 @@ public class TestDomainCreateFlowCustomLogic extends DomainCreateFlowCustomLogic
|
||||
.setMsg("Custom logic was triggered")
|
||||
.build();
|
||||
return EntityChanges.newBuilder()
|
||||
.setSaves(parameters.entityChanges().getSaves())
|
||||
.addSave(extraPollMessage)
|
||||
.setInserts(parameters.entityChanges().getInserts())
|
||||
.addInsert(extraPollMessage)
|
||||
.setUpdates(parameters.entityChanges().getUpdates())
|
||||
.setDeletes(parameters.entityChanges().getDeletes())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -261,11 +261,11 @@ td.section {
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">generated on</td>
|
||||
<td class="property_value">2025-09-29 21:19:42</td>
|
||||
<td class="property_value">2025-10-10 17:24:50</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">last flyway file</td>
|
||||
<td id="lastFlywayFile" class="property_value">V209__poll_message_hash.sql</td>
|
||||
<td id="lastFlywayFile" class="property_value">V213__graceperiodhistory_history_revision_id_hash.sql</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -273,7 +273,7 @@ td.section {
|
||||
<p> </p>
|
||||
<svg viewBox="0.00 0.00 4903.00 3732.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 3728)">
|
||||
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="transparent" points="-4,4 -4,-3728 4899,-3728 4899,4 -4,4" /> <text text-anchor="start" x="4655" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text text-anchor="start" x="4738" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.27.1</text> <text text-anchor="start" x="4654" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-09-29 21:19:42</text> <polygon fill="none" stroke="#888888" points="4651,-4 4651,-44 4887,-44 4887,-4 4651,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="transparent" points="-4,4 -4,-3728 4899,-3728 4899,4 -4,4" /> <text text-anchor="start" x="4655" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text text-anchor="start" x="4738" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.27.1</text> <text text-anchor="start" x="4654" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-10-10 17:24:50</text> <polygon fill="none" stroke="#888888" points="4651,-4 4651,-44 4887,-44 4887,-4 4651,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<g id="node1" class="node">
|
||||
<title>allocationtoken_a08ccbef</title> <polygon fill="#e9c2f2" stroke="transparent" points="481.5,-978 481.5,-997 667.5,-997 667.5,-978 481.5,-978" /> <text text-anchor="start" x="483.5" y="-984.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."AllocationToken"</text> <polygon fill="#e9c2f2" stroke="transparent" points="667.5,-978 667.5,-997 741.5,-997 741.5,-978 667.5,-978" /> <text text-anchor="start" x="702.5" y="-983.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text text-anchor="start" x="483.5" y="-965.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">token</text> <text text-anchor="start" x="661.5" y="-964.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-964.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text text-anchor="start" x="483.5" y="-945.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text> <text text-anchor="start" x="661.5" y="-945.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-945.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text text-anchor="start" x="483.5" y="-926.8" font-family="Helvetica,sans-Serif" font-size="14.00">redemption_domain_repo_id</text> <text text-anchor="start" x="661.5" y="-926.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-926.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text text-anchor="start" x="483.5" y="-907.8" font-family="Helvetica,sans-Serif" font-size="14.00">token_type</text> <text text-anchor="start" x="661.5" y="-907.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-907.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="480.5,-901.5 480.5,-998.5 742.5,-998.5 742.5,-901.5 480.5,-901.5" />
|
||||
</g>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -207,3 +207,7 @@ V206__grace_period_hash.sql
|
||||
V207__grace_period_history_hash.sql
|
||||
V208__host_hash.sql
|
||||
V209__poll_message_hash.sql
|
||||
V210__allocationtoken_token_hash.sql
|
||||
V211__domainhistoryhost_history_revision_id_hash.sql
|
||||
V212__domaindsdatahistory_history_revision_id_hash.sql
|
||||
V213__graceperiodhistory_history_revision_id_hash.sql
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 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.
|
||||
|
||||
-- Add hash indexes on columns that are commonly queried with a direct equals
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS allocationtoken_token_hash ON "AllocationToken" USING hash (token);
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 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.
|
||||
|
||||
-- Add hash indexes on columns that are commonly queried with a direct equals
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS domainhistoryhost_domain_history_history_revision_id_hash ON "DomainHistoryHost"
|
||||
USING hash (domain_history_history_revision_id);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
-- 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.
|
||||
|
||||
-- Add hash indexes on columns that are commonly queried with a direct equals
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS domaindsdatahistory_domain_history_revision_id_hash ON "DomainDsDataHistory"
|
||||
USING hash (domain_history_revision_id);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
-- 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.
|
||||
|
||||
-- Add hash indexes on columns that are commonly queried with a direct equals
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS graceperiodhistory_domain_history_revision_id_hash ON "GracePeriodHistory" USING
|
||||
hash (domain_history_revision_id);
|
||||
@@ -1924,6 +1924,13 @@ ALTER TABLE ONLY public."User"
|
||||
CREATE INDEX allocation_token_domain_name_idx ON public."AllocationToken" USING btree (domain_name);
|
||||
|
||||
|
||||
--
|
||||
-- Name: allocationtoken_token_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX allocationtoken_token_hash ON public."AllocationToken" USING hash (token);
|
||||
|
||||
|
||||
--
|
||||
-- Name: billingcancellation_billing_cancellation_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -1980,6 +1987,13 @@ CREATE INDEX domain_history_to_ds_data_history_idx ON public."DomainDsDataHistor
|
||||
CREATE INDEX domain_history_to_transaction_record_idx ON public."DomainTransactionRecord" USING btree (domain_repo_id, history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domaindsdatahistory_domain_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX domaindsdatahistory_domain_history_revision_id_hash ON public."DomainDsDataHistory" USING hash (domain_history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domainhistory_domain_repo_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -1994,6 +2008,13 @@ CREATE INDEX domainhistory_domain_repo_id_hash ON public."DomainHistory" USING h
|
||||
CREATE INDEX domainhistory_history_revision_id_hash ON public."DomainHistory" USING hash (history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domainhistoryhost_domain_history_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX domainhistoryhost_domain_history_history_revision_id_hash ON public."DomainHistoryHost" USING hash (domain_history_history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domainhost_domain_repo_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2029,6 +2050,13 @@ CREATE INDEX graceperiod_domain_repo_id_hash ON public."GracePeriod" USING hash
|
||||
CREATE INDEX graceperiod_grace_period_id_hash ON public."GracePeriod" USING hash (grace_period_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: graceperiodhistory_domain_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX graceperiodhistory_domain_history_revision_id_hash ON public."GracePeriodHistory" USING hash (domain_history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: graceperiodhistory_grace_period_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
# nom_build), run ./nom_build --help.
|
||||
#
|
||||
# DO NOT EDIT THIS FILE BY HAND
|
||||
org.gradle.jvmargs=-Xmx1024m
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
mavenUrl=
|
||||
|
||||
Reference in New Issue
Block a user