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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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())));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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> </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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user