Restore commit logs from other project (#1236)

* Restore commit logs from other project

Allow non-production projects to restore commit logs from another
project. This feature can be used to duplicate a realistic testing
environment.

An optional parameter is added that can override the default commit log
location.

Tested successfully in QA.
This commit is contained in:
Weimin Yu 2021-07-12 16:56:47 -04:00 committed by GitHub
parent 535f84a912
commit 62c556cebf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 54 additions and 21 deletions

View file

@ -18,8 +18,10 @@ import static com.google.appengine.api.ThreadManager.currentRequestThreadFactory
import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator;
import static google.registry.backup.ExportCommitLogDiffAction.LOWER_CHECKPOINT_TIME_PARAM; import static google.registry.backup.ExportCommitLogDiffAction.LOWER_CHECKPOINT_TIME_PARAM;
import static google.registry.backup.ExportCommitLogDiffAction.UPPER_CHECKPOINT_TIME_PARAM; import static google.registry.backup.ExportCommitLogDiffAction.UPPER_CHECKPOINT_TIME_PARAM;
import static google.registry.backup.RestoreCommitLogsAction.BUCKET_OVERRIDE_PARAM;
import static google.registry.backup.RestoreCommitLogsAction.FROM_TIME_PARAM; import static google.registry.backup.RestoreCommitLogsAction.FROM_TIME_PARAM;
import static google.registry.backup.RestoreCommitLogsAction.TO_TIME_PARAM; import static google.registry.backup.RestoreCommitLogsAction.TO_TIME_PARAM;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredDatetimeParameter; import static google.registry.request.RequestParameters.extractRequiredDatetimeParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter; import static google.registry.request.RequestParameters.extractRequiredParameter;
import static java.util.concurrent.Executors.newFixedThreadPool; import static java.util.concurrent.Executors.newFixedThreadPool;
@ -32,6 +34,7 @@ import google.registry.cron.CommitLogFanoutAction;
import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter; import google.registry.request.Parameter;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.util.Optional;
import javax.inject.Qualifier; import javax.inject.Qualifier;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -75,6 +78,12 @@ public final class BackupModule {
return extractRequiredDatetimeParameter(req, UPPER_CHECKPOINT_TIME_PARAM); return extractRequiredDatetimeParameter(req, UPPER_CHECKPOINT_TIME_PARAM);
} }
@Provides
@Parameter(BUCKET_OVERRIDE_PARAM)
static Optional<String> provideBucketOverride(HttpServletRequest req) {
return extractOptionalParameter(req, BUCKET_OVERRIDE_PARAM);
}
@Provides @Provides
@Parameter(FROM_TIME_PARAM) @Parameter(FROM_TIME_PARAM)
static DateTime provideFromTime(HttpServletRequest req) { static DateTime provideFromTime(HttpServletRequest req) {

View file

@ -32,7 +32,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import google.registry.backup.BackupModule.Backups; import google.registry.backup.BackupModule.Backups;
import google.registry.config.RegistryConfig.Config;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
@ -47,7 +46,6 @@ class GcsDiffFileLister {
private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject GcsService gcsService; @Inject GcsService gcsService;
@Inject @Config("commitLogGcsBucket") String gcsBucket;
@Inject @Backups ListeningExecutorService executor; @Inject @Backups ListeningExecutorService executor;
@Inject GcsDiffFileLister() {} @Inject GcsDiffFileLister() {}
@ -57,6 +55,7 @@ class GcsDiffFileLister {
* more files are missing. * more files are missing.
*/ */
private boolean constructDiffSequence( private boolean constructDiffSequence(
String gcsBucket,
Map<DateTime, ListenableFuture<GcsFileMetadata>> upperBoundTimesToMetadata, Map<DateTime, ListenableFuture<GcsFileMetadata>> upperBoundTimesToMetadata,
DateTime fromTime, DateTime fromTime,
DateTime lastTime, DateTime lastTime,
@ -69,7 +68,7 @@ class GcsDiffFileLister {
} else { } else {
String filename = DIFF_FILE_PREFIX + checkpointTime; String filename = DIFF_FILE_PREFIX + checkpointTime;
logger.atInfo().log("Patching GCS list; discovered file: %s", filename); logger.atInfo().log("Patching GCS list; discovered file: %s", filename);
metadata = getMetadata(filename); metadata = getMetadata(gcsBucket, filename);
// If we hit a gap, quit. // If we hit a gap, quit.
if (metadata == null) { if (metadata == null) {
@ -87,7 +86,8 @@ class GcsDiffFileLister {
return true; return true;
} }
ImmutableList<GcsFileMetadata> listDiffFiles(DateTime fromTime, @Nullable DateTime toTime) { ImmutableList<GcsFileMetadata> listDiffFiles(
String gcsBucket, DateTime fromTime, @Nullable DateTime toTime) {
logger.atInfo().log("Requested restore from time: %s", fromTime); logger.atInfo().log("Requested restore from time: %s", fromTime);
if (toTime != null) { if (toTime != null) {
logger.atInfo().log(" Until time: %s", toTime); logger.atInfo().log(" Until time: %s", toTime);
@ -111,7 +111,8 @@ class GcsDiffFileLister {
final String filename = listItems.next().getName(); final String filename = listItems.next().getName();
DateTime upperBoundTime = DateTime.parse(filename.substring(DIFF_FILE_PREFIX.length())); DateTime upperBoundTime = DateTime.parse(filename.substring(DIFF_FILE_PREFIX.length()));
if (isInRange(upperBoundTime, fromTime, toTime)) { if (isInRange(upperBoundTime, fromTime, toTime)) {
upperBoundTimesToMetadata.put(upperBoundTime, executor.submit(() -> getMetadata(filename))); upperBoundTimesToMetadata.put(
upperBoundTime, executor.submit(() -> getMetadata(gcsBucket, filename)));
lastUpperBoundTime = latestOf(upperBoundTime, lastUpperBoundTime); lastUpperBoundTime = latestOf(upperBoundTime, lastUpperBoundTime);
} }
} }
@ -130,8 +131,9 @@ class GcsDiffFileLister {
// may be missing files at the end). // may be missing files at the end).
TreeMap<DateTime, GcsFileMetadata> sequence = new TreeMap<>(); TreeMap<DateTime, GcsFileMetadata> sequence = new TreeMap<>();
logger.atInfo().log("Restoring until: %s", lastUpperBoundTime); logger.atInfo().log("Restoring until: %s", lastUpperBoundTime);
boolean inconsistentFileSet = !constructDiffSequence( boolean inconsistentFileSet =
upperBoundTimesToMetadata, fromTime, lastUpperBoundTime, sequence); !constructDiffSequence(
gcsBucket, upperBoundTimesToMetadata, fromTime, lastUpperBoundTime, sequence);
// Verify that all of the elements in the original set are represented in the sequence. If we // Verify that all of the elements in the original set are represented in the sequence. If we
// find anything that's not represented, construct a sequence for it. // find anything that's not represented, construct a sequence for it.
@ -143,7 +145,7 @@ class GcsDiffFileLister {
break; break;
} }
if (!sequence.containsKey(key)) { if (!sequence.containsKey(key)) {
constructDiffSequence(upperBoundTimesToMetadata, fromTime, key, sequence); constructDiffSequence(gcsBucket, upperBoundTimesToMetadata, fromTime, key, sequence);
checkForMoreExtraDiffs = true; checkForMoreExtraDiffs = true;
inconsistentFileSet = true; inconsistentFileSet = true;
break; break;
@ -175,7 +177,7 @@ class GcsDiffFileLister {
return DateTime.parse(metadata.getOptions().getUserMetadata().get(LOWER_BOUND_CHECKPOINT)); return DateTime.parse(metadata.getOptions().getUserMetadata().get(LOWER_BOUND_CHECKPOINT));
} }
private GcsFileMetadata getMetadata(String filename) { private GcsFileMetadata getMetadata(String gcsBucket, String filename) {
try { try {
return gcsService.getMetadata(new GcsFilename(gcsBucket, filename)); return gcsService.getMetadata(new GcsFilename(gcsBucket, filename));
} catch (IOException e) { } catch (IOException e) {

View file

@ -30,6 +30,7 @@ import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.common.DatabaseMigrationStateSchedule; import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState; import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection; import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection;
@ -82,6 +83,10 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
@Inject GcsDiffFileLister diffLister; @Inject GcsDiffFileLister diffLister;
@Inject Clock clock; @Inject Clock clock;
@Inject
@Config("commitLogGcsBucket")
String gcsBucket;
/** If true, will exit after logging the commit log files that would otherwise be replayed. */ /** If true, will exit after logging the commit log files that would otherwise be replayed. */
@Inject @Inject
@Parameter(DRY_RUN_PARAM) @Parameter(DRY_RUN_PARAM)
@ -154,7 +159,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
// If there's an inconsistent file set, this will throw IllegalStateException and the job // If there's an inconsistent file set, this will throw IllegalStateException and the job
// will try later -- this is likely because an export hasn't finished yet. // will try later -- this is likely because an export hasn't finished yet.
ImmutableList<GcsFileMetadata> commitLogFiles = ImmutableList<GcsFileMetadata> commitLogFiles =
diffLister.listDiffFiles(fromTime, /* current time */ null); diffLister.listDiffFiles(gcsBucket, fromTime, /* current time */ null);
logger.atInfo().log("Found %d new commit log files to process.", commitLogFiles.size()); logger.atInfo().log("Found %d new commit log files to process.", commitLogFiles.size());
return commitLogFiles; return commitLogFiles;
} }

View file

@ -26,6 +26,7 @@ import com.google.appengine.api.datastore.EntityTranslator;
import com.google.appengine.tools.cloudstorage.GcsFileMetadata; import com.google.appengine.tools.cloudstorage.GcsFileMetadata;
import com.google.appengine.tools.cloudstorage.GcsService; import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.PeekingIterator; import com.google.common.collect.PeekingIterator;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
@ -33,6 +34,7 @@ import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result; import com.googlecode.objectify.Result;
import com.googlecode.objectify.util.ResultNow; import com.googlecode.objectify.util.ResultNow;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment; import google.registry.config.RegistryEnvironment;
import google.registry.model.ImmutableObject; import google.registry.model.ImmutableObject;
import google.registry.model.ofy.CommitLogBucket; import google.registry.model.ofy.CommitLogBucket;
@ -50,6 +52,7 @@ import java.nio.channels.Channels;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.inject.Inject; import javax.inject.Inject;
@ -72,27 +75,41 @@ public class RestoreCommitLogsAction implements Runnable {
static final String DRY_RUN_PARAM = "dryRun"; static final String DRY_RUN_PARAM = "dryRun";
static final String FROM_TIME_PARAM = "fromTime"; static final String FROM_TIME_PARAM = "fromTime";
static final String TO_TIME_PARAM = "toTime"; static final String TO_TIME_PARAM = "toTime";
static final String BUCKET_OVERRIDE_PARAM = "gcsBucket";
private static final ImmutableSet<RegistryEnvironment> FORBIDDEN_ENVIRONMENTS =
ImmutableSet.of(RegistryEnvironment.PRODUCTION, RegistryEnvironment.SANDBOX);
@Inject GcsService gcsService; @Inject GcsService gcsService;
@Inject @Parameter(DRY_RUN_PARAM) boolean dryRun; @Inject @Parameter(DRY_RUN_PARAM) boolean dryRun;
@Inject @Parameter(FROM_TIME_PARAM) DateTime fromTime; @Inject @Parameter(FROM_TIME_PARAM) DateTime fromTime;
@Inject @Parameter(TO_TIME_PARAM) DateTime toTime; @Inject @Parameter(TO_TIME_PARAM) DateTime toTime;
@Inject
@Parameter(BUCKET_OVERRIDE_PARAM)
Optional<String> gcsBucketOverride;
@Inject DatastoreService datastoreService; @Inject DatastoreService datastoreService;
@Inject GcsDiffFileLister diffLister; @Inject GcsDiffFileLister diffLister;
@Inject
@Config("commitLogGcsBucket")
String defaultGcsBucket;
@Inject Retrier retrier; @Inject Retrier retrier;
@Inject RestoreCommitLogsAction() {} @Inject RestoreCommitLogsAction() {}
@Override @Override
public void run() { public void run() {
checkArgument( checkArgument(
RegistryEnvironment.get() == RegistryEnvironment.ALPHA !FORBIDDEN_ENVIRONMENTS.contains(RegistryEnvironment.get()),
|| RegistryEnvironment.get() == RegistryEnvironment.CRASH "DO NOT RUN IN PRODUCTION OR SANDBOX.");
|| RegistryEnvironment.get() == RegistryEnvironment.UNITTEST,
"DO NOT RUN ANYWHERE ELSE EXCEPT ALPHA, CRASH OR TESTS.");
if (dryRun) { if (dryRun) {
logger.atInfo().log("Running in dryRun mode"); logger.atInfo().log("Running in dryRun mode");
} }
List<GcsFileMetadata> diffFiles = diffLister.listDiffFiles(fromTime, toTime); String gcsBucket = gcsBucketOverride.orElse(defaultGcsBucket);
logger.atInfo().log("Restoring from %s.", gcsBucket);
List<GcsFileMetadata> diffFiles = diffLister.listDiffFiles(gcsBucket, fromTime, toTime);
if (diffFiles.isEmpty()) { if (diffFiles.isEmpty()) {
logger.atInfo().log("Nothing to restore"); logger.atInfo().log("Nothing to restore");
return; return;

View file

@ -65,7 +65,6 @@ public class GcsDiffFileListerTest {
@BeforeEach @BeforeEach
void beforeEach() throws Exception { void beforeEach() throws Exception {
diffLister.gcsService = gcsService; diffLister.gcsService = gcsService;
diffLister.gcsBucket = GCS_BUCKET;
diffLister.executor = newDirectExecutorService(); diffLister.executor = newDirectExecutorService();
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
gcsService.createOrReplace( gcsService.createOrReplace(
@ -87,7 +86,7 @@ public class GcsDiffFileListerTest {
} }
private Iterable<DateTime> listDiffFiles(DateTime fromTime, DateTime toTime) { private Iterable<DateTime> listDiffFiles(DateTime fromTime, DateTime toTime) {
return extractTimesFromDiffFiles(diffLister.listDiffFiles(fromTime, toTime)); return extractTimesFromDiffFiles(diffLister.listDiffFiles(GCS_BUCKET, fromTime, toTime));
} }
private void addGcsFile(int fileAge, int prevAge) throws IOException { private void addGcsFile(int fileAge, int prevAge) throws IOException {

View file

@ -17,7 +17,6 @@ package google.registry.backup;
import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
import static google.registry.backup.RestoreCommitLogsActionTest.GCS_BUCKET;
import static google.registry.backup.RestoreCommitLogsActionTest.createCheckpoint; import static google.registry.backup.RestoreCommitLogsActionTest.createCheckpoint;
import static google.registry.backup.RestoreCommitLogsActionTest.saveDiffFile; import static google.registry.backup.RestoreCommitLogsActionTest.saveDiffFile;
import static google.registry.backup.RestoreCommitLogsActionTest.saveDiffFileNotToRestore; import static google.registry.backup.RestoreCommitLogsActionTest.saveDiffFileNotToRestore;
@ -128,9 +127,9 @@ public class ReplayCommitLogsToSqlActionTest {
action.response = response; action.response = response;
action.requestStatusChecker = requestStatusChecker; action.requestStatusChecker = requestStatusChecker;
action.clock = fakeClock; action.clock = fakeClock;
action.gcsBucket = "gcs bucket";
action.diffLister = new GcsDiffFileLister(); action.diffLister = new GcsDiffFileLister();
action.diffLister.gcsService = gcsService; action.diffLister.gcsService = gcsService;
action.diffLister.gcsBucket = GCS_BUCKET;
action.diffLister.executor = newDirectExecutorService(); action.diffLister.executor = newDirectExecutorService();
ofyTm() ofyTm()
.transact( .transact(

View file

@ -55,6 +55,7 @@ import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -83,9 +84,10 @@ public class RestoreCommitLogsActionTest {
action.datastoreService = DatastoreServiceFactory.getDatastoreService(); action.datastoreService = DatastoreServiceFactory.getDatastoreService();
action.fromTime = now.minusMillis(1); action.fromTime = now.minusMillis(1);
action.retrier = new Retrier(new FakeSleeper(new FakeClock()), 1); action.retrier = new Retrier(new FakeSleeper(new FakeClock()), 1);
action.defaultGcsBucket = GCS_BUCKET;
action.gcsBucketOverride = Optional.empty();
action.diffLister = new GcsDiffFileLister(); action.diffLister = new GcsDiffFileLister();
action.diffLister.gcsService = gcsService; action.diffLister.gcsService = gcsService;
action.diffLister.gcsBucket = GCS_BUCKET;
action.diffLister.executor = newDirectExecutorService(); action.diffLister.executor = newDirectExecutorService();
} }