Add dual read for cursors (#473)

* Add dual read for cursors

* Fix loadAndCompareAll to batch load cursors

* fix javadocs
This commit is contained in:
sarahcaseybot 2020-02-19 16:10:19 -05:00 committed by GitHub
parent be395611ca
commit f53aa8d55e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 323 additions and 50 deletions

View file

@ -25,6 +25,8 @@ import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.schema.cursor.Cursor.GLOBAL;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.CollectionUtils.union; import static google.registry.util.CollectionUtils.union;
import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.earliestOf; import static google.registry.util.DateTimeUtils.earliestOf;
@ -93,6 +95,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
@Override @Override
public void run() { public void run() {
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now(); Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
loadAndCompare(cursor, GLOBAL);
DateTime executeTime = clock.nowUtc(); DateTime executeTime = clock.nowUtc();
DateTime persistedCursorTime = (cursor == null ? START_OF_TIME : cursor.getCursorTime()); DateTime persistedCursorTime = (cursor == null ? START_OF_TIME : cursor.getCursorTime());
DateTime cursorTime = cursorTimeParam.orElse(persistedCursorTime); DateTime cursorTime = cursorTimeParam.orElse(persistedCursorTime);
@ -317,6 +320,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
tm().transact( tm().transact(
() -> { () -> {
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now(); Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
loadAndCompare(cursor, GLOBAL);
DateTime currentCursorTime = DateTime currentCursorTime =
(cursor == null ? START_OF_TIME : cursor.getCursorTime()); (cursor == null ? START_OF_TIME : cursor.getCursorTime());
if (!currentCursorTime.equals(expectedPersistedCursorTime)) { if (!currentCursorTime.equals(expectedPersistedCursorTime)) {
@ -327,8 +331,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
} }
if (!isDryRun) { if (!isDryRun) {
CursorDao.saveCursor( CursorDao.saveCursor(
Cursor.createGlobal(RECURRING_BILLING, executionTime), Cursor.createGlobal(RECURRING_BILLING, executionTime), GLOBAL);
google.registry.schema.cursor.Cursor.GLOBAL);
} }
}); });
} }

View file

@ -25,6 +25,8 @@ import static google.registry.model.registrar.RegistrarContact.Type.LEGAL;
import static google.registry.model.registrar.RegistrarContact.Type.MARKETING; import static google.registry.model.registrar.RegistrarContact.Type.MARKETING;
import static google.registry.model.registrar.RegistrarContact.Type.TECH; import static google.registry.model.registrar.RegistrarContact.Type.TECH;
import static google.registry.model.registrar.RegistrarContact.Type.WHOIS; import static google.registry.model.registrar.RegistrarContact.Type.WHOIS;
import static google.registry.schema.cursor.Cursor.GLOBAL;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
@ -62,6 +64,7 @@ class SyncRegistrarsSheet {
*/ */
boolean wereRegistrarsModified() { boolean wereRegistrarsModified() {
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(SYNC_REGISTRAR_SHEET)).now(); Cursor cursor = ofy().load().key(Cursor.createGlobalKey(SYNC_REGISTRAR_SHEET)).now();
loadAndCompare(cursor, GLOBAL);
DateTime lastUpdateTime = (cursor == null) ? START_OF_TIME : cursor.getCursorTime(); DateTime lastUpdateTime = (cursor == null) ? START_OF_TIME : cursor.getCursorTime();
for (Registrar registrar : Registrar.loadAllCached()) { for (Registrar registrar : Registrar.loadAllCached()) {
if (DateTimeUtils.isAtOrAfter(registrar.getLastUpdateTime(), lastUpdateTime)) { if (DateTimeUtils.isAtOrAfter(registrar.getLastUpdateTime(), lastUpdateTime)) {

View file

@ -15,6 +15,7 @@
package google.registry.rde; package google.registry.rde;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import google.registry.model.common.Cursor; import google.registry.model.common.Cursor;
@ -91,6 +92,7 @@ class EscrowTaskRunner {
logger.atInfo().log("TLD: %s", registry.getTld()); logger.atInfo().log("TLD: %s", registry.getTld());
DateTime startOfToday = clock.nowUtc().withTimeAtStartOfDay(); DateTime startOfToday = clock.nowUtc().withTimeAtStartOfDay();
Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now(); Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now();
loadAndCompare(cursor, registry.getTldStr());
final DateTime nextRequiredRun = (cursor == null ? startOfToday : cursor.getCursorTime()); final DateTime nextRequiredRun = (cursor == null ? startOfToday : cursor.getCursorTime());
if (nextRequiredRun.isAfter(startOfToday)) { if (nextRequiredRun.isAfter(startOfToday)) {
throw new NoContentException("Already completed"); throw new NoContentException("Already completed");

View file

@ -17,6 +17,7 @@ package google.registry.rde;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
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.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.DateTimeUtils.isBeforeOrAt; import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.ImmutableSetMultimap;
@ -91,6 +92,7 @@ public final class PendingDepositChecker {
} }
// Avoid creating a transaction unless absolutely necessary. // Avoid creating a transaction unless absolutely necessary.
Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now(); Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now();
loadAndCompare(cursor, registry.getTldStr());
DateTime cursorValue = (cursor != null ? cursor.getCursorTime() : startingPoint); DateTime cursorValue = (cursor != null ? cursor.getCursorTime() : startingPoint);
if (isBeforeOrAt(cursorValue, now)) { if (isBeforeOrAt(cursorValue, now)) {
DateTime watermark = (cursor != null DateTime watermark = (cursor != null
@ -111,6 +113,7 @@ public final class PendingDepositChecker {
return tm().transact( return tm().transact(
() -> { () -> {
Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now(); Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now();
loadAndCompare(cursor, registry.getTldStr());
if (cursor != null) { if (cursor != null) {
return cursor.getCursorTime(); return cursor.getCursorTime();
} }

View file

@ -20,6 +20,7 @@ import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.rde.RdeMode.FULL; import static google.registry.model.rde.RdeMode.FULL;
import static google.registry.request.Action.Method.POST; import static google.registry.request.Action.Method.POST;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.DateTimeUtils.isBeforeOrAt; import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.appengine.tools.cloudstorage.GcsFilename;
@ -76,9 +77,10 @@ public final class RdeReportAction implements Runnable, EscrowTask {
@Override @Override
public void runWithLock(DateTime watermark) throws Exception { public void runWithLock(DateTime watermark) throws Exception {
DateTime cursorTime = Cursor cursor =
getCursorTimeOrStartOfTime( ofy().load().key(Cursor.createKey(CursorType.RDE_UPLOAD, Registry.get(tld))).now();
ofy().load().key(Cursor.createKey(CursorType.RDE_UPLOAD, Registry.get(tld))).now()); loadAndCompare(cursor, tld);
DateTime cursorTime = getCursorTimeOrStartOfTime(cursor);
if (isBeforeOrAt(cursorTime, watermark)) { if (isBeforeOrAt(cursorTime, watermark)) {
throw new NoContentException( throw new NoContentException(
String.format( String.format(

View file

@ -22,6 +22,7 @@ import static com.google.common.base.Verify.verify;
import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime; import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
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.schema.cursor.CursorDao.loadAndCompare;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.appengine.tools.cloudstorage.GcsFilename;
@ -207,9 +208,9 @@ public final class RdeStagingReducer extends Reducer<PendingDeposit, DepositFrag
tm().transact( tm().transact(
() -> { () -> {
Registry registry = Registry.get(tld); Registry registry = Registry.get(tld);
DateTime position = Cursor cursor = ofy().load().key(Cursor.createKey(key.cursor(), registry)).now();
getCursorTimeOrStartOfTime( loadAndCompare(cursor, tld);
ofy().load().key(Cursor.createKey(key.cursor(), registry)).now()); DateTime position = getCursorTimeOrStartOfTime(cursor);
checkState(key.interval() != null, "Interval must be present"); checkState(key.interval() != null, "Interval must be present");
DateTime newPosition = key.watermark().plus(key.interval()); DateTime newPosition = key.watermark().plus(key.interval());
if (!position.isBefore(newPosition)) { if (!position.isBefore(newPosition)) {

View file

@ -24,6 +24,7 @@ import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.rde.RdeMode.FULL; import static google.registry.model.rde.RdeMode.FULL;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST; import static google.registry.request.Action.Method.POST;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.DateTimeUtils.isBeforeOrAt; import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@ -132,8 +133,10 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
@Override @Override
public void runWithLock(final DateTime watermark) throws Exception { public void runWithLock(final DateTime watermark) throws Exception {
logger.atInfo().log("Verifying readiness to upload the RDE deposit."); logger.atInfo().log("Verifying readiness to upload the RDE deposit.");
DateTime stagingCursorTime = getCursorTimeOrStartOfTime( Cursor cursor =
ofy().load().key(Cursor.createKey(CursorType.RDE_STAGING, Registry.get(tld))).now()); ofy().load().key(Cursor.createKey(CursorType.RDE_STAGING, Registry.get(tld))).now();
loadAndCompare(cursor, tld);
DateTime stagingCursorTime = getCursorTimeOrStartOfTime(cursor);
if (isBeforeOrAt(stagingCursorTime, watermark)) { if (isBeforeOrAt(stagingCursorTime, watermark)) {
throw new NoContentException( throw new NoContentException(
String.format( String.format(
@ -141,9 +144,10 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
+ "last RDE staging completion was at %s", + "last RDE staging completion was at %s",
tld, watermark, stagingCursorTime)); tld, watermark, stagingCursorTime));
} }
DateTime sftpCursorTime = Cursor sftpCursor =
getCursorTimeOrStartOfTime( ofy().load().key(Cursor.createKey(RDE_UPLOAD_SFTP, Registry.get(tld))).now();
ofy().load().key(Cursor.createKey(RDE_UPLOAD_SFTP, Registry.get(tld))).now()); loadAndCompare(sftpCursor, tld);
DateTime sftpCursorTime = getCursorTimeOrStartOfTime(sftpCursor);
Duration timeSinceLastSftp = new Duration(sftpCursorTime, clock.nowUtc()); Duration timeSinceLastSftp = new Duration(sftpCursorTime, clock.nowUtc());
if (timeSinceLastSftp.isShorterThan(sftpCooldown)) { if (timeSinceLastSftp.isShorterThan(sftpCooldown)) {
throw new NoContentException( throw new NoContentException(

View file

@ -21,10 +21,10 @@ import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
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.request.Action.Method.POST; import static google.registry.request.Action.Method.POST;
import static google.registry.schema.cursor.CursorDao.loadAndCompareAll;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
@ -54,7 +54,6 @@ import java.util.Optional;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -108,7 +107,7 @@ public final class IcannReportingUploadAction implements Runnable {
() -> { () -> {
ImmutableMap.Builder<String, Boolean> reportSummaryBuilder = new ImmutableMap.Builder<>(); ImmutableMap.Builder<String, Boolean> reportSummaryBuilder = new ImmutableMap.Builder<>();
ImmutableMap<Cursor, CursorInfo> cursors = loadCursors(); ImmutableMap<Cursor, String> cursors = loadCursors();
// If cursor time is before now, upload the corresponding report // If cursor time is before now, upload the corresponding report
cursors.entrySet().stream() cursors.entrySet().stream()
@ -118,8 +117,8 @@ public final class IcannReportingUploadAction implements Runnable {
DateTime cursorTime = getCursorTimeOrStartOfTime(entry.getKey()); DateTime cursorTime = getCursorTimeOrStartOfTime(entry.getKey());
uploadReport( uploadReport(
cursorTime, cursorTime,
entry.getValue().getType(), entry.getKey().getType(),
entry.getValue().getTld(), entry.getValue(),
reportSummaryBuilder); reportSummaryBuilder);
}); });
// Send email of which reports were uploaded // Send email of which reports were uploaded
@ -205,8 +204,8 @@ public final class IcannReportingUploadAction implements Runnable {
cursorTimeMinusMonth.monthOfYear().get()); cursorTimeMinusMonth.monthOfYear().get());
} }
/** Returns a map of each cursor to the CursorType and tld. */ /** Returns a map of each cursor to the tld. */
private ImmutableMap<Cursor, CursorInfo> loadCursors() { private ImmutableMap<Cursor, String> loadCursors() {
ImmutableSet<Registry> registries = Registries.getTldEntitiesOfType(TldType.REAL); ImmutableSet<Registry> registries = Registries.getTldEntitiesOfType(TldType.REAL);
@ -220,11 +219,13 @@ public final class IcannReportingUploadAction implements Runnable {
keys.addAll(transactionKeyMap.keySet()); keys.addAll(transactionKeyMap.keySet());
Map<Key<Cursor>, Cursor> cursorMap = ofy().load().keys(keys.build()); Map<Key<Cursor>, Cursor> cursorMap = ofy().load().keys(keys.build());
ImmutableMap.Builder<Cursor, CursorInfo> cursors = new ImmutableMap.Builder<>(); ImmutableMap.Builder<Cursor, String> cursors = new ImmutableMap.Builder<>();
cursors.putAll(
defaultNullCursorsToNextMonthAndAddToMap( defaultNullCursorsToNextMonthAndAddToMap(
activityKeyMap, CursorType.ICANN_UPLOAD_ACTIVITY, cursorMap, cursors); activityKeyMap, CursorType.ICANN_UPLOAD_ACTIVITY, cursorMap));
cursors.putAll(
defaultNullCursorsToNextMonthAndAddToMap( defaultNullCursorsToNextMonthAndAddToMap(
transactionKeyMap, CursorType.ICANN_UPLOAD_TX, cursorMap, cursors); transactionKeyMap, CursorType.ICANN_UPLOAD_TX, cursorMap));
return cursors.build(); return cursors.build();
} }
@ -234,15 +235,13 @@ public final class IcannReportingUploadAction implements Runnable {
} }
/** /**
* Populate the cursors map with the Cursor and CursorInfo for each key in the keyMap. If the key * Return a map with the Cursor and scope for each key in the keyMap. If the key from the keyMap
* from the keyMap does not have an existing cursor, create a new cursor with a default cursorTime * does not have an existing cursor, create a new cursor with a default cursorTime of the first of
* of the first of next month. * next month.
*/ */
private void defaultNullCursorsToNextMonthAndAddToMap( private ImmutableMap<Cursor, String> defaultNullCursorsToNextMonthAndAddToMap(
Map<Key<Cursor>, Registry> keyMap, Map<Key<Cursor>, Registry> keyMap, CursorType type, Map<Key<Cursor>, Cursor> cursorMap) {
CursorType type, ImmutableMap.Builder<Cursor, String> cursors = new ImmutableMap.Builder<>();
Map<Key<Cursor>, Cursor> cursorMap,
ImmutableMap.Builder<Cursor, CursorInfo> cursors) {
keyMap.forEach( keyMap.forEach(
(key, registry) -> { (key, registry) -> {
// Cursor time is defaulted to the first of next month since a new tld will not yet have a // Cursor time is defaulted to the first of next month since a new tld will not yet have a
@ -257,8 +256,10 @@ public final class IcannReportingUploadAction implements Runnable {
if (!cursorMap.containsValue(cursor)) { if (!cursorMap.containsValue(cursor)) {
tm().transact(() -> ofy().save().entity(cursor)); tm().transact(() -> ofy().save().entity(cursor));
} }
cursors.put(cursor, CursorInfo.create(type, registry.getTldStr())); cursors.put(cursor, registry.getTldStr());
}); });
loadAndCompareAll(cursors.build(), type);
return cursors.build();
} }
/** Don't retry when reports are already uploaded or can't be uploaded. */ /** Don't retry when reports are already uploaded or can't be uploaded. */
@ -305,15 +306,4 @@ public final class IcannReportingUploadAction implements Runnable {
gcsFilename.getBucketName()); gcsFilename.getBucketName());
} }
@AutoValue
abstract static class CursorInfo {
static CursorInfo create(CursorType type, @Nullable String tld) {
return new AutoValue_IcannReportingUploadAction_CursorInfo(type, tld);
}
public abstract CursorType getType();
@Nullable
abstract String getTld();
}
} }

View file

@ -15,6 +15,7 @@
package google.registry.schema.cursor; package google.registry.schema.cursor;
import static com.google.appengine.api.search.checkers.Preconditions.checkNotNull; import static com.google.appengine.api.search.checkers.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@ -25,6 +26,7 @@ import com.google.common.flogger.FluentLogger;
import google.registry.model.common.Cursor.CursorType; import google.registry.model.common.Cursor.CursorType;
import google.registry.schema.cursor.Cursor.CursorId; import google.registry.schema.cursor.Cursor.CursorId;
import java.util.List; import java.util.List;
import javax.annotation.Nullable;
/** Data access object class for {@link Cursor}. */ /** Data access object class for {@link Cursor}. */
public class CursorDao { public class CursorDao {
@ -130,4 +132,73 @@ public class CursorDao {
logger.atSevere().withCause(e).log("Error saving cursor to Cloud SQL."); logger.atSevere().withCause(e).log("Error saving cursor to Cloud SQL.");
} }
} }
/**
* Loads in cursor from Cloud SQL and compares it to the Datastore cursor
*
* <p>This takes in a cursor from Datastore and checks to see if it exists in Cloud SQL and has
* the same value. If a difference is detected, or the Cloud SQL cursor does not exist, a warning
* is logged.
*/
public static void loadAndCompare(
@Nullable google.registry.model.common.Cursor datastoreCursor, String scope) {
if (datastoreCursor == null) {
return;
}
try {
// Load the corresponding cursor from Cloud SQL
Cursor cloudSqlCursor = load(datastoreCursor.getType(), scope);
compare(datastoreCursor, cloudSqlCursor, scope);
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error comparing cursors.");
}
}
/**
* Loads in all cursors of a given type from Cloud SQL and compares them to Datastore
*
* <p>This takes in cursors from Datastore and checks to see if they exists in Cloud SQL and have
* the same value. If a difference is detected, or a Cloud SQL cursor does not exist, a warning is
* logged.
*/
public static void loadAndCompareAll(
ImmutableMap<google.registry.model.common.Cursor, String> cursors, CursorType type) {
try {
// Load all the cursors of that type from Cloud SQL
List<Cursor> cloudSqlCursors = loadByType(type);
// Create a map of each tld to its cursor if one exists
ImmutableMap<String, Cursor> cloudSqlCursorMap =
cloudSqlCursors.stream().collect(toImmutableMap(c -> c.getScope(), c -> c));
// Compare each Datastore cursor with its corresponding Cloud SQL cursor
for (google.registry.model.common.Cursor cursor : cursors.keySet()) {
Cursor cloudSqlCursor = cloudSqlCursorMap.get(cursors.get(cursor));
compare(cursor, cloudSqlCursor, cursors.get(cursor));
}
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error comparing cursors.");
}
}
private static void compare(
google.registry.model.common.Cursor datastoreCursor,
@Nullable Cursor cloudSqlCursor,
String scope) {
if (cloudSqlCursor == null) {
logger.atWarning().log(
String.format(
"Cursor of type %s with the scope %s was not found in Cloud SQL.",
datastoreCursor.getType().name(), scope));
} else if (!datastoreCursor.getCursorTime().equals(cloudSqlCursor.getCursorTime())) {
logger.atWarning().log(
String.format(
"This cursor of type %s with the scope %s has a cursorTime of %s in Datastore and %s"
+ " in Cloud SQL.",
datastoreCursor.getType(),
scope,
datastoreCursor.getCursorTime(),
cloudSqlCursor.getCursorTime()));
}
}
} }

View file

@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters; import com.beust.jcommander.Parameters;
@ -74,6 +75,9 @@ final class ListCursorsCommand implements CommandWithRemoteApi {
} }
private static String renderLine(String tld, Optional<Cursor> cursor) { private static String renderLine(String tld, Optional<Cursor> cursor) {
if (cursor.isPresent()) {
loadAndCompare(cursor.get(), tld);
}
return String.format( return String.format(
OUTPUT_FMT, OUTPUT_FMT,
tld, tld,

View file

@ -19,6 +19,8 @@ import static google.registry.model.common.Cursor.CursorType.BRDA;
import static google.registry.model.common.Cursor.CursorType.RDE_UPLOAD; import static google.registry.model.common.Cursor.CursorType.RDE_UPLOAD;
import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING; import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.schema.cursor.Cursor.GLOBAL;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.testing.DatastoreHelper.persistActiveDomain; import static google.registry.testing.DatastoreHelper.persistActiveDomain;
import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME;
@ -27,19 +29,30 @@ import static org.junit.Assert.assertThrows;
import google.registry.model.EntityTestCase; import google.registry.model.EntityTestCase;
import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainBase;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageRule;
import google.registry.schema.cursor.CursorDao; import google.registry.schema.cursor.CursorDao;
import google.registry.testing.FakeClock;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
/** Unit tests for {@link Cursor}. */ /** Unit tests for {@link Cursor}. */
public class CursorTest extends EntityTestCase { public class CursorTest extends EntityTestCase {
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2010-10-17TZ"));
@Rule
public final JpaIntegrationWithCoverageRule jpaRule =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageRule();
@Test @Test
public void testSuccess_persistScopedCursor() { public void testSuccess_persistScopedCursor() {
createTld("tld"); createTld("tld");
clock.advanceOneMilli(); clock.advanceOneMilli();
final DateTime time = DateTime.parse("2012-07-12T03:30:00.000Z"); final DateTime time = DateTime.parse("2012-07-12T03:30:00.000Z");
CursorDao.saveCursor(Cursor.create(RDE_UPLOAD, time, Registry.get("tld")), "tld"); Cursor cursor = Cursor.create(RDE_UPLOAD, time, Registry.get("tld"));
CursorDao.saveCursor(cursor, "tld");
assertThat(ofy().load().key(Cursor.createKey(BRDA, Registry.get("tld"))).now()).isNull(); assertThat(ofy().load().key(Cursor.createKey(BRDA, Registry.get("tld"))).now()).isNull();
assertThat( assertThat(
ofy() ofy()
@ -48,23 +61,24 @@ public class CursorTest extends EntityTestCase {
.now() .now()
.getCursorTime()) .getCursorTime())
.isEqualTo(time); .isEqualTo(time);
loadAndCompare(cursor, "tld");
} }
@Test @Test
public void testSuccess_persistGlobalCursor() { public void testSuccess_persistGlobalCursor() {
final DateTime time = DateTime.parse("2012-07-12T03:30:00.000Z"); final DateTime time = DateTime.parse("2012-07-12T03:30:00.000Z");
CursorDao.saveCursor( CursorDao.saveCursor(Cursor.createGlobal(RECURRING_BILLING, time), GLOBAL);
Cursor.createGlobal(RECURRING_BILLING, time), google.registry.schema.cursor.Cursor.GLOBAL);
assertThat(ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now().getCursorTime()) assertThat(ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now().getCursorTime())
.isEqualTo(time); .isEqualTo(time);
loadAndCompare(Cursor.createGlobal(RECURRING_BILLING, time), GLOBAL);
} }
@Test @Test
public void testIndexing() throws Exception { public void testIndexing() throws Exception {
final DateTime time = DateTime.parse("2012-07-12T03:30:00.000Z"); final DateTime time = DateTime.parse("2012-07-12T03:30:00.000Z");
CursorDao.saveCursor( CursorDao.saveCursor(Cursor.createGlobal(RECURRING_BILLING, time), GLOBAL);
Cursor.createGlobal(RECURRING_BILLING, time), google.registry.schema.cursor.Cursor.GLOBAL);
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now(); Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
loadAndCompare(cursor, GLOBAL);
verifyIndexing(cursor); verifyIndexing(cursor);
} }

View file

@ -259,4 +259,178 @@ public class CursorDaoTest {
assertThat(createdCursor3.getCursorTime()).isEqualTo(cursor3.getCursorTime()); assertThat(createdCursor3.getCursorTime()).isEqualTo(cursor3.getCursorTime());
assertThat(cursor3).isEqualTo(dataStoreCursor3); assertThat(cursor3).isEqualTo(dataStoreCursor3);
} }
@Test
public void loadAndCompare_worksSuccessfully() {
loggerToIntercept.addHandler(logHandler);
createTld("tld");
google.registry.model.common.Cursor cursor =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("tld"));
CursorDao.saveCursor(cursor, "tld");
CursorDao.loadAndCompare(cursor, "tld");
assertAboutLogs().that(logHandler).hasNoLogsAtLevel(Level.WARNING);
}
@Test
public void loadAndCompare_worksSuccessfullyGlobalCursor() {
loggerToIntercept.addHandler(logHandler);
google.registry.model.common.Cursor cursor =
google.registry.model.common.Cursor.createGlobal(
CursorType.RECURRING_BILLING, fakeClock.nowUtc());
CursorDao.saveCursor(cursor, Cursor.GLOBAL);
CursorDao.loadAndCompare(cursor, Cursor.GLOBAL);
assertAboutLogs().that(logHandler).hasNoLogsAtLevel(Level.WARNING);
}
@Test
public void loadAndCompare_worksSuccessfullyCursorNotInCloudSql() {
loggerToIntercept.addHandler(logHandler);
createTld("tld");
google.registry.model.common.Cursor cursor =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("tld"));
CursorDao.loadAndCompare(cursor, "tld");
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"Cursor of type ICANN_UPLOAD_ACTIVITY with the scope tld was not found in Cloud SQL.");
}
@Test
public void loadAndCompare_worksSuccessfullyGlobalCursorNotInCloudSql() {
loggerToIntercept.addHandler(logHandler);
google.registry.model.common.Cursor cursor =
google.registry.model.common.Cursor.createGlobal(
CursorType.RECURRING_BILLING, fakeClock.nowUtc());
CursorDao.loadAndCompare(cursor, Cursor.GLOBAL);
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"Cursor of type RECURRING_BILLING with the scope GLOBAL was not found in Cloud SQL.");
}
@Test
public void loadAndCompare_worksSuccessfullyCursorsNotEqual() {
loggerToIntercept.addHandler(logHandler);
createTld("tld");
google.registry.model.common.Cursor cursor =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("tld"));
CursorDao.saveCursor(cursor, "tld");
cursor =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc().minusDays(5), Registry.get("tld"));
CursorDao.loadAndCompare(cursor, "tld");
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"This cursor of type ICANN_UPLOAD_ACTIVITY with the scope tld has a cursorTime of"
+ " 1969-12-27T00:00:00.000Z in Datastore and 1970-01-01T00:00:00.000Z in Cloud"
+ " SQL.");
}
@Test
public void loadAndCompareAll_worksSuccessfully() {
loggerToIntercept.addHandler(logHandler);
// Create Datastore cursors
createTlds("tld", "foo");
google.registry.model.common.Cursor cursor1 =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("tld"));
google.registry.model.common.Cursor cursor2 =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("foo"));
// Save cursors to Cloud SQL
ImmutableMap<google.registry.model.common.Cursor, String> cursors =
ImmutableMap.<google.registry.model.common.Cursor, String>builder()
.put(cursor1, "tld")
.put(cursor2, "foo")
.build();
CursorDao.saveCursors(cursors);
CursorDao.loadAndCompareAll(cursors, CursorType.ICANN_UPLOAD_ACTIVITY);
assertAboutLogs().that(logHandler).hasNoLogsAtLevel(Level.WARNING);
}
@Test
public void loadAndCompareAll_worksSuccessfullyMissingOne() {
loggerToIntercept.addHandler(logHandler);
// Create Datastore cursors
createTlds("tld", "foo", "lol");
google.registry.model.common.Cursor cursor1 =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("tld"));
google.registry.model.common.Cursor cursor2 =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("foo"));
// Save Cursors to Cloud SQL
ImmutableMap.Builder<google.registry.model.common.Cursor, String> cursors =
ImmutableMap.<google.registry.model.common.Cursor, String>builder()
.put(cursor1, "tld")
.put(cursor2, "foo");
CursorDao.saveCursors(cursors.build());
// Create a new Datastore cursor that is not saved to Cloud SQL
google.registry.model.common.Cursor cursor3 =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc().minusDays(4), Registry.get("lol"));
// Call loadAndCompareAll with all three Datastore cursors
CursorDao.loadAndCompareAll(
cursors.put(cursor3, "lol").build(), CursorType.ICANN_UPLOAD_ACTIVITY);
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"Cursor of type ICANN_UPLOAD_ACTIVITY with the scope lol was not found in Cloud SQL.");
}
@Test
public void loadAndCompareAll_worksSuccessfullyOneWithWrongTime() {
loggerToIntercept.addHandler(logHandler);
// Create Datastore cursors
createTlds("tld", "foo");
google.registry.model.common.Cursor cursor1 =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("tld"));
google.registry.model.common.Cursor cursor2 =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc(), Registry.get("foo"));
// Save Cursors to Cloud SQL
CursorDao.saveCursors(ImmutableMap.of(cursor1, "tld", cursor2, "foo"));
// Change time of first Datastore cursor, but don't save new time to Cloud SQL
google.registry.model.common.Cursor cursor3 =
google.registry.model.common.Cursor.create(
CursorType.ICANN_UPLOAD_ACTIVITY, fakeClock.nowUtc().minusDays(4), Registry.get("tld"));
CursorDao.loadAndCompareAll(
ImmutableMap.of(cursor2, "foo", cursor3, "tld"), CursorType.ICANN_UPLOAD_ACTIVITY);
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"This cursor of type ICANN_UPLOAD_ACTIVITY with the scope tld has a cursorTime of"
+ " 1969-12-28T00:00:00.000Z in Datastore and 1970-01-01T00:00:00.000Z in Cloud"
+ " SQL.");
}
@Test
public void loadAndCompareAll_worksSuccessfullyEmptyMap() {
loggerToIntercept.addHandler(logHandler);
CursorDao.loadAndCompareAll(ImmutableMap.of(), CursorType.ICANN_UPLOAD_ACTIVITY);
assertAboutLogs().that(logHandler).hasNoLogsAtLevel(Level.WARNING);
}
} }

View file

@ -15,6 +15,7 @@
package google.registry.schema.integration; package google.registry.schema.integration;
import com.google.common.truth.Expect; import com.google.common.truth.Expect;
import google.registry.model.common.CursorTest;
import google.registry.model.domain.DomainBaseSqlTest; import google.registry.model.domain.DomainBaseSqlTest;
import google.registry.model.registry.RegistryLockDaoTest; import google.registry.model.registry.RegistryLockDaoTest;
import google.registry.persistence.transaction.JpaEntityCoverage; import google.registry.persistence.transaction.JpaEntityCoverage;
@ -60,6 +61,7 @@ import org.junit.runners.Suite.SuiteClasses;
CreateRegistrarCommandTest.class, CreateRegistrarCommandTest.class,
CreateReservedListCommandTest.class, CreateReservedListCommandTest.class,
CursorDaoTest.class, CursorDaoTest.class,
CursorTest.class,
DomainLockUtilsTest.class, DomainLockUtilsTest.class,
LockDaoTest.class, LockDaoTest.class,
LockDomainCommandTest.class, LockDomainCommandTest.class,