mirror of
https://github.com/google/nomulus.git
synced 2025-07-07 19:53:30 +02:00
Create a ClaimsListDualDatabaseDao (#1011)
The dual DAO takes care of switching between databases, comparing the results of one to the results of the other, and caching the result. All calls to ClaimsList retrieval or storing should use the dual-database-DAO. Previously, calls to comparing the lists were somewhat scattered throughout the codebase. Now, there is one class for retrieval and comparison (the dual DAO), one class for retrieval from SQL (the SQL DAO), and one class for retrieval from Datastore (ClaimsListShard itself, though the retrieval could be moved in to a separate DAO if we wished). In addition, we rename the ClaimsListDao to ClaimsListSqlDao
This commit is contained in:
parent
6bee440194
commit
87f096ae40
18 changed files with 454 additions and 254 deletions
|
@ -45,7 +45,7 @@ import google.registry.model.eppinput.ResourceCommand;
|
||||||
import google.registry.model.eppoutput.EppResponse;
|
import google.registry.model.eppoutput.EppResponse;
|
||||||
import google.registry.model.registry.Registry;
|
import google.registry.model.registry.Registry;
|
||||||
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
|
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
|
||||||
import google.registry.model.tmch.ClaimsListShard;
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.util.Clock;
|
import google.registry.util.Clock;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -104,7 +104,8 @@ public final class DomainClaimsCheckFlow implements Flow {
|
||||||
verifyClaimsPeriodNotEnded(registry, now);
|
verifyClaimsPeriodNotEnded(registry, now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Optional<String> claimKey = ClaimsListShard.get().getClaimKey(parsedDomain.parts().get(0));
|
Optional<String> claimKey =
|
||||||
|
ClaimsListDualDatabaseDao.get().getClaimKey(parsedDomain.parts().get(0));
|
||||||
launchChecksBuilder.add(
|
launchChecksBuilder.add(
|
||||||
LaunchCheck.create(
|
LaunchCheck.create(
|
||||||
LaunchCheckName.create(claimKey.isPresent(), domainName), claimKey.orElse(null)));
|
LaunchCheckName.create(claimKey.isPresent(), domainName), claimKey.orElse(null)));
|
||||||
|
|
|
@ -129,7 +129,7 @@ import google.registry.model.registry.label.ReservedList;
|
||||||
import google.registry.model.reporting.DomainTransactionRecord;
|
import google.registry.model.reporting.DomainTransactionRecord;
|
||||||
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
|
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
|
||||||
import google.registry.model.reporting.HistoryEntry;
|
import google.registry.model.reporting.HistoryEntry;
|
||||||
import google.registry.model.tmch.ClaimsListShard;
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import google.registry.tldconfig.idn.IdnLabelValidator;
|
import google.registry.tldconfig.idn.IdnLabelValidator;
|
||||||
import google.registry.util.Idn;
|
import google.registry.util.Idn;
|
||||||
|
@ -994,7 +994,7 @@ public class DomainFlowUtils {
|
||||||
InternetDomainName domainName, boolean hasSignedMarks, boolean hasClaimsNotice)
|
InternetDomainName domainName, boolean hasSignedMarks, boolean hasClaimsNotice)
|
||||||
throws EppException {
|
throws EppException {
|
||||||
boolean isInClaimsList =
|
boolean isInClaimsList =
|
||||||
ClaimsListShard.get().getClaimKey(domainName.parts().get(0)).isPresent();
|
ClaimsListDualDatabaseDao.get().getClaimKey(domainName.parts().get(0)).isPresent();
|
||||||
if (hasClaimsNotice && !isInClaimsList) {
|
if (hasClaimsNotice && !isInClaimsList) {
|
||||||
throw new UnexpectedClaimsNoticeException(domainName.toString());
|
throw new UnexpectedClaimsNoticeException(domainName.toString());
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ import google.registry.model.common.TimedTransitionProperty.TimedTransition;
|
||||||
import google.registry.model.registry.label.PremiumList;
|
import google.registry.model.registry.label.PremiumList;
|
||||||
import google.registry.model.registry.label.ReservedList;
|
import google.registry.model.registry.label.ReservedList;
|
||||||
import google.registry.model.smd.SignedMarkRevocationList;
|
import google.registry.model.smd.SignedMarkRevocationList;
|
||||||
|
import google.registry.model.tmch.ClaimsListShard;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import google.registry.schema.replay.DatastoreOnlyEntity;
|
import google.registry.schema.replay.DatastoreOnlyEntity;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -58,6 +59,8 @@ public class DatabaseTransitionSchedule extends ImmutableObject implements Datas
|
||||||
|
|
||||||
/** The id of the transition schedule. */
|
/** The id of the transition schedule. */
|
||||||
public enum TransitionId {
|
public enum TransitionId {
|
||||||
|
/** The schedule for migration of {@link ClaimsListShard} entities. */
|
||||||
|
CLAIMS_LIST,
|
||||||
/** The schedule for the migration of {@link PremiumList} and {@link ReservedList}. */
|
/** The schedule for the migration of {@link PremiumList} and {@link ReservedList}. */
|
||||||
DOMAIN_LABEL_LISTS,
|
DOMAIN_LABEL_LISTS,
|
||||||
/** The schedule for the migration of the {@link SignedMarkRevocationList} entity. */
|
/** The schedule for the migration of the {@link SignedMarkRevocationList} entity. */
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
// Copyright 2019 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.model.tmch;
|
|
||||||
|
|
||||||
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
|
|
||||||
import static google.registry.model.CacheUtils.tryMemoizeWithExpiration;
|
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
|
||||||
|
|
||||||
import com.google.common.base.Supplier;
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
|
||||||
import google.registry.util.NonFinalForTesting;
|
|
||||||
import java.util.Optional;
|
|
||||||
import javax.persistence.EntityManager;
|
|
||||||
|
|
||||||
/** Data access object for {@link ClaimsListShard}. */
|
|
||||||
public class ClaimsListDao {
|
|
||||||
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
||||||
|
|
||||||
/** In-memory cache for claims list. */
|
|
||||||
@NonFinalForTesting
|
|
||||||
private static Supplier<Optional<ClaimsListShard>> cacheClaimsList =
|
|
||||||
tryMemoizeWithExpiration(getDomainLabelListCacheDuration(), ClaimsListDao::getLatestRevision);
|
|
||||||
|
|
||||||
private static void save(ClaimsListShard claimsList) {
|
|
||||||
jpaTm().transact(() -> jpaTm().getEntityManager().persist(claimsList));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to save the given {@link ClaimsListShard} into Cloud SQL. If the save fails, the error will
|
|
||||||
* be logged but no exception will be thrown.
|
|
||||||
*
|
|
||||||
* <p>This method is used during the dual-write phase of database migration as Datastore is still
|
|
||||||
* the authoritative database.
|
|
||||||
*/
|
|
||||||
static void trySave(ClaimsListShard claimsList) {
|
|
||||||
try {
|
|
||||||
ClaimsListDao.save(claimsList);
|
|
||||||
logger.atInfo().log(
|
|
||||||
"Inserted %,d claims into Cloud SQL, created at %s",
|
|
||||||
claimsList.getLabelsToKeys().size(), claimsList.getTmdbGenerationTime());
|
|
||||||
} catch (Throwable e) {
|
|
||||||
logger.atSevere().withCause(e).log("Error inserting claims into Cloud SQL");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the most recent revision of the {@link ClaimsListShard} in Cloud SQL, if it exists.
|
|
||||||
* TODO(b/177569979): 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 Optional<ClaimsListShard> getLatestRevision() {
|
|
||||||
return jpaTm()
|
|
||||||
.transact(
|
|
||||||
() -> {
|
|
||||||
EntityManager em = jpaTm().getEntityManager();
|
|
||||||
Long revisionId =
|
|
||||||
em.createQuery("SELECT MAX(revisionId) FROM ClaimsList", Long.class)
|
|
||||||
.getSingleResult();
|
|
||||||
return em.createQuery(
|
|
||||||
"FROM ClaimsList cl LEFT JOIN FETCH cl.labelsToKeys WHERE cl.revisionId ="
|
|
||||||
+ " :revisionId",
|
|
||||||
ClaimsListShard.class)
|
|
||||||
.setParameter("revisionId", revisionId)
|
|
||||||
.getResultStream()
|
|
||||||
.findFirst();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the most recent revision of the {@link ClaimsListShard}, from cache. */
|
|
||||||
public static Optional<ClaimsListShard> getLatestRevisionCached() {
|
|
||||||
return cacheClaimsList.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClaimsListDao() {}
|
|
||||||
}
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
// Copyright 2021 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.model.tmch;
|
||||||
|
|
||||||
|
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
|
||||||
|
import static google.registry.model.CacheUtils.tryMemoizeWithExpiration;
|
||||||
|
import static google.registry.model.DatabaseMigrationUtils.isDatastore;
|
||||||
|
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
|
||||||
|
import static google.registry.model.common.DatabaseTransitionSchedule.TransitionId.CLAIMS_LIST;
|
||||||
|
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
|
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.MapDifference;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import google.registry.util.NonFinalForTesting;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO for {@link ClaimsListShard} objects that handles the branching paths for SQL and Datastore.
|
||||||
|
*
|
||||||
|
* <p>For write actions, this class will perform the action against the primary database then, after
|
||||||
|
* * that success or failure, against the secondary database. If the secondary database fails, an
|
||||||
|
* error is logged (but not thrown).
|
||||||
|
*
|
||||||
|
* <p>For read actions, we will log if the primary and secondary databases * have different values
|
||||||
|
* (or if the retrieval from the second database fails).
|
||||||
|
*/
|
||||||
|
public class ClaimsListDualDatabaseDao {
|
||||||
|
|
||||||
|
/** In-memory cache for claims list. */
|
||||||
|
@NonFinalForTesting
|
||||||
|
private static Supplier<ClaimsListShard> claimsListCache =
|
||||||
|
tryMemoizeWithExpiration(
|
||||||
|
getDomainLabelListCacheDuration(), ClaimsListDualDatabaseDao::getUncached);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given {@link ClaimsListShard} to both the primary and secondary databases, logging
|
||||||
|
* and skipping errors in the secondary DB.
|
||||||
|
*/
|
||||||
|
public static void save(ClaimsListShard claimsList) {
|
||||||
|
if (isDatastore(CLAIMS_LIST)) {
|
||||||
|
claimsList.saveToDatastore();
|
||||||
|
suppressExceptionUnlessInTest(
|
||||||
|
() -> ClaimsListSqlDao.save(claimsList), "Error saving ClaimsList to SQL.");
|
||||||
|
} else {
|
||||||
|
ClaimsListSqlDao.save(claimsList);
|
||||||
|
suppressExceptionUnlessInTest(
|
||||||
|
claimsList::saveToDatastore, "Error saving ClaimsListShard to Datastore.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the most recent revision of the {@link ClaimsListShard}, from cache. */
|
||||||
|
public static ClaimsListShard get() {
|
||||||
|
return claimsListCache.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieves and compares the latest revision from the databases. */
|
||||||
|
private static ClaimsListShard getUncached() {
|
||||||
|
Optional<ClaimsListShard> primaryResult;
|
||||||
|
if (isDatastore(CLAIMS_LIST)) {
|
||||||
|
primaryResult = ClaimsListShard.getFromDatastore();
|
||||||
|
suppressExceptionUnlessInTest(
|
||||||
|
() -> {
|
||||||
|
Optional<ClaimsListShard> secondaryResult = ClaimsListSqlDao.get();
|
||||||
|
compareClaimsLists(primaryResult, secondaryResult);
|
||||||
|
},
|
||||||
|
"Error loading ClaimsList from SQL.");
|
||||||
|
} else {
|
||||||
|
primaryResult = ClaimsListSqlDao.get();
|
||||||
|
suppressExceptionUnlessInTest(
|
||||||
|
() -> {
|
||||||
|
Optional<ClaimsListShard> secondaryResult = ClaimsListShard.getFromDatastore();
|
||||||
|
compareClaimsLists(primaryResult, secondaryResult);
|
||||||
|
},
|
||||||
|
"Error loading ClaimsListShard from Datastore.");
|
||||||
|
}
|
||||||
|
return primaryResult.orElse(ClaimsListShard.create(START_OF_TIME, ImmutableMap.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void compareClaimsLists(
|
||||||
|
Optional<ClaimsListShard> maybePrimary, Optional<ClaimsListShard> maybeSecondary) {
|
||||||
|
if (maybePrimary.isPresent() && !maybeSecondary.isPresent()) {
|
||||||
|
throw new IllegalStateException("Claims list found in primary DB but not in secondary DB.");
|
||||||
|
}
|
||||||
|
if (!maybePrimary.isPresent() && maybeSecondary.isPresent()) {
|
||||||
|
throw new IllegalStateException("Claims list found in secondary DB but not in primary DB.");
|
||||||
|
}
|
||||||
|
if (!maybePrimary.isPresent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ClaimsListShard primary = maybePrimary.get();
|
||||||
|
ClaimsListShard secondary = maybeSecondary.get();
|
||||||
|
MapDifference<String, String> diff =
|
||||||
|
Maps.difference(primary.labelsToKeys, secondary.getLabelsToKeys());
|
||||||
|
if (!diff.areEqual()) {
|
||||||
|
if (diff.entriesDiffering().size()
|
||||||
|
+ diff.entriesOnlyOnRight().size()
|
||||||
|
+ diff.entriesOnlyOnLeft().size()
|
||||||
|
> 10) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format(
|
||||||
|
"Unequal claims lists detected, secondary list with revision id %d has %d"
|
||||||
|
+ " different records than the current primary list.",
|
||||||
|
secondary.getRevisionId(), diff.entriesDiffering().size()));
|
||||||
|
} else {
|
||||||
|
StringBuilder diffMessage = new StringBuilder("Unequal claims lists detected:\n");
|
||||||
|
diff.entriesDiffering()
|
||||||
|
.forEach(
|
||||||
|
(label, valueDiff) ->
|
||||||
|
diffMessage.append(
|
||||||
|
String.format(
|
||||||
|
"Domain label %s has key %s in the primary DB and key %s "
|
||||||
|
+ "in the secondary DB.\n",
|
||||||
|
label, valueDiff.leftValue(), valueDiff.rightValue())));
|
||||||
|
diff.entriesOnlyOnLeft()
|
||||||
|
.forEach(
|
||||||
|
(label, valueDiff) ->
|
||||||
|
diffMessage.append(
|
||||||
|
String.format(
|
||||||
|
"Domain label %s with key %s only appears in the primary DB.\n",
|
||||||
|
label, valueDiff)));
|
||||||
|
diff.entriesOnlyOnRight()
|
||||||
|
.forEach(
|
||||||
|
(label, valueDiff) ->
|
||||||
|
diffMessage.append(
|
||||||
|
String.format(
|
||||||
|
"Domain label %s with key %s only appears in the secondary DB.\n",
|
||||||
|
label, valueDiff)));
|
||||||
|
throw new IllegalStateException(diffMessage.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClaimsListDualDatabaseDao() {}
|
||||||
|
}
|
|
@ -18,19 +18,13 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
import static com.google.common.base.Throwables.throwIfUnchecked;
|
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||||
import static com.google.common.base.Verify.verify;
|
import static com.google.common.base.Verify.verify;
|
||||||
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
|
|
||||||
import static google.registry.model.ofy.ObjectifyService.allocateId;
|
import static google.registry.model.ofy.ObjectifyService.allocateId;
|
||||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
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.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;
|
||||||
|
@ -71,8 +65,8 @@ import org.joda.time.DateTime;
|
||||||
* A list of TMCH claims labels and their associated claims keys.
|
* A list of TMCH claims labels and their associated claims keys.
|
||||||
*
|
*
|
||||||
* <p>The claims list is actually sharded into multiple {@link ClaimsListShard} entities to work
|
* <p>The claims list is actually sharded into multiple {@link ClaimsListShard} entities to work
|
||||||
* around the Datastore limitation of 1M max size per entity. However, when calling {@link #get} all
|
* around the Datastore limitation of 1M max size per entity. However, when calling {@link
|
||||||
* of the shards are recombined into one {@link ClaimsListShard} object.
|
* #getFromDatastore} all of the shards are recombined into one {@link ClaimsListShard} object.
|
||||||
*
|
*
|
||||||
* <p>ClaimsList shards are tied to a specific revision and are persisted individually, then the
|
* <p>ClaimsList shards are tied to a specific revision and are persisted individually, then the
|
||||||
* entire claims list is atomically shifted over to using the new shards by persisting the new
|
* entire claims list is atomically shifted over to using the new shards by persisting the new
|
||||||
|
@ -97,8 +91,6 @@ import org.joda.time.DateTime;
|
||||||
@Table
|
@Table
|
||||||
public class ClaimsListShard extends ImmutableObject implements NonReplicatedEntity {
|
public class ClaimsListShard extends ImmutableObject implements NonReplicatedEntity {
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
@ -143,13 +135,15 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
|
||||||
|
|
||||||
private static final Retrier LOADER_RETRIER = new Retrier(new SystemSleeper(), 2);
|
private static final Retrier LOADER_RETRIER = new Retrier(new SystemSleeper(), 2);
|
||||||
|
|
||||||
private static ClaimsListShard loadClaimsListShard() {
|
private static Optional<ClaimsListShard> loadClaimsListShard() {
|
||||||
// Find the most recent revision.
|
// Find the most recent revision.
|
||||||
Key<ClaimsListRevision> revisionKey = getCurrentRevision();
|
Key<ClaimsListRevision> revisionKey = getCurrentRevision();
|
||||||
|
if (revisionKey == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> combinedLabelsToKeys = new HashMap<>();
|
Map<String, String> combinedLabelsToKeys = new HashMap<>();
|
||||||
DateTime creationTime = START_OF_TIME;
|
DateTime creationTime = START_OF_TIME;
|
||||||
if (revisionKey != null) {
|
|
||||||
// Grab all of the keys for the shards that belong to the current revision.
|
// Grab all of the keys for the shards that belong to the current revision.
|
||||||
final List<Key<ClaimsListShard>> shardKeys =
|
final List<Key<ClaimsListShard>> shardKeys =
|
||||||
ofy().load().type(ClaimsListShard.class).ancestor(revisionKey).keys().list();
|
ofy().load().type(ClaimsListShard.class).ancestor(revisionKey).keys().list();
|
||||||
|
@ -189,61 +183,9 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
|
||||||
"Inconsistent claims list shard creation times.");
|
"Inconsistent claims list shard creation times.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ClaimsListShard datastoreList = create(creationTime, ImmutableMap.copyOf(combinedLabelsToKeys));
|
return Optional.of(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 claims lists.");
|
|
||||||
}
|
}
|
||||||
return datastoreList;
|
|
||||||
};
|
|
||||||
|
|
||||||
private static void loadAndCompareCloudSqlList(ClaimsListShard datastoreList) {
|
|
||||||
Optional<ClaimsListShard> maybeCloudSqlList = ClaimsListDao.getLatestRevision();
|
|
||||||
if (maybeCloudSqlList.isPresent()) {
|
|
||||||
ClaimsListShard 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
|
|
||||||
* single {@link ClaimsListShard} object.
|
|
||||||
*/
|
|
||||||
private static final Supplier<ClaimsListShard> CACHE =
|
|
||||||
memoizeWithShortExpiration(
|
|
||||||
() ->
|
|
||||||
LOADER_RETRIER.callWithRetry(
|
|
||||||
ClaimsListShard::loadClaimsListShard, IllegalStateException.class));
|
|
||||||
|
|
||||||
/** Returns the revision id of this claims list, or throws exception if it is null. */
|
/** Returns the revision id of this claims list, or throws exception if it is null. */
|
||||||
public Long getRevisionId() {
|
public Long getRevisionId() {
|
||||||
|
@ -286,9 +228,8 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
|
||||||
* Save the Claims list to Datastore by writing the new shards in a series of transactions,
|
* Save the Claims list to Datastore by writing the new shards in a series of transactions,
|
||||||
* switching over to using them atomically, then deleting the old ones.
|
* switching over to using them atomically, then deleting the old ones.
|
||||||
*/
|
*/
|
||||||
public void save() {
|
void saveToDatastore() {
|
||||||
saveToDatastore(SHARD_SIZE);
|
saveToDatastore(SHARD_SIZE);
|
||||||
ClaimsListDao.trySave(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@ -301,7 +242,7 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
|
||||||
Concurrent.transform(
|
Concurrent.transform(
|
||||||
CollectionUtils.partitionMap(labelsToKeys, shardSize),
|
CollectionUtils.partitionMap(labelsToKeys, shardSize),
|
||||||
(final ImmutableMap<String, String> labelsToKeysShard) ->
|
(final ImmutableMap<String, String> labelsToKeysShard) ->
|
||||||
tm().transactNew(
|
tm().transact(
|
||||||
() -> {
|
() -> {
|
||||||
ClaimsListShard shard = create(creationTime, labelsToKeysShard);
|
ClaimsListShard shard = create(creationTime, labelsToKeysShard);
|
||||||
shard.isShard = true;
|
shard.isShard = true;
|
||||||
|
@ -311,7 +252,7 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Persist the new revision, thus causing the newly created shards to go live.
|
// Persist the new revision, thus causing the newly created shards to go live.
|
||||||
tm().transactNew(
|
tm().transact(
|
||||||
() -> {
|
() -> {
|
||||||
verify(
|
verify(
|
||||||
(getCurrentRevision() == null && oldRevision == null)
|
(getCurrentRevision() == null && oldRevision == null)
|
||||||
|
@ -337,9 +278,9 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return a single logical instance that combines all Datastore shards. */
|
/** Return a single logical instance that combines all Datastore shards. */
|
||||||
@Nullable
|
static Optional<ClaimsListShard> getFromDatastore() {
|
||||||
public static ClaimsListShard get() {
|
return LOADER_RETRIER.callWithRetry(
|
||||||
return CACHE.get();
|
ClaimsListShard::loadClaimsListShard, IllegalStateException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** As a safety mechanism, fail if someone tries to save this class directly. */
|
/** As a safety mechanism, fail if someone tries to save this class directly. */
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright 2021 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.model.tmch;
|
||||||
|
|
||||||
|
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
|
||||||
|
/** Data access object for {@link ClaimsListShard}. */
|
||||||
|
public class ClaimsListSqlDao {
|
||||||
|
|
||||||
|
/** Saves the given {@link ClaimsListShard} to Cloud SQL. */
|
||||||
|
static void save(ClaimsListShard claimsList) {
|
||||||
|
jpaTm().transact(() -> jpaTm().getEntityManager().persist(claimsList));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most recent revision of the {@link ClaimsListShard} in SQL or an empty list if it
|
||||||
|
* doesn't exist.
|
||||||
|
*/
|
||||||
|
static Optional<ClaimsListShard> get() {
|
||||||
|
return jpaTm()
|
||||||
|
.transact(
|
||||||
|
() -> {
|
||||||
|
EntityManager em = jpaTm().getEntityManager();
|
||||||
|
Long revisionId =
|
||||||
|
em.createQuery("SELECT MAX(revisionId) FROM ClaimsList", Long.class)
|
||||||
|
.getSingleResult();
|
||||||
|
return em.createQuery(
|
||||||
|
"FROM ClaimsList cl LEFT JOIN FETCH cl.labelsToKeys WHERE cl.revisionId ="
|
||||||
|
+ " :revisionId",
|
||||||
|
ClaimsListShard.class)
|
||||||
|
.setParameter("revisionId", revisionId)
|
||||||
|
.getResultStream()
|
||||||
|
.findFirst();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClaimsListSqlDao() {}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import static google.registry.request.Action.Method.POST;
|
||||||
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
import google.registry.keyring.api.KeyModule.Key;
|
import google.registry.keyring.api.KeyModule.Key;
|
||||||
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.model.tmch.ClaimsListShard;
|
import google.registry.model.tmch.ClaimsListShard;
|
||||||
import google.registry.request.Action;
|
import google.registry.request.Action;
|
||||||
import google.registry.request.auth.Auth;
|
import google.registry.request.auth.Auth;
|
||||||
|
@ -55,9 +56,9 @@ public final class TmchDnlAction implements Runnable {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
ClaimsListShard claims = ClaimsListParser.parse(lines);
|
ClaimsListShard claims = ClaimsListParser.parse(lines);
|
||||||
claims.save();
|
ClaimsListDualDatabaseDao.save(claims);
|
||||||
logger.atInfo().log(
|
logger.atInfo().log(
|
||||||
"Inserted %,d claims into Datastore, created at %s",
|
"Inserted %,d claims into the DB(s), created at %s",
|
||||||
claims.size(), claims.getTmdbGenerationTime());
|
claims.size(), claims.getTmdbGenerationTime());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.beust.jcommander.Parameter;
|
||||||
import com.beust.jcommander.Parameters;
|
import com.beust.jcommander.Parameters;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.io.Files;
|
import com.google.common.io.Files;
|
||||||
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.model.tmch.ClaimsListShard;
|
import google.registry.model.tmch.ClaimsListShard;
|
||||||
import google.registry.tools.params.PathParameter;
|
import google.registry.tools.params.PathParameter;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
@ -43,7 +44,7 @@ final class GetClaimsListCommand implements CommandWithRemoteApi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() throws Exception {
|
public void run() throws Exception {
|
||||||
ClaimsListShard cl = checkNotNull(ClaimsListShard.get(), "Couldn't load ClaimsList");
|
ClaimsListShard cl = checkNotNull(ClaimsListDualDatabaseDao.get(), "Couldn't load ClaimsList");
|
||||||
String csv = Joiner.on('\n').withKeyValueSeparator(",").join(cl.getLabelsToKeys()) + "\n";
|
String csv = Joiner.on('\n').withKeyValueSeparator(",").join(cl.getLabelsToKeys()) + "\n";
|
||||||
Files.asCharSink(output.toFile(), UTF_8).write(csv);
|
Files.asCharSink(output.toFile(), UTF_8).write(csv);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.beust.jcommander.Parameter;
|
||||||
import com.beust.jcommander.Parameters;
|
import com.beust.jcommander.Parameters;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.io.Files;
|
import com.google.common.io.Files;
|
||||||
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.model.tmch.ClaimsListShard;
|
import google.registry.model.tmch.ClaimsListShard;
|
||||||
import google.registry.tmch.ClaimsListParser;
|
import google.registry.tmch.ClaimsListParser;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -56,7 +57,7 @@ final class UploadClaimsListCommand extends ConfirmingCommand implements Command
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String execute() {
|
public String execute() {
|
||||||
claimsList.save();
|
ClaimsListDualDatabaseDao.save(claimsList);
|
||||||
return String.format("Successfully uploaded claims list %s", claimsListFilename);
|
return String.format("Successfully uploaded claims list %s", claimsListFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||||
import static google.registry.model.tmch.ClaimsListShardTest.createTestClaimsListShard;
|
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||||
|
@ -39,8 +38,8 @@ import google.registry.model.eppinput.EppInput.ResourceCommandWrapper;
|
||||||
import google.registry.model.eppinput.ResourceCommand;
|
import google.registry.model.eppinput.ResourceCommand;
|
||||||
import google.registry.model.index.EppResourceIndex;
|
import google.registry.model.index.EppResourceIndex;
|
||||||
import google.registry.model.index.EppResourceIndexBucket;
|
import google.registry.model.index.EppResourceIndexBucket;
|
||||||
import google.registry.model.tmch.ClaimsListShard.ClaimsListRevision;
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.model.tmch.ClaimsListShard.ClaimsListSingleton;
|
import google.registry.model.tmch.ClaimsListShard;
|
||||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||||
import google.registry.util.TypeUtils.TypeInstantiator;
|
import google.registry.util.TypeUtils.TypeInstantiator;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
@ -103,22 +102,12 @@ public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource
|
||||||
}
|
}
|
||||||
|
|
||||||
private Class<R> getResourceClass() {
|
private Class<R> getResourceClass() {
|
||||||
return new TypeInstantiator<R>(getClass()){}.getExactType();
|
return new TypeInstantiator<R>(getClass()) {}.getExactType();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Persists a testing claims list to Datastore that contains a single shard. */
|
||||||
* Persists a testing claims list to Datastore that contains a single shard.
|
|
||||||
*/
|
|
||||||
protected void persistClaimsList(ImmutableMap<String, String> labelsToKeys) {
|
protected void persistClaimsList(ImmutableMap<String, String> labelsToKeys) {
|
||||||
ClaimsListSingleton singleton = new ClaimsListSingleton();
|
ClaimsListDualDatabaseDao.save(ClaimsListShard.create(clock.nowUtc(), labelsToKeys));
|
||||||
Key<ClaimsListRevision> revision = ClaimsListRevision.createKey(singleton);
|
|
||||||
singleton.setActiveRevision(revision);
|
|
||||||
ofy().saveWithoutBackup().entity(singleton).now();
|
|
||||||
if (!labelsToKeys.isEmpty()) {
|
|
||||||
ofy().saveWithoutBackup()
|
|
||||||
.entity(createTestClaimsListShard(clock.nowUtc(), labelsToKeys, revision))
|
|
||||||
.now();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
// Copyright 2021 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.model.tmch;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||||
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||||
|
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||||
|
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.ImmutableSortedMap;
|
||||||
|
import google.registry.config.RegistryEnvironment;
|
||||||
|
import google.registry.model.EntityTestCase;
|
||||||
|
import google.registry.model.common.DatabaseTransitionSchedule;
|
||||||
|
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||||
|
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
|
||||||
|
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||||
|
import google.registry.model.common.TimedTransitionProperty;
|
||||||
|
import google.registry.testing.SystemPropertyExtension;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
|
||||||
|
/** Unit tests for {@link ClaimsListDualDatabaseDao}. */
|
||||||
|
public class ClaimsListDualDatabaseDaoTest extends EntityTestCase {
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(value = Integer.MAX_VALUE)
|
||||||
|
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void beforeEach() {
|
||||||
|
DatabaseTransitionSchedule schedule =
|
||||||
|
DatabaseTransitionSchedule.create(
|
||||||
|
TransitionId.CLAIMS_LIST,
|
||||||
|
TimedTransitionProperty.fromValueMap(
|
||||||
|
ImmutableSortedMap.of(
|
||||||
|
START_OF_TIME,
|
||||||
|
PrimaryDatabase.DATASTORE,
|
||||||
|
fakeClock.nowUtc().plusDays(1),
|
||||||
|
PrimaryDatabase.CLOUD_SQL),
|
||||||
|
PrimaryDatabaseTransition.class));
|
||||||
|
tm().transactNew(() -> ofy().saveWithoutBackup().entity(schedule).now());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetList_missingSql() {
|
||||||
|
createClaimsList().saveToDatastore();
|
||||||
|
assertThat(assertThrows(IllegalStateException.class, ClaimsListDualDatabaseDao::get))
|
||||||
|
.hasMessageThat()
|
||||||
|
.isEqualTo("Claims list found in primary DB but not in secondary DB.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetList_missingOfy() {
|
||||||
|
fakeClock.advanceBy(Duration.standardDays(5));
|
||||||
|
ClaimsListSqlDao.save(createClaimsList());
|
||||||
|
assertThat(assertThrows(IllegalStateException.class, ClaimsListDualDatabaseDao::get))
|
||||||
|
.hasMessageThat()
|
||||||
|
.isEqualTo("Claims list found in primary DB but not in secondary DB.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetList_fromOfy_different() {
|
||||||
|
createClaimsList().saveToDatastore();
|
||||||
|
ClaimsListSqlDao.save(
|
||||||
|
ClaimsListShard.create(fakeClock.nowUtc(), ImmutableMap.of("foo", "bar")));
|
||||||
|
assertThat(assertThrows(IllegalStateException.class, ClaimsListDualDatabaseDao::get))
|
||||||
|
.hasMessageThat()
|
||||||
|
.isEqualTo(
|
||||||
|
"Unequal claims lists detected:\n"
|
||||||
|
+ "Domain label label1 with key key1 only appears in the primary DB.\n"
|
||||||
|
+ "Domain label label2 with key key2 only appears in the primary DB.\n"
|
||||||
|
+ "Domain label foo with key bar only appears in the secondary DB.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetList_fromSql_different() {
|
||||||
|
fakeClock.advanceBy(Duration.standardDays(5));
|
||||||
|
ClaimsListShard.create(fakeClock.nowUtc(), ImmutableMap.of("foo", "bar")).saveToDatastore();
|
||||||
|
ClaimsListSqlDao.save(createClaimsList());
|
||||||
|
assertThat(assertThrows(IllegalStateException.class, ClaimsListDualDatabaseDao::get))
|
||||||
|
.hasMessageThat()
|
||||||
|
.isEqualTo(
|
||||||
|
"Unequal claims lists detected:\n"
|
||||||
|
+ "Domain label label1 with key key1 only appears in the primary DB.\n"
|
||||||
|
+ "Domain label label2 with key key2 only appears in the primary DB.\n"
|
||||||
|
+ "Domain label foo with key bar only appears in the secondary DB.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSaveAndGet() {
|
||||||
|
tm().transact(() -> ClaimsListDualDatabaseDao.save(createClaimsList()));
|
||||||
|
assertAboutImmutableObjects()
|
||||||
|
.that(ClaimsListDualDatabaseDao.get())
|
||||||
|
.isEqualExceptFields(createClaimsList(), "id", "revisionId", "creationTimestamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGet_empty() {
|
||||||
|
assertThat(tm().transact(ClaimsListDualDatabaseDao::get).getLabelsToKeys()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetList_missingSql_notInTest() {
|
||||||
|
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||||
|
createClaimsList().saveToDatastore();
|
||||||
|
// Shouldn't fail in production
|
||||||
|
assertThat(ClaimsListDualDatabaseDao.get().getLabelsToKeys())
|
||||||
|
.isEqualTo(createClaimsList().getLabelsToKeys());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetList_missingOfy_notInTest() {
|
||||||
|
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||||
|
fakeClock.advanceBy(Duration.standardDays(5));
|
||||||
|
ClaimsListSqlDao.save(createClaimsList());
|
||||||
|
// Shouldn't fail in production
|
||||||
|
assertThat(ClaimsListDualDatabaseDao.get().getLabelsToKeys())
|
||||||
|
.isEqualTo(createClaimsList().getLabelsToKeys());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClaimsListShard createClaimsList() {
|
||||||
|
return ClaimsListShard.create(
|
||||||
|
fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth8.assertThat;
|
import static com.google.common.truth.Truth8.assertThat;
|
||||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
|
||||||
import static org.joda.time.DateTimeZone.UTC;
|
import static org.joda.time.DateTimeZone.UTC;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
@ -48,8 +47,7 @@ public class ClaimsListShardTest {
|
||||||
assertThrows(
|
assertThrows(
|
||||||
UnshardedSaveException.class,
|
UnshardedSaveException.class,
|
||||||
() ->
|
() ->
|
||||||
tm()
|
tm().transact(
|
||||||
.transact(
|
|
||||||
() -> {
|
() -> {
|
||||||
ClaimsListShard claimsList =
|
ClaimsListShard claimsList =
|
||||||
ClaimsListShard.create(
|
ClaimsListShard.create(
|
||||||
|
@ -62,8 +60,7 @@ public class ClaimsListShardTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGet_safelyLoadsEmptyClaimsList_whenNoShardsExist() {
|
void testGet_safelyLoadsEmptyClaimsList_whenNoShardsExist() {
|
||||||
assertThat(ClaimsListShard.get().labelsToKeys).isEmpty();
|
assertThat(ClaimsListShard.getFromDatastore()).isEmpty();
|
||||||
assertThat(ClaimsListShard.get().creationTime).isEqualTo(START_OF_TIME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -77,11 +74,12 @@ public class ClaimsListShardTest {
|
||||||
// Save it with sharding, and make sure that reloading it works.
|
// Save it with sharding, and make sure that reloading it works.
|
||||||
ClaimsListShard unsharded = ClaimsListShard.create(now, ImmutableMap.copyOf(labelsToKeys));
|
ClaimsListShard unsharded = ClaimsListShard.create(now, ImmutableMap.copyOf(labelsToKeys));
|
||||||
unsharded.saveToDatastore(shardSize);
|
unsharded.saveToDatastore(shardSize);
|
||||||
assertThat(ClaimsListShard.get().labelsToKeys).isEqualTo(unsharded.labelsToKeys);
|
assertThat(ClaimsListShard.getFromDatastore().get().labelsToKeys)
|
||||||
|
.isEqualTo(unsharded.labelsToKeys);
|
||||||
List<ClaimsListShard> shards1 = ofy().load().type(ClaimsListShard.class).list();
|
List<ClaimsListShard> shards1 = ofy().load().type(ClaimsListShard.class).list();
|
||||||
assertThat(shards1).hasSize(4);
|
assertThat(shards1).hasSize(4);
|
||||||
assertThat(ClaimsListShard.get().getClaimKey("1")).hasValue("1");
|
assertThat(ClaimsListShard.getFromDatastore().get().getClaimKey("1")).hasValue("1");
|
||||||
assertThat(ClaimsListShard.get().getClaimKey("a")).isEmpty();
|
assertThat(ClaimsListShard.getFromDatastore().get().getClaimKey("a")).isEmpty();
|
||||||
assertThat(ClaimsListShard.getCurrentRevision()).isEqualTo(shards1.get(0).parent);
|
assertThat(ClaimsListShard.getCurrentRevision()).isEqualTo(shards1.get(0).parent);
|
||||||
|
|
||||||
// Create a smaller ClaimsList that will need only 2 shards to save.
|
// Create a smaller ClaimsList that will need only 2 shards to save.
|
||||||
|
@ -92,8 +90,10 @@ public class ClaimsListShardTest {
|
||||||
unsharded = ClaimsListShard.create(now.plusDays(1), ImmutableMap.copyOf(labelsToKeys));
|
unsharded = ClaimsListShard.create(now.plusDays(1), ImmutableMap.copyOf(labelsToKeys));
|
||||||
unsharded.saveToDatastore(shardSize);
|
unsharded.saveToDatastore(shardSize);
|
||||||
ofy().clearSessionCache();
|
ofy().clearSessionCache();
|
||||||
assertThat(ClaimsListShard.get().labelsToKeys).hasSize(unsharded.labelsToKeys.size());
|
assertThat(ClaimsListShard.getFromDatastore().get().labelsToKeys)
|
||||||
assertThat(ClaimsListShard.get().labelsToKeys).isEqualTo(unsharded.labelsToKeys);
|
.hasSize(unsharded.labelsToKeys.size());
|
||||||
|
assertThat(ClaimsListShard.getFromDatastore().get().labelsToKeys)
|
||||||
|
.isEqualTo(unsharded.labelsToKeys);
|
||||||
List<ClaimsListShard> shards2 = ofy().load().type(ClaimsListShard.class).list();
|
List<ClaimsListShard> shards2 = ofy().load().type(ClaimsListShard.class).list();
|
||||||
assertThat(shards2).hasSize(2);
|
assertThat(shards2).hasSize(2);
|
||||||
|
|
||||||
|
|
|
@ -15,18 +15,21 @@
|
||||||
package google.registry.model.tmch;
|
package google.registry.model.tmch;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.truth.Truth8;
|
||||||
import google.registry.persistence.transaction.JpaTestRules;
|
import google.registry.persistence.transaction.JpaTestRules;
|
||||||
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
|
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
|
||||||
import google.registry.testing.DatastoreEntityExtension;
|
import google.registry.testing.DatastoreEntityExtension;
|
||||||
import google.registry.testing.FakeClock;
|
import google.registry.testing.FakeClock;
|
||||||
|
import javax.persistence.PersistenceException;
|
||||||
import org.junit.jupiter.api.Order;
|
import org.junit.jupiter.api.Order;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
|
||||||
/** Unit tests for {@link ClaimsListDao}. */
|
/** Unit tests for {@link ClaimsListSqlDao}. */
|
||||||
public class ClaimsListDaoTest {
|
public class ClaimsListSqlDaoTest {
|
||||||
|
|
||||||
private final FakeClock fakeClock = new FakeClock();
|
private final FakeClock fakeClock = new FakeClock();
|
||||||
|
|
||||||
|
@ -39,40 +42,40 @@ public class ClaimsListDaoTest {
|
||||||
final DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
|
final DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void trySave_insertsClaimsListSuccessfully() {
|
void save_insertsClaimsListSuccessfully() {
|
||||||
ClaimsListShard claimsList =
|
ClaimsListShard claimsList =
|
||||||
ClaimsListShard.create(
|
ClaimsListShard.create(
|
||||||
fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||||
ClaimsListDao.trySave(claimsList);
|
ClaimsListSqlDao.save(claimsList);
|
||||||
ClaimsListShard insertedClaimsList = ClaimsListDao.getLatestRevision().get();
|
ClaimsListShard insertedClaimsList = ClaimsListSqlDao.get().get();
|
||||||
assertClaimsListEquals(claimsList, insertedClaimsList);
|
assertClaimsListEquals(claimsList, insertedClaimsList);
|
||||||
assertThat(insertedClaimsList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
|
assertThat(insertedClaimsList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void trySave_noExceptionThrownWhenSaveFail() {
|
void save_fail_duplicateId() {
|
||||||
ClaimsListShard claimsList =
|
ClaimsListShard claimsList =
|
||||||
ClaimsListShard.create(
|
ClaimsListShard.create(
|
||||||
fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||||
ClaimsListDao.trySave(claimsList);
|
ClaimsListSqlDao.save(claimsList);
|
||||||
ClaimsListShard insertedClaimsList = ClaimsListDao.getLatestRevision().get();
|
ClaimsListShard insertedClaimsList = ClaimsListSqlDao.get().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);
|
assertThrows(PersistenceException.class, () -> ClaimsListSqlDao.save(insertedClaimsList));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void trySave_claimsListWithNoEntries() {
|
void save_claimsListWithNoEntries() {
|
||||||
ClaimsListShard claimsList = ClaimsListShard.create(fakeClock.nowUtc(), ImmutableMap.of());
|
ClaimsListShard claimsList = ClaimsListShard.create(fakeClock.nowUtc(), ImmutableMap.of());
|
||||||
ClaimsListDao.trySave(claimsList);
|
ClaimsListSqlDao.save(claimsList);
|
||||||
ClaimsListShard insertedClaimsList = ClaimsListDao.getLatestRevision().get();
|
ClaimsListShard insertedClaimsList = ClaimsListSqlDao.get().get();
|
||||||
assertClaimsListEquals(claimsList, insertedClaimsList);
|
assertClaimsListEquals(claimsList, insertedClaimsList);
|
||||||
assertThat(insertedClaimsList.getLabelsToKeys()).isEmpty();
|
assertThat(insertedClaimsList.getLabelsToKeys()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getCurrent_returnsEmptyListIfTableIsEmpty() {
|
void getCurrent_returnsEmptyListIfTableIsEmpty() {
|
||||||
assertThat(ClaimsListDao.getLatestRevision().isPresent()).isFalse();
|
Truth8.assertThat(ClaimsListSqlDao.get()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -83,9 +86,9 @@ public class ClaimsListDaoTest {
|
||||||
ClaimsListShard newClaimsList =
|
ClaimsListShard newClaimsList =
|
||||||
ClaimsListShard.create(
|
ClaimsListShard.create(
|
||||||
fakeClock.nowUtc(), ImmutableMap.of("label3", "key3", "label4", "key4"));
|
fakeClock.nowUtc(), ImmutableMap.of("label3", "key3", "label4", "key4"));
|
||||||
ClaimsListDao.trySave(oldClaimsList);
|
ClaimsListSqlDao.save(oldClaimsList);
|
||||||
ClaimsListDao.trySave(newClaimsList);
|
ClaimsListSqlDao.save(newClaimsList);
|
||||||
assertClaimsListEquals(newClaimsList, ClaimsListDao.getLatestRevision().get());
|
assertClaimsListEquals(newClaimsList, ClaimsListSqlDao.get().get());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertClaimsListEquals(ClaimsListShard left, ClaimsListShard right) {
|
private void assertClaimsListEquals(ClaimsListShard left, ClaimsListShard right) {
|
|
@ -33,7 +33,7 @@ import google.registry.model.reporting.Spec11ThreatMatchTest;
|
||||||
import google.registry.model.server.KmsSecretRevisionSqlDaoTest;
|
import google.registry.model.server.KmsSecretRevisionSqlDaoTest;
|
||||||
import google.registry.model.server.ServerSecretTest;
|
import google.registry.model.server.ServerSecretTest;
|
||||||
import google.registry.model.smd.SignedMarkRevocationListDaoTest;
|
import google.registry.model.smd.SignedMarkRevocationListDaoTest;
|
||||||
import google.registry.model.tmch.ClaimsListDaoTest;
|
import google.registry.model.tmch.ClaimsListSqlDaoTest;
|
||||||
import google.registry.model.tmch.TmchCrlTest;
|
import google.registry.model.tmch.TmchCrlTest;
|
||||||
import google.registry.persistence.transaction.JpaEntityCoverageExtension;
|
import google.registry.persistence.transaction.JpaEntityCoverageExtension;
|
||||||
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
|
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
|
||||||
|
@ -82,7 +82,7 @@ import org.junit.runner.RunWith;
|
||||||
BeforeSuiteTest.class,
|
BeforeSuiteTest.class,
|
||||||
AllocationTokenTest.class,
|
AllocationTokenTest.class,
|
||||||
BillingEventTest.class,
|
BillingEventTest.class,
|
||||||
ClaimsListDaoTest.class,
|
ClaimsListSqlDaoTest.class,
|
||||||
ContactHistoryTest.class,
|
ContactHistoryTest.class,
|
||||||
ContactResourceTest.class,
|
ContactResourceTest.class,
|
||||||
CursorTest.class,
|
CursorTest.class,
|
||||||
|
|
|
@ -20,6 +20,7 @@ import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.model.tmch.ClaimsListShard;
|
import google.registry.model.tmch.ClaimsListShard;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
@ -37,7 +38,7 @@ class TmchDnlActionTest extends TmchActionTestCase {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDnl() throws Exception {
|
void testDnl() throws Exception {
|
||||||
assertThat(ClaimsListShard.get().getClaimKey("xn----7sbejwbn3axu3d")).isEmpty();
|
assertThat(ClaimsListDualDatabaseDao.get().getClaimKey("xn----7sbejwbn3axu3d")).isEmpty();
|
||||||
when(httpResponse.getContent())
|
when(httpResponse.getContent())
|
||||||
.thenReturn(TmchTestData.loadBytes("dnl-latest.csv").read())
|
.thenReturn(TmchTestData.loadBytes("dnl-latest.csv").read())
|
||||||
.thenReturn(TmchTestData.loadBytes("dnl-latest.sig").read());
|
.thenReturn(TmchTestData.loadBytes("dnl-latest.sig").read());
|
||||||
|
@ -49,7 +50,7 @@ class TmchDnlActionTest extends TmchActionTestCase {
|
||||||
.isEqualTo(MARKSDB_URL + "/dnl/dnl-latest.sig");
|
.isEqualTo(MARKSDB_URL + "/dnl/dnl-latest.sig");
|
||||||
|
|
||||||
// Make sure the contents of testdata/dnl-latest.csv got inserted into the database.
|
// Make sure the contents of testdata/dnl-latest.csv got inserted into the database.
|
||||||
ClaimsListShard claimsList = ClaimsListShard.get();
|
ClaimsListShard claimsList = ClaimsListDualDatabaseDao.get();
|
||||||
assertThat(claimsList.getTmdbGenerationTime())
|
assertThat(claimsList.getTmdbGenerationTime())
|
||||||
.isEqualTo(DateTime.parse("2013-11-24T23:15:37.4Z"));
|
.isEqualTo(DateTime.parse("2013-11-24T23:15:37.4Z"));
|
||||||
assertThat(claimsList.getClaimKey("xn----7sbejwbn3axu3d"))
|
assertThat(claimsList.getClaimKey("xn----7sbejwbn3axu3d"))
|
||||||
|
|
|
@ -20,6 +20,7 @@ import static java.nio.file.Files.readAllLines;
|
||||||
import static org.joda.time.DateTimeZone.UTC;
|
import static org.joda.time.DateTimeZone.UTC;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.model.tmch.ClaimsListShard;
|
import google.registry.model.tmch.ClaimsListShard;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
@ -31,7 +32,8 @@ class GetClaimsListCommandTest extends CommandTestCase<GetClaimsListCommand> {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSuccess_getWorks() throws Exception {
|
void testSuccess_getWorks() throws Exception {
|
||||||
ClaimsListShard.create(DateTime.now(UTC), ImmutableMap.of("a", "1", "b", "2")).save();
|
ClaimsListDualDatabaseDao.save(
|
||||||
|
ClaimsListShard.create(DateTime.now(UTC), ImmutableMap.of("a", "1", "b", "2")));
|
||||||
File output = tmpDir.resolve("claims.txt").toFile();
|
File output = tmpDir.resolve("claims.txt").toFile();
|
||||||
runCommand("--output=" + output.getAbsolutePath());
|
runCommand("--output=" + output.getAbsolutePath());
|
||||||
assertThat(readAllLines(output.toPath(), UTF_8)).containsExactly("a,1", "b,2");
|
assertThat(readAllLines(output.toPath(), UTF_8)).containsExactly("a,1", "b,2");
|
||||||
|
@ -39,7 +41,8 @@ class GetClaimsListCommandTest extends CommandTestCase<GetClaimsListCommand> {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSuccess_endsWithNewline() throws Exception {
|
void testSuccess_endsWithNewline() throws Exception {
|
||||||
ClaimsListShard.create(DateTime.now(UTC), ImmutableMap.of("a", "1")).save();
|
ClaimsListDualDatabaseDao.save(
|
||||||
|
ClaimsListShard.create(DateTime.now(UTC), ImmutableMap.of("a", "1")));
|
||||||
File output = tmpDir.resolve("claims.txt").toFile();
|
File output = tmpDir.resolve("claims.txt").toFile();
|
||||||
runCommand("--output=" + output.getAbsolutePath());
|
runCommand("--output=" + output.getAbsolutePath());
|
||||||
assertThat(new String(Files.readAllBytes(output.toPath()), UTF_8)).endsWith("\n");
|
assertThat(new String(Files.readAllBytes(output.toPath()), UTF_8)).endsWith("\n");
|
||||||
|
|
|
@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth8.assertThat;
|
import static com.google.common.truth.Truth8.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
|
||||||
import google.registry.model.tmch.ClaimsListShard;
|
import google.registry.model.tmch.ClaimsListShard;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
@ -36,7 +37,7 @@ class UploadClaimsListCommandTest extends CommandTestCase<UploadClaimsListComman
|
||||||
"anotherexample,2013041500/A/C/7/rHdC4wnrWRvPY6nneCVtQhFj0000000003,2011-08-16T12:00:00.0Z");
|
"anotherexample,2013041500/A/C/7/rHdC4wnrWRvPY6nneCVtQhFj0000000003,2011-08-16T12:00:00.0Z");
|
||||||
runCommand("--force", filename);
|
runCommand("--force", filename);
|
||||||
|
|
||||||
ClaimsListShard claimsList = ClaimsListShard.get();
|
ClaimsListShard claimsList = ClaimsListDualDatabaseDao.get();
|
||||||
assertThat(claimsList.getTmdbGenerationTime())
|
assertThat(claimsList.getTmdbGenerationTime())
|
||||||
.isEqualTo(DateTime.parse("2012-08-16T00:00:00.0Z"));
|
.isEqualTo(DateTime.parse("2012-08-16T00:00:00.0Z"));
|
||||||
assertThat(claimsList.getClaimKey("example"))
|
assertThat(claimsList.getClaimKey("example"))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue