1
0
mirror of https://github.com/google/nomulus synced 2026-05-13 03:11:49 +00:00

Add an action to sync the remote Valkey cache (#3032)

This uses two cursors (one for hosts and one for domains) to track our
progress in "catching up", or syncing recent changes to the database,
using the update-timestamp field. When the cache is in use and fully
caught up, these
cursors should be kept relatively up-to-date to the actual time, i.e.
less than one hour behind
This commit is contained in:
gbrodman
2026-05-05 11:29:43 -04:00
committed by GitHub
parent f44642fe28
commit 81b3a2fc5b
10 changed files with 480 additions and 28 deletions

View File

@@ -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<SimplifiedJedisClient<Domain>> domainJedisClient;
private final Optional<SimplifiedJedisClient<Host>> hostJedisClient;
@Inject
public SyncRemoteCacheAction(
LockHandler lockHandler,
Response response,
Optional<SimplifiedJedisClient<Domain>> domainJedisClient,
Optional<SimplifiedJedisClient<Host>> 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<Void> 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<String> realTlds = Tlds.getTldsOfType(Tld.TldType.REAL);
List<Domain> 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<Host> 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 <T extends EppResource> void processResources(
SimplifiedJedisClient<T> jedisClient, List<T> resources, Function<T, String> getKeyFunction) {
ImmutableList.Builder<String> toDeleteBuilder = new ImmutableList.Builder<>();
ImmutableList.Builder<SimplifiedJedisClient.JedisResource<T>> 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<String> toDelete = toDeleteBuilder.build();
ImmutableList<SimplifiedJedisClient.JedisResource<T>> 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<? extends EppResource> 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);
}
}

View File

@@ -85,25 +85,37 @@ public final class CacheModule {
@Provides
@Singleton
public static DomainCache provideDomainCache(Optional<UnifiedJedis> jedis, Clock clock) {
if (jedis.isEmpty()) {
return domainName ->
ForeignKeyUtils.loadResourceByCache(Domain.class, domainName, clock.now());
}
SimplifiedJedisClient<Domain> jedisClient =
SimplifiedJedisClient.create(Domain.class, jedis.get());
return new MultilayerDomainCache(jedisClient, clock);
public static Optional<SimplifiedJedisClient<Domain>> provideDomainJedisClient(
Optional<UnifiedJedis> jedis) {
return jedis.map(j -> SimplifiedJedisClient.create(Domain.class, j));
}
@Provides
@Singleton
public static HostCache provideHostCache(Optional<UnifiedJedis> jedis) {
if (jedis.isEmpty()) {
public static Optional<SimplifiedJedisClient<Host>> provideHostJedisClient(
Optional<UnifiedJedis> jedis) {
return jedis.map(j -> SimplifiedJedisClient.create(Host.class, j));
}
@Provides
@Singleton
public static DomainCache provideDomainCache(
Optional<SimplifiedJedisClient<Domain>> 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<SimplifiedJedisClient<Host>> hostJedisClient) {
if (hostJedisClient.isEmpty()) {
return repoId ->
Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)));
}
SimplifiedJedisClient<Host> jedisClient = SimplifiedJedisClient.create(Host.class, jedis.get());
return new MultilayerHostCache(jedisClient);
return new MultilayerHostCache(hostJedisClient.get());
}
@Provides

View File

@@ -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<V extends EppResource> {
public record JedisResource<V extends EppResource>(String key, V value) {}
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final int BATCH_SIZE = 500;
private final Schema<V> valueSchema;
@@ -77,15 +80,17 @@ public class SimplifiedJedisClient<V extends EppResource> {
/** Sets multiple values in the remote cache using a Jedis {@link AbstractPipeline}. */
public void setAll(ImmutableCollection<JedisResource<V>> resources) {
logger.atInfo().log("Processing %d resources", resources.size());
for (Iterable<JedisResource<V>> 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();
}
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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<Domain> domainJedisClient;
@Mock private SimplifiedJedisClient<Host> 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())));
}
}

View File

@@ -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

View File

@@ -257,11 +257,11 @@ td.section {
<tbody>
<tr>
<td class="property_name">generated by</td>
<td class="property_value">SchemaCrawler 17.8.1</td>
<td class="property_value">SchemaCrawler 17.10.2</td>
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2026-03-21 03:39:17</td>
<td class="property_value">2026-05-01 18:52:49</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
@@ -273,7 +273,7 @@ td.section {
<p>&nbsp;</p>
<svg viewBox="0.00 0.00 4797.00 3528.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 3524)">
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="transparent" points="-4,4 -4,-3524 4793,-3524 4793,4 -4,4" /> <text text-anchor="start" x="4556.5" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text text-anchor="start" x="4639.5" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.8.1</text> <text text-anchor="start" x="4555.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text text-anchor="start" x="4639.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2026-03-21 03:39:17</text> <polygon fill="none" stroke="#888888" points="4552,-4 4552,-44 4781,-44 4781,-4 4552,-4" /> <!-- allocationtoken_a08ccbef -->
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="transparent" points="-4,4 -4,-3524 4793,-3524 4793,4 -4,4" /> <text text-anchor="start" x="4549" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text text-anchor="start" x="4632" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.10.2</text> <text text-anchor="start" x="4548" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text text-anchor="start" x="4632" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2026-05-01 18:52:49</text> <polygon fill="none" stroke="#888888" points="4545,-4 4545,-44 4781,-44 4781,-4 4545,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>allocationtoken_a08ccbef</title> <polygon fill="#e9c2f2" stroke="transparent" points="481.5,-993 481.5,-1012 667.5,-1012 667.5,-993 481.5,-993" /> <text text-anchor="start" x="483.5" y="-999.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,-993 667.5,-1012 741.5,-1012 741.5,-993 667.5,-993" /> <text text-anchor="start" x="702.5" y="-998.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text text-anchor="start" x="483.5" y="-980.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="-979.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-979.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text text-anchor="start" x="483.5" y="-960.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text> <text text-anchor="start" x="661.5" y="-960.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-960.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text text-anchor="start" x="483.5" y="-941.8" font-family="Helvetica,sans-Serif" font-size="14.00">redemption_domain_repo_id</text> <text text-anchor="start" x="661.5" y="-941.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-941.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text text-anchor="start" x="483.5" y="-922.8" font-family="Helvetica,sans-Serif" font-size="14.00">token_type</text> <text text-anchor="start" x="661.5" y="-922.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-922.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="480.5,-916.5 480.5,-1013.5 742.5,-1013.5 742.5,-916.5 480.5,-916.5" />
</g>

File diff suppressed because one or more lines are too long

View File

@@ -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)