Skip to content

Add RDAP special notice for domains in XAP - #3221

Open
CydeWeys wants to merge 1 commit into
google:masterfrom
CydeWeys:rdap-xap-message
Open

CydeWeys wants to merge 1 commit into
google:masterfrom
CydeWeys:rdap-xap-message

Conversation

@CydeWeys

@CydeWeys CydeWeys commented Aug 27, 2026 •

Copy link
Copy Markdown
Member

When an RDAP lookup is performed for a domain that is currently in the Expiry Access Period (XAP), return HTTP 404 with a custom status notice: "This domain is currently available for registration in the Expiry Access Period". This behavior mirrors how BSA-blocked domains return specialized RDAP notices via a shared DomainNotFoundErrorResponse base class.

To optimize performance and eliminate redundant Cloud SQL database calls on 404 queries:

  • Expose loadByDomainNameIncludingDeleted in DomainCache and MultilayerDomainCache (backed by loadFromCachesIncludingDeleted and ForeignKeyUtils.loadResourceByCacheIncludingDeleted) to retrieve soft-deleted domains without filtering them out, while preserving temporal projections.
  • Update SyncRemoteCacheAction, MultilayerDomainCache, and MultilayerEppResourceCache to set the Valkey expiration time (pxAt) and retention cutoff for all Domain resources to deletionTime.plus(domainExpiryAccessPeriodTotalLength), ensuring pending-delete domains remain cached across the XAP transition and recently deleted domains are warm if XAP is enabled on a TLD.
  • Evaluate XAP eligibility entirely in-memory in RdapDomainAction using the already-loaded domain object and constructor-injected domainExpiryAccessPeriodTotalLength configuration.
  • Add comprehensive unit tests in RdapDomainActionTest, MultilayerDomainCacheTest, MultilayerHostCacheTest, SimplifiedJedisClientTest, SyncRemoteCacheActionTest, ForeignKeyUtilsTest, and DomainFlowUtilsTest.

TAG=agy
CONV=214408e9-2dbb-44b8-af91-1ca29401d1f5
BUG=http://b/553509095


This change is Reviewable

@CydeWeys
CydeWeys requested a review from gbrodman August 27, 2026 20:19

@gbrodman gbrodman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gbrodman reviewed 16 files and all commit messages, and made 8 comments.
Reviewable status: all files reviewed, 7 unresolved discussions (waiting on CydeWeys).


core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java line 96 at r1 (raw file):

  @Inject
  @Config("domainExpiryAccessPeriodTotalLength")
  Duration domainExpiryAccessPeriodTotalLength = Duration.ofDays(10);

Inject via constructor, like the rest of the fields. No need for default.


core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java line 200 at r1 (raw file):

      String key = getKeyFunction.apply(resource);
      if (shouldSaveResourceInRemoteCache(resource, tm().getTxTime())) {
        toSaveBuilder.add(new SimplifiedJedisClient.JedisResource<>(key, resource));

The simplified jedis client, for now, sets the expiration time of the object to the deletion time. We'll probably need to change that, I would think.


core/src/main/java/google/registry/cache/DomainCache.java line 30 at r1 (raw file):

   * domains.
   */
  default Optional<Domain> loadMostRecentByDomainName(String domainName) {

this is not a good name, especially in relation to the other method

like, if i read this i'm like "what, does the other method not load the most recent?"

And if it's meant for use in CacheModule when Valkey isn't configured, it doesn't work -- that delegates loadByDomainName() to FKUtils.loadResourceByCache which filters out deleted domains


core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java line 67 at r1 (raw file):

  @SuppressWarnings("unchecked")
  protected Optional<V> loadMostRecentFromCaches(Class<V> clazz, String key) {

so this is just the method above without the deletion filtering? that method should probably just call this one then and do the filtering


core/src/main/java/google/registry/model/ForeignKeyUtils.java line 413 at r1 (raw file):

   */
  @SuppressWarnings("unchecked")
  public static <E extends EppResource> Optional<E> loadMostRecentResourceByCache(

again i don't like the name here. Emphasize that this includes deleted resources.


core/src/main/java/google/registry/rdap/RdapDomainAction.java line 50 at r1 (raw file):

public class RdapDomainAction extends RdapActionBase {

  @Inject

inject this in the constructor


core/src/main/java/google/registry/rdap/RdapObjectClasses.java line 560 at r1 (raw file):

  @RestrictJsonNames({})
  @SuppressWarnings("UnusedVariable")
  public static class DomainInExpiryAccessPeriodErrorResponse extends ReplyPayloadBase {

this could possibly share a common base class with the BSA error exception but it's not a big deal


core/src/test/java/google/registry/batch/SyncRemoteCacheActionTest.java line 172 at r1 (raw file):

    verifyMetrics(SUCCESS);
  }

will also need to test a proper Valkey expiry for when XAP is enabled

@CydeWeys
CydeWeys force-pushed the rdap-xap-message branch 2 times, most recently from dfaf7b1 to 9513105 Compare October 2, 2026 16:29

@CydeWeys CydeWeys left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CydeWeys made 4 comments.
Reviewable status: 4 of 23 files reviewed, 9 unresolved discussions (waiting on gbrodman).


core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java line 96 at r1 (raw file):

Previously, gbrodman wrote…

Inject via constructor, like the rest of the fields. No need for default.

Done.


core/src/main/java/google/registry/cache/DomainCache.java line 30 at r1 (raw file):

Previously, gbrodman wrote…

this is not a good name, especially in relation to the other method

like, if i read this i'm like "what, does the other method not load the most recent?"

And if it's meant for use in CacheModule when Valkey isn't configured, it doesn't work -- that delegates loadByDomainName() to FKUtils.loadResourceByCache which filters out deleted domains

Done.


core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java line 220 at r3 (raw file):

    if (resource instanceof Domain domain) {
      Tld tld = Tld.get(domain.getTld());
      if (isDomainInXap(domain, tld, now)) {

Is this necessary? What about simply always using domain.getDeletionTime().plus(domainExpiryAccessPeriodTotalLength) when we know XAP is enabled on that TLD?

For that matter, for greater simplicity, what about just always using domain.getDeletionTime().plus(domainExpiryAccessPeriodTotalLength) period? Is there any downside to potentially caching a few extra domains on closed TLDs? (The vast majority of domains in the system will be on XAP-enabled TLDs.)

I'm worried about the cut-over moment from when XAP launches; will there be an issue with not having a bunch of domains already cached there, that should've been cached in advance?


core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java line 228 at r3 (raw file):

  private <T extends EppResource> boolean shouldSaveResourceInRemoteCache(T resource, Instant now) {
    if (resource.getDeletionTime().isAfter(now)) {

Same comment as above -- maybe just have this be resource's deletion time plus the XAP length (if it's a domain) is after now?

@CydeWeys CydeWeys left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CydeWeys made 3 comments.
Reviewable status: 4 of 23 files reviewed, 9 unresolved discussions (waiting on gbrodman).


core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java line 200 at r1 (raw file):

Previously, gbrodman wrote…

The simplified jedis client, for now, sets the expiration time of the object to the deletion time. We'll probably need to change that, I would think.

Done.


core/src/main/java/google/registry/model/ForeignKeyUtils.java line 413 at r1 (raw file):

Previously, gbrodman wrote…

again i don't like the name here. Emphasize that this includes deleted resources.

Done.


core/src/main/java/google/registry/rdap/RdapDomainAction.java line 50 at r1 (raw file):

Previously, gbrodman wrote…

inject this in the constructor

Done.

@CydeWeys CydeWeys left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java line 67 at r1

Previously, gbrodman wrote…

so this is just the method above without the deletion filtering? that method should probably just call this one then and do the filtering

Done.


core/src/main/java/google/registry/rdap/RdapObjectClasses.java line 560 at r1

Previously, gbrodman wrote…

this could possibly share a common base class with the BSA error exception but it's not a big deal

Done.


core/src/test/java/google/registry/batch/SyncRemoteCacheActionTest.java line 172 at r1

Previously, gbrodman wrote…

will also need to test a proper Valkey expiry for when XAP is enabled

Done.

@CydeWeys CydeWeys left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PTAL

@CydeWeys made 1 comment.
Reviewable status: 4 of 24 files reviewed, 9 unresolved discussions (waiting on gbrodman).

@CydeWeys CydeWeys left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CydeWeys made 4 comments and resolved 2 discussions.
Reviewable status: 4 of 24 files reviewed, 8 unresolved discussions (waiting on gbrodman).


core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java line 220 at r3 (raw file):

Previously, CydeWeys (Ben McIlwain) wrote…

Is this necessary? What about simply always using domain.getDeletionTime().plus(domainExpiryAccessPeriodTotalLength) when we know XAP is enabled on that TLD?

For that matter, for greater simplicity, what about just always using domain.getDeletionTime().plus(domainExpiryAccessPeriodTotalLength) period? Is there any downside to potentially caching a few extra domains on closed TLDs? (The vast majority of domains in the system will be on XAP-enabled TLDs.)

I'm worried about the cut-over moment from when XAP launches; will there be an issue with not having a bunch of domains already cached there, that should've been cached in advance?

Done.


core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java line 228 at r3 (raw file):

Previously, CydeWeys (Ben McIlwain) wrote…

Same comment as above -- maybe just have this be resource's deletion time plus the XAP length (if it's a domain) is after now?

Done.


core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java line 67 at r1 (raw file):

Previously, gbrodman wrote…

so this is just the method above without the deletion filtering? that method should probably just call this one then and do the filtering

Done.


core/src/main/java/google/registry/rdap/RdapDomainAction.java line 83 at r4 (raw file):

            : domainCache.loadByDomainNameIncludingDeleted(pathSearchString);
    if (domain.isEmpty() || !isAuthorized(domain.get())) {
      handlePossibleExpiryAccessPeriod(domainName, domain);

This looks cleaner to me as domain.ifPresent(....handlePossibleExpiryAccessPeriod(...)) and then removing the conditional short circuit block at the beginning of handlePossibleExpiryAccessPeriod(), changing the method signature to have Domain instead of Optional<Domain> as the second parameter.

When an RDAP lookup is performed for a domain that is currently in the
Expiry Access Period (XAP), return HTTP 404 with a custom status notice:
"This domain is currently available for registration in the Expiry
Access Period". This behavior mirrors how BSA-blocked domains return
specialized RDAP notices via a shared DomainNotFoundErrorResponse base
class.

To optimize performance and eliminate redundant Cloud SQL database calls
on 404 queries:
- Expose loadByDomainNameIncludingDeleted in DomainCache and
  MultilayerDomainCache (backed by loadFromCachesIncludingDeleted and
  ForeignKeyUtils.loadResourceByCacheIncludingDeleted) to retrieve
  soft-deleted domains without filtering them out, while preserving
  temporal projections.
- Update SyncRemoteCacheAction, MultilayerDomainCache, and
  MultilayerEppResourceCache to set the Valkey expiration time (pxAt)
  and retention cutoff for all Domain resources to
  deletionTime.plus(domainExpiryAccessPeriodTotalLength), ensuring
  pending-delete domains remain cached across the XAP transition and
  recently deleted domains are warm if XAP is enabled on a TLD.
- Evaluate XAP eligibility entirely in-memory in RdapDomainAction using
  the already-loaded domain object and constructor-injected
  domainExpiryAccessPeriodTotalLength configuration.
- Add comprehensive unit tests in RdapDomainActionTest,
  MultilayerDomainCacheTest, MultilayerHostCacheTest,
  SimplifiedJedisClientTest, SyncRemoteCacheActionTest,
  ForeignKeyUtilsTest, and DomainFlowUtilsTest.

TAG=agy
CONV=214408e9-2dbb-44b8-af91-1ca29401d1f5
BUG=http://b/553509095

@gbrodman gbrodman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gbrodman reviewed 20 files and all commit messages, made 15 comments, and resolved 7 discussions.
Reviewable status: all files reviewed, 16 unresolved discussions (waiting on CydeWeys).


core/src/main/java/google/registry/cache/MultilayerDomainCache.java line 39 at r5 (raw file):

  private final Duration domainExpiryAccessPeriodTotalLength;

  @Inject

this is constructed manually, not injected


core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java line 41 at r5 (raw file):

  private final SimplifiedJedisClient jedisClient;
  protected final Clock clock;

this can remain private


core/src/main/java/google/registry/cache/SimplifiedJedisClient.java line 60 at r5 (raw file):

  public record JedisResource<V extends EppResource>(
      String key, V value, Optional<Instant> expirationTime) {

this is all more complicated than necessary, i think you can just make expirationTime an Instant, then the three-arg constructor can go away and the two-arg constructor can just delegate to this(key, value, v.getDeletionTime())

this will probably simplify callers too


core/src/main/java/google/registry/cache/SimplifiedJedisClient.java line 156 at r5 (raw file):

  /** Deletes the given resource in Valkey. */
  public void delete(JedisResource<?> resource) {

i don't think this method is ever called in non-test code?


core/src/main/java/google/registry/cache/SimplifiedJedisClient.java line 161 at r5 (raw file):

  }

  byte[] getRedisKey(Class<?> clazz, String key) {

this method is not necessary


core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java line 239 at r5 (raw file):

  DomainCreateFlow() {}

  public String getTargetId() {

is this method necessary?


core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java line 472 at r5 (raw file):

                .build());
    persistEntityChanges(entityChanges);
    if (!isDryRun) {

i think we should only need to do this in the cases where we're in XAP, right? In the vast majority of cases we don't need to contact Valkey


core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java line 473 at r5 (raw file):

    persistEntityChanges(entityChanges);
    if (!isDryRun) {
      jedisClient.ifPresent(client -> client.delete(Domain.class, getTargetId()));

jedis changes aren't reverted on transaction failure, so we should probably move this even later in the method

also if this call fails for some reason it might abort the whole flow.


core/src/main/java/google/registry/rdap/RdapDomainAction.java line 79 at r5 (raw file):

    // The query string is not used; the RDAP syntax is /rdap/domain/mydomain.com.
    Optional<Domain> domain =
        shouldIncludeDeleted() // the remote domain cache cannot handle times in the past

can we just simplify this now to the loadByDomainNameIncludingDeleted call?


core/src/main/java/google/registry/rdap/RdapDomainAction.java line 96 at r5 (raw file):

  private void handlePossibleExpiryAccessPeriod(InternetDomainName domainName, Domain domain) {
    Instant now = clock.now();

use requestTime


core/src/test/java/google/registry/rdap/RdapDomainActionTest.java line 662 at r5 (raw file):

  @Test
  void testDomainInExpiryAccessPeriod_postExpiry_returnsStandard404() {

this has a duplicated test testDomainInExpiryAccessPeriod_oneMilliAfterExpiry_returnsStandard404


core/src/test/java/google/registry/rdap/RdapDomainActionTest.java line 682 at r5 (raw file):

  @Test
  void testDomainInExpiryAccessPeriod_oneMilliBeforeDeletion_activeReturns200() {

this has a duplicated test testDomainInExpiryAccessPeriod_oneMilliBeforeDeletion_pendingDelete


core/src/test/java/google/registry/rdap/RdapDomainActionTest.java line 696 at r5 (raw file):

  @Test
  void testDomainInExpiryAccessPeriod_oneMilliBeforeExpiry_returnsXap404() {

this also has a duplicated test testDomainInExpiryAccessPeriod_nearExpiryBoundary_returnsXap404


core/src/test/java/google/registry/rdap/RdapDomainActionTest.java line 1188 at r5 (raw file):

  @Test
  void testDomainInExpiryAccessPeriod_mixedCasePunycode_returnsXap404() {

we prob don't need all of these separate tests for IDN/punycode/mixed-case. one would likely be enough because we canonicalize the name


core/src/test/java/google/registry/rdap/RdapDomainActionTest.java line 1228 at r5 (raw file):

  @Test
  void testDomainInExpiryAccessPeriod_oneMilliBeforeDeletion_pendingDelete() {

we already have a test for this above

@gbrodman gbrodman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gbrodman made 6 comments.
Reviewable status: all files reviewed, 21 unresolved discussions (waiting on CydeWeys).


core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java line 472 at r5 (raw file):

Previously, gbrodman wrote…

i think we should only need to do this in the cases where we're in XAP, right? In the vast majority of cases we don't need to contact Valkey

actually nah because it could have been deleted during AGP. I'm not sure what exactly is the best way of accomplishing this, but we want to avoid contacting Valkey if we're sure that there's no entry there.


core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java line 69 at r5 (raw file):

  @SuppressWarnings("unchecked")
  protected Optional<V> loadFromCachesIncludingDeleted(Class<V> clazz, String key) {
    Instant now = clock.now();

multiple calls to clock.now() -- this is moving (not a transactional time) so we probably want to have only one call


core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java line 1228 at r5 (raw file):

   * for XAP.
   */
  public static Optional<Domain> loadDomainIfInXap(

i think we can remove this and its associated tests once we make the change in RdapDomainAction


core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java line 4777 at r5 (raw file):

      assertThat(domain.getDeletionTime()).isEqualTo(END_INSTANT);
    } finally {
      settings.registryPolicy.domainExpiryAccessPeriod.initialFee =

probably should separate the finally block to be in a helper method so that if something needs to be added to the "reset the state of the world" block, it gets added everywhere at once


core/src/test/java/google/registry/flows/domain/DomainFlowUtilsTest.java line 214 at r5 (raw file):

  @Test
  void testIsDomainEligibleForXap_activeDomain_returnsFalse() {
    Domain domain = persistActiveDomain("active.tld");

for this and the similar tests, we don't need to persist anything to Postgres right? it's just checking in memory things on the in-memory domain


core/src/test/java/google/registry/rdap/RdapDomainActionTest.java line 1291 at r5 (raw file):

  @Test
  void testWorkloadIsolation_zeroPrimaryDbTransactionsDuringRdapExecution() {

this may not be necessary since as far as i'm aware we already test replica usage in MultilayerDomainCacheTest

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants