1
0
mirror of https://github.com/google/nomulus synced 2026-05-23 00:01:58 +00:00

Compare commits

...

9 Commits

Author SHA1 Message Date
Weimin Yu
b9fcabbc36 Save db discrepancies to GCS (#1527)
* Save db discrepancies to GCS
2022-02-28 16:52:09 -05:00
Weimin Yu
4f33de10f3 Add a tools command to launch SQL validation job (#1526)
* Add a tools command to launch SQL validation job

Stopping using Pipeline.run().waitUntilFinish in
ValidateDatastorePipeline. Flex-templalate does not support blocking
wait in the main thread.

This PR adds a new ValidateSqlCommand that launches the pipeline and
maintains the SQL snapshot while the pipeline is running.

This PR also added more parameters to both ValidateSqlCommand and
ValidateDatastoreCommand:
- The -c option to supply an optional incremental comparison start time
- The -r option to supply an optional release tag that is not 'live',
  e.g., nomulus-DDDDYYMM-RC00

If the manual launch option (-m) is enabled, the commands will print the
gcloud command that can launch the pipeline.

Tested with sandbox, qa and the dev project.
2022-02-28 13:14:57 -05:00
Michael Muller
d6bb83f6d3 Nullify contact fields in setContactFields (#1533)
When setting contact fields from a set of DesignatedContact's, nullify the
existing fields so they don't stick around if they're not in the new set.
2022-02-28 10:57:35 -05:00
Michael Muller
a8d3d22c5a Don't reset the update time for TLD updates (#1532)
* Don't reset the update time for TLD updates

It turns out that the reason that the Registrar update timestamp isn't updated
for some of the tests is because the record is updated unchanged.  We can
avoid this problem by not trying to update the registrar to the same value.
So in this case, if the registrar alreay contains the TLD we're adding, don't
try to add it.
2022-02-25 13:09:36 -05:00
Rachel Guan
fac659b520 Inject CloudTasksUtils to DomainLockUtils (#1519)
* Move enqueueDomainRelock to DomainLockUtils

* Rebase and improve PR

* Inject CloudTaskUtils to DomainLockUtils
2022-02-25 11:38:38 -05:00
gbrodman
178702ded3 Fix DTR creation in one location and clean up replay comparison (#1529)
* Fix DTR creation in one location and clean up replay comparison
2022-02-23 11:07:10 -05:00
Lai Jiang
59bca1a9ed Disable sending cert expiration emails on sandbox (#1528) 2022-02-22 14:46:27 -05:00
Michael Muller
f8198fa590 Do full database comparison during replay tests (#1524)
* Fix entity delete replication, compare db @ replay

Replay tests currently only verify that the contents of a transaction are
can be successfully replicated to the other database.  They do not verify that
the contents of both databases are equivalent.  As a result, we miss any
changes omitted from the transaction (as was the case with entity deletions).

This change adds a final database comparison to ReplayExtension so we can
safely say that the databases are in the same state.

This comparison is introduced in part as a unit test for the one-line fix for
replication of an "entity delete" operation (where we delete using an entity
object instead of the object's key) which so far has only affected PollMessage
deletion.  The fix is also included in this commit within
JpaTransactionManagerImpl.

* Exclude tests and entities with failing comparisons

* Get all tests to pass and fix more timestamp

Fix most of the unit tests that were broken by this change.

- Fix timestamp updates after grace period changes in DomainContent and for
  TLD changes in Registrar.
- Reenable full database comparison for most DomainCreateFlowTest's.
- Make some test entities NonReplicated so they don't break when used with
  jpaTm().delete()
- Diable checking of a few more entity types that are failing comparisons.
- Add some formatting fixes.

* Remove unnecessary "NoDatabaseCompare"

I turns out that after other fixes/elisions we no longer need these for
any tests in DomainCreateFlowTest.

* Changes for review

* Remove old "compare" flag.

* Reformatted.
2022-02-22 10:49:57 -05:00
Lai Jiang
bbac81996b Make a few quality-of-life improvements in CloudTasksUtils (#1521)
* Make a few quality-of-life improvements in CloudTasksUtils

1. Update the method names. There are too many overloaded methods and it
   is hard to figure out which one does which without checking the
   javadoc.

2. Added a method in the task matcher to specify the delay time in
   DateTime, so the caller does not need to convert it to Timestamp.

3. Remove the expilict dependency on a clock when enqueueing a task with
   delay, the clock is now injected directly into the util instance
   itself.
2022-02-18 20:21:56 -05:00
79 changed files with 1282 additions and 1065 deletions

View File

@@ -707,10 +707,7 @@ createToolTask(
'initSqlPipeline', 'google.registry.beam.initsql.InitSqlPipeline')
createToolTask(
'validateSqlPipeline', 'google.registry.beam.comparedb.ValidateSqlPipeline')
createToolTask(
'validateDatastorePipeline', 'google.registry.beam.comparedb.ValidateDatastorePipeline')
'validateDatabasePipeline', 'google.registry.beam.comparedb.ValidateDatabasePipeline')
createToolTask(
@@ -797,15 +794,10 @@ if (environment == 'alpha') {
mainClass: 'google.registry.beam.rde.RdePipeline',
metaData : 'google/registry/beam/rde_pipeline_metadata.json'
],
validateDatastore :
validateDatabase :
[
mainClass: 'google.registry.beam.comparedb.ValidateDatastorePipeline',
metaData: 'google/registry/beam/validate_datastore_pipeline_metadata.json'
],
validateSql :
[
mainClass: 'google.registry.beam.comparedb.ValidateSqlPipeline',
metaData: 'google/registry/beam/validate_sql_pipeline_metadata.json'
mainClass: 'google.registry.beam.comparedb.ValidateDatabasePipeline',
metaData: 'google/registry/beam/validate_database_pipeline_metadata.json'
],
]
project.tasks.create("stageBeamPipelines") {

View File

@@ -96,7 +96,7 @@ public final class CommitLogCheckpointAction implements Runnable {
// Enqueue a diff task between previous and current checkpoints.
cloudTasksUtils.enqueue(
QUEUE_NAME,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTask(
ExportCommitLogDiffAction.PATH,
Service.BACKEND.toString(),
ImmutableMultimap.of(

View File

@@ -76,7 +76,10 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Duration LEASE_LENGTH = standardHours(1);
public static final String REPLAY_TO_SQL_LOCK_NAME =
ReplayCommitLogsToSqlAction.class.getSimpleName();
public static final Duration REPLAY_TO_SQL_LOCK_LEASE_LENGTH = standardHours(1);
// Stop / pause where we are if we've been replaying for more than five minutes to avoid GAE
// request timeouts
private static final Duration REPLAY_TIMEOUT_DURATION = Duration.standardMinutes(5);
@@ -115,7 +118,11 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
}
Optional<Lock> lock =
Lock.acquireSql(
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
REPLAY_TO_SQL_LOCK_NAME,
null,
REPLAY_TO_SQL_LOCK_LEASE_LENGTH,
requestStatusChecker,
false);
if (!lock.isPresent()) {
String message = "Can't acquire SQL commit log replay lock, aborting.";
logger.atSevere().log(message);

View File

@@ -48,8 +48,7 @@ import org.joda.time.Duration;
* </ul>
*
* The caller may release the replication lock upon receiving the response from this action. Please
* refer to {@link google.registry.tools.ValidateDatastoreWithSqlCommand} for more information on
* usage.
* refer to {@link google.registry.tools.ValidateDatastoreCommand} for more information on usage.
*
* <p>This action plays SQL transactions up to the user-specified snapshot, creates a new CommitLog
* checkpoint, and exports all CommitLogs to GCS up to this checkpoint. The timestamp of this

View File

@@ -26,7 +26,6 @@ import com.google.common.collect.ImmutableSortedSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.domain.RegistryLock;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostResource;
import google.registry.persistence.VKey;
@@ -152,32 +151,6 @@ public final class AsyncTaskEnqueuer {
.param(PARAM_REQUESTED_TIME, now.toString()));
}
/**
* Enqueues a task to asynchronously re-lock a registry-locked domain after it was unlocked.
*
* <p>Note: the relockDuration must be present on the lock object.
*/
public void enqueueDomainRelock(RegistryLock lock) {
checkArgument(
lock.getRelockDuration().isPresent(),
"Lock with ID %s not configured for relock",
lock.getRevisionId());
enqueueDomainRelock(lock.getRelockDuration().get(), lock.getRevisionId(), 0);
}
/** Enqueues a task to asynchronously re-lock a registry-locked domain after it was unlocked. */
void enqueueDomainRelock(Duration countdown, long lockRevisionId, int previousAttempts) {
String backendHostname = appEngineServiceUtils.getServiceHostname("backend");
addTaskToQueueWithRetry(
asyncActionsPushQueue,
TaskOptions.Builder.withUrl(RelockDomainAction.PATH)
.method(Method.POST)
.header("Host", backendHostname)
.param(RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM, String.valueOf(lockRevisionId))
.param(RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM, String.valueOf(previousAttempts))
.countdownMillis(countdown.getMillis()));
}
/**
* Adds a task to a queue with retrying, to avoid aborting the entire flow over a transient issue
* enqueuing a task.

View File

@@ -88,7 +88,6 @@ public class RelockDomainAction implements Runnable {
private final SendEmailService sendEmailService;
private final DomainLockUtils domainLockUtils;
private final Response response;
private final AsyncTaskEnqueuer asyncTaskEnqueuer;
@Inject
public RelockDomainAction(
@@ -99,8 +98,7 @@ public class RelockDomainAction implements Runnable {
@Config("supportEmail") String supportEmail,
SendEmailService sendEmailService,
DomainLockUtils domainLockUtils,
Response response,
AsyncTaskEnqueuer asyncTaskEnqueuer) {
Response response) {
this.oldUnlockRevisionId = oldUnlockRevisionId;
this.previousAttempts = previousAttempts;
this.alertRecipientAddress = alertRecipientAddress;
@@ -109,7 +107,6 @@ public class RelockDomainAction implements Runnable {
this.sendEmailService = sendEmailService;
this.domainLockUtils = domainLockUtils;
this.response = response;
this.asyncTaskEnqueuer = asyncTaskEnqueuer;
}
@Override
@@ -245,8 +242,7 @@ public class RelockDomainAction implements Runnable {
}
}
Duration timeBeforeRetry = previousAttempts < ATTEMPTS_BEFORE_SLOWDOWN ? TEN_MINUTES : ONE_HOUR;
asyncTaskEnqueuer.enqueueDomainRelock(
timeBeforeRetry, oldUnlockRevisionId, previousAttempts + 1);
domainLockUtils.enqueueDomainRelock(timeBeforeRetry, oldUnlockRevisionId, previousAttempts + 1);
}
private void sendSuccessEmail(RegistryLock oldLock) {

View File

@@ -0,0 +1,206 @@
// 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.beam.comparedb;
import static com.google.common.base.Verify.verify;
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import google.registry.beam.common.RegistryPipelineOptions;
import google.registry.beam.common.RegistryPipelineWorkerInitializer;
import google.registry.beam.comparedb.LatestDatastoreSnapshotFinder.DatastoreSnapshotInfo;
import google.registry.beam.comparedb.ValidateSqlUtils.CompareSqlEntity;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.replay.SqlEntity;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.util.SystemClock;
import java.io.Serializable;
import java.util.Optional;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.WithKeys;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* Validates the asynchronous data replication process between Datastore and Cloud SQL.
*
* <p>This pipeline is to be launched by {@link google.registry.tools.ValidateDatastoreCommand} or
* {@link google.registry.tools.ValidateSqlCommand}.
*/
@DeleteAfterMigration
public class ValidateDatabasePipeline {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Specifies the extra CommitLogs to load before the start of a Database export. */
private static final Duration COMMITLOG_START_TIME_MARGIN = Duration.standardMinutes(10);
private final ValidateDatabasePipelineOptions options;
private final LatestDatastoreSnapshotFinder datastoreSnapshotFinder;
public ValidateDatabasePipeline(
ValidateDatabasePipelineOptions options,
LatestDatastoreSnapshotFinder datastoreSnapshotFinder) {
this.options = options;
this.datastoreSnapshotFinder = datastoreSnapshotFinder;
}
@VisibleForTesting
void run(Pipeline pipeline) {
DateTime latestCommitLogTime = DateTime.parse(options.getLatestCommitLogTimestamp());
DatastoreSnapshotInfo mostRecentExport =
datastoreSnapshotFinder.getSnapshotInfo(latestCommitLogTime.toInstant());
logger.atInfo().log(
"Comparing datastore export at %s and commitlog timestamp %s.",
mostRecentExport.exportDir(), latestCommitLogTime);
Optional<String> outputPath =
Optional.ofNullable(options.getDiffOutputGcsBucket())
.map(
bucket ->
String.format(
"gs://%s/validate_database/%s/diffs.txt",
bucket, new SystemClock().nowUtc()));
outputPath.ifPresent(path -> logger.atInfo().log("Discrepancies will be logged to %s", path));
setupPipeline(
pipeline,
Optional.ofNullable(options.getSqlSnapshotId()),
mostRecentExport,
latestCommitLogTime,
Optional.ofNullable(options.getComparisonStartTimestamp()).map(DateTime::parse),
outputPath);
pipeline.run();
}
static void setupPipeline(
Pipeline pipeline,
Optional<String> sqlSnapshotId,
DatastoreSnapshotInfo mostRecentExport,
DateTime latestCommitLogTime,
Optional<DateTime> compareStartTime,
Optional<String> diffOutputPath) {
pipeline
.getCoderRegistry()
.registerCoderForClass(SqlEntity.class, SerializableCoder.of(Serializable.class));
PCollectionTuple datastoreSnapshot =
DatastoreSnapshots.loadDatastoreSnapshotByKind(
pipeline,
mostRecentExport.exportDir(),
mostRecentExport.commitLogDir(),
mostRecentExport.exportInterval().getStart().minus(COMMITLOG_START_TIME_MARGIN),
// Increase by 1ms since we want to include commitLogs latestCommitLogTime but
// this parameter is exclusive.
latestCommitLogTime.plusMillis(1),
DatastoreSnapshots.ALL_DATASTORE_KINDS,
compareStartTime);
PCollectionTuple cloudSqlSnapshot =
SqlSnapshots.loadCloudSqlSnapshotByType(
pipeline, SqlSnapshots.ALL_SQL_ENTITIES, sqlSnapshotId, compareStartTime);
verify(
datastoreSnapshot.getAll().keySet().equals(cloudSqlSnapshot.getAll().keySet()),
"Expecting the same set of types in both snapshots.");
PCollectionList<String> diffLogs = PCollectionList.empty(pipeline);
for (Class<? extends SqlEntity> clazz : SqlSnapshots.ALL_SQL_ENTITIES) {
TupleTag<SqlEntity> tag = ValidateSqlUtils.createSqlEntityTupleTag(clazz);
verify(
datastoreSnapshot.has(tag), "Missing %s in Datastore snapshot.", clazz.getSimpleName());
verify(cloudSqlSnapshot.has(tag), "Missing %s in Cloud SQL snapshot.", clazz.getSimpleName());
diffLogs =
diffLogs.and(
PCollectionList.of(datastoreSnapshot.get(tag))
.and(cloudSqlSnapshot.get(tag))
.apply(
"Combine from both snapshots: " + clazz.getSimpleName(),
Flatten.pCollections())
.apply(
"Assign primary key to merged " + clazz.getSimpleName(),
WithKeys.of(ValidateDatabasePipeline::getPrimaryKeyString)
.withKeyType(strings()))
.apply("Group by primary key " + clazz.getSimpleName(), GroupByKey.create())
.apply("Compare " + clazz.getSimpleName(), ParDo.of(new CompareSqlEntity())));
}
if (diffOutputPath.isPresent()) {
diffLogs
.apply("Gather diff logs", Flatten.pCollections())
.apply(
"Output diffs",
TextIO.write()
.to(diffOutputPath.get())
/**
* Output to a single file for ease of use since diffs should be few. If this
* assumption turns out not to be false, user should abort the pipeline and
* investigate why.
*/
.withoutSharding()
.withDelimiter((Strings.repeat("-", 80) + "\n").toCharArray()));
}
}
private static String getPrimaryKeyString(SqlEntity sqlEntity) {
// SqlEntity.getPrimaryKeyString only works with entities registered with Hibernate.
// We are using the BulkQueryJpaTransactionManager, which does not recognize DomainBase and
// DomainHistory. See BulkQueryEntities.java for more information.
if (sqlEntity instanceof DomainBase) {
return "DomainBase_" + ((DomainBase) sqlEntity).getRepoId();
}
if (sqlEntity instanceof DomainHistory) {
return "DomainHistory_" + ((DomainHistory) sqlEntity).getDomainHistoryId().toString();
}
return sqlEntity.getPrimaryKeyString();
}
public static void main(String[] args) {
ValidateDatabasePipelineOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(ValidateDatabasePipelineOptions.class);
RegistryPipelineOptions.validateRegistryPipelineOptions(options);
// Defensively set important options.
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ);
options.setJpaTransactionManagerType(JpaTransactionManagerType.BULK_QUERY);
// Set up JPA in the pipeline harness (the locally executed part of the main() method). Reuse
// code in RegistryPipelineWorkerInitializer, which only applies to pipeline worker VMs.
new RegistryPipelineWorkerInitializer().beforeProcessing(options);
LatestDatastoreSnapshotFinder datastoreSnapshotFinder =
DaggerLatestDatastoreSnapshotFinder_LatestDatastoreSnapshotFinderFinderComponent.create()
.datastoreSnapshotInfoFinder();
new ValidateDatabasePipeline(options, datastoreSnapshotFinder).run(Pipeline.create(options));
}
}

View File

@@ -14,14 +14,15 @@
package google.registry.beam.comparedb;
import google.registry.beam.common.RegistryPipelineOptions;
import google.registry.model.annotations.DeleteAfterMigration;
import javax.annotation.Nullable;
import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.Validation;
/** BEAM pipeline options for {@link ValidateDatastorePipelineOptions}. */
/** BEAM pipeline options for {@link ValidateDatabasePipeline}. */
@DeleteAfterMigration
public interface ValidateDatastorePipelineOptions extends ValidateSqlPipelineOptions {
public interface ValidateDatabasePipelineOptions extends RegistryPipelineOptions {
@Description(
"The id of the SQL snapshot to be compared with Datastore. "
@@ -36,4 +37,19 @@ public interface ValidateDatastorePipelineOptions extends ValidateSqlPipelineOpt
String getLatestCommitLogTimestamp();
void setLatestCommitLogTimestamp(String commitLogEndTimestamp);
@Description(
"For history entries and EPP resources, only those modified strictly after this time are "
+ "included in comparison. Value is in ISO8601 format. "
+ "Other entity types are not affected.")
@Nullable
String getComparisonStartTimestamp();
void setComparisonStartTimestamp(String comparisonStartTimestamp);
@Description("The GCS bucket where discrepancies found during comparison should be logged.")
@Nullable
String getDiffOutputGcsBucket();
void setDiffOutputGcsBucket(String gcsBucket);
}

View File

@@ -1,83 +0,0 @@
// Copyright 2022 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.beam.comparedb;
import google.registry.beam.common.RegistryPipelineOptions;
import google.registry.beam.common.RegistryPipelineWorkerInitializer;
import google.registry.beam.comparedb.LatestDatastoreSnapshotFinder.DatastoreSnapshotInfo;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import java.util.Optional;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.joda.time.DateTime;
/**
* Validates the asynchronous data replication process from Cloud SQL (primary) to Datastore
* (secondary).
*
* <p>This pipeline simply compares the snapshots provided by an invoker, which is responsible for
* obtaining two consistent snapshots for the same point of time.
*/
// TODO(weiminyu): Implement the invoker action in a followup PR.
@DeleteAfterMigration
public class ValidateDatastorePipeline {
private final ValidateDatastorePipelineOptions options;
private final LatestDatastoreSnapshotFinder datastoreSnapshotFinder;
public ValidateDatastorePipeline(
ValidateDatastorePipelineOptions options,
LatestDatastoreSnapshotFinder datastoreSnapshotFinder) {
this.options = options;
this.datastoreSnapshotFinder = datastoreSnapshotFinder;
}
void run(Pipeline pipeline) {
DateTime latestCommitLogTime = DateTime.parse(options.getLatestCommitLogTimestamp());
DatastoreSnapshotInfo mostRecentExport =
datastoreSnapshotFinder.getSnapshotInfo(latestCommitLogTime.toInstant());
ValidateSqlPipeline.setupPipeline(
pipeline,
Optional.ofNullable(options.getSqlSnapshotId()),
mostRecentExport,
latestCommitLogTime,
Optional.ofNullable(options.getComparisonStartTimestamp()).map(DateTime::parse));
pipeline.run();
}
public static void main(String[] args) {
ValidateDatastorePipelineOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(ValidateDatastorePipelineOptions.class);
RegistryPipelineOptions.validateRegistryPipelineOptions(options);
// Defensively set important options.
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ);
options.setJpaTransactionManagerType(JpaTransactionManagerType.BULK_QUERY);
// Reuse Dataflow worker initialization code to set up JPA in the pipeline harness.
new RegistryPipelineWorkerInitializer().beforeProcessing(options);
LatestDatastoreSnapshotFinder datastoreSnapshotFinder =
DaggerLatestDatastoreSnapshotFinder_LatestDatastoreSnapshotFinderFinderComponent.create()
.datastoreSnapshotInfoFinder();
new ValidateDatastorePipeline(options, datastoreSnapshotFinder).run(Pipeline.create(options));
}
}

View File

@@ -1,264 +0,0 @@
// 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.beam.comparedb;
import static com.google.common.base.Verify.verify;
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.flogger.FluentLogger;
import google.registry.beam.common.DatabaseSnapshot;
import google.registry.beam.common.RegistryPipelineWorkerInitializer;
import google.registry.beam.comparedb.LatestDatastoreSnapshotFinder.DatastoreSnapshotInfo;
import google.registry.beam.comparedb.ValidateSqlUtils.CompareSqlEntity;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.replay.SqlEntity;
import google.registry.model.replay.SqlReplayCheckpoint;
import google.registry.model.server.Lock;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.util.RequestStatusChecker;
import google.registry.util.SystemClock;
import java.io.Serializable;
import java.util.Optional;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult.State;
import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.WithKeys;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* Validates the asynchronous data replication process from Datastore (primary storage) to Cloud SQL
* (secondary storage).
*/
@DeleteAfterMigration
public class ValidateSqlPipeline {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Specifies the extra CommitLogs to load before the start of a Database export. */
private static final Duration COMMITLOG_START_TIME_MARGIN = Duration.standardMinutes(10);
/**
* Name of the lock used by the commitlog replay process.
*
* <p>See {@link google.registry.backup.ReplayCommitLogsToSqlAction} for more information.
*/
private static final String COMMITLOG_REPLAY_LOCK_NAME = "ReplayCommitLogsToSqlAction";
private static final Duration REPLAY_LOCK_LEASE_LENGTH = Duration.standardHours(1);
private static final java.time.Duration REPLAY_LOCK_ACQUIRE_TIMEOUT =
java.time.Duration.ofMinutes(6);
private static final java.time.Duration REPLAY_LOCK_ACQUIRE_DELAY =
java.time.Duration.ofSeconds(30);
private final ValidateSqlPipelineOptions options;
private final LatestDatastoreSnapshotFinder datastoreSnapshotFinder;
public ValidateSqlPipeline(
ValidateSqlPipelineOptions options, LatestDatastoreSnapshotFinder datastoreSnapshotFinder) {
this.options = options;
this.datastoreSnapshotFinder = datastoreSnapshotFinder;
}
@VisibleForTesting
void run(Pipeline pipeline) {
Optional<Lock> lock = acquireCommitLogReplayLock();
if (lock.isPresent()) {
logger.atInfo().log("Acquired CommitLog Replay lock.");
} else {
throw new RuntimeException("Failed to acquire CommitLog Replay lock.");
}
try {
DateTime latestCommitLogTime =
TransactionManagerFactory.jpaTm().transact(() -> SqlReplayCheckpoint.get());
DatastoreSnapshotInfo mostRecentExport =
datastoreSnapshotFinder.getSnapshotInfo(latestCommitLogTime.toInstant());
Preconditions.checkState(
latestCommitLogTime.isAfter(mostRecentExport.exportInterval().getEnd()),
"Cannot recreate Datastore snapshot since target time is in the middle of an export.");
try (DatabaseSnapshot databaseSnapshot = DatabaseSnapshot.createSnapshot()) {
// Eagerly release the commitlog replay lock so that replay can resume.
lock.ifPresent(Lock::releaseSql);
lock = Optional.empty();
logger.atInfo().log(
"Starting comparison with export at %s and latestCommitLogTime at %s",
mostRecentExport.exportDir(), latestCommitLogTime);
setupPipeline(
pipeline,
Optional.of(databaseSnapshot.getSnapshotId()),
mostRecentExport,
latestCommitLogTime,
Optional.ofNullable(options.getComparisonStartTimestamp()).map(DateTime::parse));
State state = pipeline.run().waitUntilFinish();
if (!State.DONE.equals(state)) {
throw new IllegalStateException("Unexpected pipeline state: " + state);
}
}
} finally {
lock.ifPresent(Lock::releaseSql);
}
}
static void setupPipeline(
Pipeline pipeline,
Optional<String> sqlSnapshotId,
DatastoreSnapshotInfo mostRecentExport,
DateTime latestCommitLogTime,
Optional<DateTime> compareStartTime) {
pipeline
.getCoderRegistry()
.registerCoderForClass(SqlEntity.class, SerializableCoder.of(Serializable.class));
PCollectionTuple datastoreSnapshot =
DatastoreSnapshots.loadDatastoreSnapshotByKind(
pipeline,
mostRecentExport.exportDir(),
mostRecentExport.commitLogDir(),
mostRecentExport.exportInterval().getStart().minus(COMMITLOG_START_TIME_MARGIN),
// Increase by 1ms since we want to include commitLogs latestCommitLogTime but
// this parameter is exclusive.
latestCommitLogTime.plusMillis(1),
DatastoreSnapshots.ALL_DATASTORE_KINDS,
compareStartTime);
PCollectionTuple cloudSqlSnapshot =
SqlSnapshots.loadCloudSqlSnapshotByType(
pipeline, SqlSnapshots.ALL_SQL_ENTITIES, sqlSnapshotId, compareStartTime);
verify(
datastoreSnapshot.getAll().keySet().equals(cloudSqlSnapshot.getAll().keySet()),
"Expecting the same set of types in both snapshots.");
for (Class<? extends SqlEntity> clazz : SqlSnapshots.ALL_SQL_ENTITIES) {
TupleTag<SqlEntity> tag = ValidateSqlUtils.createSqlEntityTupleTag(clazz);
verify(
datastoreSnapshot.has(tag), "Missing %s in Datastore snapshot.", clazz.getSimpleName());
verify(cloudSqlSnapshot.has(tag), "Missing %s in Cloud SQL snapshot.", clazz.getSimpleName());
PCollectionList.of(datastoreSnapshot.get(tag))
.and(cloudSqlSnapshot.get(tag))
.apply("Combine from both snapshots: " + clazz.getSimpleName(), Flatten.pCollections())
.apply(
"Assign primary key to merged " + clazz.getSimpleName(),
WithKeys.of(ValidateSqlPipeline::getPrimaryKeyString).withKeyType(strings()))
.apply("Group by primary key " + clazz.getSimpleName(), GroupByKey.create())
.apply("Compare " + clazz.getSimpleName(), ParDo.of(new CompareSqlEntity()));
}
}
private static String getPrimaryKeyString(SqlEntity sqlEntity) {
// SqlEntity.getPrimaryKeyString only works with entities registered with Hibernate.
// We are using the BulkQueryJpaTransactionManager, which does not recognize DomainBase and
// DomainHistory. See BulkQueryEntities.java for more information.
if (sqlEntity instanceof DomainBase) {
return "DomainBase_" + ((DomainBase) sqlEntity).getRepoId();
}
if (sqlEntity instanceof DomainHistory) {
return "DomainHistory_" + ((DomainHistory) sqlEntity).getDomainHistoryId().toString();
}
return sqlEntity.getPrimaryKeyString();
}
private static Optional<Lock> acquireCommitLogReplayLock() {
Stopwatch stopwatch = Stopwatch.createStarted();
while (stopwatch.elapsed().minus(REPLAY_LOCK_ACQUIRE_TIMEOUT).isNegative()) {
Optional<Lock> lock = tryAcquireCommitLogReplayLock();
if (lock.isPresent()) {
return lock;
}
logger.atInfo().log("Failed to acquired CommitLog Replay lock. Will retry...");
try {
Thread.sleep(REPLAY_LOCK_ACQUIRE_DELAY.toMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted.");
}
}
return Optional.empty();
}
private static Optional<Lock> tryAcquireCommitLogReplayLock() {
return Lock.acquireSql(
COMMITLOG_REPLAY_LOCK_NAME,
null,
REPLAY_LOCK_LEASE_LENGTH,
getLockingRequestStatusChecker(),
false);
}
/**
* Returns a fake implementation of {@link RequestStatusChecker} that is required for lock
* acquisition. The default implementation is AppEngine-specific and is unusable on GCE.
*/
private static RequestStatusChecker getLockingRequestStatusChecker() {
return new RequestStatusChecker() {
@Override
public String getLogId() {
return "ValidateSqlPipeline";
}
LatestDatastoreSnapshotFinder datastoreSnapshotFinder =
DaggerLatestDatastoreSnapshotFinder_LatestDatastoreSnapshotFinderFinderComponent.create()
.datastoreSnapshotInfoFinder();
@Override
public boolean isRunning(String requestLogId) {
return true;
}
};
}
public static void main(String[] args) {
ValidateSqlPipelineOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(ValidateSqlPipelineOptions.class);
// Defensively set important options.
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ);
options.setJpaTransactionManagerType(JpaTransactionManagerType.BULK_QUERY);
// Reuse Dataflow worker initialization code to set up JPA in the pipeline harness.
new RegistryPipelineWorkerInitializer().beforeProcessing(options);
MigrationState state =
DatabaseMigrationStateSchedule.getValueAtTime(new SystemClock().nowUtc());
if (!state.getReplayDirection().equals(ReplayDirection.DATASTORE_TO_SQL)) {
throw new IllegalStateException("This pipeline is not designed for migration phase " + state);
}
LatestDatastoreSnapshotFinder datastoreSnapshotFinder =
DaggerLatestDatastoreSnapshotFinder_LatestDatastoreSnapshotFinderFinderComponent.create()
.datastoreSnapshotInfoFinder();
new ValidateSqlPipeline(options, datastoreSnapshotFinder).run(Pipeline.create(options));
}
}

View File

@@ -1,34 +0,0 @@
// 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.beam.comparedb;
import google.registry.beam.common.RegistryPipelineOptions;
import google.registry.model.annotations.DeleteAfterMigration;
import javax.annotation.Nullable;
import org.apache.beam.sdk.options.Description;
/** BEAM pipeline options for {@link ValidateSqlPipeline}. */
@DeleteAfterMigration
public interface ValidateSqlPipelineOptions extends RegistryPipelineOptions {
@Description(
"For history entries and EPP resources, only those modified strictly after this time are "
+ "included in comparison. Value is in ISO8601 format. "
+ "Other entity types are not affected.")
@Nullable
String getComparisonStartTimestamp();
void setComparisonStartTimestamp(String comparisonStartTimestamp);
}

View File

@@ -20,7 +20,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.beam.initsql.Transforms;
import google.registry.config.RegistryEnvironment;
import google.registry.model.EppResource;
@@ -37,6 +36,7 @@ import google.registry.model.poll.PollMessage;
import google.registry.model.registrar.Registrar;
import google.registry.model.replay.SqlEntity;
import google.registry.model.reporting.HistoryEntry;
import google.registry.util.DiffUtils;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.HashMap;
@@ -51,10 +51,9 @@ import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.TupleTag;
/** Helpers for use by {@link ValidateSqlPipeline}. */
/** Helpers for use by {@link ValidateDatabasePipeline}. */
@DeleteAfterMigration
final class ValidateSqlUtils {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ValidateSqlUtils() {}
@@ -66,7 +65,7 @@ final class ValidateSqlUtils {
* Query template for finding the median value of the {@code history_revision_id} column in one of
* the History tables.
*
* <p>The {@link ValidateSqlPipeline} uses this query to parallelize the query to some of the
* <p>The {@link ValidateDatabasePipeline} uses this query to parallelize the query to some of the
* history tables. Although the {@code repo_id} column is the leading column in the primary keys
* of these tables, in practice and with production data, division by {@code history_revision_id}
* works slightly faster for unknown reasons.
@@ -98,15 +97,13 @@ final class ValidateSqlUtils {
return new TupleTag<SqlEntity>(actualType.getSimpleName()) {};
}
static class CompareSqlEntity extends DoFn<KV<String, Iterable<SqlEntity>>, Void> {
static class CompareSqlEntity extends DoFn<KV<String, Iterable<SqlEntity>>, String> {
private final HashMap<String, Counter> totalCounters = new HashMap<>();
private final HashMap<String, Counter> missingCounters = new HashMap<>();
private final HashMap<String, Counter> unequalCounters = new HashMap<>();
private final HashMap<String, Counter> badEntityCounters = new HashMap<>();
private final HashMap<String, Counter> duplicateEntityCounters = new HashMap<>();
private volatile boolean logPrinted = false;
private String getCounterKey(Class<?> clazz) {
return PollMessage.class.isAssignableFrom(clazz) ? "PollMessage" : clazz.getSimpleName();
}
@@ -124,39 +121,36 @@ final class ValidateSqlUtils {
counterKey, Metrics.counter("CompareDB", "Duplicate Entities:" + counterKey));
}
String duplicateEntityLog(String key, ImmutableList<SqlEntity> entities) {
return String.format("%s: %d entities.", key, entities.size());
}
String unmatchedEntityLog(String key, SqlEntity entry) {
// For a PollMessage only found in Datastore, key is not enough to query for it.
return String.format("Missing in one DB:\n%s", entry instanceof PollMessage ? entry : key);
}
/**
* A rudimentary debugging helper that prints the first pair of unequal entities in each worker.
* This will be removed when we start exporting such entities to GCS.
*/
void logDiff(String key, Object entry0, Object entry1) {
if (logPrinted) {
return;
}
logPrinted = true;
String unEqualEntityLog(String key, SqlEntity entry0, SqlEntity entry1) {
Map<String, Object> fields0 = ((ImmutableObject) entry0).toDiffableFieldMap();
Map<String, Object> fields1 = ((ImmutableObject) entry1).toDiffableFieldMap();
StringBuilder sb = new StringBuilder();
fields0.forEach(
(field, value) -> {
if (fields1.containsKey(field)) {
if (!Objects.equals(value, fields1.get(field))) {
sb.append(field + " not match: " + value + " -> " + fields1.get(field) + "\n");
}
} else {
sb.append(field + "Not found in entity 2\n");
}
});
fields1.forEach(
(field, value) -> {
if (!fields0.containsKey(field)) {
sb.append(field + "Not found in entity 1\n");
}
});
logger.atWarning().log(key + " " + sb.toString());
return key + " " + DiffUtils.prettyPrintEntityDeepDiff(fields0, fields1);
}
String badEntitiesLog(String key, SqlEntity entry0, SqlEntity entry1) {
Map<String, Object> fields0 = ((ImmutableObject) entry0).toDiffableFieldMap();
Map<String, Object> fields1 = ((ImmutableObject) entry1).toDiffableFieldMap();
return String.format(
"Failed to parse one or both entities for key %s:\n%s\n",
key, DiffUtils.prettyPrintEntityDeepDiff(fields0, fields1));
}
@ProcessElement
public void processElement(@Element KV<String, Iterable<SqlEntity>> kv) {
public void processElement(
@Element KV<String, Iterable<SqlEntity>> kv, OutputReceiver<String> out) {
ImmutableList<SqlEntity> entities = ImmutableList.copyOf(kv.getValue());
verify(!entities.isEmpty(), "Can't happen: no value for key %s.", kv.getKey());
@@ -169,6 +163,7 @@ final class ValidateSqlUtils {
// Duplicates may happen with Cursors if imported across projects. Its key in Datastore, the
// id field, encodes the project name and is not fixed by the importing job.
duplicateEntityCounters.get(counterKey).inc();
out.output(duplicateEntityLog(kv.getKey(), entities) + "\n");
return;
}
@@ -177,11 +172,7 @@ final class ValidateSqlUtils {
return;
}
missingCounters.get(counterKey).inc();
// Temporary debugging help. See logDiff() above.
if (!logPrinted) {
logPrinted = true;
logger.atWarning().log("Unexpected single entity: %s", kv.getKey());
}
out.output(unmatchedEntityLog(kv.getKey(), entities.get(0)) + "\n");
return;
}
SqlEntity entity0 = entities.get(0);
@@ -198,17 +189,14 @@ final class ValidateSqlUtils {
entity0 = normalizeEntity(entity0);
entity1 = normalizeEntity(entity1);
} catch (Exception e) {
// Temporary debugging help. See logDiff() above.
if (!logPrinted) {
logPrinted = true;
badEntityCounters.get(counterKey).inc();
}
badEntityCounters.get(counterKey).inc();
out.output(badEntitiesLog(kv.getKey(), entity0, entity1));
return;
}
if (!Objects.equals(entity0, entity1)) {
unequalCounters.get(counterKey).inc();
logDiff(kv.getKey(), entities.get(0), entities.get(1));
out.output(unEqualEntityLog(kv.getKey(), entities.get(0), entities.get(1)));
}
}
}

View File

@@ -304,7 +304,7 @@ public class RdeIO {
if (key.mode() == RdeMode.FULL) {
cloudTasksUtils.enqueue(
RDE_UPLOAD_QUEUE,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTask(
RdeUploadAction.PATH,
Service.BACKEND.getServiceId(),
ImmutableMultimap.of(
@@ -315,7 +315,7 @@ public class RdeIO {
} else {
cloudTasksUtils.enqueue(
BRDA_QUEUE,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTask(
BrdaCopyAction.PATH,
Service.BACKEND.getServiceId(),
ImmutableMultimap.of(

View File

@@ -21,6 +21,7 @@ import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.DefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.Clock;
import google.registry.util.CloudTasksUtils;
import google.registry.util.CloudTasksUtils.GcpCloudTasksClient;
import google.registry.util.CloudTasksUtils.SerializableCloudTasksClient;
@@ -46,8 +47,9 @@ public abstract class CloudTasksUtilsModule {
@Config("projectId") String projectId,
@Config("locationId") String locationId,
SerializableCloudTasksClient client,
Retrier retrier) {
return new CloudTasksUtils(retrier, projectId, locationId, client);
Retrier retrier,
Clock clock) {
return new CloudTasksUtils(retrier, clock, projectId, locationId, client);
}
// Provides a supplier instead of using a Dagger @Provider because the latter is not serializable.

View File

@@ -20,7 +20,6 @@ import google.registry.request.Action;
import google.registry.request.Action.Service;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.CloudTasksUtils;
import java.util.Optional;
import javax.inject.Inject;
@@ -35,7 +34,6 @@ public final class CommitLogFanoutAction implements Runnable {
public static final String BUCKET_PARAM = "bucket";
@Inject Clock clock;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject @Parameter("endpoint") String endpoint;
@@ -43,18 +41,15 @@ public final class CommitLogFanoutAction implements Runnable {
@Inject @Parameter("jitterSeconds") Optional<Integer> jitterSeconds;
@Inject CommitLogFanoutAction() {}
@Override
public void run() {
for (int bucketId : CommitLogBucket.getBucketIds()) {
cloudTasksUtils.enqueue(
queue,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTaskWithJitter(
endpoint,
Service.BACKEND.toString(),
ImmutableMultimap.of(BUCKET_PARAM, Integer.toString(bucketId)),
clock,
jitterSeconds));
}
}

View File

@@ -45,7 +45,6 @@ import google.registry.request.ParameterMap;
import google.registry.request.RequestParameters;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.CloudTasksUtils;
import java.util.Optional;
import java.util.stream.Stream;
@@ -98,7 +97,6 @@ public final class TldFanoutAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject Clock clock;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject Response response;
@Inject @Parameter(ENDPOINT_PARAM) String endpoint;
@@ -159,7 +157,7 @@ public final class TldFanoutAction implements Runnable {
params = ArrayListMultimap.create(params);
params.put(RequestParameters.PARAM_TLD, tld);
}
return CloudTasksUtils.createPostTask(
endpoint, Service.BACKEND.toString(), params, clock, jitterSeconds);
return cloudTasksUtils.createPostTaskWithJitter(
endpoint, Service.BACKEND.toString(), params, jitterSeconds);
}
}

View File

@@ -168,15 +168,6 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/sendExpiringCertificateNotificationEmail]]></url>
<description>
This job runs an action that sends emails to partners if their certificates are expiring soon.
</description>
<schedule>every day 04:30</schedule>
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/cron/fanout?queue=export-snapshot&endpoint=/_dr/task/backupDatastore&runInEmpty]]></url>
<description>

View File

@@ -1112,7 +1112,16 @@ public class DomainFlowUtils {
// Only cancel fields which are cancelable
if (cancelableFields.contains(record.getReportField())) {
int cancelledAmount = -1 * record.getReportAmount();
recordsBuilder.add(record.asBuilder().setReportAmount(cancelledAmount).build());
// NB: It's necessary to create a new DomainTransactionRecord from scratch so that we
// don't retain the ID of the previous record to cancel. If we keep the ID, Hibernate
// will remove that record from the DB entirely as the record will be re-parented on
// this DomainHistory being created now.
recordsBuilder.add(
DomainTransactionRecord.create(
record.getTld(),
record.getReportingTime(),
record.getReportField(),
cancelledAmount));
}
}
}

View File

@@ -334,7 +334,8 @@ public class LoadTestAction implements Runnable {
tasks.add(
Task.newBuilder()
.setAppEngineHttpRequest(
CloudTasksUtils.createPostTask(
cloudTasksUtils
.createPostTask(
"/_dr/epptool",
Service.TOOLS.toString(),
ImmutableMultimap.of(

View File

@@ -64,7 +64,7 @@ public class ContactHistory extends HistoryEntry implements SqlEntity, UnsafeSer
// Store ContactBase instead of ContactResource so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@Nullable ContactBase contactBase;
@DoNotCompare @Nullable ContactBase contactBase;
@Id
@Access(AccessType.PROPERTY)

View File

@@ -779,6 +779,10 @@ public class DomainContent extends EppResource
*/
void setContactFields(Set<DesignatedContact> contacts, boolean includeRegistrant) {
// Set the individual contact fields.
billingContact = techContact = adminContact = null;
if (includeRegistrant) {
registrantContact = null;
}
for (DesignatedContact contact : contacts) {
switch (contact.getType()) {
case BILLING:
@@ -1035,17 +1039,20 @@ public class DomainContent extends EppResource
public B setGracePeriods(ImmutableSet<GracePeriod> gracePeriods) {
getInstance().gracePeriods = gracePeriods;
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}
public B addGracePeriod(GracePeriod gracePeriod) {
getInstance().gracePeriods = union(getInstance().getGracePeriods(), gracePeriod);
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}
public B removeGracePeriod(GracePeriod gracePeriod) {
getInstance().gracePeriods =
CollectionUtils.difference(getInstance().getGracePeriods(), gracePeriod);
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}

View File

@@ -86,7 +86,7 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
// Store DomainContent instead of DomainBase so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@Nullable DomainContent domainContent;
@DoNotCompare @Nullable DomainContent domainContent;
@Id
@Access(AccessType.PROPERTY)
@@ -105,6 +105,7 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
// We could have reused domainContent.nsHosts here, but Hibernate throws a weird exception after
// we change to use a composite primary key.
// TODO(b/166776754): Investigate if we can reuse domainContent.nsHosts for storing host keys.
@DoNotCompare
@ElementCollection
@JoinTable(
name = "DomainHistoryHost",
@@ -118,6 +119,7 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
@Column(name = "host_repo_id")
Set<VKey<HostResource>> nsHosts;
@DoNotCompare
@OneToMany(
cascade = {CascadeType.ALL},
fetch = FetchType.EAGER,
@@ -137,6 +139,7 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
// HashSet rather than ImmutableSet so that Hibernate can fill them out lazily on request
Set<DomainDsDataHistory> dsDataHistories = new HashSet<>();
@DoNotCompare
@OneToMany(
cascade = {CascadeType.ALL},
fetch = FetchType.EAGER,

View File

@@ -66,7 +66,7 @@ public class HostHistory extends HistoryEntry implements SqlEntity, UnsafeSerial
// Store HostBase instead of HostResource so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@Nullable HostBase hostBase;
@DoNotCompare @Nullable HostBase hostBase;
@Id
@Access(AccessType.PROPERTY)

View File

@@ -41,7 +41,7 @@ import org.joda.time.DateTime;
/** Wrapper for {@link Supplier} that associates a time with each attempt. */
@DeleteAfterMigration
class CommitLoggedWork<R> implements Runnable {
public class CommitLoggedWork<R> implements Runnable {
private final Supplier<R> work;
private final Clock clock;

View File

@@ -46,6 +46,7 @@ import static google.registry.util.X509Utils.loadCertificate;
import static java.util.Comparator.comparing;
import static java.util.function.Predicate.isEqual;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
@@ -991,6 +992,16 @@ public class Registrar extends ImmutableObject
return this;
}
/**
* This lets tests set the update timestamp in cases where setting fields resets the timestamp
* and breaks the verification that an object has not been updated since it was copied.
*/
@VisibleForTesting
public Builder setLastUpdateTime(DateTime timestamp) {
getInstance().lastUpdateTime = UpdateAutoTimestamp.create(timestamp);
return this;
}
/** Build the registrar, nullifying empty fields. */
@Override
public Registrar build() {

View File

@@ -324,7 +324,7 @@ public class HistoryEntry extends ImmutableObject
// Note: how we wish to treat this Hibernate setter depends on the current state of the object
// and what's passed in. The key principle is that we wish to maintain the link between parent
// and child objects, meaning that we should keep around whichever of the two sets (the
// parameter vs the class variable and clear/populate that as appropriate.
// parameter vs the class variable) and clear/populate that as appropriate.
//
// If the class variable is a PersistentSet and we overwrite it here, Hibernate will throw
// an exception "A collection with cascade=”all-delete-orphan” was no longer referenced by the
@@ -539,7 +539,7 @@ public class HistoryEntry extends ImmutableObject
public B setDomainTransactionRecords(
ImmutableSet<DomainTransactionRecord> domainTransactionRecords) {
getInstance().domainTransactionRecords = domainTransactionRecords;
getInstance().setDomainTransactionRecords(domainTransactionRecords);
return thisCastToDerived();
}
}

View File

@@ -46,6 +46,7 @@ import javax.inject.Singleton;
@Component(
modules = {
AuthModule.class,
CloudTasksUtilsModule.class,
ConfigModule.class,
ConsoleConfigModule.class,
CredentialModule.class,

View File

@@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.model.ImmutableObject;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection;
@@ -604,6 +605,13 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
managedEntity = getEntityManager().merge(entity);
}
getEntityManager().remove(managedEntity);
// We check shouldReplicate() in TransactionInfo.addDelete(), but we have to check it here as
// well prior to attempting to create a datastore key because a non-replicated entity may not
// have one.
if (shouldReplicate(entity.getClass())) {
transactionInfo.get().addDelete(VKey.from(Key.create(entity)));
}
return managedEntity;
}
@@ -827,6 +835,12 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
replaySqlToDatastoreOverrideForTest.set(Optional.empty());
}
/** Returns true if the entity class should be replicated from SQL to datastore. */
private static boolean shouldReplicate(Class<?> entityClass) {
return !NonReplicatedEntity.class.isAssignableFrom(entityClass)
&& !SqlOnlyEntity.class.isAssignableFrom(entityClass);
}
private static class TransactionInfo {
ReadOnlyCheckingEntityManager entityManager;
boolean inTransaction = false;
@@ -883,12 +897,6 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
/** Returns true if the entity class should be replicated from SQL to datastore. */
private boolean shouldReplicate(Class<?> entityClass) {
return !NonReplicatedEntity.class.isAssignableFrom(entityClass)
&& !SqlOnlyEntity.class.isAssignableFrom(entityClass);
}
private void recordTransaction() {
if (contentsBuilder != null) {
Transaction persistedTxn = contentsBuilder.build();

View File

@@ -233,14 +233,14 @@ public final class RdeStagingReducer extends Reducer<PendingDeposit, DepositFrag
if (mode == RdeMode.FULL) {
cloudTasksUtils.enqueue(
"rde-upload",
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTask(
RdeUploadAction.PATH,
Service.BACKEND.toString(),
ImmutableMultimap.of(RequestParameters.PARAM_TLD, tld)));
} else {
cloudTasksUtils.enqueue(
"brda",
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTask(
BrdaCopyAction.PATH,
Service.BACKEND.toString(),
ImmutableMultimap.of(

View File

@@ -134,7 +134,7 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
}
cloudTasksUtils.enqueue(
RDE_REPORT_QUEUE,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTask(
RdeReportAction.PATH, Service.BACKEND.getServiceId(), params));
}

View File

@@ -151,7 +151,7 @@ public class GenerateInvoicesAction implements Runnable {
if (shouldPublish) {
cloudTasksUtils.enqueue(
ReportingModule.BEAM_QUEUE,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTaskWithDelay(
PublishInvoicesAction.PATH,
Service.BACKEND.toString(),
ImmutableMultimap.of(
@@ -159,7 +159,6 @@ public class GenerateInvoicesAction implements Runnable {
jobId,
ReportingModule.PARAM_YEAR_MONTH,
yearMonth.toString()),
clock,
Duration.standardMinutes(ReportingModule.ENQUEUE_DELAY_MINUTES)));
}
response.setStatus(SC_OK);

View File

@@ -125,7 +125,7 @@ public class PublishInvoicesAction implements Runnable {
private void enqueueCopyDetailReportsTask() {
cloudTasksUtils.enqueue(
BillingModule.CRON_QUEUE,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTask(
CopyDetailReportsAction.PATH,
Service.BACKEND.toString(),
ImmutableMultimap.of(PARAM_YEAR_MONTH, yearMonth.toString())));

View File

@@ -34,7 +34,6 @@ import google.registry.request.Action.Service;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.CloudTasksUtils;
import google.registry.util.EmailMessage;
import google.registry.util.Retrier;
@@ -86,7 +85,6 @@ public final class IcannReportingStagingAction implements Runnable {
@Inject @Config("gSuiteOutgoingEmailAddress") InternetAddress sender;
@Inject @Config("alertRecipientEmailAddress") InternetAddress recipient;
@Inject SendEmailService emailService;
@Inject Clock clock;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject IcannReportingStagingAction() {}
@@ -123,11 +121,10 @@ public final class IcannReportingStagingAction implements Runnable {
logger.atInfo().log("Enqueueing report upload.");
cloudTasksUtils.enqueue(
CRON_QUEUE,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTaskWithDelay(
IcannReportingUploadAction.PATH,
Service.BACKEND.toString(),
null,
clock,
Duration.standardMinutes(2)));
return null;
},

View File

@@ -144,7 +144,7 @@ public class GenerateSpec11ReportAction implements Runnable {
if (sendEmail) {
cloudTasksUtils.enqueue(
ReportingModule.BEAM_QUEUE,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTaskWithDelay(
PublishSpec11ReportAction.PATH,
Service.BACKEND.toString(),
ImmutableMultimap.of(
@@ -152,7 +152,6 @@ public class GenerateSpec11ReportAction implements Runnable {
jobId,
ReportingModule.PARAM_DATE,
date.toString()),
clock,
Duration.standardMinutes(ReportingModule.ENQUEUE_DELAY_MINUTES)));
}
response.setStatus(SC_OK);

View File

@@ -15,14 +15,16 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import google.registry.batch.AsyncTaskEnqueuer;
import google.registry.batch.RelockDomainAction;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
@@ -32,6 +34,8 @@ import google.registry.model.domain.RegistryLock;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry;
import google.registry.model.tld.RegistryLockDao;
import google.registry.request.Action.Service;
import google.registry.util.CloudTasksUtils;
import google.registry.util.StringGenerator;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -53,16 +57,16 @@ public final class DomainLockUtils {
private final StringGenerator stringGenerator;
private final String registryAdminRegistrarId;
private final AsyncTaskEnqueuer asyncTaskEnqueuer;
private CloudTasksUtils cloudTasksUtils;
@Inject
public DomainLockUtils(
@Named("base58StringGenerator") StringGenerator stringGenerator,
@Config("registryAdminClientId") String registryAdminRegistrarId,
AsyncTaskEnqueuer asyncTaskEnqueuer) {
CloudTasksUtils cloudTasksUtils) {
this.stringGenerator = stringGenerator;
this.registryAdminRegistrarId = registryAdminRegistrarId;
this.asyncTaskEnqueuer = asyncTaskEnqueuer;
this.cloudTasksUtils = cloudTasksUtils;
}
/**
@@ -203,10 +207,38 @@ public final class DomainLockUtils {
private void submitRelockIfNecessary(RegistryLock lock) {
if (lock.getRelockDuration().isPresent()) {
asyncTaskEnqueuer.enqueueDomainRelock(lock);
enqueueDomainRelock(lock);
}
}
/**
* Enqueues a task to asynchronously re-lock a registry-locked domain after it was unlocked.
*
* <p>Note: the relockDuration must be present on the lock object.
*/
public void enqueueDomainRelock(RegistryLock lock) {
checkArgument(
lock.getRelockDuration().isPresent(),
"Lock with ID %s not configured for relock",
lock.getRevisionId());
enqueueDomainRelock(lock.getRelockDuration().get(), lock.getRevisionId(), 0);
}
/** Enqueues a task to asynchronously re-lock a registry-locked domain after it was unlocked. */
public void enqueueDomainRelock(Duration countdown, long lockRevisionId, int previousAttempts) {
cloudTasksUtils.enqueue(
QUEUE_ASYNC_ACTIONS,
cloudTasksUtils.createPostTaskWithDelay(
RelockDomainAction.PATH,
Service.BACKEND.toString(),
ImmutableMultimap.of(
RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM,
String.valueOf(lockRevisionId),
RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM,
String.valueOf(previousAttempts)),
countdown));
}
private void setAsRelock(RegistryLock newLock) {
jpaTm()
.transact(

View File

@@ -133,7 +133,7 @@ final class GenerateEscrowDepositCommand implements CommandWithRemoteApi {
}
cloudTasksUtils.enqueue(
RDE_REPORT_QUEUE,
CloudTasksUtils.createPostTask(
cloudTasksUtils.createPostTask(
RdeStagingAction.PATH, Service.BACKEND.toString(), paramsBuilder.build()));
}

View File

@@ -123,9 +123,10 @@ public final class RegistryTool {
.put("update_server_locks", UpdateServerLocksCommand.class)
.put("update_tld", UpdateTldCommand.class)
.put("upload_claims_list", UploadClaimsListCommand.class)
.put("validate_datastore_with_sql", ValidateDatastoreWithSqlCommand.class)
.put("validate_datastore", ValidateDatastoreCommand.class)
.put("validate_escrow_deposit", ValidateEscrowDepositCommand.class)
.put("validate_login_credentials", ValidateLoginCredentialsCommand.class)
.put("validate_sql", ValidateSqlCommand.class)
.put("verify_ote", VerifyOteCommand.class)
.put("whois_query", WhoisQueryCommand.class)
.build();

View File

@@ -171,12 +171,14 @@ interface RegistryToolComponent {
void inject(UpdateTldCommand command);
void inject(ValidateDatastoreWithSqlCommand command);
void inject(ValidateDatastoreCommand command);
void inject(ValidateEscrowDepositCommand command);
void inject(ValidateLoginCredentialsCommand command);
void inject(ValidateSqlCommand command);
void inject(WhoisQueryCommand command);
AppEngineConnection appEngineConnection();

View File

@@ -0,0 +1,232 @@
// Copyright 2022 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.tools;
import static google.registry.beam.BeamUtils.createJobName;
import com.beust.jcommander.Parameter;
import com.google.api.services.dataflow.Dataflow;
import com.google.api.services.dataflow.model.Job;
import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter;
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.beam.common.DatabaseSnapshot;
import google.registry.config.RegistryConfig.Config;
import google.registry.tools.params.DateTimeParameter;
import google.registry.util.Clock;
import google.registry.util.RequestStatusChecker;
import google.registry.util.Sleeper;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/** Shared setup for commands that validate the data replication between Datastore and Cloud SQL. */
abstract class ValidateDatabaseMigrationCommand
implements CommandWithConnection, CommandWithRemoteApi {
private static final String PIPELINE_NAME = "validate_database_pipeline";
private static final String MANUAL_PIPELINE_LAUNCH_COMMAND_TEMPLATE =
"gcloud dataflow flex-template run "
+ "\"%s-${USER}-$(date +%%Y%%m%%dt%%H%%M%%S)\" "
+ "--template-file-gcs-location %s "
+ "--project %s "
+ "--region=%s "
+ "--worker-machine-type=n2-standard-8 --num-workers=8 "
+ "--parameters registryEnvironment=%s "
+ "--parameters sqlSnapshotId=%s "
+ "--parameters latestCommitLogTimestamp=%s "
+ "--parameters diffOutputGcsBucket=%s ";
// States indicating a job is not finished yet.
static final ImmutableSet<String> DATAFLOW_JOB_RUNNING_STATES =
ImmutableSet.of(
"JOB_STATE_UNKNOWN",
"JOB_STATE_RUNNING",
"JOB_STATE_STOPPED",
"JOB_STATE_PENDING",
"JOB_STATE_QUEUED");
static final Duration JOB_POLLING_INTERVAL = Duration.standardSeconds(60);
@Parameter(
names = {"-m", "--manual"},
description =
"If true, let user launch the comparison pipeline manually out of band. "
+ "Command will wait for user key-press to exit after syncing Datastore.")
boolean manualLaunchPipeline;
@Parameter(
names = {"-r", "--release"},
description = "The release tag of the BEAM pipeline to run. It defaults to 'live'.")
String release = "live";
@Parameter(
names = {"-c", "--comparisonStartTimestamp"},
description =
"When comparing History and Epp Resource entities, ignore those that have not"
+ " changed since this time.",
converter = DateTimeParameter.class)
DateTime comparisonStartTimestamp;
@Parameter(
names = {"-o", "--outputBucket"},
description =
"The GCS bucket where data discrepancies are logged. "
+ "It defaults to ${projectId}-beam")
String outputBucket;
@Inject Clock clock;
@Inject Dataflow dataflow;
@Inject
@Config("defaultJobRegion")
String jobRegion;
@Inject
@Config("beamStagingBucketUrl")
String stagingBucketUrl;
@Inject
@Config("projectId")
String projectId;
@Inject Sleeper sleeper;
AppEngineConnection connection;
@Override
public void setConnection(AppEngineConnection connection) {
this.connection = connection;
}
String getDataflowJobStatus(String jobId) {
try {
return dataflow
.projects()
.locations()
.jobs()
.get(projectId, jobRegion, jobId)
.execute()
.getCurrentState();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
void launchPipelineAndWaitUntilFinish(
String pipelineName, DatabaseSnapshot snapshot, String latestCommitTimestamp) {
Job pipelineJob =
launchComparisonPipeline(pipelineName, snapshot.getSnapshotId(), latestCommitTimestamp)
.getJob();
String jobId = pipelineJob.getId();
System.out.printf("Launched comparison pipeline %s (%s).\n", pipelineJob.getName(), jobId);
while (DATAFLOW_JOB_RUNNING_STATES.contains(getDataflowJobStatus(jobId))) {
sleeper.sleepInterruptibly(JOB_POLLING_INTERVAL);
}
System.out.printf(
"Pipeline ended with %s state. Please check counters for results.\n",
getDataflowJobStatus(jobId));
}
String getOutputBucket() {
return Optional.ofNullable(outputBucket).orElse(projectId + "-beam");
}
String getContainerSpecGcsPath() {
return String.format(
"%s/%s_metadata.json", stagingBucketUrl.replace("live", release), PIPELINE_NAME);
}
String getManualLaunchCommand(
String jobName, String snapshotId, String latestCommitLogTimestamp) {
String baseCommand =
String.format(
MANUAL_PIPELINE_LAUNCH_COMMAND_TEMPLATE,
jobName,
getContainerSpecGcsPath(),
projectId,
jobRegion,
RegistryToolEnvironment.get().name(),
snapshotId,
latestCommitLogTimestamp,
getOutputBucket());
if (comparisonStartTimestamp == null) {
return baseCommand;
}
return baseCommand + "--parameters comparisonStartTimestamp=" + comparisonStartTimestamp;
}
LaunchFlexTemplateResponse launchComparisonPipeline(
String jobName, String sqlSnapshotId, String latestCommitLogTimestamp) {
try {
// Hardcode machine type and initial workers to force a quick start.
ImmutableMap.Builder<String, String> paramsBuilder =
new ImmutableMap.Builder()
.put("workerMachineType", "n2-standard-8")
.put("numWorkers", "8")
.put("sqlSnapshotId", sqlSnapshotId)
.put("latestCommitLogTimestamp", latestCommitLogTimestamp)
.put("registryEnvironment", RegistryToolEnvironment.get().name())
.put("diffOutputGcsBucket", getOutputBucket());
if (comparisonStartTimestamp != null) {
paramsBuilder.put("comparisonStartTimestamp", comparisonStartTimestamp.toString());
}
LaunchFlexTemplateParameter parameter =
new LaunchFlexTemplateParameter()
.setJobName(createJobName(Ascii.toLowerCase(jobName).replace('_', '-'), clock))
.setContainerSpecGcsPath(getContainerSpecGcsPath())
.setParameters(paramsBuilder.build());
return dataflow
.projects()
.locations()
.flexTemplates()
.launch(
projectId, jobRegion, new LaunchFlexTemplateRequest().setLaunchParameter(parameter))
.execute();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* A fake implementation of {@link RequestStatusChecker} for managing SQL-backed locks from
* non-AppEngine platforms. This is only required until the Nomulus server is migrated off
* AppEngine.
*/
static class FakeRequestStatusChecker implements RequestStatusChecker {
private final String logId =
ValidateDatastoreCommand.class.getSimpleName() + "-" + UUID.randomUUID();
@Override
public String getLogId() {
return logId;
}
@Override
public boolean isRunning(String requestLogId) {
return false;
}
}
}

View File

@@ -0,0 +1,105 @@
// Copyright 2022 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.tools;
import static google.registry.model.replay.ReplicateToDatastoreAction.REPLICATE_TO_DATASTORE_LOCK_LEASE_LENGTH;
import static google.registry.model.replay.ReplicateToDatastoreAction.REPLICATE_TO_DATASTORE_LOCK_NAME;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.MediaType;
import google.registry.backup.SyncDatastoreToSqlSnapshotAction;
import google.registry.beam.common.DatabaseSnapshot;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection;
import google.registry.model.server.Lock;
import google.registry.request.Action.Service;
import java.util.Optional;
/**
* Validates asynchronously replicated data from the primary Cloud SQL database to Datastore.
*
* <p>This command suspends the replication process (by acquiring the replication lock), take a
* snapshot of the Cloud SQL database, invokes a Nomulus server action to sync Datastore to this
* snapshot (See {@link SyncDatastoreToSqlSnapshotAction} for details), and finally launches a BEAM
* pipeline to compare Datastore with the given SQL snapshot.
*
* <p>This command does not lock up the SQL database. Normal processing can proceed.
*/
@Parameters(commandDescription = "Validates Datastore with the primary Cloud SQL database.")
public class ValidateDatastoreCommand extends ValidateDatabaseMigrationCommand {
private static final Service NOMULUS_SERVICE = Service.BACKEND;
@Override
public void run() throws Exception {
MigrationState state = DatabaseMigrationStateSchedule.getValueAtTime(clock.nowUtc());
if (!state.getReplayDirection().equals(ReplayDirection.SQL_TO_DATASTORE)) {
throw new IllegalStateException("Cannot validate Datastore in migration step " + state);
}
Optional<Lock> lock =
Lock.acquireSql(
REPLICATE_TO_DATASTORE_LOCK_NAME,
null,
REPLICATE_TO_DATASTORE_LOCK_LEASE_LENGTH,
new FakeRequestStatusChecker(),
false);
if (!lock.isPresent()) {
throw new IllegalStateException("Cannot acquire the async propagation lock.");
}
try {
try (DatabaseSnapshot snapshot = DatabaseSnapshot.createSnapshot()) {
System.out.printf("Obtained snapshot %s\n", snapshot.getSnapshotId());
AppEngineConnection connectionToService = connection.withService(NOMULUS_SERVICE);
String response =
connectionToService.sendPostRequest(
getNomulusEndpoint(snapshot.getSnapshotId()),
ImmutableMap.<String, String>of(),
MediaType.PLAIN_TEXT_UTF_8,
"".getBytes(UTF_8));
System.out.println(response);
lock.ifPresent(Lock::releaseSql);
lock = Optional.empty();
// See SyncDatastoreToSqlSnapshotAction for response format.
String latestCommitTimestamp =
response.substring(response.lastIndexOf('(') + 1, response.lastIndexOf(')'));
if (manualLaunchPipeline) {
System.out.printf(
"To launch the pipeline manually, use the following command:\n%s\n",
getManualLaunchCommand(
"validate-datastore", snapshot.getSnapshotId(), latestCommitTimestamp));
System.out.print("\nEnter any key to continue when the pipeline ends:");
System.in.read();
} else {
launchPipelineAndWaitUntilFinish("validate-datastore", snapshot, latestCommitTimestamp);
}
}
} finally {
lock.ifPresent(Lock::releaseSql);
}
}
private static String getNomulusEndpoint(String sqlSnapshotId) {
return String.format(
"%s?sqlSnapshotId=%s", SyncDatastoreToSqlSnapshotAction.PATH, sqlSnapshotId);
}
}

View File

@@ -1,229 +0,0 @@
// Copyright 2022 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.tools;
import static google.registry.beam.BeamUtils.createJobName;
import static google.registry.model.replay.ReplicateToDatastoreAction.REPLICATE_TO_DATASTORE_LOCK_NAME;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.api.services.dataflow.Dataflow;
import com.google.api.services.dataflow.model.Job;
import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter;
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.MediaType;
import google.registry.backup.SyncDatastoreToSqlSnapshotAction;
import google.registry.beam.common.DatabaseSnapshot;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection;
import google.registry.model.replay.ReplicateToDatastoreAction;
import google.registry.model.server.Lock;
import google.registry.request.Action.Service;
import google.registry.util.Clock;
import google.registry.util.RequestStatusChecker;
import google.registry.util.Sleeper;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import org.joda.time.Duration;
/**
* Validates asynchronously replicated data from the primary Cloud SQL database to Datastore.
*
* <p>This command suspends the replication process (by acquiring the replication lock), take a
* snapshot of the Cloud SQL database, invokes a Nomulus server action to sync Datastore to this
* snapshot (See {@link SyncDatastoreToSqlSnapshotAction} for details), and finally launches a BEAM
* pipeline to compare Datastore with the given SQL snapshot.
*
* <p>This command does not lock up the SQL database. Normal processing can proceed.
*/
@Parameters(commandDescription = "Validates Datastore with Cloud SQL.")
public class ValidateDatastoreWithSqlCommand
implements CommandWithConnection, CommandWithRemoteApi {
private static final Service NOMULUS_SERVICE = Service.BACKEND;
private static final String PIPELINE_NAME = "validate_datastore_pipeline";
// States indicating a job is not finished yet.
private static final ImmutableSet<String> DATAFLOW_JOB_RUNNING_STATES =
ImmutableSet.of(
"JOB_STATE_RUNNING", "JOB_STATE_STOPPED", "JOB_STATE_PENDING", "JOB_STATE_QUEUED");
private static final Duration JOB_POLLING_INTERVAL = Duration.standardSeconds(60);
@Parameter(
names = {"-m", "--manual"},
description =
"If true, let user launch the comparison pipeline manually out of band. "
+ "Command will wait for user key-press to exit after syncing Datastore.")
boolean manualLaunchPipeline;
@Inject Clock clock;
@Inject Dataflow dataflow;
@Inject
@Config("defaultJobRegion")
String jobRegion;
@Inject
@Config("beamStagingBucketUrl")
String stagingBucketUrl;
@Inject
@Config("projectId")
String projectId;
@Inject Sleeper sleeper;
private AppEngineConnection connection;
@Override
public void setConnection(AppEngineConnection connection) {
this.connection = connection;
}
@Override
public void run() throws Exception {
MigrationState state = DatabaseMigrationStateSchedule.getValueAtTime(clock.nowUtc());
if (!state.getReplayDirection().equals(ReplayDirection.SQL_TO_DATASTORE)) {
throw new IllegalStateException("Cannot sync Datastore to SQL in migration step " + state);
}
Optional<Lock> lock =
Lock.acquireSql(
REPLICATE_TO_DATASTORE_LOCK_NAME,
null,
ReplicateToDatastoreAction.REPLICATE_TO_DATASTORE_LOCK_LEASE_LENGTH,
new FakeRequestStatusChecker(),
false);
if (!lock.isPresent()) {
throw new IllegalStateException("Cannot acquire the async propagation lock.");
}
try {
try (DatabaseSnapshot snapshot = DatabaseSnapshot.createSnapshot()) {
System.out.printf("Obtained snapshot %s\n", snapshot.getSnapshotId());
AppEngineConnection connectionToService = connection.withService(NOMULUS_SERVICE);
String response =
connectionToService.sendPostRequest(
getNomulusEndpoint(snapshot.getSnapshotId()),
ImmutableMap.<String, String>of(),
MediaType.PLAIN_TEXT_UTF_8,
"".getBytes(UTF_8));
System.out.println(response);
lock.ifPresent(Lock::releaseSql);
lock = Optional.empty();
// See SyncDatastoreToSqlSnapshotAction for response format.
String latestCommitTimestamp =
response.substring(response.lastIndexOf('(') + 1, response.lastIndexOf(')'));
if (manualLaunchPipeline) {
System.out.print("\nEnter any key to continue when the pipeline ends:");
System.in.read();
} else {
Job pipelineJob =
launchComparisonPipeline(snapshot.getSnapshotId(), latestCommitTimestamp).getJob();
String jobId = pipelineJob.getId();
System.out.printf(
"Launched comparison pipeline %s (%s).\n", pipelineJob.getName(), jobId);
while (DATAFLOW_JOB_RUNNING_STATES.contains(getDataflowJobStatus(jobId))) {
sleeper.sleepInterruptibly(JOB_POLLING_INTERVAL);
}
System.out.printf(
"Pipeline ended with %s state. Please check counters for results.\n",
getDataflowJobStatus(jobId));
}
}
} finally {
lock.ifPresent(Lock::releaseSql);
}
}
private static String getNomulusEndpoint(String sqlSnapshotId) {
return String.format(
"%s?sqlSnapshotId=%s", SyncDatastoreToSqlSnapshotAction.PATH, sqlSnapshotId);
}
private LaunchFlexTemplateResponse launchComparisonPipeline(
String sqlSnapshotId, String latestCommitLogTimestamp) {
try {
LaunchFlexTemplateParameter parameter =
new LaunchFlexTemplateParameter()
.setJobName(createJobName("validate-datastore", clock))
.setContainerSpecGcsPath(
String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME))
.setParameters(
ImmutableMap.of(
"sqlSnapshotId",
sqlSnapshotId,
"latestCommitLogTimestamp",
latestCommitLogTimestamp,
"registryEnvironment",
RegistryToolEnvironment.get().name()));
return dataflow
.projects()
.locations()
.flexTemplates()
.launch(
projectId, jobRegion, new LaunchFlexTemplateRequest().setLaunchParameter(parameter))
.execute();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String getDataflowJobStatus(String jobId) {
try {
return dataflow
.projects()
.locations()
.jobs()
.get(projectId, jobRegion, jobId)
.execute()
.getCurrentState();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* A fake implementation of {@link RequestStatusChecker} for managing SQL-backed locks from
* non-AppEngine platforms. This is only required until the Nomulus server is migrated off
* AppEngine.
*/
static class FakeRequestStatusChecker implements RequestStatusChecker {
@Override
public String getLogId() {
return ValidateDatastoreWithSqlCommand.class.getSimpleName() + "-" + UUID.randomUUID();
}
@Override
public boolean isRunning(String requestLogId) {
return false;
}
}
}

View File

@@ -0,0 +1,91 @@
// Copyright 2022 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.tools;
import static google.registry.backup.ReplayCommitLogsToSqlAction.REPLAY_TO_SQL_LOCK_LEASE_LENGTH;
import static google.registry.backup.ReplayCommitLogsToSqlAction.REPLAY_TO_SQL_LOCK_NAME;
import com.beust.jcommander.Parameters;
import google.registry.beam.common.DatabaseSnapshot;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection;
import google.registry.model.replay.SqlReplayCheckpoint;
import google.registry.model.server.Lock;
import google.registry.persistence.transaction.TransactionManagerFactory;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* Validates asynchronously replicated data from the primary Datastore to Cloud SQL.
*
* <p>This command suspends the replication process (by acquiring the replication lock), take a
* snapshot of the Cloud SQL database, finds the corresponding Datastore snapshot, and finally
* launches a BEAM pipeline to compare the two snapshots.
*
* <p>This command does not lock up either database. Normal processing can proceed.
*/
@Parameters(commandDescription = "Validates Cloud SQL with the primary Datastore.")
public class ValidateSqlCommand extends ValidateDatabaseMigrationCommand {
@Override
public void run() throws Exception {
MigrationState state = DatabaseMigrationStateSchedule.getValueAtTime(clock.nowUtc());
if (!state.getReplayDirection().equals(ReplayDirection.DATASTORE_TO_SQL)) {
throw new IllegalStateException("Cannot validate SQL in migration step " + state);
}
Optional<Lock> lock =
Lock.acquireSql(
REPLAY_TO_SQL_LOCK_NAME,
null,
REPLAY_TO_SQL_LOCK_LEASE_LENGTH,
new FakeRequestStatusChecker(),
false);
if (!lock.isPresent()) {
throw new IllegalStateException("Cannot acquire the async propagation lock.");
}
try {
DateTime latestCommitLogTime =
TransactionManagerFactory.jpaTm().transact(() -> SqlReplayCheckpoint.get());
try (DatabaseSnapshot databaseSnapshot = DatabaseSnapshot.createSnapshot()) {
// Eagerly release the commitlog replay lock so that replay can resume.
lock.ifPresent(Lock::releaseSql);
lock = Optional.empty();
System.out.printf(
"Start comparison with SQL snapshot (%s) and CommitLog timestamp (%s).\n",
databaseSnapshot.getSnapshotId(), latestCommitLogTime);
if (manualLaunchPipeline) {
System.out.printf(
"To launch the pipeline manually, use the following command:\n%s\n",
getManualLaunchCommand(
"validate-sql",
databaseSnapshot.getSnapshotId(),
latestCommitLogTime.toString()));
System.out.print("\nEnter any key to continue when the pipeline ends:");
System.in.read();
} else {
launchPipelineAndWaitUntilFinish(
"validate-sql", databaseSnapshot, latestCommitLogTime.toString());
}
}
} finally {
lock.ifPresent(Lock::releaseSql);
}
}
}

View File

@@ -453,6 +453,13 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
Map<?, ?> diffs =
DiffUtils.deepDiff(
originalRegistrar.toDiffableFieldMap(), updatedRegistrar.toDiffableFieldMap(), true);
// It's expected that the update timestamp will be changed, as it gets reset whenever we change
// nested collections. If it's the only change, just return the original registrar.
if (diffs.keySet().equals(ImmutableSet.of("lastUpdateTime"))) {
return originalRegistrar;
}
throw new ForbiddenException(
String.format("Unauthorized: only %s can change fields %s", allowedRole, diffs.keySet()));
}
@@ -641,7 +648,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
// there's an update besides the lastUpdateTime
cloudTasksUtils.enqueue(
SyncRegistrarsSheetAction.QUEUE,
CloudTasksUtils.createGetTask(
cloudTasksUtils.createGetTask(
SyncRegistrarsSheetAction.PATH, Service.BACKEND.toString(), ImmutableMultimap.of()));
}
String environment = Ascii.toLowerCase(String.valueOf(RegistryEnvironment.get()));

View File

@@ -1,6 +1,6 @@
{
"name": "Validate Datastore with Cloud SQL",
"description": "An Apache Beam batch pipeline that compares Datastore with the primary Cloud SQL database.",
"name": "Validate Datastore and Cloud SQL",
"description": "An Apache Beam batch pipeline that compares the data in Datastore and Cloud SQL database.",
"parameters": [
{
"name": "registryEnvironment",
@@ -24,7 +24,7 @@
"name": "sqlSnapshotId",
"label": "The ID of an exported Cloud SQL (Postgresql) snapshot.",
"helpText": "The ID of an exported Cloud SQL (Postgresql) snapshot.",
"is_optional": true
"is_optional": false
},
{
"name": "latestCommitLogTimestamp",
@@ -37,6 +37,12 @@
"label": "Only entities updated at or after this time are included for validation.",
"helpText": "The earliest entity update time allowed for inclusion in validation, in ISO8601 format.",
"is_optional": true
},
{
"name": "diffOutputGcsBucket",
"label": "The GCS bucket where data discrepancies should be output to.",
"helpText": "The GCS bucket where data discrepancies should be output to.",
"is_optional": true
}
]
}

View File

@@ -1,21 +0,0 @@
{
"name": "Validate Cloud SQL with Datastore being primary",
"description": "An Apache Beam batch pipeline that compares Cloud SQL with the primary Datastore.",
"parameters": [
{
"name": "registryEnvironment",
"label": "The Registry environment.",
"helpText": "The Registry environment.",
"is_optional": false,
"regexes": [
"^PRODUCTION|SANDBOX|CRASH|QA|ALPHA$"
]
},
{
"name": "comparisonStartTimestamp",
"label": "Only entities updated at or after this time are included for validation.",
"helpText": "The earliest entity update time allowed for inclusion in validation, in ISO8601 format.",
"is_optional": true
}
]
}

View File

@@ -15,7 +15,6 @@
package google.registry.batch;
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESAVE_TIMES;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY;
@@ -23,19 +22,16 @@ import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
import static google.registry.testing.TestLogHandlerUtils.assertLogMessage;
import static org.joda.time.Duration.standardDays;
import static org.joda.time.Duration.standardHours;
import static org.joda.time.Duration.standardSeconds;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableSortedSet;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.RegistryLock;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeSleeper;
@@ -142,59 +138,4 @@ public class AsyncTaskEnqueuerTest {
assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
assertLogMessage(logHandler, Level.INFO, "Ignoring async re-save");
}
@Test
void testEnqueueRelock() {
RegistryLock lock =
saveRegistryLock(
new RegistryLock.Builder()
.setLockCompletionTime(clock.nowUtc())
.setUnlockRequestTime(clock.nowUtc())
.setUnlockCompletionTime(clock.nowUtc())
.isSuperuser(false)
.setDomainName("example.tld")
.setRepoId("repoId")
.setRelockDuration(standardHours(6))
.setRegistrarId("TheRegistrar")
.setRegistrarPocId("someone@example.com")
.setVerificationCode("hi")
.build());
asyncTaskEnqueuer.enqueueDomainRelock(lock.getRelockDuration().get(), lock.getRevisionId(), 0);
assertTasksEnqueued(
QUEUE_ASYNC_ACTIONS,
new TaskMatcher()
.url(RelockDomainAction.PATH)
.method("POST")
.header("Host", "backend.hostname.fake")
.param(
RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM,
String.valueOf(lock.getRevisionId()))
.param(RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM, "0")
.etaDelta(
standardHours(6).minus(standardSeconds(30)),
standardHours(6).plus(standardSeconds(30))));
}
@MockitoSettings(strictness = Strictness.LENIENT)
@Test
void testFailure_enqueueRelock_noDuration() {
RegistryLock lockWithoutDuration =
saveRegistryLock(
new RegistryLock.Builder()
.isSuperuser(false)
.setDomainName("example.tld")
.setRepoId("repoId")
.setRegistrarId("TheRegistrar")
.setRegistrarPocId("someone@example.com")
.setVerificationCode("hi")
.build());
assertThat(
assertThrows(
IllegalArgumentException.class,
() -> asyncTaskEnqueuer.enqueueDomainRelock(lockWithoutDuration)))
.hasMessageThat()
.isEqualTo(
String.format(
"Lock with ID %s not configured for relock", lockWithoutDuration.getRevisionId()));
}
}

View File

@@ -28,27 +28,26 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.getMostRecentVerifiedRegistryLockByRepoId;
import static google.registry.testing.SqlHelper.getRegistryLockByVerificationCode;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.joda.time.Duration.standardSeconds;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableSet;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.RegistryLock;
import google.registry.model.host.HostResource;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.TaskQueueHelper.TaskMatcher;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.UserInfo;
import google.registry.tools.DomainLockUtils;
@@ -78,12 +77,12 @@ public class RelockDomainActionTest {
private final FakeResponse response = new FakeResponse();
private final FakeClock clock = new FakeClock(DateTime.parse("2015-05-18T12:34:56Z"));
private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(clock);
private final DomainLockUtils domainLockUtils =
new DomainLockUtils(
new DeterministicStringGenerator(Alphabets.BASE_58),
"adminreg",
AsyncTaskEnqueuerTest.createForTesting(
mock(AppEngineServiceUtils.class), clock, Duration.ZERO));
cloudTasksHelper.getTestCloudTasksUtils());
@RegisterExtension
public final AppEngineExtension appEngineExtension =
@@ -96,7 +95,6 @@ public class RelockDomainActionTest {
private DomainBase domain;
private RegistryLock oldLock;
@Mock private SendEmailService sendEmailService;
private AsyncTaskEnqueuer asyncTaskEnqueuer;
private RelockDomainAction action;
@BeforeEach
@@ -118,8 +116,6 @@ public class RelockDomainActionTest {
.when(appEngineServiceUtils.getServiceHostname("backend"))
.thenReturn("backend.hostname.fake");
asyncTaskEnqueuer =
AsyncTaskEnqueuerTest.createForTesting(appEngineServiceUtils, clock, Duration.ZERO);
action = createAction(oldLock.getRevisionId());
}
@@ -158,7 +154,7 @@ public class RelockDomainActionTest {
assertThat(response.getPayload())
.isEqualTo(String.format("Re-lock failed: %s", expectedFailureMessage));
assertNonTransientFailureEmail(expectedFailureMessage);
assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
cloudTasksHelper.assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
}
@TestOfyAndSql
@@ -170,7 +166,7 @@ public class RelockDomainActionTest {
assertThat(response.getPayload())
.isEqualTo(String.format("Re-lock failed: %s", expectedFailureMessage));
assertNonTransientFailureEmail(expectedFailureMessage);
assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
cloudTasksHelper.assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
}
@TestOfyAndSql
@@ -180,7 +176,7 @@ public class RelockDomainActionTest {
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload())
.isEqualTo("Domain example.tld is already manually re-locked, skipping automated re-lock.");
assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
cloudTasksHelper.assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
}
@TestOfyAndSql
@@ -192,7 +188,7 @@ public class RelockDomainActionTest {
assertThat(response.getPayload())
.isEqualTo(String.format("Re-lock failed: %s", expectedFailureMessage));
assertNonTransientFailureEmail(expectedFailureMessage);
assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
cloudTasksHelper.assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
}
@TestOfyAndSql
@@ -207,7 +203,7 @@ public class RelockDomainActionTest {
assertThat(response.getPayload())
.isEqualTo(String.format("Re-lock failed: %s", expectedFailureMessage));
assertNonTransientFailureEmail(expectedFailureMessage);
assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
cloudTasksHelper.assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
}
@TestOfyAndSql
@@ -253,7 +249,7 @@ public class RelockDomainActionTest {
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload())
.isEqualTo("Domain example.tld is already manually re-locked, skipping automated re-lock.");
assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
cloudTasksHelper.assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
}
@TestOfyAndSql
@@ -320,17 +316,16 @@ public class RelockDomainActionTest {
}
private void assertTaskEnqueued(int numAttempts, long oldUnlockRevisionId, Duration duration) {
assertTasksEnqueued(
cloudTasksHelper.assertTasksEnqueued(
QUEUE_ASYNC_ACTIONS,
new TaskMatcher()
.url(RelockDomainAction.PATH)
.method("POST")
.header("Host", "backend.hostname.fake")
.method(HttpMethod.POST)
.param(
RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM,
String.valueOf(oldUnlockRevisionId))
.param(RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM, String.valueOf(numAttempts))
.etaDelta(duration.minus(standardSeconds(30)), duration.plus(standardSeconds(30))));
.scheduleTime(clock.nowUtc().plus(duration)));
}
private RelockDomainAction createAction(Long oldUnlockRevisionId) throws Exception {
@@ -349,7 +344,6 @@ public class RelockDomainActionTest {
"support@example.com",
sendEmailService,
domainLockUtils,
response,
asyncTaskEnqueuer);
response);
}
}

View File

@@ -33,7 +33,7 @@ class CommitLogFanoutActionTest {
private static final String ENDPOINT = "/the/servlet";
private static final String QUEUE = "the-queue";
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(new FakeClock());
@RegisterExtension
final AppEngineExtension appEngineExtension =
@@ -58,7 +58,6 @@ class CommitLogFanoutActionTest {
action.endpoint = ENDPOINT;
action.queue = QUEUE;
action.jitterSeconds = Optional.empty();
action.clock = new FakeClock();
action.run();
List<TaskMatcher> matchers = new ArrayList<>();
for (int bucketId : CommitLogBucket.getBucketIds()) {

View File

@@ -45,7 +45,7 @@ class TldFanoutActionTest {
private static final String ENDPOINT = "/the/servlet";
private static final String QUEUE = "the-queue";
private final FakeResponse response = new FakeResponse();
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(new FakeClock());
@RegisterExtension
final AppEngineExtension appEngine =
@@ -61,7 +61,6 @@ class TldFanoutActionTest {
private void run(ImmutableListMultimap<String, String> params) {
TldFanoutAction action = new TldFanoutAction();
action.clock = new FakeClock();
action.params = params;
action.endpoint = ENDPOINT;
action.queue = QUEUE;

View File

@@ -553,6 +553,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.hasValue(HistoryEntry.createVKey(Key.create(historyEntry)));
}
// DomainTransactionRecord is not propagated.
@TestOfyAndSql
void testSuccess_validAllocationToken_multiUse() throws Exception {
setEppInput(

View File

@@ -1281,6 +1281,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
// Contacts mismatch.
@TestOfyAndSql
void testFailure_sameContactAddedAndRemoved() throws Exception {
setEppInput("domain_update_add_remove_same_contact.xml");

View File

@@ -75,6 +75,7 @@ public class DomainBaseTest extends EntityTestCase {
private VKey<BillingEvent.OneTime> oneTimeBillKey;
private VKey<BillingEvent.Recurring> recurringBillKey;
private Key<HistoryEntry> historyEntryKey;
private VKey<ContactResource> contact1Key, contact2Key;
@BeforeEach
void setUp() {
@@ -88,14 +89,14 @@ public class DomainBaseTest extends EntityTestCase {
.setRepoId("1-COM")
.build())
.createVKey();
VKey<ContactResource> contact1Key =
contact1Key =
persistResource(
new ContactResource.Builder()
.setContactId("contact_id1")
.setRepoId("2-COM")
.build())
.createVKey();
VKey<ContactResource> contact2Key =
contact2Key =
persistResource(
new ContactResource.Builder()
.setContactId("contact_id2")
@@ -947,4 +948,61 @@ public class DomainBaseTest extends EntityTestCase {
this.billingEventOneTime = billingEventOneTime;
}
}
@Test
void testContactFields() {
VKey<ContactResource> contact3Key =
persistResource(
new ContactResource.Builder()
.setContactId("contact_id3")
.setRepoId("4-COM")
.build())
.createVKey();
VKey<ContactResource> contact4Key =
persistResource(
new ContactResource.Builder()
.setContactId("contact_id4")
.setRepoId("5-COM")
.build())
.createVKey();
// Set all of the contacts.
domain.setContactFields(
ImmutableSet.of(
DesignatedContact.create(Type.REGISTRANT, contact1Key),
DesignatedContact.create(Type.ADMIN, contact2Key),
DesignatedContact.create(Type.BILLING, contact3Key),
DesignatedContact.create(Type.TECH, contact4Key)),
true);
assertThat(domain.getRegistrant()).isEqualTo(contact1Key);
assertThat(domain.getAdminContact()).isEqualTo(contact2Key);
assertThat(domain.getBillingContact()).isEqualTo(contact3Key);
assertThat(domain.getTechContact()).isEqualTo(contact4Key);
// Make sure everything gets nulled out.
domain.setContactFields(ImmutableSet.of(), true);
assertThat(domain.getRegistrant()).isNull();
assertThat(domain.getAdminContact()).isNull();
assertThat(domain.getBillingContact()).isNull();
assertThat(domain.getTechContact()).isNull();
// Make sure that changes don't affect the registrant unless requested.
domain.setContactFields(
ImmutableSet.of(
DesignatedContact.create(Type.REGISTRANT, contact1Key),
DesignatedContact.create(Type.ADMIN, contact2Key),
DesignatedContact.create(Type.BILLING, contact3Key),
DesignatedContact.create(Type.TECH, contact4Key)),
false);
assertThat(domain.getRegistrant()).isNull();
assertThat(domain.getAdminContact()).isEqualTo(contact2Key);
assertThat(domain.getBillingContact()).isEqualTo(contact3Key);
assertThat(domain.getTechContact()).isEqualTo(contact4Key);
domain = domain.asBuilder().setRegistrant(contact1Key).build();
domain.setContactFields(ImmutableSet.of(), false);
assertThat(domain.getRegistrant()).isEqualTo(contact1Key);
assertThat(domain.getAdminContact()).isNull();
assertThat(domain.getBillingContact()).isNull();
assertThat(domain.getTechContact()).isNull();
}
}

View File

@@ -22,6 +22,7 @@ import static google.registry.testing.DatabaseHelper.insertInDb;
import com.google.common.collect.ImmutableSet;
import google.registry.model.ImmutableObject;
import google.registry.model.replay.NonReplicatedEntity;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
import java.lang.reflect.Method;
@@ -168,7 +169,7 @@ class EntityCallbacksListenerTest {
}
@Entity(name = "TestEntity")
private static class TestEntity extends ParentEntity {
private static class TestEntity extends ParentEntity implements NonReplicatedEntity {
@Id String name = "id";
int nonTransientField = 0;

View File

@@ -49,7 +49,7 @@ public class JpaEntityCoverageExtension implements BeforeEachCallback, AfterEach
// TransactionEntity is trivial; its persistence is tested in TransactionTest.
"TransactionEntity");
private static final ImmutableSet<Class<?>> ALL_JPA_ENTITIES =
public static final ImmutableSet<Class<?>> ALL_JPA_ENTITIES =
PersistenceXmlUtility.getManagedClasses().stream()
.filter(e -> !IGNORE_ENTITIES.contains(e.getSimpleName()))
.filter(e -> e.isAnnotationPresent(Entity.class))

View File

@@ -30,6 +30,7 @@ import com.googlecode.objectify.annotation.Id;
import google.registry.model.ImmutableObject;
import google.registry.model.ofy.DatastoreTransactionManager;
import google.registry.model.ofy.Ofy;
import google.registry.model.replay.NonReplicatedEntity;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.TransactionManagerFactory.ReadOnlyModeException;
import google.registry.testing.AppEngineExtension;
@@ -448,7 +449,7 @@ public class TransactionManagerTest {
@Entity(name = "TxnMgrTestEntity")
@javax.persistence.Entity(name = "TestEntity")
private static class TestEntity extends TestEntityBase {
private static class TestEntity extends TestEntityBase implements NonReplicatedEntity {
private String data;

View File

@@ -23,7 +23,6 @@ import static org.mockito.Mockito.when;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.net.MediaType;
import com.google.protobuf.util.Timestamps;
import google.registry.beam.BeamActionTestBase;
import google.registry.model.common.DatabaseMigrationStateSchedule.PrimaryDatabase;
import google.registry.reporting.ReportingModule;
@@ -83,11 +82,9 @@ class GenerateInvoicesActionTest extends BeamActionTestBase {
.param("jobId", "jobid")
.param("yearMonth", "2017-10")
.scheduleTime(
Timestamps.fromMillis(
clock
.nowUtc()
.plus(Duration.standardMinutes(ReportingModule.ENQUEUE_DELAY_MINUTES))
.getMillis())));
clock
.nowUtc()
.plus(Duration.standardMinutes(ReportingModule.ENQUEUE_DELAY_MINUTES))));
}
@TestOfyAndSql

View File

@@ -24,7 +24,6 @@ import static org.mockito.Mockito.when;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.protobuf.util.Timestamps;
import google.registry.bigquery.BigqueryJobFailureException;
import google.registry.reporting.icann.IcannReportingModule.ReportType;
import google.registry.request.HttpException.BadRequestException;
@@ -54,7 +53,8 @@ class IcannReportingStagingActionTest {
private YearMonth yearMonth = new YearMonth(2017, 6);
private String subdir = "default/dir";
private IcannReportingStagingAction action;
private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private FakeClock clock = new FakeClock(DateTime.parse("2021-01-02T11:00:00Z"));
private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(clock);
@RegisterExtension
final AppEngineExtension appEngine =
@@ -77,7 +77,6 @@ class IcannReportingStagingActionTest {
action.recipient = new InternetAddress("recipient@example.com");
action.emailService = mock(SendEmailService.class);
action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
action.clock = new FakeClock(DateTime.parse("2021-01-02T11:00:00Z"));
when(stager.stageReports(yearMonth, subdir, ReportType.ACTIVITY))
.thenReturn(ImmutableList.of("a", "b"));
@@ -91,9 +90,7 @@ class IcannReportingStagingActionTest {
new TaskMatcher()
.url("/_dr/task/icannReportingUpload")
.method(HttpMethod.POST)
.scheduleTime(
Timestamps.fromMillis(
action.clock.nowUtc().plus(Duration.standardMinutes(2)).getMillis())));
.scheduleTime(clock.nowUtc().plus(Duration.standardMinutes(2))));
}
@Test

View File

@@ -21,7 +21,6 @@ import static org.mockito.Mockito.when;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.net.MediaType;
import com.google.protobuf.util.Timestamps;
import google.registry.beam.BeamActionTestBase;
import google.registry.model.common.DatabaseMigrationStateSchedule.PrimaryDatabase;
import google.registry.reporting.ReportingModule;
@@ -44,7 +43,7 @@ class GenerateSpec11ReportActionTest extends BeamActionTestBase {
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
private final FakeClock clock = new FakeClock(DateTime.parse("2018-06-11T12:23:56Z"));
private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(clock);
private CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
private GenerateSpec11ReportAction action;
@@ -101,11 +100,9 @@ class GenerateSpec11ReportActionTest extends BeamActionTestBase {
.param("jobId", "jobid")
.param("date", "2018-06-11")
.scheduleTime(
Timestamps.fromMillis(
clock
.nowUtc()
.plus(Duration.standardMinutes(ReportingModule.ENQUEUE_DELAY_MINUTES))
.getMillis())));
clock
.nowUtc()
.plus(Duration.standardMinutes(ReportingModule.ENQUEUE_DELAY_MINUTES))));
}
@Test

View File

@@ -40,6 +40,7 @@ import com.google.common.net.HttpHeaders;
import com.google.common.net.MediaType;
import com.google.common.truth.Truth8;
import com.google.protobuf.Timestamp;
import com.google.protobuf.util.Timestamps;
import google.registry.model.ImmutableObject;
import google.registry.util.CloudTasksUtils;
import google.registry.util.Retrier;
@@ -60,6 +61,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.annotation.Nonnull;
import org.joda.time.DateTime;
/**
* Static utility functions for testing task queues.
@@ -92,13 +94,22 @@ public class CloudTasksHelper implements Serializable {
private static final String PROJECT_ID = "test-project";
private static final String LOCATION_ID = "test-location";
private final Retrier retrier = new Retrier(new FakeSleeper(new FakeClock()), 1);
private final int instanceId = nextInstanceId.getAndIncrement();
private final CloudTasksUtils cloudTasksUtils =
new CloudTasksUtils(retrier, PROJECT_ID, LOCATION_ID, new FakeCloudTasksClient());
private final CloudTasksUtils cloudTasksUtils;
public CloudTasksHelper(FakeClock clock) {
this.cloudTasksUtils =
new CloudTasksUtils(
new Retrier(new FakeSleeper(clock), 1),
clock,
PROJECT_ID,
LOCATION_ID,
new FakeCloudTasksClient());
testTasks.put(instanceId, Multimaps.synchronizedListMultimap(LinkedListMultimap.create()));
}
public CloudTasksHelper() {
testTasks.put(instanceId, Multimaps.synchronizedListMultimap(LinkedListMultimap.create()));
this(new FakeClock());
}
public CloudTasksUtils getTestCloudTasksUtils() {
@@ -302,6 +313,10 @@ public class CloudTasksHelper implements Serializable {
return this;
}
public TaskMatcher scheduleTime(DateTime scheduleTime) {
return scheduleTime(Timestamps.fromMillis(scheduleTime.getMillis()));
}
public TaskMatcher param(String key, String value) {
checkNotNull(value, "Test error: A param can never have a null value, so don't assert it");
expected.params.put(key, value);

View File

@@ -472,8 +472,10 @@ public class DatabaseHelper {
public static void allowRegistrarAccess(String registrarId, String tld) {
Registrar registrar = loadRegistrar(registrarId);
persistResource(
registrar.asBuilder().setAllowedTlds(union(registrar.getAllowedTlds(), tld)).build());
if (!registrar.getAllowedTlds().contains(tld)) {
persistResource(
registrar.asBuilder().setAllowedTlds(union(registrar.getAllowedTlds(), tld)).build());
}
}
public static void disallowRegistrarAccess(String registrarId, String tld) {

View File

@@ -14,17 +14,25 @@
package google.registry.testing;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.junit.Assert.fail;
import com.google.common.base.Ascii;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.model.EntityClasses;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.ofy.CommitLogBucket;
@@ -34,6 +42,7 @@ import google.registry.model.replay.DatastoreEntity;
import google.registry.model.replay.ReplicateToDatastoreAction;
import google.registry.model.replay.SqlEntity;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaEntityCoverageExtension;
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
import google.registry.persistence.transaction.Transaction;
import google.registry.persistence.transaction.Transaction.Delete;
@@ -42,10 +51,15 @@ import google.registry.persistence.transaction.Transaction.Update;
import google.registry.persistence.transaction.TransactionEntity;
import google.registry.util.RequestStatusChecker;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
@@ -58,30 +72,69 @@ import org.mockito.Mockito;
* that extension are also replayed. If AppEngineExtension is not used,
* JpaTransactionManagerExtension must be, and this extension should be ordered _after_
* JpaTransactionManagerExtension so that writes to SQL work.
*
* <p>If the "compare" flag is set in the constructor, this will also compare all touched objects in
* both databases after performing the replay.
*/
public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static ImmutableSet<String> NON_REPLICATED_TYPES =
ImmutableSet.of(
"PremiumList",
"PremiumListRevision",
"PremiumListEntry",
"ReservedList",
"RdeRevision",
"ServerSecret",
"SignedMarkRevocationList",
"ClaimsListShard",
"TmchCrl",
"EppResourceIndex",
"ForeignKeyIndex",
"ForeignKeyHostIndex",
"ForeignKeyContactIndex",
"ForeignKeyDomainIndex");
// Entity classes to be ignored during the final database comparison. Note that this is just a
// mash-up of Datastore and SQL classes, and used for filtering both sets. We could split them
// out, but there is plenty of overlap and no name collisions so it doesn't matter very much.
private static ImmutableSet<String> IGNORED_ENTITIES =
Streams.concat(
ImmutableSet.of(
// These entities are @Embed-ded in Datastore
"DelegationSignerData",
"DomainDsDataHistory",
"DomainTransactionRecord",
"GracePeriod",
"GracePeriodHistory",
// These entities are legitimately not comparable.
"ClaimsEntry",
"ClaimsList",
"CommitLogBucket",
"CommitLogManifest",
"CommitLogMutation",
"PremiumEntry",
"ReservedListEntry")
.stream(),
NON_REPLICATED_TYPES.stream())
.collect(toImmutableSet());
FakeClock clock;
boolean compare;
boolean replayed = false;
boolean inOfyContext;
InjectExtension injectExtension = new InjectExtension();
@Nullable ReplicateToDatastoreAction sqlToDsReplicator;
List<DomainBase> expectedUpdates = new ArrayList<>();
boolean enableDomainTimestampChecks;
boolean enableDatabaseCompare = true;
private ReplayExtension(
FakeClock clock, boolean compare, @Nullable ReplicateToDatastoreAction sqlToDsReplicator) {
private ReplayExtension(FakeClock clock, @Nullable ReplicateToDatastoreAction sqlToDsReplicator) {
this.clock = clock;
this.compare = compare;
this.sqlToDsReplicator = sqlToDsReplicator;
}
public static ReplayExtension createWithCompare(FakeClock clock) {
return new ReplayExtension(clock, true, null);
return new ReplayExtension(clock, null);
}
// This allows us to disable the replay tests from an environment variable in specific
@@ -104,7 +157,6 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
if (replayTestsEnabled()) {
return new ReplayExtension(
clock,
true,
new ReplicateToDatastoreAction(
clock, Mockito.mock(RequestStatusChecker.class), new FakeResponse()));
} else {
@@ -138,6 +190,11 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
Optional<Method> elem = context.getTestMethod();
if (elem.isPresent() && elem.get().isAnnotationPresent(NoDatabaseCompare.class)) {
enableDatabaseCompare = false;
}
// Use a single bucket to expose timestamp inversion problems. This typically happens when
// a test with this extension rolls back the fake clock in the setup method, creating inverted
// timestamp with the canned data preloaded by AppengineExtension. The solution is to move
@@ -170,23 +227,6 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
}
}
private static ImmutableSet<String> NON_REPLICATED_TYPES =
ImmutableSet.of(
"PremiumList",
"PremiumListRevision",
"PremiumListEntry",
"ReservedList",
"RdeRevision",
"ServerSecret",
"SignedMarkRevocationList",
"ClaimsListShard",
"TmchCrl",
"EppResourceIndex",
"ForeignKeyIndex",
"ForeignKeyHostIndex",
"ForeignKeyContactIndex",
"ForeignKeyDomainIndex");
public void replay() {
if (!replayed) {
if (inOfyContext) {
@@ -211,34 +251,32 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
ImmutableMap<Key<?>, Object> changes = ReplayQueue.replay();
// Compare JPA to OFY, if requested.
if (compare) {
for (ImmutableMap.Entry<Key<?>, Object> entry : changes.entrySet()) {
// Don't verify non-replicated types.
if (NON_REPLICATED_TYPES.contains(entry.getKey().getKind())) {
continue;
}
// Since the object may have changed in datastore by the time we're doing the replay, we
// have to compare the current value in SQL (which we just mutated) against the value that
// we originally would have persisted (that being the object in the entry).
VKey<?> vkey = VKey.from(entry.getKey());
jpaTm()
.transact(
() -> {
Optional<?> jpaValue = jpaTm().loadByKeyIfPresent(vkey);
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
assertThat(jpaValue.isPresent()).isFalse();
} else {
ImmutableObject immutJpaObject = (ImmutableObject) jpaValue.get();
assertAboutImmutableObjects().that(immutJpaObject).hasCorrectHashValue();
assertAboutImmutableObjects()
.that(immutJpaObject)
.isEqualAcrossDatabases(
(ImmutableObject)
((DatastoreEntity) entry.getValue()).toSqlEntity().get());
}
});
for (ImmutableMap.Entry<Key<?>, Object> entry : changes.entrySet()) {
// Don't verify non-replicated types.
if (NON_REPLICATED_TYPES.contains(entry.getKey().getKind())) {
continue;
}
// Since the object may have changed in datastore by the time we're doing the replay, we
// have to compare the current value in SQL (which we just mutated) against the value that
// we originally would have persisted (that being the object in the entry).
VKey<?> vkey = VKey.from(entry.getKey());
jpaTm()
.transact(
() -> {
Optional<?> jpaValue = jpaTm().loadByKeyIfPresent(vkey);
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
assertThat(jpaValue.isPresent()).isFalse();
} else {
ImmutableObject immutJpaObject = (ImmutableObject) jpaValue.get();
assertAboutImmutableObjects().that(immutJpaObject).hasCorrectHashValue();
assertAboutImmutableObjects()
.that(immutJpaObject)
.isEqualAcrossDatabases(
(ImmutableObject)
((DatastoreEntity) entry.getValue()).toSqlEntity().get());
}
});
}
}
@@ -252,12 +290,15 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
transactionBatch = sqlToDsReplicator.getTransactionBatchAtSnapshot();
for (TransactionEntity txn : transactionBatch) {
ReplicateToDatastoreAction.applyTransaction(txn);
if (compare) {
ofyTm().transact(() -> compareSqlTransaction(txn));
}
ofyTm().transact(() -> compareSqlTransaction(txn));
clock.advanceOneMilli();
}
} while (!transactionBatch.isEmpty());
// Now that everything has been replayed, compare the databases.
if (enableDatabaseCompare) {
compareDatabases();
}
}
/** Verifies that the replaying the SQL transaction created the same entities in Datastore. */
@@ -305,4 +346,84 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
}
}
}
/** Compares the final state of both databases after replay is complete. */
private void compareDatabases() {
boolean gotDiffs = false;
// Build a map containing all of the SQL entities indexed by their key.
HashMap<Object, Object> sqlEntities = new HashMap<>();
for (Class<?> cls : JpaEntityCoverageExtension.ALL_JPA_ENTITIES) {
if (IGNORED_ENTITIES.contains(cls.getSimpleName())) {
continue;
}
jpaTm()
.transact(
() -> jpaTm().loadAllOfStream(cls).forEach(e -> sqlEntities.put(getSqlKey(e), e)));
}
for (Class<? extends ImmutableObject> cls : EntityClasses.ALL_CLASSES) {
if (IGNORED_ENTITIES.contains(cls.getSimpleName())) {
continue;
}
for (ImmutableObject entity : auditedOfy().load().type(cls).list()) {
// Find the entity in SQL and verify that it's the same.
Key<?> ofyKey = Key.create(entity);
Object sqlKey = VKey.from(ofyKey).getSqlKey();
ImmutableObject sqlEntity = (ImmutableObject) sqlEntities.get(sqlKey);
Optional<SqlEntity> expectedSqlEntity = ((DatastoreEntity) entity).toSqlEntity();
if (expectedSqlEntity.isPresent()) {
// Check for null just so we get a better error message.
if (sqlEntity == null) {
logger.atSevere().log("Entity %s is in Datastore but not in SQL.", ofyKey);
gotDiffs = true;
} else {
try {
assertAboutImmutableObjects()
.that((ImmutableObject) expectedSqlEntity.get())
.isEqualAcrossDatabases(sqlEntity);
} catch (AssertionError e) {
// Show the message but swallow the stack trace (we'll get that from the fail() at
// the end of the comparison).
logger.atSevere().log("For entity %s: %s", ofyKey, e.getMessage());
gotDiffs = true;
}
}
} else {
logger.atInfo().log("Datastore entity has no sql representation for %s", ofyKey);
}
sqlEntities.remove(sqlKey);
}
}
// Report any objects in the SQL set that we didn't remove while iterating over the Datastore
// objects.
if (!sqlEntities.isEmpty()) {
for (Object item : sqlEntities.values()) {
logger.atSevere().log(
"Entity of %s found in SQL but not in datastore: %s", item.getClass().getName(), item);
}
gotDiffs = true;
}
if (gotDiffs) {
fail("There were differences between the final SQL and Datastore contents.");
}
}
private static Object getSqlKey(Object entity) {
return jpaTm()
.getEntityManager()
.getEntityManagerFactory()
.getPersistenceUnitUtil()
.getIdentifier(entity);
}
/** Annotation to use for test methods where we don't want to do a database comparison yet. */
@Target({METHOD})
@Retention(RUNTIME)
@TestTemplate
public @interface NoDatabaseCompare {}
}

View File

@@ -26,17 +26,16 @@ import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.getRegistryLockByRevisionId;
import static google.registry.testing.SqlHelper.getRegistryLockByVerificationCode;
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static org.joda.time.Duration.standardDays;
import static org.joda.time.Duration.standardHours;
import static org.joda.time.Duration.standardSeconds;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableList;
import google.registry.batch.AsyncTaskEnqueuerTest;
import google.registry.batch.RelockDomainAction;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
@@ -47,12 +46,13 @@ import google.registry.model.host.HostResource;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.SqlHelper;
import google.registry.testing.TaskQueueHelper.TaskMatcher;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.UserInfo;
import google.registry.util.AppEngineServiceUtils;
@@ -62,8 +62,11 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
/** Unit tests for {@link google.registry.tools.DomainLockUtils}. */
@DualDatabaseTest
@@ -74,6 +77,7 @@ public final class DomainLockUtilsTest {
private final FakeClock clock = new FakeClock(DateTime.now(DateTimeZone.UTC));
private DomainLockUtils domainLockUtils;
private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(clock);
@RegisterExtension
public final AppEngineExtension appEngineExtension =
@@ -98,8 +102,7 @@ public final class DomainLockUtilsTest {
new DomainLockUtils(
new DeterministicStringGenerator(Alphabets.BASE_58),
"adminreg",
AsyncTaskEnqueuerTest.createForTesting(
appEngineServiceUtils, clock, standardSeconds(90)));
cloudTasksHelper.getTestCloudTasksUtils());
}
@TestOfyAndSql
@@ -265,19 +268,17 @@ public final class DomainLockUtilsTest {
domainLockUtils.saveNewRegistryUnlockRequest(
DOMAIN_NAME, "TheRegistrar", false, Optional.of(standardHours(6)));
domainLockUtils.verifyAndApplyUnlock(lock.getVerificationCode(), false);
assertTasksEnqueued(
cloudTasksHelper.assertTasksEnqueued(
QUEUE_ASYNC_ACTIONS,
new TaskMatcher()
.url(RelockDomainAction.PATH)
.method("POST")
.header("Host", "backend.hostname.fake")
.method(HttpMethod.POST)
.service("backend")
.param(
RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM,
String.valueOf(lock.getRevisionId()))
.param(RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM, "0")
.etaDelta(
standardHours(6).minus(standardSeconds(30)),
standardDays(6).plus(standardSeconds(30))));
.scheduleTime(clock.nowUtc().plus(lock.getRelockDuration().get())));
}
@TestOfyAndSql
@@ -477,6 +478,59 @@ public final class DomainLockUtilsTest {
assertNoDomainChanges();
}
@TestOfyAndSql
void testEnqueueRelock() {
RegistryLock lock =
saveRegistryLock(
new RegistryLock.Builder()
.setLockCompletionTime(clock.nowUtc())
.setUnlockRequestTime(clock.nowUtc())
.setUnlockCompletionTime(clock.nowUtc())
.isSuperuser(false)
.setDomainName("example.tld")
.setRepoId("repoId")
.setRelockDuration(standardHours(6))
.setRegistrarId("TheRegistrar")
.setRegistrarPocId("someone@example.com")
.setVerificationCode("hi")
.build());
domainLockUtils.enqueueDomainRelock(lock.getRelockDuration().get(), lock.getRevisionId(), 0);
cloudTasksHelper.assertTasksEnqueued(
QUEUE_ASYNC_ACTIONS,
new CloudTasksHelper.TaskMatcher()
.url(RelockDomainAction.PATH)
.method(HttpMethod.POST)
.service("backend")
.param(
RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM,
String.valueOf(lock.getRevisionId()))
.param(RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM, "0")
.scheduleTime(clock.nowUtc().plus(lock.getRelockDuration().get())));
}
@MockitoSettings(strictness = Strictness.LENIENT)
@TestOfyAndSql
void testFailure_enqueueRelock_noDuration() {
RegistryLock lockWithoutDuration =
saveRegistryLock(
new RegistryLock.Builder()
.isSuperuser(false)
.setDomainName("example.tld")
.setRepoId("repoId")
.setRegistrarId("TheRegistrar")
.setRegistrarPocId("someone@example.com")
.setVerificationCode("hi")
.build());
assertThat(
Assert.assertThrows(
IllegalArgumentException.class,
() -> domainLockUtils.enqueueDomainRelock(lockWithoutDuration)))
.hasMessageThat()
.isEqualTo(
String.format(
"Lock with ID %s not configured for relock", lockWithoutDuration.getRevisionId()));
}
private void verifyProperlyLockedDomain(boolean isAdmin) {
assertThat(loadByEntity(domain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);

View File

@@ -24,19 +24,16 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.getMostRecentRegistryLockByRepoId;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableList;
import google.registry.batch.AsyncTaskEnqueuerTest;
import google.registry.model.domain.DomainBase;
import google.registry.model.registrar.Registrar.Type;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.util.AppEngineServiceUtils;
import google.registry.util.StringGenerator.Alphabets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -52,8 +49,7 @@ class LockDomainCommandTest extends CommandTestCase<LockDomainCommand> {
new DomainLockUtils(
new DeterministicStringGenerator(Alphabets.BASE_58),
"adminreg",
AsyncTaskEnqueuerTest.createForTesting(
mock(AppEngineServiceUtils.class), fakeClock, Duration.ZERO));
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
}
@Test

View File

@@ -25,21 +25,18 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.getMostRecentRegistryLockByRepoId;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.batch.AsyncTaskEnqueuerTest;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.RegistryLock;
import google.registry.model.registrar.Registrar.Type;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.util.AppEngineServiceUtils;
import google.registry.util.StringGenerator.Alphabets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -55,8 +52,7 @@ class UnlockDomainCommandTest extends CommandTestCase<UnlockDomainCommand> {
new DomainLockUtils(
new DeterministicStringGenerator(Alphabets.BASE_58),
"adminreg",
AsyncTaskEnqueuerTest.createForTesting(
mock(AppEngineServiceUtils.class), fakeClock, Duration.ZERO));
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
}
private DomainBase persistLockedDomain(String domainName, String registrarId) {

View File

@@ -267,9 +267,16 @@ class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase {
Map<String, Object> response =
action.handleJsonRequest(
ImmutableMap.of(
"op", "update",
"id", CLIENT_ID,
"args", setter.apply(registrar.asBuilder(), newValue).build().toJsonMap()));
"op",
"update",
"id",
CLIENT_ID,
"args",
setter
.apply(registrar.asBuilder(), newValue)
.setLastUpdateTime(registrar.getLastUpdateTime())
.build()
.toJsonMap()));
Registrar updatedRegistrar = loadRegistrar(CLIENT_ID);
persistResource(registrar);
@@ -320,7 +327,11 @@ class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase {
"id",
CLIENT_ID,
"args",
setter.apply(registrar.asBuilder(), newValue).build().toJsonMap()));
setter
.apply(registrar.asBuilder(), newValue)
.setLastUpdateTime(registrar.getLastUpdateTime())
.build()
.toJsonMap()));
Registrar updatedRegistrar = loadRegistrar(CLIENT_ID);
persistResource(registrar);

View File

@@ -26,7 +26,6 @@ import static google.registry.testing.SqlHelper.getRegistryLockByVerificationCod
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static google.registry.ui.server.registrar.RegistryLockGetActionTest.userFromRegistrarContact;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -35,7 +34,6 @@ import com.google.appengine.api.users.User;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.batch.AsyncTaskEnqueuerTest;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.RegistryLock;
import google.registry.request.JsonActionRunner;
@@ -47,10 +45,10 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.tools.DomainLockUtils;
import google.registry.util.AppEngineServiceUtils;
import google.registry.util.EmailMessage;
import google.registry.util.SendEmailService;
import google.registry.util.StringGenerator.Alphabets;
@@ -469,8 +467,7 @@ final class RegistryLockPostActionTest {
new DomainLockUtils(
new DeterministicStringGenerator(Alphabets.BASE_58),
"adminreg",
AsyncTaskEnqueuerTest.createForTesting(
mock(AppEngineServiceUtils.class), clock, Duration.ZERO));
new CloudTasksHelper(clock).getTestCloudTasksUtils());
return new RegistryLockPostAction(
mockRequest,
jsonActionRunner,

View File

@@ -33,7 +33,6 @@ import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.common.collect.ImmutableMap;
import google.registry.batch.AsyncTaskEnqueuerTest;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainBase;
@@ -47,13 +46,13 @@ import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.XsrfTokenManager;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.UserInfo;
import google.registry.tools.DomainLockUtils;
import google.registry.util.AppEngineServiceUtils;
import google.registry.util.StringGenerator;
import google.registry.util.StringGenerator.Alphabets;
import javax.servlet.http.HttpServletRequest;
@@ -86,6 +85,7 @@ final class RegistryLockVerifyActionTest {
private DomainBase domain;
private AuthResult authResult;
private RegistryLockVerifyAction action;
private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(fakeClock);
@BeforeEach
void beforeEach() {
@@ -329,10 +329,7 @@ final class RegistryLockVerifyActionTest {
RegistryLockVerifyAction action =
new RegistryLockVerifyAction(
new DomainLockUtils(
stringGenerator,
"adminreg",
AsyncTaskEnqueuerTest.createForTesting(
mock(AppEngineServiceUtils.class), fakeClock, Duration.ZERO)),
stringGenerator, "adminreg", cloudTasksHelper.getTestCloudTasksUtils()),
lockVerificationCode,
isLock);
authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false));

View File

@@ -30,6 +30,7 @@ com.google.http-client:google-http-client-gson:1.39.2
com.google.http-client:google-http-client:1.39.2
com.google.j2objc:j2objc-annotations:1.3
com.google.oauth-client:google-oauth-client:1.31.4
com.google.protobuf:protobuf-java-util:3.17.3
com.google.protobuf:protobuf-java:3.17.3
com.google.re2j:re2j:1.6
com.ibm.icu:icu4j:68.2

View File

@@ -34,6 +34,7 @@ com.google.http-client:google-http-client-gson:1.39.2
com.google.http-client:google-http-client:1.39.2
com.google.j2objc:j2objc-annotations:1.3
com.google.oauth-client:google-oauth-client:1.31.4
com.google.protobuf:protobuf-java-util:3.17.3
com.google.protobuf:protobuf-java:3.17.3
com.google.re2j:re2j:1.6
com.google.truth:truth:1.1.2

View File

@@ -32,6 +32,7 @@ com.google.http-client:google-http-client:1.39.2
com.google.j2objc:j2objc-annotations:1.3
com.google.monitoring-client:metrics:1.0.7
com.google.oauth-client:google-oauth-client:1.31.4
com.google.protobuf:protobuf-java-util:3.17.3
com.google.protobuf:protobuf-java:3.17.3
com.google.re2j:re2j:1.6
com.ibm.icu:icu4j:68.2

View File

@@ -37,6 +37,7 @@ com.google.j2objc:j2objc-annotations:1.3
com.google.monitoring-client:contrib:1.0.7
com.google.monitoring-client:metrics:1.0.7
com.google.oauth-client:google-oauth-client:1.31.4
com.google.protobuf:protobuf-java-util:3.17.3
com.google.protobuf:protobuf-java:3.17.3
com.google.re2j:re2j:1.6
com.google.truth:truth:1.1.2

View File

@@ -45,7 +45,7 @@ com.google.j2objc:j2objc-annotations:1.3
com.google.monitoring-client:metrics:1.0.7
com.google.monitoring-client:stackdriver:1.0.7
com.google.oauth-client:google-oauth-client:1.31.4
com.google.protobuf:protobuf-java-util:3.15.3
com.google.protobuf:protobuf-java-util:3.17.3
com.google.protobuf:protobuf-java:3.17.3
com.google.re2j:re2j:1.6
com.ibm.icu:icu4j:68.2

View File

@@ -50,7 +50,7 @@ com.google.monitoring-client:contrib:1.0.7
com.google.monitoring-client:metrics:1.0.7
com.google.monitoring-client:stackdriver:1.0.7
com.google.oauth-client:google-oauth-client:1.31.4
com.google.protobuf:protobuf-java-util:3.15.3
com.google.protobuf:protobuf-java-util:3.17.3
com.google.protobuf:protobuf-java:3.17.3
com.google.re2j:re2j:1.6
com.google.truth:truth:1.1.2

View File

@@ -97,10 +97,8 @@ steps:
google/registry/beam/invoicing_pipeline_metadata.json \
google.registry.beam.rde.RdePipeline \
google/registry/beam/rde_pipeline_metadata.json \
google.registry.beam.comparedb.ValidateDatastorePipeline \
google/registry/beam/validate_datastore_pipeline_metadata.json \
google.registry.beam.comparedb.ValidateSqlPipeline \
google/registry/beam/validate_sql_pipeline_metadata.json
google.registry.beam.comparedb.ValidateDatabasePipeline \
google/registry/beam/validate_database_pipeline_metadata.json
# Tentatively build and publish Cloud SQL schema jar here, before schema release
# process is finalized. Also publish nomulus:core jars that are needed for
# server/schema compatibility tests.

View File

@@ -30,6 +30,7 @@ dependencies {
compile deps['com.google.guava:guava']
compile deps['com.google.http-client:google-http-client']
compile deps['com.google.protobuf:protobuf-java']
compile deps['com.google.protobuf:protobuf-java-util']
compile deps['com.google.re2j:re2j']
compile deps['com.ibm.icu:icu4j']
compile deps['commons-codec:commons-codec']

View File

@@ -29,6 +29,7 @@ com.google.http-client:google-http-client-gson:1.39.2
com.google.http-client:google-http-client:1.39.2
com.google.j2objc:j2objc-annotations:1.3
com.google.oauth-client:google-oauth-client:1.31.4
com.google.protobuf:protobuf-java-util:3.17.3
com.google.protobuf:protobuf-java:3.17.3
com.google.re2j:re2j:1.6
com.ibm.icu:icu4j:68.2

View File

@@ -35,6 +35,7 @@ com.google.http-client:google-http-client-gson:1.39.2
com.google.http-client:google-http-client:1.39.2
com.google.j2objc:j2objc-annotations:1.3
com.google.oauth-client:google-oauth-client:1.31.4
com.google.protobuf:protobuf-java-util:3.17.3
com.google.protobuf:protobuf-java:3.17.3
com.google.re2j:re2j:1.6
com.google.truth:truth:1.1.2

View File

@@ -35,10 +35,9 @@ import com.google.common.net.HttpHeaders;
import com.google.common.net.MediaType;
import com.google.common.net.UrlEscapers;
import com.google.protobuf.ByteString;
import com.google.protobuf.Timestamp;
import com.google.protobuf.util.Timestamps;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
import java.util.Optional;
import java.util.Random;
@@ -53,13 +52,19 @@ public class CloudTasksUtils implements Serializable {
private static final Random random = new Random();
private final Retrier retrier;
private final Clock clock;
private final String projectId;
private final String locationId;
private final SerializableCloudTasksClient client;
public CloudTasksUtils(
Retrier retrier, String projectId, String locationId, SerializableCloudTasksClient client) {
Retrier retrier,
Clock clock,
String projectId,
String locationId,
SerializableCloudTasksClient client) {
this.retrier = retrier;
this.clock = clock;
this.projectId = projectId;
this.locationId = locationId;
this.client = client;
@@ -102,7 +107,7 @@ public class CloudTasksUtils implements Serializable {
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
*/
private static Task createTask(
private Task createTask(
String path, HttpMethod method, String service, Multimap<String, String> params) {
checkArgument(
path != null && !path.isEmpty() && path.charAt(0) == '/',
@@ -151,29 +156,26 @@ public class CloudTasksUtils implements Serializable {
* needs to be explicitly specified.
* @param params a multi-map of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param clock a source of time.
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
*/
private static Task createTask(
private Task createTaskWithJitter(
String path,
HttpMethod method,
String service,
Multimap<String, String> params,
Clock clock,
Optional<Integer> jitterSeconds) {
if (!jitterSeconds.isPresent() || jitterSeconds.get() <= 0) {
return createTask(path, method, service, params);
}
return createTask(
return createTaskWithDelay(
path,
method,
service,
params,
clock,
Duration.millis(random.nextInt((int) SECONDS.toMillis(jitterSeconds.get()))));
}
@@ -188,76 +190,67 @@ public class CloudTasksUtils implements Serializable {
* needs to be explicitly specified.
* @param params a multi-map of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param clock a source of time.
* @param delay the amount of time that a task needs to delayed for.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
*/
private static Task createTask(
private Task createTaskWithDelay(
String path,
HttpMethod method,
String service,
Multimap<String, String> params,
Clock clock,
Duration delay) {
if (delay.isEqual(Duration.ZERO)) {
return createTask(path, method, service, params);
}
checkArgument(delay.isLongerThan(Duration.ZERO), "Negative duration is not supported.");
Instant scheduleTime = Instant.ofEpochMilli(clock.nowUtc().getMillis() + delay.getMillis());
return Task.newBuilder(createTask(path, method, service, params))
.setScheduleTime(
Timestamp.newBuilder()
.setSeconds(scheduleTime.getEpochSecond())
.setNanos(scheduleTime.getNano())
.build())
.setScheduleTime(Timestamps.fromMillis(clock.nowUtc().plus(delay).getMillis()))
.build();
}
public static Task createPostTask(String path, String service, Multimap<String, String> params) {
public Task createPostTask(String path, String service, Multimap<String, String> params) {
return createTask(path, HttpMethod.POST, service, params);
}
public static Task createGetTask(String path, String service, Multimap<String, String> params) {
public Task createGetTask(String path, String service, Multimap<String, String> params) {
return createTask(path, HttpMethod.GET, service, params);
}
/**
* Create a {@link Task} via HTTP.POST that will be randomly delayed up to {@code jitterSeconds}.
*/
public static Task createPostTask(
public Task createPostTaskWithJitter(
String path,
String service,
Multimap<String, String> params,
Clock clock,
Optional<Integer> jitterSeconds) {
return createTask(path, HttpMethod.POST, service, params, clock, jitterSeconds);
return createTaskWithJitter(path, HttpMethod.POST, service, params, jitterSeconds);
}
/**
* Create a {@link Task} via HTTP.GET that will be randomly delayed up to {@code jitterSeconds}.
*/
public static Task createGetTask(
public Task createGetTaskWithJitter(
String path,
String service,
Multimap<String, String> params,
Clock clock,
Optional<Integer> jitterSeconds) {
return createTask(path, HttpMethod.GET, service, params, clock, jitterSeconds);
return createTaskWithJitter(path, HttpMethod.GET, service, params, jitterSeconds);
}
/** Create a {@link Task} via HTTP.POST that will be delayed for {@code delay}. */
public static Task createPostTask(
String path, String service, Multimap<String, String> params, Clock clock, Duration delay) {
return createTask(path, HttpMethod.POST, service, params, clock, delay);
public Task createPostTaskWithDelay(
String path, String service, Multimap<String, String> params, Duration delay) {
return createTaskWithDelay(path, HttpMethod.POST, service, params, delay);
}
/** Create a {@link Task} via HTTP.GET that will be delayed for {@code delay}. */
public static Task createGetTask(
String path, String service, Multimap<String, String> params, Clock clock, Duration delay) {
return createTask(path, HttpMethod.GET, service, params, clock, delay);
public Task createGetTaskWithDelay(
String path, String service, Multimap<String, String> params, Duration delay) {
return createTaskWithDelay(path, HttpMethod.GET, service, params, delay);
}
public abstract static class SerializableCloudTasksClient implements Serializable {

View File

@@ -43,10 +43,10 @@ public class CloudTasksUtilsTest {
// Use a LinkedListMultimap to preserve order of the inserted entries for assertion.
private final LinkedListMultimap<String, String> params = LinkedListMultimap.create();
private final SerializableCloudTasksClient mockClient = mock(SerializableCloudTasksClient.class);
private final FakeClock clock = new FakeClock(DateTime.parse("2021-11-08"));
private final CloudTasksUtils cloudTasksUtils =
new CloudTasksUtils(
new Retrier(new FakeSleeper(new FakeClock()), 1), "project", "location", mockClient);
private final Clock clock = new FakeClock(DateTime.parse("2021-11-08"));
new Retrier(new FakeSleeper(clock), 1), clock, "project", "location", mockClient);
@BeforeEach
void beforeEach() {
@@ -59,7 +59,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createGetTasks() {
Task task = CloudTasksUtils.createGetTask("/the/path", "myservice", params);
Task task = cloudTasksUtils.createGetTask("/the/path", "myservice", params);
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
@@ -70,7 +70,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createPostTasks() {
Task task = CloudTasksUtils.createPostTask("/the/path", "myservice", params);
Task task = cloudTasksUtils.createPostTask("/the/path", "myservice", params);
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -84,7 +84,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createGetTasks_withNullParams() {
Task task = CloudTasksUtils.createGetTask("/the/path", "myservice", null);
Task task = cloudTasksUtils.createGetTask("/the/path", "myservice", null);
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -94,7 +94,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createPostTasks_withNullParams() {
Task task = CloudTasksUtils.createPostTask("/the/path", "myservice", null);
Task task = cloudTasksUtils.createPostTask("/the/path", "myservice", null);
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -105,7 +105,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createGetTasks_withEmptyParams() {
Task task = CloudTasksUtils.createGetTask("/the/path", "myservice", ImmutableMultimap.of());
Task task = cloudTasksUtils.createGetTask("/the/path", "myservice", ImmutableMultimap.of());
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -115,7 +115,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createPostTasks_withEmptyParams() {
Task task = CloudTasksUtils.createPostTask("/the/path", "myservice", ImmutableMultimap.of());
Task task = cloudTasksUtils.createPostTask("/the/path", "myservice", ImmutableMultimap.of());
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -128,7 +128,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createGetTasks_withJitterSeconds() {
Task task =
CloudTasksUtils.createGetTask("/the/path", "myservice", params, clock, Optional.of(100));
cloudTasksUtils.createGetTaskWithJitter("/the/path", "myservice", params, Optional.of(100));
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
@@ -147,7 +147,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createPostTasks_withJitterSeconds() {
Task task =
CloudTasksUtils.createPostTask("/the/path", "myservice", params, clock, Optional.of(1));
cloudTasksUtils.createPostTaskWithJitter("/the/path", "myservice", params, Optional.of(1));
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -169,7 +169,8 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createPostTasks_withEmptyJitterSeconds() {
Task task =
CloudTasksUtils.createPostTask("/the/path", "myservice", params, clock, Optional.empty());
cloudTasksUtils.createPostTaskWithJitter(
"/the/path", "myservice", params, Optional.empty());
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -184,7 +185,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createGetTasks_withEmptyJitterSeconds() {
Task task =
CloudTasksUtils.createGetTask("/the/path", "myservice", params, clock, Optional.empty());
cloudTasksUtils.createGetTaskWithJitter("/the/path", "myservice", params, Optional.empty());
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
@@ -196,7 +197,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createPostTasks_withZeroJitterSeconds() {
Task task =
CloudTasksUtils.createPostTask("/the/path", "myservice", params, clock, Optional.of(0));
cloudTasksUtils.createPostTaskWithJitter("/the/path", "myservice", params, Optional.of(0));
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -211,7 +212,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createGetTasks_withZeroJitterSeconds() {
Task task =
CloudTasksUtils.createGetTask("/the/path", "myservice", params, clock, Optional.of(0));
cloudTasksUtils.createGetTaskWithJitter("/the/path", "myservice", params, Optional.of(0));
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
@@ -223,8 +224,8 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createGetTasks_withDelay() {
Task task =
CloudTasksUtils.createGetTask(
"/the/path", "myservice", params, clock, Duration.standardMinutes(10));
cloudTasksUtils.createGetTaskWithDelay(
"/the/path", "myservice", params, Duration.standardMinutes(10));
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
@@ -237,8 +238,8 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createPostTasks_withDelay() {
Task task =
CloudTasksUtils.createPostTask(
"/the/path", "myservice", params, clock, Duration.standardMinutes(10));
cloudTasksUtils.createPostTaskWithDelay(
"/the/path", "myservice", params, Duration.standardMinutes(10));
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -258,8 +259,8 @@ public class CloudTasksUtilsTest {
assertThrows(
IllegalArgumentException.class,
() ->
CloudTasksUtils.createGetTask(
"/the/path", "myservice", params, clock, Duration.standardMinutes(-10)));
cloudTasksUtils.createGetTaskWithDelay(
"/the/path", "myservice", params, Duration.standardMinutes(-10)));
assertThat(thrown).hasMessageThat().isEqualTo("Negative duration is not supported.");
}
@@ -269,15 +270,15 @@ public class CloudTasksUtilsTest {
assertThrows(
IllegalArgumentException.class,
() ->
CloudTasksUtils.createGetTask(
"/the/path", "myservice", params, clock, Duration.standardMinutes(-10)));
cloudTasksUtils.createGetTaskWithDelay(
"/the/path", "myservice", params, Duration.standardMinutes(-10)));
assertThat(thrown).hasMessageThat().isEqualTo("Negative duration is not supported.");
}
@Test
void testSuccess_createPostTasks_withZeroDelay() {
Task task =
CloudTasksUtils.createPostTask("/the/path", "myservice", params, clock, Duration.ZERO);
cloudTasksUtils.createPostTaskWithDelay("/the/path", "myservice", params, Duration.ZERO);
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
@@ -292,7 +293,7 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_createGetTasks_withZeroDelay() {
Task task =
CloudTasksUtils.createGetTask("/the/path", "myservice", params, clock, Duration.ZERO);
cloudTasksUtils.createGetTaskWithDelay("/the/path", "myservice", params, Duration.ZERO);
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
@@ -305,26 +306,26 @@ public class CloudTasksUtilsTest {
void testFailure_illegalPath() {
assertThrows(
IllegalArgumentException.class,
() -> CloudTasksUtils.createPostTask("the/path", "myservice", params));
() -> cloudTasksUtils.createPostTask("the/path", "myservice", params));
assertThrows(
IllegalArgumentException.class,
() -> CloudTasksUtils.createPostTask(null, "myservice", params));
() -> cloudTasksUtils.createPostTask(null, "myservice", params));
assertThrows(
IllegalArgumentException.class,
() -> CloudTasksUtils.createPostTask("", "myservice", params));
() -> cloudTasksUtils.createPostTask("", "myservice", params));
}
@Test
void testSuccess_enqueueTask() {
Task task = CloudTasksUtils.createGetTask("/the/path", "myservice", params);
Task task = cloudTasksUtils.createGetTask("/the/path", "myservice", params);
cloudTasksUtils.enqueue("test-queue", task);
verify(mockClient).enqueue("project", "location", "test-queue", task);
}
@Test
void testSuccess_enqueueTasks_varargs() {
Task task1 = CloudTasksUtils.createGetTask("/the/path", "myservice", params);
Task task2 = CloudTasksUtils.createGetTask("/other/path", "yourservice", params);
Task task1 = cloudTasksUtils.createGetTask("/the/path", "myservice", params);
Task task2 = cloudTasksUtils.createGetTask("/other/path", "yourservice", params);
cloudTasksUtils.enqueue("test-queue", task1, task2);
verify(mockClient).enqueue("project", "location", "test-queue", task1);
verify(mockClient).enqueue("project", "location", "test-queue", task2);
@@ -332,8 +333,8 @@ public class CloudTasksUtilsTest {
@Test
void testSuccess_enqueueTasks_iterable() {
Task task1 = CloudTasksUtils.createGetTask("/the/path", "myservice", params);
Task task2 = CloudTasksUtils.createGetTask("/other/path", "yourservice", params);
Task task1 = cloudTasksUtils.createGetTask("/the/path", "myservice", params);
Task task2 = cloudTasksUtils.createGetTask("/other/path", "yourservice", params);
cloudTasksUtils.enqueue("test-queue", ImmutableList.of(task1, task2));
verify(mockClient).enqueue("project", "location", "test-queue", task1);
verify(mockClient).enqueue("project", "location", "test-queue", task2);