mirror of
https://github.com/google/nomulus
synced 2026-09-24 08:54:56 +00:00
Add Jedis (for Valkey) caches for domains and hosts (#3013)
We add optional Valkey caching of hosts and domains for future use. Eventually, this will allow us to pre-warm large amounts of data in Valkey for quick retrieval during actions like RDAP. Note: this doesn't actually use the caches yet. We use Jedis instead of Redisson for speed purposes (https://www.instaclustr.com/blog/redis-java-clients-and-client-side-caching/) which means that we have to implement our own multilayer cache but that's not the worst thing in the world. Tested on crash with logging and RDAP code that's not included in this PR -- it behaves as you'd expect, where the local cache works for immediate re-lookups and the remote cache works after a restart.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// 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.cache;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Tests for {@link MultilayerDomainCache}. */
|
||||
public class MultilayerDomainCacheTest {
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
||||
|
||||
private final SimplifiedJedisClient<Domain> jedisClient = mock(SimplifiedJedisClient.class);
|
||||
private final FakeClock clock = new FakeClock();
|
||||
private MultilayerDomainCache cache;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
cache = new MultilayerDomainCache(jedisClient, clock);
|
||||
createTld("tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_fromDatabase_populatesCaches() {
|
||||
Domain domain = persistActiveDomain("example.tld");
|
||||
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
|
||||
|
||||
// We should have filled the caches after one attempt to load from Valkey
|
||||
verify(jedisClient).get("Domain__example.tld");
|
||||
verify(jedisClient).set("Domain__example.tld", domain);
|
||||
|
||||
// Further loads hit the local cache
|
||||
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
|
||||
verifyNoMoreInteractions(jedisClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_fromValkey() {
|
||||
// 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("Domain__example.tld"))).thenReturn(Optional.of(domain));
|
||||
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_missing() {
|
||||
assertThat(cache.loadByDomainName("nonexistent.tld")).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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.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;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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 java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Tests for {@link MultilayerHostCache}. */
|
||||
public class MultilayerHostCacheTest {
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
||||
|
||||
private final SimplifiedJedisClient<Host> jedisClient = mock(SimplifiedJedisClient.class);
|
||||
private MultilayerHostCache cache;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
cache = new MultilayerHostCache(jedisClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_fromDatabase_populatesCaches() {
|
||||
Host host = persistActiveHost("ns1.example.tld");
|
||||
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
|
||||
|
||||
// We should have filled the caches after one attempt to load from Valkey
|
||||
verify(jedisClient).get("Host__" + host.getRepoId());
|
||||
verify(jedisClient).set("Host__" + host.getRepoId(), host);
|
||||
|
||||
// Further loads hit the local cache
|
||||
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
|
||||
verifyNoMoreInteractions(jedisClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_fromValkey() {
|
||||
// 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("Host__" + host.getRepoId()))).thenReturn(Optional.of(host));
|
||||
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_missing() {
|
||||
assertThat(cache.loadByRepoId("nonexistent")).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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.cache;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveHost;
|
||||
|
||||
import google.registry.model.EppResource;
|
||||
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.FakeClock;
|
||||
import io.github.ss_bhatt.testcontainers.valkey.ValkeyContainer;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
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
|
||||
public class SimplifiedJedisClientTest {
|
||||
|
||||
@Container private static final ValkeyContainer valkey = new ValkeyContainer();
|
||||
|
||||
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2025-01-01T00:00:00.000Z"));
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension();
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
createTld("tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClient_roundTrip_domain() {
|
||||
Domain domain = persistActiveDomain("example.tld");
|
||||
SimplifiedJedisClient<Domain> client = createSimplifiedClient(Domain.class);
|
||||
client.set("Domain__example.tld", domain);
|
||||
// dsData and gracePeriods get serialized as null instead of the empty set, which is fine
|
||||
assertAboutImmutableObjects()
|
||||
.that(client.get("Domain__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("Host__ns1.example.tld", host);
|
||||
assertThat(client.get("Host__ns1.example.tld")).hasValue(host);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClient_nonexistent() {
|
||||
SimplifiedJedisClient<Domain> domainClient = createSimplifiedClient(Domain.class);
|
||||
SimplifiedJedisClient<Host> hostClient = createSimplifiedClient(Host.class);
|
||||
assertThat(domainClient.get("Domain__nonexistent.tld")).isEmpty();
|
||||
assertThat(hostClient.get("Host__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();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import dagger.Component;
|
||||
import dagger.Lazy;
|
||||
import google.registry.batch.BatchModule;
|
||||
import google.registry.bigquery.BigqueryModule;
|
||||
import google.registry.cache.CacheModule;
|
||||
import google.registry.config.CloudTasksUtilsModule;
|
||||
import google.registry.config.CredentialModule;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
@@ -52,6 +53,7 @@ import jakarta.inject.Singleton;
|
||||
AuthModule.class,
|
||||
BatchModule.class,
|
||||
BigqueryModule.class,
|
||||
CacheModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
ConfigModule.class,
|
||||
CredentialModule.class,
|
||||
|
||||
@@ -170,6 +170,12 @@ public final class FakeKeyringModule {
|
||||
return ImmutableList.of(SQL_REPLICA_CONNECTION_1, SQL_REPLICA_CONNECTION_2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValkeyCertificateAuthority() {
|
||||
// This isn't necessary for keyring testing
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user