1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Weimin Yu 9eddecf70f Bypass config check for caching when safe (#2773)
Pubapi actions should always use cache, regardless of the config
settings on caching.

In EppResource.java, the original `loadCached(Iterable<VKey>)`
method is renamed to `loadByCacheIfEnabled`. The original
`loadCached(Vkey)` method is renamed to `loadByCache` and always
uses cache.

In EppResourceUtils.java, the original `loadByForeignKeyCached`
method is renamed to `loadByForeignKeyByCacheIfEnabled`. A new
`loadByForeignKeyByCache` method, which always uses cache.

In ForeighKeyUtils.java, the original `loadCached` method is
renamed to `loadByCacheIfEnabled`, and a new `loadCached` method
is added which always uses cache.

Also added a `getContactsFromReplica` method in Registrar,
for use by RDAP actions.
2025-06-20 21:25:02 +00:00
gbrodman d4bcff0c31 Add password reset Java object (#2765)
A future PR will add the actions that save and use this object. That
future PR will also require loading RegistrarPoc objects given the
registrar ID, hence the change in that class.
2025-06-17 19:00:50 +00:00
Ben McIlwain 62065f88fb Remove spurious parenthesis in URS command output (#2767)
It was making the undo nomulus command look like this:

)nomulus ...
2025-06-16 20:23:48 +00:00
32 changed files with 363 additions and 90 deletions
@@ -185,7 +185,8 @@ public class CheckApiAction implements Runnable {
}
private boolean checkExists(String domainString, DateTime now) {
return !ForeignKeyUtils.loadCached(Domain.class, ImmutableList.of(domainString), now).isEmpty();
return !ForeignKeyUtils.loadByCache(Domain.class, ImmutableList.of(domainString), now)
.isEmpty();
}
private Optional<String> checkReserved(InternetDomainName domainName) {
@@ -432,7 +432,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
.filter(existingDomains::containsKey)
.collect(toImmutableMap(d -> d, existingDomains::get));
ImmutableMap<VKey<? extends EppResource>, EppResource> loadedDomains =
EppResource.loadCached(ImmutableList.copyOf(existingDomainsToLoad.values()));
EppResource.loadByCacheIfEnabled(ImmutableList.copyOf(existingDomainsToLoad.values()));
return ImmutableMap.copyOf(
Maps.transformEntries(existingDomainsToLoad, (k, v) -> (Domain) loadedDomains.get(v)));
}
@@ -417,7 +417,7 @@ public class DomainFlowUtils {
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
registrant.ifPresent(keysToLoad::add);
keysToLoad.addAll(nameservers);
verifyNotInPendingDelete(EppResource.loadCached(keysToLoad.build()).values());
verifyNotInPendingDelete(EppResource.loadByCacheIfEnabled(keysToLoad.build()).values());
}
private static void verifyNotInPendingDelete(Iterable<EppResource> resources)
@@ -404,7 +404,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
* <p>Don't use this unless you really need it for performance reasons, and be sure that you are
* OK with the trade-offs in loss of transactional consistency.
*/
public static ImmutableMap<VKey<? extends EppResource>, EppResource> loadCached(
public static ImmutableMap<VKey<? extends EppResource>, EppResource> loadByCacheIfEnabled(
Iterable<VKey<? extends EppResource>> keys) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return tm().reTransact(() -> tm().loadByKeys(keys));
@@ -413,15 +413,12 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
}
/**
* Loads a given EppResource by its key using the cache (if enabled).
* Loads a given EppResource by its key using the cache.
*
* <p>Don't use this unless you really need it for performance reasons, and be sure that you are
* OK with the trade-offs in loss of transactional consistency.
* <p>This method ignores the `isEppResourceCachingEnabled` config setting. It is reserved for use
* cases that can tolerate slightly stale data, e.g., RDAP queries.
*/
public static <T extends EppResource> T loadCached(VKey<T> key) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return tm().reTransact(() -> tm().loadByKey(key));
}
public static <T extends EppResource> T loadByCache(VKey<T> key) {
// Safe to cast because loading a Key<T> returns an entity of type T.
@SuppressWarnings("unchecked")
T resource = (T) cacheEppResources.get(key);
@@ -16,6 +16,7 @@ package google.registry.model;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
@@ -40,6 +41,7 @@ import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.TransactionManager;
import jakarta.persistence.Query;
import java.util.Collection;
import java.util.Comparator;
@@ -109,12 +111,12 @@ public final class EppResourceUtils {
*/
public static <T extends EppResource> Optional<T> loadByForeignKey(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(clazz, foreignKey, now, false);
return loadByForeignKeyHelper(tm(), clazz, foreignKey, now, false);
}
/**
* Loads the last created version of an {@link EppResource} from the database by foreign key,
* using a cache.
* using a cache, if caching is enabled in config settings.
*
* <p>Returns null if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
@@ -134,20 +136,36 @@ public final class EppResourceUtils {
* @param foreignKey id to match
* @param now the current logical time to project resources at
*/
public static <T extends EppResource> Optional<T> loadByForeignKeyCached(
public static <T extends EppResource> Optional<T> loadByForeignKeyByCacheIfEnabled(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(
clazz, foreignKey, now, RegistryConfig.isEppResourceCachingEnabled());
tm(), clazz, foreignKey, now, RegistryConfig.isEppResourceCachingEnabled());
}
/**
* Loads the last created version of an {@link EppResource} from the replica database by foreign
* key, using a cache.
*
* <p>This method ignores the config setting for caching, and is reserved for use cases that can
* tolerate slightly stale data.
*/
public static <T extends EppResource> Optional<T> loadByForeignKeyByCache(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(replicaTm(), clazz, foreignKey, now, true);
}
private static <T extends EppResource> Optional<T> loadByForeignKeyHelper(
Class<T> clazz, String foreignKey, DateTime now, boolean useCache) {
TransactionManager txnManager,
Class<T> clazz,
String foreignKey,
DateTime now,
boolean useCache) {
checkArgument(
ForeignKeyedEppResource.class.isAssignableFrom(clazz),
"loadByForeignKey may only be called for foreign keyed EPP resources");
VKey<T> key =
useCache
? ForeignKeyUtils.loadCached(clazz, ImmutableList.of(foreignKey), now).get(foreignKey)
? ForeignKeyUtils.loadByCache(clazz, ImmutableList.of(foreignKey), now).get(foreignKey)
: ForeignKeyUtils.load(clazz, foreignKey, now);
// The returned key is null if the resource is hard deleted or soft deleted by the given time.
if (key == null) {
@@ -155,10 +173,10 @@ public final class EppResourceUtils {
}
T resource =
useCache
? EppResource.loadCached(key)
? EppResource.loadByCache(key)
// This transaction is buried very deeply inside many outer nested calls, hence merits
// the use of reTransact() for now pending a substantial refactoring.
: tm().reTransact(() -> tm().loadByKeyIfPresent(key).orElse(null));
: txnManager.reTransact(() -> txnManager.loadByKeyIfPresent(key).orElse(null));
if (resource == null || isAtOrAfter(now, resource.getDeletionTime())) {
return Optional.empty();
}
@@ -204,11 +204,25 @@ public final class ForeignKeyUtils {
* <p>Don't use the cached version of this method unless you really need it for performance
* reasons, and are OK with the trade-offs in loss of transactional consistency.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadCached(
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadByCacheIfEnabled(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return load(clazz, foreignKeys, now);
}
return loadByCache(clazz, foreignKeys, now);
}
/**
* Load a list of {@link VKey} to {@link EppResource} instances by class and foreign key strings
* that are active at or after the specified moment in time, using the cache.
*
* <p>The returned map will omit any keys for which the {@link EppResource} doesn't exist or has
* been soft-deleted.
*
* <p>This method is reserved for use cases that can tolerate slightly stale data.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadByCache(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
return foreignKeyCache
.getAll(foreignKeys.stream().map(fk -> VKey.create(clazz, fk)).collect(toImmutableList()))
.entrySet()
@@ -0,0 +1,150 @@
// 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.model.console;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.persistence.WithVKey;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import java.util.Optional;
import java.util.UUID;
import org.joda.time.DateTime;
/**
* Represents a password reset request of some type.
*
* <p>Password reset requests must be performed within an hour of the time that they were requested,
* as well as requiring that the requester and the fulfiller have the proper respective permissions.
*/
@Entity
@WithVKey(String.class)
public class PasswordResetRequest extends ImmutableObject implements Buildable {
public enum Type {
EPP,
REGISTRY_LOCK
}
@Id private String verificationCode;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
Type type;
@AttributeOverrides({
@AttributeOverride(
name = "creationTime",
column = @Column(name = "requestTime", nullable = false))
})
CreateAutoTimestamp requestTime = CreateAutoTimestamp.create(null);
@Column(nullable = false)
String requester;
@Column DateTime fulfillmentTime;
@Column(nullable = false)
String destinationEmail;
@Column(nullable = false)
String registrarId;
public String getVerificationCode() {
return verificationCode;
}
public Type getType() {
return type;
}
public DateTime getRequestTime() {
return requestTime.getTimestamp();
}
public String getRequester() {
return requester;
}
public Optional<DateTime> getFulfillmentTime() {
return Optional.ofNullable(fulfillmentTime);
}
public String getDestinationEmail() {
return destinationEmail;
}
public String getRegistrarId() {
return registrarId;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** Builder for constructing immutable {@link PasswordResetRequest} objects. */
public static class Builder extends Buildable.Builder<PasswordResetRequest> {
public Builder() {}
private Builder(PasswordResetRequest instance) {
super(instance);
}
@Override
public PasswordResetRequest build() {
checkArgumentNotNull(getInstance().type, "Type must be specified");
checkArgumentNotNull(getInstance().requester, "Requester must be specified");
checkArgumentNotNull(getInstance().destinationEmail, "Destination email must be specified");
checkArgumentNotNull(getInstance().registrarId, "Registrar ID must be specified");
getInstance().verificationCode = UUID.randomUUID().toString();
return super.build();
}
public Builder setType(Type type) {
getInstance().type = type;
return this;
}
public Builder setRequester(String requester) {
getInstance().requester = requester;
return this;
}
public Builder setDestinationEmail(String destinationEmail) {
getInstance().destinationEmail = destinationEmail;
return this;
}
public Builder setRegistrarId(String registrarId) {
getInstance().registrarId = registrarId;
return this;
}
public Builder setFulfillmentTime(DateTime fulfillmentTime) {
getInstance().fulfillmentTime = fulfillmentTime;
return this;
}
}
}
@@ -441,7 +441,8 @@ public class DomainCommand {
private static <T extends EppResource> ImmutableMap<String, VKey<T>> loadByForeignKeysCached(
final Set<String> foreignKeys, final Class<T> clazz, final DateTime now)
throws InvalidReferencesException {
ImmutableMap<String, VKey<T>> fks = ForeignKeyUtils.loadCached(clazz, foreignKeys, now);
ImmutableMap<String, VKey<T>> fks =
ForeignKeyUtils.loadByCacheIfEnabled(clazz, foreignKeys, now);
if (!fks.keySet().equals(foreignKeys)) {
throw new InvalidReferencesException(
clazz, ImmutableSet.copyOf(difference(foreignKeys, fks.keySet())));
@@ -27,6 +27,7 @@ import static com.google.common.io.BaseEncoding.base64;
import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.tld.Tlds.assertTldsExist;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
@@ -62,6 +63,7 @@ import google.registry.model.tld.Tld.TldType;
import google.registry.persistence.VKey;
import google.registry.persistence.converter.CidrBlockListUserType;
import google.registry.persistence.converter.CurrencyToStringMapUserType;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.util.CidrAddressBlock;
import google.registry.util.PasswordUtils;
import jakarta.mail.internet.AddressException;
@@ -576,7 +578,20 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
* address.
*/
public ImmutableSortedSet<RegistrarPoc> getContacts() {
return getContactPocs().stream()
return getContactPocs(tm()).stream()
.filter(Objects::nonNull)
.collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR));
}
/**
* Returns a list of all {@link RegistrarPoc} objects for this registrar sorted by their email
* address.
*
* <p>This method queries the replica database. It is reserved for use cases that can tolerate
* slightly stale data.
*/
public ImmutableSortedSet<RegistrarPoc> getContactsFromReplica() {
return getContactPocs(replicaTm()).stream()
.filter(Objects::nonNull)
.collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR));
}
@@ -586,7 +601,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
* their email address.
*/
public ImmutableSortedSet<RegistrarPoc> getContactsOfType(final RegistrarPoc.Type type) {
return getContactPocs().stream()
return getContactPocs(tm()).stream()
.filter(Objects::nonNull)
.filter((@Nullable RegistrarPoc contact) -> contact.getTypes().contains(type))
.collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR));
@@ -600,13 +615,8 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
return getContacts().stream().filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse).findFirst();
}
private ImmutableSet<RegistrarPoc> getContactPocs() {
return tm().transact(
() ->
tm().query("FROM RegistrarPoc WHERE registrarId = :registrarId", RegistrarPoc.class)
.setParameter("registrarId", registrarId)
.getResultStream()
.collect(toImmutableSet()));
private ImmutableList<RegistrarPoc> getContactPocs(TransactionManager txnManager) {
return txnManager.transact(() -> RegistrarPoc.loadForRegistrar(registrarId));
}
@Override
@@ -27,6 +27,7 @@ import static google.registry.util.PasswordUtils.hashPassword;
import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
@@ -36,6 +37,7 @@ import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.QueryComposer;
import google.registry.util.PasswordUtils;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -432,6 +434,12 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
}
}
public static ImmutableList<RegistrarPoc> loadForRegistrar(String registrarId) {
return tm().createQueryComposer(RegistrarPoc.class)
.where("registrarId", QueryComposer.Comparator.EQ, registrarId)
.list();
}
/** Class to represent the composite primary key for {@link RegistrarPoc} entity. */
@VisibleForTesting
public static class RegistrarPocId extends ImmutableObject implements Serializable {
@@ -15,7 +15,7 @@
package google.registry.rdap;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -65,7 +65,7 @@ public class RdapDomainAction extends RdapActionBase {
}
// The query string is not used; the RDAP syntax is /rdap/domain/mydomain.com.
Optional<Domain> domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());
@@ -15,7 +15,7 @@
package google.registry.rdap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
@@ -184,7 +184,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
private DomainSearchResponse searchByDomainNameWithoutWildcard(
final RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class, partialStringQuery.getInitialString(), getRequestTime());
return makeSearchResults(
shouldBeVisible(domain) ? ImmutableList.of(domain.get()) : ImmutableList.of());
@@ -339,7 +339,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
Optional<String> desiredRegistrar = getDesiredRegistrar();
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class,
partialStringQuery.getInitialString(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
@@ -364,7 +364,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
// through the subordinate hosts. This is more efficient, and lets us permit wildcard searches
// with no initial string.
Domain domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class,
partialStringQuery.getSuffix(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime())
@@ -381,7 +381,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
if (partialStringQuery.matches(fqhn)) {
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
if (host.isPresent()
&& desiredRegistrar
@@ -740,7 +740,7 @@ public class RdapJsonFormatter {
//
if (outputDataType != OutputDataType.SUMMARY) {
ImmutableList<RdapContactEntity> registrarContacts =
registrar.getContacts().stream()
registrar.getContactsFromReplica().stream()
.map(RdapJsonFormatter::makeRdapJsonForRegistrarContact)
.filter(Optional::isPresent)
.map(Optional::get)
@@ -15,7 +15,7 @@
package google.registry.rdap;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -63,7 +63,7 @@ public class RdapNameserverAction extends RdapActionBase {
// If there are no undeleted nameservers with the given name, the foreign key should point to
// the most recently deleted one.
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
@@ -14,7 +14,7 @@
package google.registry.rdap;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
@@ -159,7 +159,8 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
.setIncompletenessWarningType(IncompletenessWarningType.COMPLETE);
Optional<Host> host =
loadByForeignKeyCached(Host.class, partialStringQuery.getInitialString(), getRequestTime());
loadByForeignKeyByCache(
Host.class, partialStringQuery.getInitialString(), getRequestTime());
metricInformationBuilder.setNumHostsRetrieved(host.isPresent() ? 1 : 0);
@@ -175,7 +176,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
private NameserverSearchResponse searchByNameUsingSuperordinateDomain(
RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKeyCached(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
loadByForeignKeyByCache(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
if (domain.isEmpty()) {
// Don't allow wildcards with suffixes which are not domains we manage. That would risk a
// table scan in many easily foreseeable cases. The user might ask for ns*.zombo.com,
@@ -193,7 +194,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
// We can't just check that the host name starts with the initial query string, because
// then the query ns.exam*.example.com would match against nameserver ns.example.com.
if (partialStringQuery.matches(fqhn)) {
Optional<Host> host = loadByForeignKeyCached(Host.class, fqhn, getRequestTime());
Optional<Host> host = loadByForeignKeyByCache(Host.class, fqhn, getRequestTime());
if (shouldBeVisible(host)) {
hostList.add(host.get());
if (hostList.size() > rdapResultSetMaxSize) {
@@ -16,7 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCacheIfEnabled;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
@@ -326,7 +326,7 @@ public final class DomainLockUtils {
private Domain getDomain(String domainName, String registrarId, DateTime now) {
Domain domain =
loadByForeignKeyCached(Domain.class, domainName, now)
loadByForeignKeyByCacheIfEnabled(Domain.class, domainName, now)
.orElseThrow(() -> new IllegalArgumentException("Domain doesn't exist"));
// The user must have specified either the correct registrar ID or the admin registrar ID
checkArgument(
@@ -251,11 +251,12 @@ final class UniformRapidSuspensionCommand extends MutatingEppToolCommand {
if (undo) {
return "";
}
StringBuilder undoBuilder = new StringBuilder("UNDO COMMAND:\n\n)")
.append("nomulus -e ")
.append(RegistryToolEnvironment.get())
.append(" uniform_rapid_suspension --undo --domain_name ")
.append(domainName);
StringBuilder undoBuilder =
new StringBuilder("UNDO COMMAND:\n\n")
.append("nomulus -e ")
.append(RegistryToolEnvironment.get())
.append(" uniform_rapid_suspension --undo --domain_name ")
.append(domainName);
if (!existingNameservers.isEmpty()) {
undoBuilder.append(" --hosts ").append(Joiner.on(',').join(existingNameservers));
}
@@ -55,7 +55,7 @@ public class ConsoleDomainGetAction extends ConsoleApiAction {
Optional<Domain> possibleDomain =
tm().transact(
() ->
EppResourceUtils.loadByForeignKeyCached(
EppResourceUtils.loadByForeignKeyByCacheIfEnabled(
Domain.class, paramDomain, tm().getTransactionTime()));
if (possibleDomain.isEmpty()) {
consoleApiParams.response().setStatus(SC_NOT_FOUND);
@@ -15,7 +15,6 @@
package google.registry.ui.server.console.settings;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.difference;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -35,7 +34,6 @@ import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPoc.Type;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.GkeService;
@@ -77,14 +75,7 @@ public class ContactAction extends ConsoleApiAction {
protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.VIEW_REGISTRAR_DETAILS);
ImmutableList<RegistrarPoc> contacts =
tm().transact(
() ->
tm()
.createQueryComposer(RegistrarPoc.class)
.where("registrarId", Comparator.EQ, registrarId)
.stream()
.collect(toImmutableList()));
tm().transact(() -> RegistrarPoc.loadForRegistrar(registrarId));
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(contacts));
}
@@ -17,7 +17,7 @@ package google.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.flows.domain.DomainFlowUtils.isBlockedByBsa;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.model.tld.Tlds.getTlds;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
@@ -94,7 +94,7 @@ public class DomainLookupCommand implements WhoisCommand {
private Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime now) {
Optional<Domain> domainResource =
cached
? loadByForeignKeyCached(Domain.class, domainName.toString(), now)
? loadByForeignKeyByCache(Domain.class, domainName.toString(), now)
: loadByForeignKey(Domain.class, domainName.toString(), now);
return domainResource.map(
domain -> new DomainWhoisResponse(domain, fullOutput, whoisRedactedEmailText, now));
@@ -81,7 +81,7 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
domain.getCurrentSponsorRegistrarId());
Registrar registrar = registrarOptional.get();
Optional<RegistrarPoc> abuseContact =
registrar.getContacts().stream()
registrar.getContactsFromReplica().stream()
.filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse)
.findFirst();
return WhoisResponseResults.create(
@@ -154,7 +154,7 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
// If we refer to a contact that doesn't exist, that's a bug. It means referential integrity
// has somehow been broken. We skip the rest of this contact, but log it to hopefully bring it
// someone's attention.
Contact contact1 = EppResource.loadCached(contact.get());
Contact contact1 = EppResource.loadByCache(contact.get());
if (contact1 == null) {
logger.atSevere().log(
"(BUG) Broken reference found from domain %s to contact %s.",
@@ -16,7 +16,7 @@ package google.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.model.tld.Tlds.getTlds;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
@@ -57,7 +57,7 @@ public class NameserverLookupByHostCommand implements WhoisCommand {
private Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime now) {
Optional<Host> host =
cached
? loadByForeignKeyCached(Host.class, hostName.toString(), now)
? loadByForeignKeyByCache(Host.class, hostName.toString(), now)
: loadByForeignKey(Host.class, hostName.toString(), now);
return host.map(h -> new NameserverWhoisResponse(h, now));
}
@@ -44,7 +44,7 @@ class RegistrarWhoisResponse extends WhoisResponseImpl {
@Override
public WhoisResponseResults getResponse(boolean preferUnicode, String disclaimer) {
Set<RegistrarPoc> contacts = registrar.getContacts();
Set<RegistrarPoc> contacts = registrar.getContactsFromReplica();
String plaintext =
new RegistrarEmitter()
.emitField("Registrar", registrar.getRegistrarName())
@@ -47,13 +47,14 @@
<class>google.registry.model.billing.BillingRecurrence</class>
<class>google.registry.model.common.Cursor</class>
<class>google.registry.model.common.DnsRefreshRequest</class>
<class>google.registry.model.common.FeatureFlag</class>
<class>google.registry.model.console.ConsoleUpdateHistory</class>
<class>google.registry.model.console.PasswordResetRequest</class>
<class>google.registry.model.console.User</class>
<class>google.registry.model.contact.ContactHistory</class>
<class>google.registry.model.contact.Contact</class>
<class>google.registry.model.domain.Domain</class>
<class>google.registry.model.domain.DomainHistory</class>
<class>google.registry.model.common.FeatureFlag</class>
<class>google.registry.model.domain.GracePeriod</class>
<class>google.registry.model.domain.GracePeriod$GracePeriodHistory</class>
<class>google.registry.model.domain.secdns.DomainDsData</class>
@@ -36,27 +36,27 @@ public class EppResourceTest extends EntityTestCase {
new TestCacheExtension.Builder().withEppResourceCache(Duration.ofDays(1)).build();
@Test
void test_loadCached_ignoresContactChange() {
void test_loadByCacheIfEnabled_ignoresContactChange() {
Contact originalContact = persistActiveContact("contact123");
assertThat(EppResource.loadCached(ImmutableList.of(originalContact.createVKey())))
assertThat(EppResource.loadByCacheIfEnabled(ImmutableList.of(originalContact.createVKey())))
.containsExactly(originalContact.createVKey(), originalContact);
Contact modifiedContact =
persistResource(originalContact.asBuilder().setEmailAddress("different@fake.lol").build());
assertThat(EppResource.loadCached(ImmutableList.of(originalContact.createVKey())))
assertThat(EppResource.loadByCacheIfEnabled(ImmutableList.of(originalContact.createVKey())))
.containsExactly(originalContact.createVKey(), originalContact);
assertThat(loadByForeignKey(Contact.class, "contact123", fakeClock.nowUtc()))
.hasValue(modifiedContact);
}
@Test
void test_loadCached_ignoresHostChange() {
void test_loadByCacheIfEnabled_ignoresHostChange() {
Host originalHost = persistActiveHost("ns1.example.com");
assertThat(EppResource.loadCached(ImmutableList.of(originalHost.createVKey())))
assertThat(EppResource.loadByCacheIfEnabled(ImmutableList.of(originalHost.createVKey())))
.containsExactly(originalHost.createVKey(), originalHost);
Host modifiedHost =
persistResource(
originalHost.asBuilder().setLastTransferTime(fakeClock.nowUtc().minusDays(60)).build());
assertThat(EppResource.loadCached(ImmutableList.of(originalHost.createVKey())))
assertThat(EppResource.loadByCacheIfEnabled(ImmutableList.of(originalHost.createVKey())))
.containsExactly(originalHost.createVKey(), originalHost);
assertThat(loadByForeignKey(Host.class, "ns1.example.com", fakeClock.nowUtc()))
.hasValue(modifiedHost);
@@ -121,7 +121,7 @@ class ForeignKeyUtilsTest {
fakeClock.advanceOneMilli();
Host newHost1 = persistActiveHost("ns1.example.com");
assertThat(
ForeignKeyUtils.loadCached(
ForeignKeyUtils.loadByCacheIfEnabled(
Host.class,
ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"),
fakeClock.nowUtc()))
@@ -134,7 +134,7 @@ class ForeignKeyUtilsTest {
Host host2 = persistActiveHost("ns2.example.com");
persistResource(host2.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build());
assertThat(
ForeignKeyUtils.loadCached(
ForeignKeyUtils.loadByCacheIfEnabled(
Host.class,
ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"),
fakeClock.nowUtc()))
@@ -144,7 +144,7 @@ class ForeignKeyUtilsTest {
persistActiveHost("ns1.example.com");
// Even though a new host1 is now live, the cache still returns the VKey to the old one.
assertThat(
ForeignKeyUtils.loadCached(
ForeignKeyUtils.loadByCacheIfEnabled(
Host.class,
ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"),
fakeClock.nowUtc()))
@@ -0,0 +1,65 @@
// 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.model.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.Assert.assertThrows;
import google.registry.model.EntityTestCase;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import org.junit.jupiter.api.Test;
/** Tests for {@link PasswordResetRequest}. */
public class PasswordResetRequestTest extends EntityTestCase {
PasswordResetRequestTest() {
super(JpaEntityCoverageCheck.ENABLED);
}
@Test
void testSuccess_persistence() {
PasswordResetRequest request =
new PasswordResetRequest.Builder()
.setRequester("requestor@email.tld")
.setDestinationEmail("destination@email.tld")
.setType(PasswordResetRequest.Type.EPP)
.setRegistrarId("TheRegistrar")
.build();
String verificationCode = request.getVerificationCode();
assertThat(verificationCode).isNotEmpty();
persistResource(request);
PasswordResetRequest fromDatabase =
DatabaseHelper.loadByKey(VKey.create(PasswordResetRequest.class, verificationCode));
assertAboutImmutableObjects().that(fromDatabase).isEqualExceptFields(request, "requestTime");
assertThat(fromDatabase.getRequestTime()).isEqualTo(fakeClock.nowUtc());
}
@Test
void testFailure_nullFields() {
PasswordResetRequest.Builder builder = new PasswordResetRequest.Builder();
assertThrows(IllegalArgumentException.class, builder::build);
builder.setType(PasswordResetRequest.Type.EPP);
assertThrows(IllegalArgumentException.class, builder::build);
builder.setRequester("foobar@email.tld");
assertThrows(IllegalArgumentException.class, builder::build);
builder.setDestinationEmail("email@email.tld");
assertThrows(IllegalArgumentException.class, builder::build);
builder.setRegistrarId("TheRegistrar");
builder.build();
}
}
@@ -25,6 +25,7 @@ import google.registry.model.common.CursorTest;
import google.registry.model.common.DnsRefreshRequestTest;
import google.registry.model.common.FeatureFlagTest;
import google.registry.model.console.ConsoleUpdateHistoryTest;
import google.registry.model.console.PasswordResetRequestTest;
import google.registry.model.console.UserTest;
import google.registry.model.contact.ContactTest;
import google.registry.model.domain.DomainSqlTest;
@@ -104,6 +105,7 @@ import org.junit.runner.RunWith;
FeatureFlagTest.class,
HostHistoryTest.class,
LockTest.class,
PasswordResetRequestTest.class,
PollMessageTest.class,
PremiumListDaoTest.class,
RdeRevisionTest.class,
@@ -16,7 +16,7 @@ package google.registry.whois;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.bsa.persistence.BsaTestingUtils.persistBsaLabel;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCacheIfEnabled;
import static google.registry.model.registrar.Registrar.State.ACTIVE;
import static google.registry.model.registrar.Registrar.Type.PDT;
import static google.registry.model.tld.Tlds.getTlds;
@@ -147,8 +147,9 @@ public class WhoisActionTest {
persistResource(makeDomainWithRegistrar(registrar));
persistSimpleResources(makeRegistrarPocs(registrar));
// Populate the cache for both the domain and contact.
Domain domain = loadByForeignKeyCached(Domain.class, "cat.lol", clock.nowUtc()).get();
Contact contact = loadByForeignKeyCached(Contact.class, "5372808-ERL", clock.nowUtc()).get();
Domain domain = loadByForeignKeyByCacheIfEnabled(Domain.class, "cat.lol", clock.nowUtc()).get();
Contact contact =
loadByForeignKeyByCacheIfEnabled(Contact.class, "5372808-ERL", clock.nowUtc()).get();
// Make a change to the domain and contact that won't be seen because the cache will be hit.
persistResource(domain.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
persistResource(
@@ -280,7 +281,7 @@ public class WhoisActionTest {
@Test
void testRun_domainNotFound_usesCache() {
// Populate the cache with the nonexistence of this domain.
assertThat(loadByForeignKeyCached(Domain.class, "cat.lol", clock.nowUtc())).isEmpty();
assertThat(loadByForeignKeyByCacheIfEnabled(Domain.class, "cat.lol", clock.nowUtc())).isEmpty();
// Add a new valid cat.lol domain that won't be found because the cache will be hit instead.
persistActiveDomain("cat.lol");
newWhoisAction("domain cat.lol\r\n").run();
@@ -435,7 +436,8 @@ public class WhoisActionTest {
void testRun_nameserver_usesCache() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.xn--q9jyb4c", "1.2.3.4"));
// Populate the cache.
Host host = loadByForeignKeyCached(Host.class, "ns1.cat.xn--q9jyb4c", clock.nowUtc()).get();
Host host =
loadByForeignKeyByCacheIfEnabled(Host.class, "ns1.cat.xn--q9jyb4c", clock.nowUtc()).get();
// Make a change to the persisted host that won't be seen because the cache will be hit.
persistResource(
host.asBuilder()
@@ -261,7 +261,7 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2025-06-02 14:41:34</td>
<td class="property_value">2025-06-04 18:53:06</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
@@ -280,7 +280,7 @@ td.section {
<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.25.2</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-06-02 14:41:34</text>
<text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-06-04 18:53:06</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>
@@ -2702,7 +2702,7 @@ td.section {
<tr>
<td class="spacer"></td>
<td class="minwidth"></td>
<td class="minwidth">default '2021-06-01 00:00:00+00'::timestamp with time zone</td>
<td class="minwidth">default '2021-05-31 20:00:00-04'::timestamp with time zone</td>
</tr>
<tr>
<td colspan="3"></td>
@@ -261,7 +261,7 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2025-06-02 14:41:30</td>
<td class="property_value">2025-06-04 18:53:03</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
@@ -280,7 +280,7 @@ td.section {
<text text-anchor="start" x="5435" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text>
<text text-anchor="start" x="5518" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.25.2</text>
<text text-anchor="start" x="5434" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text>
<text text-anchor="start" x="5518" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-06-02 14:41:30</text>
<text text-anchor="start" x="5518" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-06-04 18:53:03</text>
<polygon fill="none" stroke="#888888" points="5431,-4 5431,-44 5667,-44 5667,-4 5431,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>
@@ -4806,7 +4806,7 @@ td.section {
<tr>
<td class="spacer"></td>
<td class="minwidth"></td>
<td class="minwidth">default '2021-06-01 00:00:00+00'::timestamp with time zone</td>
<td class="minwidth">default '2021-05-31 20:00:00-04'::timestamp with time zone</td>
</tr>
<tr>
<td colspan="3"></td>
@@ -562,6 +562,17 @@
primary key (package_promotion_id)
);
create table "PasswordResetRequest" (
verification_code text not null,
destination_email text not null,
fulfillment_time timestamp(6) with time zone,
registrar_id text not null,
request_time timestamp(6) with time zone not null,
requester text not null,
type text not null check (type in ('EPP','REGISTRY_LOCK')),
primary key (verification_code)
);
create table "PollMessage" (
type text not null,
poll_message_id bigint not null,