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

Fix remote cache prefixing by handling it all in the Jedis client (#3035)

Found this out while testing metrics. In hindsight, not the best idea to
handle prefixing outside of the client itself. Instead, we'll enforce
the prefixing closer to Valkey, all in one place.
This commit is contained in:
gbrodman
2026-05-07 16:28:56 -04:00
committed by GitHub
parent 74f9f5d478
commit 80eefc6498
10 changed files with 138 additions and 165 deletions

View File

@@ -63,25 +63,20 @@ public class SyncRemoteCacheAction implements Runnable {
private final LockHandler lockHandler;
private final Response response;
private final Optional<SimplifiedJedisClient<Domain>> domainJedisClient;
private final Optional<SimplifiedJedisClient<Host>> hostJedisClient;
private final Optional<SimplifiedJedisClient> jedisClient;
@Inject
public SyncRemoteCacheAction(
LockHandler lockHandler,
Response response,
Optional<SimplifiedJedisClient<Domain>> domainJedisClient,
Optional<SimplifiedJedisClient<Host>> hostJedisClient) {
LockHandler lockHandler, Response response, Optional<SimplifiedJedisClient> jedisClient) {
this.lockHandler = lockHandler;
this.response = response;
this.domainJedisClient = domainJedisClient;
this.hostJedisClient = hostJedisClient;
this.jedisClient = jedisClient;
}
@Override
public void run() {
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
if (domainJedisClient.isEmpty() || hostJedisClient.isEmpty()) {
if (jedisClient.isEmpty()) {
response.setStatus(SC_NO_CONTENT);
response.setPayload("No Jedis/Valkey configuration found");
return;
@@ -134,7 +129,7 @@ public class SyncRemoteCacheAction implements Runnable {
return 0;
}
logger.atInfo().log("Processing %d domains", domains.size());
processResources(domainJedisClient.get(), domains, Domain::getDomainName);
processResources(Domain.class, domains, Domain::getDomainName);
setNewCursorTime(domains, REMOTE_CACHE_DOMAIN_SYNC);
return domains.size();
}
@@ -154,13 +149,13 @@ public class SyncRemoteCacheAction implements Runnable {
return 0;
}
logger.atInfo().log("Processing %d hosts", hosts.size());
processResources(hostJedisClient.get(), hosts, Host::getRepoId);
processResources(Host.class, 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) {
Class<T> clazz, List<T> resources, Function<T, String> getKeyFunction) {
ImmutableList.Builder<String> toDeleteBuilder = new ImmutableList.Builder<>();
ImmutableList.Builder<SimplifiedJedisClient.JedisResource<T>> toSaveBuilder =
new ImmutableList.Builder<>();
@@ -176,9 +171,9 @@ public class SyncRemoteCacheAction implements Runnable {
ImmutableList<String> toDelete = toDeleteBuilder.build();
ImmutableList<SimplifiedJedisClient.JedisResource<T>> toSave = toSaveBuilder.build();
jedisClient.deleteAll(toDelete);
jedisClient.get().deleteAll(clazz, toDelete);
logger.atInfo().log("Invalidated %d from the remote cache", toDelete.size());
jedisClient.setAll(toSave);
jedisClient.get().setAll(toSave);
logger.atInfo().log("Set %d in the remote cache", toSave.size());
}

View File

@@ -85,22 +85,14 @@ public final class CacheModule {
@Provides
@Singleton
public static Optional<SimplifiedJedisClient<Domain>> provideDomainJedisClient(
Optional<UnifiedJedis> jedis) {
return jedis.map(j -> SimplifiedJedisClient.create(Domain.class, j));
}
@Provides
@Singleton
public static Optional<SimplifiedJedisClient<Host>> provideHostJedisClient(
Optional<UnifiedJedis> jedis) {
return jedis.map(j -> SimplifiedJedisClient.create(Host.class, j));
public static Optional<SimplifiedJedisClient> provideJedisClient(Optional<UnifiedJedis> jedis) {
return jedis.map(SimplifiedJedisClient::new);
}
@Provides
@Singleton
public static DomainCache provideDomainCache(
Optional<SimplifiedJedisClient<Domain>> domainJedisClient, Clock clock) {
Optional<SimplifiedJedisClient> domainJedisClient, Clock clock) {
if (domainJedisClient.isEmpty()) {
return domainName ->
ForeignKeyUtils.loadResourceByCache(Domain.class, domainName, clock.now());
@@ -110,7 +102,7 @@ public final class CacheModule {
@Provides
@Singleton
public static HostCache provideHostCache(Optional<SimplifiedJedisClient<Host>> hostJedisClient) {
public static HostCache provideHostCache(Optional<SimplifiedJedisClient> hostJedisClient) {
if (hostJedisClient.isEmpty()) {
return repoId ->
Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)));

View File

@@ -32,14 +32,14 @@ public class MultilayerDomainCache extends MultilayerEppResourceCache<Domain>
private final Clock clock;
public MultilayerDomainCache(SimplifiedJedisClient<Domain> jedisClient, Clock clock) {
public MultilayerDomainCache(SimplifiedJedisClient jedisClient, Clock clock) {
super(jedisClient);
this.clock = clock;
}
@Override
public Optional<Domain> loadByDomainName(String domainName) {
return loadFromCaches(domainName);
return loadFromCaches(Domain.class, domainName);
}
@Override
@@ -56,11 +56,6 @@ public class MultilayerDomainCache extends MultilayerEppResourceCache<Domain>
.map(domain -> domain.cloneProjectedAtTime(now));
}
@Override
protected String getJedisPrefix() {
return "d_";
}
@Override
protected boolean shouldPersistToRemoteCache(Domain domain) {
return Tld.get(domain.getTld()).getTldType().equals(Tld.TldType.REAL);

View File

@@ -35,21 +35,19 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
.maximumSize(RegistryConfig.getEppResourceMaxCachedEntries())
.build();
private final SimplifiedJedisClient<V> jedisClient;
private final SimplifiedJedisClient jedisClient;
protected MultilayerEppResourceCache(SimplifiedJedisClient<V> jedisClient) {
protected MultilayerEppResourceCache(SimplifiedJedisClient jedisClient) {
this.jedisClient = jedisClient;
}
protected abstract Optional<V> loadFromDatabase(String key);
protected abstract String getJedisPrefix();
protected boolean shouldPersistToRemoteCache(V value) {
return true;
}
protected Optional<V> loadFromCaches(String key) {
protected Optional<V> loadFromCaches(Class<V> clazz, String key) {
// hopefully the resource is in the local cache
Optional<V> possibleValue = Optional.ofNullable(localCache.getIfPresent(key));
if (possibleValue.isPresent()) {
@@ -57,8 +55,7 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
}
// if not, try the remote cache
String jedisKey = getJedisPrefix() + key;
possibleValue = jedisClient.get(jedisKey);
possibleValue = jedisClient.get(clazz, key);
if (possibleValue.isPresent()) {
localCache.put(key, possibleValue.get());
return possibleValue;
@@ -70,7 +67,7 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
v -> {
// Optional has no direct "peek" functionality to fill the caches
if (shouldPersistToRemoteCache(v)) {
jedisClient.set(new SimplifiedJedisClient.JedisResource<>(jedisKey, v));
jedisClient.set(new SimplifiedJedisClient.JedisResource<>(key, v));
}
localCache.put(key, v);
return v;

View File

@@ -27,13 +27,13 @@ import java.util.Optional;
*/
public class MultilayerHostCache extends MultilayerEppResourceCache<Host> implements HostCache {
public MultilayerHostCache(SimplifiedJedisClient<Host> jedisClient) {
public MultilayerHostCache(SimplifiedJedisClient jedisClient) {
super(jedisClient);
}
@Override
public Optional<Host> loadByRepoId(String repoId) {
return loadFromCaches(repoId);
return loadFromCaches(Host.class, repoId);
}
@Override
@@ -41,9 +41,4 @@ public class MultilayerHostCache extends MultilayerEppResourceCache<Host> implem
return replicaTm()
.transact(() -> replicaTm().loadByKeyIfPresent(VKey.create(Host.class, repoId)));
}
@Override
protected String getJedisPrefix() {
return "h_";
}
}

View File

@@ -14,13 +14,17 @@
package google.registry.cache;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.model.EppResource;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import io.protostuff.LinkedBuffer;
import io.protostuff.ProtostuffIOUtil;
import io.protostuff.Schema;
@@ -39,54 +43,57 @@ import redis.clients.jedis.params.SetParams;
* <p>{@link UnifiedJedis} pairs key-value types, so we need the key to be serialized to a byte
* array as well.
*/
public class SimplifiedJedisClient<V extends EppResource> {
public class SimplifiedJedisClient {
public record JedisResource<V extends EppResource>(String key, V value) {}
private static final ImmutableMap<Class<? extends EppResource>, String> TYPE_PREFIXES =
ImmutableMap.of(
Domain.class, "d_",
Host.class, "h_");
private static final ImmutableMap<Class<? extends EppResource>, Schema<? extends EppResource>>
VALUE_SCHEMAS =
ImmutableMap.of(
Domain.class, RuntimeSchema.getSchema(Domain.class),
Host.class, RuntimeSchema.getSchema(Host.class));
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final int BATCH_SIZE = 500;
private final Schema<V> valueSchema;
private final UnifiedJedis jedis;
public static <V extends EppResource> SimplifiedJedisClient<V> create(
Class<V> valueClass, UnifiedJedis jedis) {
Schema<V> valueSchema = RuntimeSchema.getSchema(valueClass);
return new SimplifiedJedisClient<>(valueSchema, jedis);
}
private SimplifiedJedisClient(Schema<V> valueSchema, UnifiedJedis jedis) {
this.valueSchema = valueSchema;
SimplifiedJedisClient(UnifiedJedis jedis) {
this.jedis = jedis;
}
/** Gets the value from the remote cache. Returns null if it does not exist. */
public Optional<V> get(String key) {
public <V extends EppResource> Optional<V> get(Class<V> clazz, String key) {
checkNotNull(key, "Key cannot be null");
byte[] data = jedis.get(key.getBytes(StandardCharsets.UTF_8));
return Optional.ofNullable(data).map(this::deserialize);
byte[] data = jedis.get(convertKey(clazz, key));
return Optional.ofNullable(data).map(d -> deserialize(clazz, d));
}
/** Sets the value in the remote cache. */
public void set(JedisResource<V> resource) {
public <V extends EppResource> void set(JedisResource<V> resource) {
checkNotNull(resource.key, "Key cannot be null");
checkNotNull(resource.value, "Value cannot be null");
jedis.set(
resource.key.getBytes(StandardCharsets.UTF_8),
convertKey(resource.value.getClass(), resource.key),
serialize(resource.value),
new SetParams().pxAt(resource.value.getDeletionTime().toEpochMilli()));
}
/** Sets multiple values in the remote cache using a Jedis {@link AbstractPipeline}. */
public void setAll(ImmutableCollection<JedisResource<V>> resources) {
public <V extends EppResource> void setAll(ImmutableCollection<JedisResource<V>> resources) {
logger.atInfo().log("Processing %d resources", resources.size());
for (Iterable<JedisResource<V>> batch : Iterables.partition(resources, BATCH_SIZE)) {
try (AbstractPipeline pipeline = jedis.pipelined()) {
batch.forEach(
resource ->
pipeline.set(
resource.key.getBytes(StandardCharsets.UTF_8),
convertKey(resource.value.getClass(), resource.key),
serialize(resource.value),
new SetParams().pxAt(resource.value.getDeletionTime().toEpochMilli())));
pipeline.sync();
@@ -106,18 +113,18 @@ public class SimplifiedJedisClient<V extends EppResource> {
* <p>This could also be accomplished by using {@link #setAll(ImmutableCollection)} with
* expiration times that are in the past, but this is clearer.
*/
public void deleteAll(ImmutableCollection<String> keys) {
public void deleteAll(Class<?> valueType, ImmutableCollection<String> keys) {
// we use a reasonably small batch size to avoid overwhelming the network
for (Iterable<String> batch : Iterables.partition(keys, BATCH_SIZE)) {
byte[][] keysToUnlink =
Streams.stream(batch)
.map(key -> key.getBytes(StandardCharsets.UTF_8))
.toArray(byte[][]::new);
Streams.stream(batch).map(key -> convertKey(valueType, key)).toArray(byte[][]::new);
jedis.unlink(keysToUnlink);
}
}
private byte[] serialize(V value) {
private <V extends EppResource> byte[] serialize(V value) {
@SuppressWarnings("unchecked")
Schema<V> valueSchema = (Schema<V>) getValueSchema(value.getClass());
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
return ProtostuffIOUtil.toByteArray(value, valueSchema, buffer);
@@ -126,10 +133,22 @@ public class SimplifiedJedisClient<V extends EppResource> {
}
}
private V deserialize(byte[] data) {
private <V extends EppResource> V deserialize(Class<V> clazz, byte[] data) {
// We use protobufs because other deserializers don't play nicely with immutable collections
Schema<V> valueSchema = getValueSchema(clazz);
V value = valueSchema.newMessage();
ProtostuffIOUtil.mergeFrom(data, value, valueSchema);
return value;
}
private byte[] convertKey(Class<?> clazz, String key) {
checkArgument(TYPE_PREFIXES.containsKey(clazz), "Unknown class type %s", clazz);
return (TYPE_PREFIXES.get(clazz) + key).getBytes(StandardCharsets.UTF_8);
}
@SuppressWarnings("unchecked")
private <V extends EppResource> Schema<V> getValueSchema(Class<V> clazz) {
checkArgument(VALUE_SCHEMAS.containsKey(clazz), "Unknown class type %s", clazz);
return (Schema<V>) VALUE_SCHEMAS.get(clazz);
}
}

View File

@@ -26,7 +26,6 @@ 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;
@@ -65,8 +64,7 @@ class SyncRemoteCacheActionTest {
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@Mock private SimplifiedJedisClient<Domain> domainJedisClient;
@Mock private SimplifiedJedisClient<Host> hostJedisClient;
@Mock private SimplifiedJedisClient jedisClient;
private final FakeResponse response = new FakeResponse();
private FakeLockHandler lockHandler = new FakeLockHandler(true);
@@ -75,14 +73,12 @@ class SyncRemoteCacheActionTest {
@BeforeEach
void beforeEach() {
createTld("tld");
action =
new SyncRemoteCacheAction(
lockHandler, response, Optional.of(domainJedisClient), Optional.of(hostJedisClient));
action = new SyncRemoteCacheAction(lockHandler, response, Optional.of(jedisClient));
}
@Test
void test_noJedisConfig() {
action = new SyncRemoteCacheAction(lockHandler, response, Optional.empty(), Optional.empty());
action = new SyncRemoteCacheAction(lockHandler, response, Optional.empty());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload()).contains("No Jedis/Valkey configuration found");
@@ -91,9 +87,7 @@ class SyncRemoteCacheActionTest {
@Test
void test_lockAcquisitionFails() {
lockHandler = new FakeLockHandler(false);
action =
new SyncRemoteCacheAction(
lockHandler, response, Optional.of(domainJedisClient), Optional.of(hostJedisClient));
action = new SyncRemoteCacheAction(lockHandler, response, Optional.of(jedisClient));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload()).contains("Could not acquire lock");
@@ -101,7 +95,7 @@ class SyncRemoteCacheActionTest {
@Test
void test_exceptionThrown() {
doThrow(new RuntimeException("Redis failed")).when(domainJedisClient).deleteAll(any());
doThrow(new RuntimeException("Redis failed")).when(jedisClient).deleteAll(any(), any());
persistActiveDomain("example.tld"); // So there is something to process
action.run();
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
@@ -112,7 +106,7 @@ class SyncRemoteCacheActionTest {
void test_syncDomains_noDomains() {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verifyNoInteractions(domainJedisClient);
verifyNoInteractions(jedisClient);
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_DOMAIN_SYNC)))
.isEmpty();
}
@@ -126,12 +120,11 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verify(domainJedisClient)
verify(jedisClient)
.setAll(
eq(
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("example1.tld", domain1),
new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2))));
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("example1.tld", domain1),
new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2)));
assertThat(
DatabaseHelper.loadByKey(Cursor.createGlobalVKey(REMOTE_CACHE_DOMAIN_SYNC))
@@ -148,12 +141,11 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verify(domainJedisClient)
verify(jedisClient)
.setAll(
eq(
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("active.tld", activeDomain))));
verify(domainJedisClient).deleteAll(eq(ImmutableList.of("deleted.tld")));
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("active.tld", activeDomain)));
verify(jedisClient).deleteAll(Domain.class, ImmutableList.of("deleted.tld"));
}
@Test
@@ -171,18 +163,16 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verify(domainJedisClient)
verify(jedisClient)
.setAll(
eq(
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2))));
ImmutableList.of(new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2)));
}
@Test
void test_syncHosts_noHosts() {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verifyNoInteractions(hostJedisClient);
verifyNoInteractions(jedisClient);
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_HOST_SYNC)))
.isEmpty();
}
@@ -196,12 +186,11 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verify(hostJedisClient)
verify(jedisClient)
.setAll(
eq(
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>(host1.getRepoId(), host1),
new SimplifiedJedisClient.JedisResource<>(host2.getRepoId(), host2))));
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>(host1.getRepoId(), host1),
new SimplifiedJedisClient.JedisResource<>(host2.getRepoId(), host2)));
assertThat(
DatabaseHelper.loadByKey(Cursor.createGlobalVKey(REMOTE_CACHE_HOST_SYNC))
@@ -218,11 +207,10 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verify(hostJedisClient)
verify(jedisClient)
.setAll(
eq(
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>(active.getRepoId(), active))));
verify(hostJedisClient).deleteAll(eq(ImmutableList.of(deleted.getRepoId())));
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>(active.getRepoId(), active)));
verify(jedisClient).deleteAll(Host.class, ImmutableList.of(deleted.getRepoId()));
}
}

View File

@@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -42,7 +41,7 @@ public class MultilayerDomainCacheTest {
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final SimplifiedJedisClient<Domain> jedisClient = mock(SimplifiedJedisClient.class);
private final SimplifiedJedisClient jedisClient = mock(SimplifiedJedisClient.class);
private final FakeClock clock = new FakeClock();
private MultilayerDomainCache cache;
@@ -58,8 +57,8 @@ public class MultilayerDomainCacheTest {
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
// We should have filled the caches after one attempt to load from Valkey
verify(jedisClient).get("d_example.tld");
verify(jedisClient).set(new SimplifiedJedisClient.JedisResource<>("d_example.tld", domain));
verify(jedisClient).get(Domain.class, "example.tld");
verify(jedisClient).set(new SimplifiedJedisClient.JedisResource<>("example.tld", domain));
// Further loads hit the local cache
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
@@ -71,7 +70,7 @@ public class MultilayerDomainCacheTest {
// Note: we don't save the domain to SQL
Domain domain = DatabaseHelper.newDomain("example.tld");
// We hit the Valkey cache first
when(jedisClient.get(eq("d_example.tld"))).thenReturn(Optional.of(domain));
when(jedisClient.get(Domain.class, "example.tld")).thenReturn(Optional.of(domain));
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
}
@@ -83,7 +82,7 @@ public class MultilayerDomainCacheTest {
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
// This time, we don't populate the remote cache because it's prober data
verify(jedisClient).get("d_example.tld");
verify(jedisClient).get(Domain.class, "example.tld");
verifyNoMoreInteractions(jedisClient);
}

View File

@@ -16,7 +16,6 @@ package google.registry.cache;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -38,7 +37,7 @@ public class MultilayerHostCacheTest {
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final SimplifiedJedisClient<Host> jedisClient = mock(SimplifiedJedisClient.class);
private final SimplifiedJedisClient jedisClient = mock(SimplifiedJedisClient.class);
private MultilayerHostCache cache;
@BeforeEach
@@ -52,9 +51,8 @@ public class MultilayerHostCacheTest {
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
// We should have filled the caches after one attempt to load from Valkey
verify(jedisClient).get("h_" + host.getRepoId());
verify(jedisClient)
.set(new SimplifiedJedisClient.JedisResource<>("h_" + host.getRepoId(), host));
verify(jedisClient).get(Host.class, host.getRepoId());
verify(jedisClient).set(new SimplifiedJedisClient.JedisResource<>(host.getRepoId(), host));
// Further loads hit the local cache
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
@@ -66,7 +64,7 @@ public class MultilayerHostCacheTest {
// Note: we don't save the host to SQL
Host host = DatabaseHelper.newHost("ns1.example.tld");
// We hit the Valkey cache first
when(jedisClient.get(eq("h_" + host.getRepoId()))).thenReturn(Optional.of(host));
when(jedisClient.get(Host.class, host.getRepoId())).thenReturn(Optional.of(host));
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
}

View File

@@ -23,7 +23,6 @@ import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.ImmutableList;
import google.registry.model.EppResource;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.transaction.JpaTestExtensions;
@@ -38,7 +37,6 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.UnifiedJedis;
/** Tests for {@link SimplifiedJedisClient}. */
@Testcontainers
@@ -60,29 +58,29 @@ public class SimplifiedJedisClientTest {
@Test
void testClient_roundTrip_domain() {
Domain domain = persistActiveDomain("example.tld");
SimplifiedJedisClient<Domain> client = createSimplifiedClient(Domain.class);
client.set(new SimplifiedJedisClient.JedisResource<>("d_example.tld", domain));
SimplifiedJedisClient client = createJedisClient();
client.set(new SimplifiedJedisClient.JedisResource<>("example.tld", domain));
// dsData and gracePeriods get serialized as null instead of the empty set, which is fine
assertAboutImmutableObjects()
.that(client.get("d_example.tld").get())
.that(client.get(Domain.class, "example.tld").get())
.isEqualExceptFields(domain, "dsData", "gracePeriods");
}
@Test
void testClient_roundTrip_host() {
Host host = persistActiveHost("ns1.example.tld");
SimplifiedJedisClient<Host> client = createSimplifiedClient(Host.class);
client.set(new SimplifiedJedisClient.JedisResource<>("h_repoId1", host));
assertThat(client.get("h_repoId1")).hasValue(host);
SimplifiedJedisClient client = createJedisClient();
client.set(new SimplifiedJedisClient.JedisResource<>("repoId1", host));
assertThat(client.get(Host.class, "repoId1")).hasValue(host);
}
@Test
void testSet_withExpiration() throws Exception {
SimplifiedJedisClient<Domain> client = createSimplifiedClient(Domain.class);
SimplifiedJedisClient client = createJedisClient();
Domain pendingDelete = persistDeletedDomain("example.tld", DateTime.now(UTC).plusMillis(100));
client.set(new SimplifiedJedisClient.JedisResource<>("d_example1.tld", pendingDelete));
client.set(new SimplifiedJedisClient.JedisResource<>("example1.tld", pendingDelete));
Thread.sleep(101);
assertThat(client.get("d_example1.tld")).isEmpty();
assertThat(client.get(Domain.class, "example1.tld")).isEmpty();
}
@Test
@@ -90,22 +88,22 @@ public class SimplifiedJedisClientTest {
Domain domain1 = persistActiveDomain("example1.tld");
Domain domain2 = persistActiveDomain("example2.tld");
Domain domain3 = persistActiveDomain("example3.tld");
SimplifiedJedisClient<Domain> client = createSimplifiedClient(Domain.class);
SimplifiedJedisClient client = createJedisClient();
client.setAll(
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("d_example1.tld", domain1),
new SimplifiedJedisClient.JedisResource<>("d_example2.tld", domain2),
new SimplifiedJedisClient.JedisResource<>("d_example3.tld", domain3)));
new SimplifiedJedisClient.JedisResource<>("example1.tld", domain1),
new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2),
new SimplifiedJedisClient.JedisResource<>("example3.tld", domain3)));
assertAboutImmutableObjects()
.that(client.get("d_example1.tld").get())
.that(client.get(Domain.class, "example1.tld").get())
.isEqualExceptFields(domain1, "dsData", "gracePeriods");
assertAboutImmutableObjects()
.that(client.get("d_example2.tld").get())
.that(client.get(Domain.class, "example2.tld").get())
.isEqualExceptFields(domain2, "dsData", "gracePeriods");
assertAboutImmutableObjects()
.that(client.get("d_example3.tld").get())
.that(client.get(Domain.class, "example3.tld").get())
.isEqualExceptFields(domain3, "dsData", "gracePeriods");
}
@@ -114,17 +112,17 @@ public class SimplifiedJedisClientTest {
Host host1 = persistActiveHost("ns1.example.tld");
Host host2 = persistActiveHost("ns2.example.tld");
Host host3 = persistActiveHost("ns3.example.tld");
SimplifiedJedisClient<Host> client = createSimplifiedClient(Host.class);
SimplifiedJedisClient client = createJedisClient();
client.setAll(
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("h_repoId1", host1),
new SimplifiedJedisClient.JedisResource<>("h_repoId2", host2),
new SimplifiedJedisClient.JedisResource<>("h_repoId3", host3)));
new SimplifiedJedisClient.JedisResource<>("repoId1", host1),
new SimplifiedJedisClient.JedisResource<>("repoId2", host2),
new SimplifiedJedisClient.JedisResource<>("repoId3", host3)));
assertThat(client.get("h_repoId1")).hasValue(host1);
assertThat(client.get("h_repoId2")).hasValue(host2);
assertThat(client.get("h_repoId3")).hasValue(host3);
assertThat(client.get(Host.class, "repoId1")).hasValue(host1);
assertThat(client.get(Host.class, "repoId2")).hasValue(host2);
assertThat(client.get(Host.class, "repoId3")).hasValue(host3);
}
@Test
@@ -132,35 +130,32 @@ public class SimplifiedJedisClientTest {
Host host1 = persistActiveHost("ns1.example.tld");
Host host2 = persistActiveHost("ns2.example.tld");
Host host3 = persistActiveHost("ns3.example.tld");
SimplifiedJedisClient<Host> client = createSimplifiedClient(Host.class);
SimplifiedJedisClient client = createJedisClient();
client.setAll(
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("h_repoId1", host1),
new SimplifiedJedisClient.JedisResource<>("h_repoId2", host2),
new SimplifiedJedisClient.JedisResource<>("h_repoId3", host3)));
new SimplifiedJedisClient.JedisResource<>("repoId1", host1),
new SimplifiedJedisClient.JedisResource<>("repoId2", host2),
new SimplifiedJedisClient.JedisResource<>("repoId3", host3)));
client.deleteAll(ImmutableList.of("h_repoId1", "h_repoId2", "h_nonexistent"));
assertThat(client.get("h_repoId1")).isEmpty();
assertThat(client.get("h_repoId2")).isEmpty();
assertThat(client.get("h_repoId3")).hasValue(host3);
client.deleteAll(Host.class, ImmutableList.of("repoId1", "repoId2", "nonexistent"));
assertThat(client.get(Host.class, "repoId1")).isEmpty();
assertThat(client.get(Host.class, "repoId2")).isEmpty();
assertThat(client.get(Host.class, "repoId3")).hasValue(host3);
}
@Test
void testClient_nonexistent() {
SimplifiedJedisClient<Domain> domainClient = createSimplifiedClient(Domain.class);
SimplifiedJedisClient<Host> hostClient = createSimplifiedClient(Host.class);
assertThat(domainClient.get("d_nonexistent.tld")).isEmpty();
assertThat(hostClient.get("h_ns1.nonexistent.tld")).isEmpty();
SimplifiedJedisClient domainClient = createJedisClient();
SimplifiedJedisClient hostClient = createJedisClient();
assertThat(domainClient.get(Domain.class, "nonexistent.tld")).isEmpty();
assertThat(hostClient.get(Host.class, "ns1.nonexistent.tld")).isEmpty();
}
private <T extends EppResource> SimplifiedJedisClient<T> createSimplifiedClient(Class<T> clazz) {
return SimplifiedJedisClient.create(clazz, createJedisClient());
}
private UnifiedJedis createJedisClient() {
return RedisClient.builder()
.hostAndPort(new HostAndPort(valkey.getHost(), valkey.getFirstMappedPort()))
.build();
private SimplifiedJedisClient createJedisClient() {
return new SimplifiedJedisClient(
RedisClient.builder()
.hostAndPort(new HostAndPort(valkey.getHost(), valkey.getFirstMappedPort()))
.build());
}
}