diff --git a/core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java b/core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java new file mode 100644 index 000000000..a49491c0f --- /dev/null +++ b/core/src/main/java/google/registry/batch/SyncRemoteCacheAction.java @@ -0,0 +1,197 @@ +// Copyright 2026 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.batch; + +import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_DOMAIN_SYNC; +import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_HOST_SYNC; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.request.Action.Method.POST; +import static google.registry.util.DateTimeUtils.START_INSTANT; +import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.flogger.FluentLogger; +import com.google.common.net.MediaType; +import google.registry.cache.SimplifiedJedisClient; +import google.registry.model.EppResource; +import google.registry.model.common.Cursor; +import google.registry.model.domain.Domain; +import google.registry.model.host.Host; +import google.registry.model.tld.Tld; +import google.registry.model.tld.Tlds; +import google.registry.request.Action; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import google.registry.request.lock.LockHandler; +import jakarta.inject.Inject; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.function.Function; +import org.joda.time.Duration; + +@Action( + service = Action.Service.BACKEND, + path = SyncRemoteCacheAction.PATH, + method = POST, + auth = Auth.AUTH_ADMIN) +public class SyncRemoteCacheAction implements Runnable { + + public static final String PATH = "/_dr/task/syncRemoteCache"; + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private static final String LOCK_NAME = "syncRemoteCacheAction"; + private static final int BATCH_SIZE = 10000; + + private final LockHandler lockHandler; + private final Response response; + private final Optional> domainJedisClient; + private final Optional> hostJedisClient; + + @Inject + public SyncRemoteCacheAction( + LockHandler lockHandler, + Response response, + Optional> domainJedisClient, + Optional> hostJedisClient) { + this.lockHandler = lockHandler; + this.response = response; + this.domainJedisClient = domainJedisClient; + this.hostJedisClient = hostJedisClient; + } + + @Override + public void run() { + response.setContentType(MediaType.PLAIN_TEXT_UTF_8); + if (domainJedisClient.isEmpty() || hostJedisClient.isEmpty()) { + response.setStatus(SC_NO_CONTENT); + response.setPayload("No Jedis/Valkey configuration found"); + return; + } + Callable runner = + () -> { + try { + runLocked(); + response.setStatus(SC_OK); + } catch (Exception e) { + logger.atSevere().withCause(e).log("Errored out during execution."); + response.setStatus(SC_INTERNAL_SERVER_ERROR); + response.setPayload(String.format("Errored out with cause: %s", e)); + } + return null; + }; + + if (!lockHandler.executeWithLocks(runner, null, Duration.standardHours(1), LOCK_NAME)) { + // Send a 200-series status code to prevent this conflicting action from retrying. + response.setStatus(SC_NO_CONTENT); + response.setPayload("Could not acquire lock; already running?"); + } + } + + private void runLocked() { + // Note: the transaction ordering means that cursors in our database are updated only if all the + // operations for the entity type succeeded. There is no downside to processing the same objects + // multiple times if, for some reason, saving the cursor to the DB fails. + int syncedDomains = tm().transact(this::syncDomains); + int syncedHosts = tm().transact(this::syncHosts); + String message = String.format("Synced %d domains and %d hosts.", syncedDomains, syncedHosts); + logger.atInfo().log(message); + response.setPayload(message); + } + + private int syncDomains() { + Instant domainCursorTime = getPreviousCursorTime(REMOTE_CACHE_DOMAIN_SYNC); + ImmutableSet realTlds = Tlds.getTldsOfType(Tld.TldType.REAL); + List domains = + tm().query( + "FROM Domain WHERE updateTimestamp.lastUpdateTime > :cursorTime AND tld IN" + + " :realTlds ORDER BY updateTimestamp ASC", + Domain.class) + .setParameter("cursorTime", domainCursorTime) + .setParameter("realTlds", realTlds) + .setMaxResults(BATCH_SIZE) + .getResultList(); + if (domains.isEmpty()) { + logger.atInfo().log("No domains to process"); + return 0; + } + logger.atInfo().log("Processing %d domains", domains.size()); + processResources(domainJedisClient.get(), domains, Domain::getDomainName); + setNewCursorTime(domains, REMOTE_CACHE_DOMAIN_SYNC); + return domains.size(); + } + + private int syncHosts() { + Instant hostCursorTime = getPreviousCursorTime(REMOTE_CACHE_HOST_SYNC); + List hosts = + tm().query( + "FROM Host WHERE updateTimestamp.lastUpdateTime > :cursorTime ORDER BY" + + " updateTimestamp ASC", + Host.class) + .setParameter("cursorTime", hostCursorTime) + .setMaxResults(BATCH_SIZE) + .getResultList(); + if (hosts.isEmpty()) { + logger.atInfo().log("No hosts to process"); + return 0; + } + logger.atInfo().log("Processing %d hosts", hosts.size()); + processResources(hostJedisClient.get(), hosts, Host::getRepoId); + setNewCursorTime(hosts, REMOTE_CACHE_HOST_SYNC); + return hosts.size(); + } + + private void processResources( + SimplifiedJedisClient jedisClient, List resources, Function getKeyFunction) { + ImmutableList.Builder toDeleteBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder> toSaveBuilder = + new ImmutableList.Builder<>(); + + for (T resource : resources) { + String key = getKeyFunction.apply(resource); + if (resource.getDeletionTime().isAfter(tm().getTxTime())) { + toSaveBuilder.add(new SimplifiedJedisClient.JedisResource<>(key, resource)); + } else { + toDeleteBuilder.add(key); + } + } + ImmutableList toDelete = toDeleteBuilder.build(); + ImmutableList> toSave = toSaveBuilder.build(); + + jedisClient.deleteAll(toDelete); + logger.atInfo().log("Invalidated %d from the remote cache", toDelete.size()); + jedisClient.setAll(toSave); + logger.atInfo().log("Set %d in the remote cache", toSave.size()); + } + + private Instant getPreviousCursorTime(Cursor.CursorType cursorType) { + return tm().loadByKeyIfPresent(Cursor.createGlobalVKey(cursorType)) + .map(Cursor::getCursorTimeInstant) + .orElse(START_INSTANT); + } + + private void setNewCursorTime( + List resources, Cursor.CursorType cursorType) { + Instant lastUpdateTime = Iterables.getLast(resources).getUpdateTimestamp().getTimestamp(); + tm().put(Cursor.createGlobal(cursorType, lastUpdateTime)); + logger.atInfo().log("Set new %s cursor time to %s", cursorType, lastUpdateTime); + } +} diff --git a/core/src/main/java/google/registry/cache/CacheModule.java b/core/src/main/java/google/registry/cache/CacheModule.java index 856946c10..3aee2ee8e 100644 --- a/core/src/main/java/google/registry/cache/CacheModule.java +++ b/core/src/main/java/google/registry/cache/CacheModule.java @@ -85,25 +85,37 @@ public final class CacheModule { @Provides @Singleton - public static DomainCache provideDomainCache(Optional jedis, Clock clock) { - if (jedis.isEmpty()) { - return domainName -> - ForeignKeyUtils.loadResourceByCache(Domain.class, domainName, clock.now()); - } - SimplifiedJedisClient jedisClient = - SimplifiedJedisClient.create(Domain.class, jedis.get()); - return new MultilayerDomainCache(jedisClient, clock); + public static Optional> provideDomainJedisClient( + Optional jedis) { + return jedis.map(j -> SimplifiedJedisClient.create(Domain.class, j)); } @Provides @Singleton - public static HostCache provideHostCache(Optional jedis) { - if (jedis.isEmpty()) { + public static Optional> provideHostJedisClient( + Optional jedis) { + return jedis.map(j -> SimplifiedJedisClient.create(Host.class, j)); + } + + @Provides + @Singleton + public static DomainCache provideDomainCache( + Optional> domainJedisClient, Clock clock) { + if (domainJedisClient.isEmpty()) { + return domainName -> + ForeignKeyUtils.loadResourceByCache(Domain.class, domainName, clock.now()); + } + return new MultilayerDomainCache(domainJedisClient.get(), clock); + } + + @Provides + @Singleton + public static HostCache provideHostCache(Optional> hostJedisClient) { + if (hostJedisClient.isEmpty()) { return repoId -> Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId))); } - SimplifiedJedisClient jedisClient = SimplifiedJedisClient.create(Host.class, jedis.get()); - return new MultilayerHostCache(jedisClient); + return new MultilayerHostCache(hostJedisClient.get()); } @Provides diff --git a/core/src/main/java/google/registry/cache/SimplifiedJedisClient.java b/core/src/main/java/google/registry/cache/SimplifiedJedisClient.java index b96567747..7c09b7003 100644 --- a/core/src/main/java/google/registry/cache/SimplifiedJedisClient.java +++ b/core/src/main/java/google/registry/cache/SimplifiedJedisClient.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.Iterables; import com.google.common.collect.Streams; +import com.google.common.flogger.FluentLogger; import google.registry.model.EppResource; import io.protostuff.LinkedBuffer; import io.protostuff.ProtostuffIOUtil; @@ -42,6 +43,8 @@ public class SimplifiedJedisClient { public record JedisResource(String key, V value) {} + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final int BATCH_SIZE = 500; private final Schema valueSchema; @@ -77,15 +80,17 @@ public class SimplifiedJedisClient { /** Sets multiple values in the remote cache using a Jedis {@link AbstractPipeline}. */ public void setAll(ImmutableCollection> resources) { + logger.atInfo().log("Processing %d resources", resources.size()); for (Iterable> batch : Iterables.partition(resources, BATCH_SIZE)) { - AbstractPipeline pipeline = jedis.pipelined(); - batch.forEach( - resource -> - pipeline.set( - resource.key.getBytes(StandardCharsets.UTF_8), - serialize(resource.value), - new SetParams().pxAt(resource.value.getDeletionTime().toEpochMilli()))); - pipeline.sync(); + try (AbstractPipeline pipeline = jedis.pipelined()) { + batch.forEach( + resource -> + pipeline.set( + resource.key.getBytes(StandardCharsets.UTF_8), + serialize(resource.value), + new SetParams().pxAt(resource.value.getDeletionTime().toEpochMilli()))); + pipeline.sync(); + } } } diff --git a/core/src/main/java/google/registry/model/common/Cursor.java b/core/src/main/java/google/registry/model/common/Cursor.java index 57ce27d5f..c2bf3355b 100644 --- a/core/src/main/java/google/registry/model/common/Cursor.java +++ b/core/src/main/java/google/registry/model/common/Cursor.java @@ -103,7 +103,13 @@ public class Cursor extends UpdateAutoTimestampEntity { ICANN_UPLOAD_TX(true), /** Cursor for tracking monthly uploads of ICANN activity reports. */ - ICANN_UPLOAD_ACTIVITY(true); + ICANN_UPLOAD_ACTIVITY(true), + + /** Cursor for tracking the reflection of domain changes in the remote cache. */ + REMOTE_CACHE_DOMAIN_SYNC(false), + + /** Cursor for tracking the reflection of host changes in the remote cache. */ + REMOTE_CACHE_HOST_SYNC(false); private final boolean scoped; diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java index ba8805954..c13a50e24 100644 --- a/core/src/main/java/google/registry/module/RequestComponent.java +++ b/core/src/main/java/google/registry/module/RequestComponent.java @@ -27,6 +27,7 @@ import google.registry.batch.RelockDomainAction; import google.registry.batch.ResaveAllEppResourcesPipelineAction; import google.registry.batch.ResaveEntityAction; import google.registry.batch.SendExpiringCertificateNotificationEmailAction; +import google.registry.batch.SyncRemoteCacheAction; import google.registry.bsa.BsaDownloadAction; import google.registry.bsa.BsaRefreshAction; import google.registry.bsa.BsaValidateAction; @@ -326,6 +327,8 @@ interface RequestComponent { SyncGroupMembersAction syncGroupMembersAction(); + SyncRemoteCacheAction syncRemoteCacheAction(); + SyncRegistrarsSheetAction syncRegistrarsSheetAction(); TldFanoutAction tldFanoutAction(); diff --git a/core/src/test/java/google/registry/batch/SyncRemoteCacheActionTest.java b/core/src/test/java/google/registry/batch/SyncRemoteCacheActionTest.java new file mode 100644 index 000000000..655d6fc5c --- /dev/null +++ b/core/src/test/java/google/registry/batch/SyncRemoteCacheActionTest.java @@ -0,0 +1,228 @@ +// Copyright 2026 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.batch; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_DOMAIN_SYNC; +import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_HOST_SYNC; +import static google.registry.testing.DatabaseHelper.createTld; +import static google.registry.testing.DatabaseHelper.persistActiveDomain; +import static google.registry.testing.DatabaseHelper.persistActiveHost; +import static google.registry.testing.DatabaseHelper.persistDeletedDomain; +import static google.registry.testing.DatabaseHelper.persistDeletedHost; +import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.google.common.collect.ImmutableList; +import google.registry.cache.SimplifiedJedisClient; +import google.registry.model.common.Cursor; +import google.registry.model.domain.Domain; +import google.registry.model.host.Host; +import google.registry.persistence.transaction.JpaTestExtensions; +import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; +import google.registry.testing.DatabaseHelper; +import google.registry.testing.FakeClock; +import google.registry.testing.FakeLockHandler; +import google.registry.testing.FakeResponse; +import java.time.Instant; +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.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link SyncRemoteCacheAction}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SyncRemoteCacheActionTest { + + private final FakeClock clock = new FakeClock(DateTime.parse("2025-01-01T00:00:00Z")); + + @RegisterExtension + final JpaIntegrationTestExtension jpa = + new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); + + @Mock private SimplifiedJedisClient domainJedisClient; + @Mock private SimplifiedJedisClient hostJedisClient; + + private final FakeResponse response = new FakeResponse(); + private FakeLockHandler lockHandler = new FakeLockHandler(true); + private SyncRemoteCacheAction action; + + @BeforeEach + void beforeEach() { + createTld("tld"); + action = + new SyncRemoteCacheAction( + lockHandler, response, Optional.of(domainJedisClient), Optional.of(hostJedisClient)); + } + + @Test + void test_noJedisConfig() { + action = new SyncRemoteCacheAction(lockHandler, response, Optional.empty(), Optional.empty()); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT); + assertThat(response.getPayload()).contains("No Jedis/Valkey configuration found"); + } + + @Test + void test_lockAcquisitionFails() { + lockHandler = new FakeLockHandler(false); + action = + new SyncRemoteCacheAction( + lockHandler, response, Optional.of(domainJedisClient), Optional.of(hostJedisClient)); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT); + assertThat(response.getPayload()).contains("Could not acquire lock"); + } + + @Test + void test_exceptionThrown() { + doThrow(new RuntimeException("Redis failed")).when(domainJedisClient).deleteAll(any()); + persistActiveDomain("example.tld"); // So there is something to process + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); + assertThat(response.getPayload()).contains("Errored out with cause"); + } + + @Test + void test_syncDomains_noDomains() { + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + verifyNoInteractions(domainJedisClient); + assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_DOMAIN_SYNC))) + .isEmpty(); + } + + @Test + void test_syncDomains_withDomains() { + Domain domain1 = persistActiveDomain("example1.tld"); + clock.advanceOneMilli(); + Domain domain2 = persistActiveDomain("example2.tld"); + + action.run(); + + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(domainJedisClient) + .setAll( + eq( + ImmutableList.of( + new SimplifiedJedisClient.JedisResource<>("example1.tld", domain1), + new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2)))); + + assertThat( + DatabaseHelper.loadByKey(Cursor.createGlobalVKey(REMOTE_CACHE_DOMAIN_SYNC)) + .getCursorTimeInstant() + .toString()) + .isEqualTo("2025-01-01T00:00:00.001Z"); + } + + @Test + void test_syncDomains_withDeletedDomains() { + Domain activeDomain = persistActiveDomain("active.tld"); + persistDeletedDomain("deleted.tld", clock.nowUtc().minusDays(1)); + + action.run(); + + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(domainJedisClient) + .setAll( + eq( + ImmutableList.of( + new SimplifiedJedisClient.JedisResource<>("active.tld", activeDomain)))); + verify(domainJedisClient).deleteAll(eq(ImmutableList.of("deleted.tld"))); + } + + @Test + void testCursorTime_skipsOldChange() { + persistActiveDomain("example1.tld"); + + clock.advanceOneMilli(); + Instant cursorTime = clock.now(); + + DatabaseHelper.persistResource(Cursor.createGlobal(REMOTE_CACHE_DOMAIN_SYNC, cursorTime)); + + clock.advanceOneMilli(); + Domain domain2 = persistActiveDomain("example2.tld"); + + action.run(); + + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(domainJedisClient) + .setAll( + eq( + ImmutableList.of( + new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2)))); + } + + @Test + void test_syncHosts_noHosts() { + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + verifyNoInteractions(hostJedisClient); + assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_HOST_SYNC))) + .isEmpty(); + } + + @Test + void test_syncHosts_withHosts() { + Host host1 = persistActiveHost("ns1.example.tld"); + clock.advanceOneMilli(); + Host host2 = persistActiveHost("ns2.example.tld"); + + action.run(); + + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(hostJedisClient) + .setAll( + eq( + ImmutableList.of( + new SimplifiedJedisClient.JedisResource<>(host1.getRepoId(), host1), + new SimplifiedJedisClient.JedisResource<>(host2.getRepoId(), host2)))); + + assertThat( + DatabaseHelper.loadByKey(Cursor.createGlobalVKey(REMOTE_CACHE_HOST_SYNC)) + .getCursorTimeInstant() + .toString()) + .isEqualTo("2025-01-01T00:00:00.001Z"); + } + + @Test + void test_syncHosts_withDeletedHosts() { + Host active = persistActiveHost("ns1.example.tld"); + Host deleted = persistDeletedHost("ns2.example.tld", clock.nowUtc().minusDays(1)); + + action.run(); + + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(hostJedisClient) + .setAll( + eq( + ImmutableList.of( + new SimplifiedJedisClient.JedisResource<>(active.getRepoId(), active)))); + verify(hostJedisClient).deleteAll(eq(ImmutableList.of(deleted.getRepoId()))); + } +} diff --git a/core/src/test/resources/google/registry/module/routing.txt b/core/src/test/resources/google/registry/module/routing.txt index b305c192e..516a2f30b 100644 --- a/core/src/test/resources/google/registry/module/routing.txt +++ b/core/src/test/resources/google/registry/module/routing.txt @@ -51,6 +51,7 @@ BACKEND /_dr/task/resaveEntity ResaveEntityAction BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN BACKEND /_dr/task/syncGroupMembers SyncGroupMembersAction POST n APP ADMIN BACKEND /_dr/task/syncRegistrarsSheet SyncRegistrarsSheetAction POST n APP ADMIN +BACKEND /_dr/task/syncRemoteCache SyncRemoteCacheAction POST n APP ADMIN BACKEND /_dr/task/tmchCrl TmchCrlAction POST y APP ADMIN BACKEND /_dr/task/tmchDnl TmchDnlAction POST y APP ADMIN BACKEND /_dr/task/tmchSmdrl TmchSmdrlAction POST y APP ADMIN diff --git a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html index 435ae3fd2..32e023697 100644 --- a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html @@ -257,11 +257,11 @@ td.section { generated by - SchemaCrawler 17.8.1 + SchemaCrawler 17.10.2 generated on - 2026-03-21 03:39:17 + 2026-05-01 18:52:49 last flyway file @@ -273,7 +273,7 @@ td.section {

 

- SchemaCrawler_Diagram generated by SchemaCrawler 17.8.1 generated on 2026-03-21 03:39:17 + SchemaCrawler_Diagram generated by SchemaCrawler 17.10.2 generated on 2026-05-01 18:52:49 allocationtoken_a08ccbef public."AllocationToken" [table] token text not null domain_name text redemption_domain_repo_id text token_type text diff --git a/db/src/main/resources/sql/er_diagram/full_er_diagram.html b/db/src/main/resources/sql/er_diagram/full_er_diagram.html index e2fc1e821..720615b83 100644 --- a/db/src/main/resources/sql/er_diagram/full_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/full_er_diagram.html @@ -257,11 +257,11 @@ td.section {
generated by - SchemaCrawler 17.8.1 + SchemaCrawler 17.10.2
generated on - 2026-03-21 03:39:14 + 2026-05-01 18:52:47
last flyway file @@ -273,7 +273,7 @@ td.section {

 

- SchemaCrawler_Diagram generated by SchemaCrawler 17.8.1 generated on 2026-03-21 03:39:14 + SchemaCrawler_Diagram generated by SchemaCrawler 17.10.2 generated on 2026-05-01 18:52:47 allocationtoken_a08ccbef public."AllocationToken" [table] token text not null update_timestamp timestamptz allowed_registrar_ids _text allowed_tlds _text creation_time timestamptz not null discount_fraction float8(17, 17) not null discount_premiums bool not null discount_years int4 not null domain_name text redemption_domain_repo_id text token_status_transitions hstore token_type text redemption_domain_history_id int8 renewal_price_behavior text not null registration_behavior text not null allowed_epp_actions _text renewal_price_amount numeric(19, 2) renewal_price_currency text discount_price_amount numeric(19, 2) discount_price_currency text diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index 72019c5c2..3e1c7e5eb 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -147,7 +147,7 @@ create table "Cursor" ( scope text not null, - type text not null check ((type in ('BRDA','RDE_REPORT','RDE_STAGING','RDE_UPLOAD','RDE_UPLOAD_SFTP','RECURRING_BILLING','SYNC_REGISTRAR_SHEET','ICANN_UPLOAD_TX','ICANN_UPLOAD_ACTIVITY'))), + type text not null check ((type in ('BRDA','RDE_REPORT','RDE_STAGING','RDE_UPLOAD','RDE_UPLOAD_SFTP','RECURRING_BILLING','SYNC_REGISTRAR_SHEET','ICANN_UPLOAD_TX','ICANN_UPLOAD_ACTIVITY','REMOTE_CACHE_DOMAIN_SYNC','REMOTE_CACHE_HOST_SYNC'))), last_update_time timestamp(6) with time zone not null, cursor_time timestamp(6) with time zone not null, primary key (scope, type)