mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 12:07:51 +02:00
Add dual read claims list (#413)
* Add dual read claims list * Improve warning log and use longer duration for cache * Extract the comparison logic to a method * Move cache to DAO
This commit is contained in:
parent
d75f1a8e95
commit
566b3f38ba
4 changed files with 90 additions and 16 deletions
|
@ -32,7 +32,15 @@ public class CacheUtils {
|
||||||
* lists downloaded from the TMCH get updated in Datastore and the caches need to be refreshed.)
|
* lists downloaded from the TMCH get updated in Datastore and the caches need to be refreshed.)
|
||||||
*/
|
*/
|
||||||
public static <T> Supplier<T> memoizeWithShortExpiration(Supplier<T> original) {
|
public static <T> Supplier<T> memoizeWithShortExpiration(Supplier<T> original) {
|
||||||
Duration expiration = getSingletonCacheRefreshDuration();
|
return tryMemoizeWithExpiration(getSingletonCacheRefreshDuration(), original);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memoize a supplier with the given expiration. If the expiration is zero(likely happens in a
|
||||||
|
* unit test), it returns the original supplier.
|
||||||
|
*/
|
||||||
|
public static <T> Supplier<T> tryMemoizeWithExpiration(
|
||||||
|
Duration expiration, Supplier<T> original) {
|
||||||
return expiration.isEqual(ZERO)
|
return expiration.isEqual(ZERO)
|
||||||
? original
|
? original
|
||||||
: memoizeWithExpiration(original, expiration.getMillis(), MILLISECONDS);
|
: memoizeWithExpiration(original, expiration.getMillis(), MILLISECONDS);
|
||||||
|
|
|
@ -27,6 +27,10 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Supplier;
|
import com.google.common.base.Supplier;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.MapDifference;
|
||||||
|
import com.google.common.collect.MapDifference.ValueDifference;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.flogger.FluentLogger;
|
||||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||||
import com.googlecode.objectify.Key;
|
import com.googlecode.objectify.Key;
|
||||||
import com.googlecode.objectify.annotation.EmbedMap;
|
import com.googlecode.objectify.annotation.EmbedMap;
|
||||||
|
@ -40,6 +44,8 @@ import google.registry.model.annotations.NotBackedUp;
|
||||||
import google.registry.model.annotations.NotBackedUp.Reason;
|
import google.registry.model.annotations.NotBackedUp.Reason;
|
||||||
import google.registry.model.annotations.VirtualEntity;
|
import google.registry.model.annotations.VirtualEntity;
|
||||||
import google.registry.model.common.CrossTldSingleton;
|
import google.registry.model.common.CrossTldSingleton;
|
||||||
|
import google.registry.schema.tmch.ClaimsList;
|
||||||
|
import google.registry.schema.tmch.ClaimsListDao;
|
||||||
import google.registry.util.CollectionUtils;
|
import google.registry.util.CollectionUtils;
|
||||||
import google.registry.util.Concurrent;
|
import google.registry.util.Concurrent;
|
||||||
import google.registry.util.Retrier;
|
import google.registry.util.Retrier;
|
||||||
|
@ -71,6 +77,8 @@ import org.joda.time.DateTime;
|
||||||
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
|
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
|
||||||
public class ClaimsListShard extends ImmutableObject {
|
public class ClaimsListShard extends ImmutableObject {
|
||||||
|
|
||||||
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
|
||||||
/** The number of claims list entries to store per shard. */
|
/** The number of claims list entries to store per shard. */
|
||||||
private static final int SHARD_SIZE = 10000;
|
private static final int SHARD_SIZE = 10000;
|
||||||
|
|
||||||
|
@ -112,8 +120,7 @@ public class ClaimsListShard extends ImmutableObject {
|
||||||
Concurrent.transform(
|
Concurrent.transform(
|
||||||
shardKeys,
|
shardKeys,
|
||||||
key ->
|
key ->
|
||||||
tm()
|
tm().transactNewReadOnly(
|
||||||
.transactNewReadOnly(
|
|
||||||
() -> {
|
() -> {
|
||||||
ClaimsListShard claimsListShard = ofy().load().key(key).now();
|
ClaimsListShard claimsListShard = ofy().load().key(key).now();
|
||||||
checkState(
|
checkState(
|
||||||
|
@ -142,9 +149,52 @@ public class ClaimsListShard extends ImmutableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return create(creationTime, ImmutableMap.copyOf(combinedLabelsToKeys));
|
|
||||||
|
ClaimsListShard datastoreList =
|
||||||
|
create(creationTime, ImmutableMap.copyOf(combinedLabelsToKeys));
|
||||||
|
// Also load the list from Cloud SQL, compare the two lists, and log if different.
|
||||||
|
try {
|
||||||
|
loadAndCompareCloudSqlList(datastoreList);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.atSevere().withCause(t).log("Error comparing reserved lists.");
|
||||||
|
}
|
||||||
|
return datastoreList;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static final void loadAndCompareCloudSqlList(ClaimsListShard datastoreList) {
|
||||||
|
Optional<ClaimsList> maybeCloudSqlList = ClaimsListDao.getLatestRevision();
|
||||||
|
if (maybeCloudSqlList.isPresent()) {
|
||||||
|
ClaimsList cloudSqlList = maybeCloudSqlList.get();
|
||||||
|
MapDifference<String, String> diff =
|
||||||
|
Maps.difference(datastoreList.labelsToKeys, cloudSqlList.getLabelsToKeys());
|
||||||
|
if (!diff.areEqual()) {
|
||||||
|
if (diff.entriesDiffering().size() > 10) {
|
||||||
|
logger.atWarning().log(
|
||||||
|
String.format(
|
||||||
|
"Unequal claims lists detected, Cloud SQL list with revision id %d has %d"
|
||||||
|
+ " different records than the current Datastore list.",
|
||||||
|
cloudSqlList.getRevisionId(), diff.entriesDiffering().size()));
|
||||||
|
} else {
|
||||||
|
StringBuilder diffMessage = new StringBuilder("Unequal claims lists detected:\n");
|
||||||
|
diff.entriesDiffering().entrySet().stream()
|
||||||
|
.forEach(
|
||||||
|
entry -> {
|
||||||
|
String label = entry.getKey();
|
||||||
|
ValueDifference<String> valueDiff = entry.getValue();
|
||||||
|
diffMessage.append(
|
||||||
|
String.format(
|
||||||
|
"Domain label %s has key %s in Datastore and key %s in Cloud"
|
||||||
|
+ " SQL.\n",
|
||||||
|
label, valueDiff.leftValue(), valueDiff.rightValue()));
|
||||||
|
});
|
||||||
|
logger.atWarning().log(diffMessage.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.atWarning().log("Claims list in Cloud SQL is empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cached supplier that fetches the claims list shards from Datastore and recombines them into a
|
* A cached supplier that fetches the claims list shards from Datastore and recombines them into a
|
||||||
* single {@link ClaimsListShard} object.
|
* single {@link ClaimsListShard} object.
|
||||||
|
|
|
@ -14,9 +14,14 @@
|
||||||
|
|
||||||
package google.registry.schema.tmch;
|
package google.registry.schema.tmch;
|
||||||
|
|
||||||
|
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
|
||||||
|
import static google.registry.model.CacheUtils.tryMemoizeWithExpiration;
|
||||||
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
|
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
|
||||||
|
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.util.NonFinalForTesting;
|
||||||
|
import java.util.Optional;
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
|
|
||||||
/** Data access object for {@link ClaimsList}. */
|
/** Data access object for {@link ClaimsList}. */
|
||||||
|
@ -24,6 +29,11 @@ public class ClaimsListDao {
|
||||||
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
/** In-memory cache for claims list. */
|
||||||
|
@NonFinalForTesting
|
||||||
|
private static Supplier<Optional<ClaimsList>> cacheClaimsList =
|
||||||
|
tryMemoizeWithExpiration(getDomainLabelListCacheDuration(), ClaimsListDao::getLatestRevision);
|
||||||
|
|
||||||
private static void save(ClaimsList claimsList) {
|
private static void save(ClaimsList claimsList) {
|
||||||
jpaTm().transact(() -> jpaTm().getEntityManager().persist(claimsList));
|
jpaTm().transact(() -> jpaTm().getEntityManager().persist(claimsList));
|
||||||
}
|
}
|
||||||
|
@ -47,10 +57,12 @@ public class ClaimsListDao {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current revision of the {@link ClaimsList} in Cloud SQL. Throws exception if there
|
* Returns the most recent revision of the {@link ClaimsList} in Cloud SQL, if it exists.
|
||||||
* is no claims in the table.
|
* TODO(shicong): Change this method to package level access after dual-read phase.
|
||||||
|
* ClaimsListShard uses this method to retrieve claims list in Cloud SQL for the comparison, and
|
||||||
|
* ClaimsListShard is not in this package.
|
||||||
*/
|
*/
|
||||||
public static ClaimsList getCurrent() {
|
public static Optional<ClaimsList> getLatestRevision() {
|
||||||
return jpaTm()
|
return jpaTm()
|
||||||
.transact(
|
.transact(
|
||||||
() -> {
|
() -> {
|
||||||
|
@ -63,9 +75,15 @@ public class ClaimsListDao {
|
||||||
+ " :revisionId",
|
+ " :revisionId",
|
||||||
ClaimsList.class)
|
ClaimsList.class)
|
||||||
.setParameter("revisionId", revisionId)
|
.setParameter("revisionId", revisionId)
|
||||||
.getSingleResult();
|
.getResultStream()
|
||||||
|
.findFirst();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the most recent revision of the {@link ClaimsList}, from cache. */
|
||||||
|
public static Optional<ClaimsList> getLatestRevisionCached() {
|
||||||
|
return cacheClaimsList.get();
|
||||||
|
}
|
||||||
|
|
||||||
private ClaimsListDao() {}
|
private ClaimsListDao() {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,11 @@
|
||||||
package google.registry.schema.tmch;
|
package google.registry.schema.tmch;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import google.registry.model.transaction.JpaTestRules;
|
import google.registry.model.transaction.JpaTestRules;
|
||||||
import google.registry.model.transaction.JpaTestRules.JpaIntegrationTestRule;
|
import google.registry.model.transaction.JpaTestRules.JpaIntegrationTestRule;
|
||||||
import google.registry.testing.FakeClock;
|
import google.registry.testing.FakeClock;
|
||||||
import javax.persistence.NoResultException;
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
@ -42,7 +40,7 @@ public class ClaimsListDaoTest {
|
||||||
ClaimsList claimsList =
|
ClaimsList claimsList =
|
||||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||||
ClaimsListDao.trySave(claimsList);
|
ClaimsListDao.trySave(claimsList);
|
||||||
ClaimsList insertedClaimsList = ClaimsListDao.getCurrent();
|
ClaimsList insertedClaimsList = ClaimsListDao.getLatestRevision().get();
|
||||||
assertClaimsListEquals(claimsList, insertedClaimsList);
|
assertClaimsListEquals(claimsList, insertedClaimsList);
|
||||||
assertThat(insertedClaimsList.getCreationTimestamp()).isEqualTo(jpaRule.getTxnClock().nowUtc());
|
assertThat(insertedClaimsList.getCreationTimestamp()).isEqualTo(jpaRule.getTxnClock().nowUtc());
|
||||||
}
|
}
|
||||||
|
@ -52,7 +50,7 @@ public class ClaimsListDaoTest {
|
||||||
ClaimsList claimsList =
|
ClaimsList claimsList =
|
||||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||||
ClaimsListDao.trySave(claimsList);
|
ClaimsListDao.trySave(claimsList);
|
||||||
ClaimsList insertedClaimsList = ClaimsListDao.getCurrent();
|
ClaimsList insertedClaimsList = ClaimsListDao.getLatestRevision().get();
|
||||||
assertClaimsListEquals(claimsList, insertedClaimsList);
|
assertClaimsListEquals(claimsList, insertedClaimsList);
|
||||||
// Save ClaimsList with existing revisionId should fail because revisionId is the primary key.
|
// Save ClaimsList with existing revisionId should fail because revisionId is the primary key.
|
||||||
ClaimsListDao.trySave(insertedClaimsList);
|
ClaimsListDao.trySave(insertedClaimsList);
|
||||||
|
@ -62,14 +60,14 @@ public class ClaimsListDaoTest {
|
||||||
public void trySave_claimsListWithNoEntries() {
|
public void trySave_claimsListWithNoEntries() {
|
||||||
ClaimsList claimsList = ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of());
|
ClaimsList claimsList = ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of());
|
||||||
ClaimsListDao.trySave(claimsList);
|
ClaimsListDao.trySave(claimsList);
|
||||||
ClaimsList insertedClaimsList = ClaimsListDao.getCurrent();
|
ClaimsList insertedClaimsList = ClaimsListDao.getLatestRevision().get();
|
||||||
assertClaimsListEquals(claimsList, insertedClaimsList);
|
assertClaimsListEquals(claimsList, insertedClaimsList);
|
||||||
assertThat(insertedClaimsList.getLabelsToKeys()).isEmpty();
|
assertThat(insertedClaimsList.getLabelsToKeys()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getCurrent_throwsNoResultExceptionIfTableIsEmpty() {
|
public void getCurrent_returnsEmptyListIfTableIsEmpty() {
|
||||||
assertThrows(NoResultException.class, ClaimsListDao::getCurrent);
|
assertThat(ClaimsListDao.getLatestRevision().isPresent()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -80,7 +78,7 @@ public class ClaimsListDaoTest {
|
||||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label3", "key3", "label4", "key4"));
|
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label3", "key3", "label4", "key4"));
|
||||||
ClaimsListDao.trySave(oldClaimsList);
|
ClaimsListDao.trySave(oldClaimsList);
|
||||||
ClaimsListDao.trySave(newClaimsList);
|
ClaimsListDao.trySave(newClaimsList);
|
||||||
assertClaimsListEquals(newClaimsList, ClaimsListDao.getCurrent());
|
assertClaimsListEquals(newClaimsList, ClaimsListDao.getLatestRevision().get());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertClaimsListEquals(ClaimsList left, ClaimsList right) {
|
private void assertClaimsListEquals(ClaimsList left, ClaimsList right) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue