Delete NoteDb migration code

Change-Id: I31b79a49f46cd60671db8de5b2378d43c6eed9cd
This commit is contained in:
Dave Borowitz
2018-12-07 16:06:42 -08:00
parent 42f153af24
commit 771b967cd5
10 changed files with 3 additions and 3393 deletions

View File

@@ -15,7 +15,6 @@
package com.google.gerrit.pgm;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
@@ -85,8 +84,6 @@ import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.receive.MailReceiver;
import com.google.gerrit.server.mail.send.SmtpEmailSender;
import com.google.gerrit.server.mime.MimeUtil2Module;
import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
import com.google.gerrit.server.notedb.rebuild.OnlineNoteDbMigrator;
import com.google.gerrit.server.patch.DiffExecutorModule;
import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
@@ -126,7 +123,6 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jgit.lib.Config;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
/** Run SSH daemon portions of Gerrit. */
public class Daemon extends SiteProgram {
@@ -175,15 +171,6 @@ public class Daemon extends SiteProgram {
@Option(name = "--stop-only", usage = "Stop the daemon", hidden = true)
private boolean stopOnly;
@Option(
name = "--migrate-to-note-db",
usage = "Automatically migrate changes to NoteDb",
handler = ExplicitBooleanOptionHandler.class)
private boolean migrateToNoteDb;
@Option(name = "--trial", usage = "(With --migrate-to-note-db) " + MigrateToNoteDb.TRIAL_USAGE)
private boolean trial;
private final LifecycleManager manager = new LifecycleManager();
private Injector dbInjector;
private Injector cfgInjector;
@@ -492,9 +479,6 @@ public class Daemon extends SiteProgram {
modules.add(new AccountDeactivator.Module());
modules.add(new ChangeCleanupRunner.Module());
}
if (migrateToNoteDb()) {
modules.add(new OnlineNoteDbMigrator.Module(trial));
}
if (testSysModule != null) {
modules.add(testSysModule);
}
@@ -504,18 +488,11 @@ public class Daemon extends SiteProgram {
ModuleOverloader.override(modules, LibModuleLoader.loadModules(cfgInjector)));
}
private boolean migrateToNoteDb() {
return migrateToNoteDb || NoteDbMigrator.getAutoMigrate(requireNonNull(config));
}
private Module createIndexModule() {
if (luceneModule != null) {
return luceneModule;
}
boolean onlineUpgrade =
VersionManager.getOnlineUpgrade(config)
// Schema upgrade is handled by OnlineNoteDbMigrator in this case.
&& !migrateToNoteDb();
boolean onlineUpgrade = VersionManager.getOnlineUpgrade(config);
switch (indexType) {
case LUCENE:
return onlineUpgrade

View File

@@ -1,195 +0,0 @@
// Copyright (C) 2014 The Android Open Source Project
//
// 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 com.google.gerrit.pgm;
import static com.google.common.base.MoreObjects.firstNonNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.pgm.util.BatchProgramModule;
import com.google.gerrit.pgm.util.RuntimeShutdown;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.git.GarbageCollection;
import com.google.gerrit.server.index.DummyIndexModule;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.notedb.rebuild.GcAllUsers;
import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
// TODO(dborowitz): Delete this program.
public class MigrateToNoteDb extends SiteProgram {
static final String TRIAL_USAGE =
"Trial mode: migrate changes and turn on reading from NoteDb, but leave ReviewDb as the"
+ " source of truth";
@Option(name = "--threads", usage = "Number of threads to use for rebuilding NoteDb")
private int threads = Runtime.getRuntime().availableProcessors();
@Option(
name = "--project",
usage =
"Only rebuild these projects, do no other migration; incompatible with --change;"
+ " recommended for debugging only")
private List<String> projects = new ArrayList<>();
@Option(
name = "--change",
usage =
"Only rebuild these changes, do no other migration; incompatible with --project;"
+ " recommended for debugging only")
private List<Integer> changes = new ArrayList<>();
@Option(
name = "--force",
usage =
"Force rebuilding changes where ReviewDb is still the source of truth, even if they"
+ " were previously migrated")
private boolean force;
@Option(name = "--trial", usage = TRIAL_USAGE)
private boolean trial;
@Option(
name = "--sequence-gap",
usage =
"gap in change sequence numbers between last ReviewDb number and first NoteDb number;"
+ " negative indicates using the value of noteDb.changes.initialSequenceGap (default"
+ " 1000)")
private int sequenceGap;
@Option(
name = "--reindex",
usage =
"Reindex all changes after migration; defaults to false in trial mode, true otherwise",
handler = ExplicitBooleanOptionHandler.class)
private Boolean reindex;
private Injector dbInjector;
private Injector sysInjector;
private LifecycleManager dbManager;
private LifecycleManager sysManager;
@Inject private GcAllUsers gcAllUsers;
@Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
@Override
public int run() throws Exception {
RuntimeShutdown.add(this::stop);
try {
mustHaveValidSite();
dbInjector = createDbInjector();
dbManager = new LifecycleManager();
dbManager.add(dbInjector);
dbManager.start();
sysInjector = createSysInjector();
sysInjector.injectMembers(this);
sysManager = new LifecycleManager();
sysManager.add(sysInjector);
sysInjector
.getInstance(PluginGuiceEnvironment.class)
.setDbCfgInjector(dbInjector, dbInjector);
sysManager.start();
try (NoteDbMigrator migrator =
migratorBuilderProvider
.get()
.setThreads(threads)
.setProgressOut(System.err)
.setProjects(projects.stream().map(Project.NameKey::new).collect(toList()))
.setChanges(changes.stream().map(Change.Id::new).collect(toList()))
.setTrialMode(trial)
.setForceRebuild(force)
.setSequenceGap(sequenceGap)
.build()) {
if (!projects.isEmpty() || !changes.isEmpty()) {
migrator.rebuild();
} else {
migrator.migrate();
}
}
try (PrintWriter w = new PrintWriter(new OutputStreamWriter(System.out, UTF_8), true)) {
gcAllUsers.run(w);
}
} finally {
stop();
}
boolean reindex = firstNonNull(this.reindex, !trial);
if (!reindex) {
return 0;
}
// Reindex all indices, to save the user from having to run yet another program by hand while
// their server is offline.
List<String> reindexArgs =
ImmutableList.of(
"--site-path",
getSitePath().toString(),
"--threads",
Integer.toString(threads),
"--index",
ChangeSchemaDefinitions.NAME);
System.out.println("Migration complete, reindexing changes with:");
System.out.println(" reindex " + reindexArgs.stream().collect(joining(" ")));
Reindex reindexPgm = new Reindex();
return reindexPgm.main(reindexArgs.stream().toArray(String[]::new));
}
private Injector createSysInjector() {
return dbInjector.createChildInjector(
new FactoryModule() {
@Override
public void configure() {
install(dbInjector.getInstance(BatchProgramModule.class));
install(new DummyIndexModule());
factory(ChangeResource.Factory.class);
factory(GarbageCollection.Factory.class);
}
});
}
private void stop() {
try {
LifecycleManager m = sysManager;
sysManager = null;
if (m != null) {
m.stop();
}
} finally {
LifecycleManager m = dbManager;
dbManager = null;
if (m != null) {
m.stop();
}
}
}
}

View File

@@ -48,9 +48,7 @@ public class GerritServerIdProvider implements Provider<String> {
// We're not generally supposed to do work in provider constructors, but this is a bit of a
// special case because we really need to have the ID available by the time the dbInjector
// is created. This even applies during MigrateToNoteDb, which otherwise would have been a
// reasonable place to do the ID generation. Fortunately, it's not much work, and it happens
// once.
// is created. Fortunately, it's not much work, and it happens once.
id = generate();
Config newCfg = readGerritConfig(sitePaths);
newCfg.setString(SECTION, null, KEY, id);

View File

@@ -46,8 +46,7 @@ import org.eclipse.jgit.lib.Config;
* <p>This class controls the state of the migration according to options in {@code gerrit.config}.
* In general, any changes to these options should only be made by adventurous administrators, who
* know what they're doing, on non-production data, for the purposes of testing the NoteDb
* implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For
* these reasons, the options remain undocumented.
* implementation.
*
* <p><strong>Note:</strong> Callers should not assume the values returned by {@code
* NotesMigration}'s methods will not change in a running server.

View File

@@ -1,510 +0,0 @@
// Copyright (C) 2017 The Android Open Source Project
//
// 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 com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.RepoRefCache;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.OrmRuntimeException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
/** Helper to migrate the {@link PrimaryStorage} of individual changes. */
@Singleton
public class PrimaryStorageMigrator {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* Exception thrown during migration if the change has no {@code noteDbState} field at the
* beginning of the migration.
*/
public static class NoNoteDbStateException extends RuntimeException {
private static final long serialVersionUID = 1L;
private NoNoteDbStateException(Change.Id id) {
super("change " + id + " has no note_db_state; rebuild it first");
}
}
private final AllUsersName allUsers;
private final ChangeNotes.Factory changeNotesFactory;
private final ChangeRebuilder rebuilder;
private final ChangeUpdate.Factory updateFactory;
private final GitRepositoryManager repoManager;
private final InternalUser.Factory internalUserFactory;
private final Provider<InternalChangeQuery> queryProvider;
private final Provider<ReviewDb> db;
private final RetryHelper retryHelper;
private final long skewMs;
private final long timeoutMs;
private final Retryer<NoteDbChangeState> testEnsureRebuiltRetryer;
@Inject
PrimaryStorageMigrator(
@GerritServerConfig Config cfg,
Provider<ReviewDb> db,
GitRepositoryManager repoManager,
AllUsersName allUsers,
ChangeRebuilder rebuilder,
ChangeNotes.Factory changeNotesFactory,
Provider<InternalChangeQuery> queryProvider,
ChangeUpdate.Factory updateFactory,
InternalUser.Factory internalUserFactory,
RetryHelper retryHelper) {
this(
cfg,
db,
repoManager,
allUsers,
rebuilder,
null,
changeNotesFactory,
queryProvider,
updateFactory,
internalUserFactory,
retryHelper);
}
@VisibleForTesting
public PrimaryStorageMigrator(
Config cfg,
Provider<ReviewDb> db,
GitRepositoryManager repoManager,
AllUsersName allUsers,
ChangeRebuilder rebuilder,
@Nullable Retryer<NoteDbChangeState> testEnsureRebuiltRetryer,
ChangeNotes.Factory changeNotesFactory,
Provider<InternalChangeQuery> queryProvider,
ChangeUpdate.Factory updateFactory,
InternalUser.Factory internalUserFactory,
RetryHelper retryHelper) {
this.db = db;
this.repoManager = repoManager;
this.allUsers = allUsers;
this.rebuilder = rebuilder;
this.testEnsureRebuiltRetryer = testEnsureRebuiltRetryer;
this.changeNotesFactory = changeNotesFactory;
this.queryProvider = queryProvider;
this.updateFactory = updateFactory;
this.internalUserFactory = internalUserFactory;
this.retryHelper = retryHelper;
skewMs = NoteDbChangeState.getReadOnlySkew(cfg);
String s = "notedb";
timeoutMs =
cfg.getTimeUnit(
s,
null,
"primaryStorageMigrationTimeout",
MILLISECONDS.convert(60, SECONDS),
MILLISECONDS);
}
/**
* Migrate a change's primary storage from ReviewDb to NoteDb.
*
* <p>This method will return only if the primary storage of the change is NoteDb afterwards. (It
* may return early if the primary storage was already NoteDb.)
*
* <p>If this method throws an exception, then the primary storage of the change is probably not
* NoteDb. (It is possible that the primary storage of the change is NoteDb in this case, but
* there was an error reading the state.) Moreover, after an exception, the change may be
* read-only until a lease expires. If the caller chooses to retry, they should wait until the
* read-only lease expires; this method will fail relatively quickly if called on a read-only
* change.
*
* <p>Note that if the change is read-only after this method throws an exception, that does not
* necessarily guarantee that the read-only lease was acquired during that particular method
* invocation; this call may have in fact failed because another thread acquired the lease first.
*
* @param id change ID.
* @throws OrmException if a ReviewDb-level error occurs.
* @throws IOException if a repo-level error occurs.
*/
public void migrateToNoteDbPrimary(Change.Id id) throws OrmException, IOException {
// Since there are multiple non-atomic steps in this method, we need to
// consider what happens when there is another writer concurrent with the
// thread executing this method.
//
// Let:
// * OR = other writer writes noteDbState & new data to ReviewDb (in one
// transaction)
// * ON = other writer writes to NoteDb
// * MRO = migrator sets state to read-only
// * MR = ensureRebuilt writes rebuilt noteDbState to ReviewDb (but does not
// otherwise update ReviewDb in this transaction)
// * MN = ensureRebuilt writes rebuilt state to NoteDb
//
// Consider all the interleavings of these operations.
//
// * OR,ON,MRO,...
// Other writer completes before migrator begins; this is not a concurrent
// write.
// * MRO,...,OR,...
// OR will fail, since it atomically checks that the noteDbState is not
// read-only before proceeding. This results in an exception, but not a
// concurrent write.
//
// Thus all the "interesting" interleavings start with OR,MRO, and differ on
// where ON falls relative to MR/MN.
//
// * OR,MRO,ON,MR,MN
// The other NoteDb write succeeds despite the noteDbState being
// read-only. Because the read-only state from MRO includes the update
// from OR, the change is up-to-date at this point. Thus MR,MN is a no-op.
// The end result is an up-to-date, read-only change.
//
// * OR,MRO,MR,ON,MN
// The change is out-of-date when ensureRebuilt begins, because OR
// succeeded but the corresponding ON has not happened yet. ON will
// succeed, because there have been no intervening NoteDb writes. MN will
// fail, because ON updated the state in NoteDb to something other than
// what MR claimed. This leaves the change in an out-of-date, read-only
// state.
//
// If this method threw an exception in this case, the change would
// eventually switch back to read-write when the read-only lease expires,
// so this situation is recoverable. However, it would be inconvenient for
// a change to be read-only for so long.
//
// Thus, as an optimization, we have a retry loop that attempts
// ensureRebuilt while still holding the same read-only lease. This
// effectively results in the interleaving OR,MR,ON,MR,MN; in contrast
// with the previous case, here, MR/MN actually rebuilds the change. In
// the case of a write failure, MR/MN might fail and get retried again. If
// it exceeds the maximum number of retries, an exception is thrown.
//
// * OR,MRO,MR,MN,ON
// The change is out-of-date when ensureRebuilt begins. The change is
// rebuilt, leaving a new state in NoteDb. ON will fail, because the old
// NoteDb state has changed since the ref state was read when the update
// began (prior to OR). This results in an exception from ON, but the end
// result is still an up-to-date, read-only change. The end user that
// initiated the other write observes an error, but this is no different
// from other errors that need retrying, e.g. due to a backend write
// failure.
Stopwatch sw = Stopwatch.createStarted();
Change readOnlyChange = setReadOnlyInReviewDb(id); // MRO
if (readOnlyChange == null) {
return; // Already migrated.
}
NoteDbChangeState rebuiltState;
try {
// MR,MN
rebuiltState =
ensureRebuiltRetryer(sw)
.call(
() ->
ensureRebuilt(
readOnlyChange.getProject(),
id,
NoteDbChangeState.parse(readOnlyChange)));
} catch (RetryException | ExecutionException e) {
throw new OrmException(e);
}
// At this point, the noteDbState in ReviewDb is read-only, and it is
// guaranteed to match the state actually in NoteDb. Now it is safe to set
// the primary storage to NoteDb.
setPrimaryStorageNoteDb(id, rebuiltState);
logger.atFine().log(
"Migrated change %s to NoteDb primary in %sms", id, sw.elapsed(MILLISECONDS));
}
private Change setReadOnlyInReviewDb(Change.Id id) throws OrmException {
AtomicBoolean alreadyMigrated = new AtomicBoolean(false);
Change result =
db().changes()
.atomicUpdate(
id,
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
NoteDbChangeState state = NoteDbChangeState.parse(change);
if (state == null) {
// Could rebuild the change here, but that's more complexity, and this
// normally shouldn't happen.
//
// Known cases where this happens are described in and handled by
// NoteDbMigrator#canSkipPrimaryStorageMigration.
throw new NoNoteDbStateException(id);
}
// If the change is already read-only, then the lease is held by another
// (likely failed) migrator thread. Fail early, as we can't take over
// the lease.
NoteDbChangeState.checkNotReadOnly(change, skewMs);
if (state.getPrimaryStorage() != PrimaryStorage.NOTE_DB) {
Timestamp now = TimeUtil.nowTs();
Timestamp until = new Timestamp(now.getTime() + timeoutMs);
change.setNoteDbState(state.withReadOnlyUntil(until).toString());
} else {
alreadyMigrated.set(true);
}
return change;
}
});
return alreadyMigrated.get() ? null : result;
}
private Retryer<NoteDbChangeState> ensureRebuiltRetryer(Stopwatch sw) {
if (testEnsureRebuiltRetryer != null) {
return testEnsureRebuiltRetryer;
}
// Retry the ensureRebuilt step with backoff until half the timeout has
// expired, leaving the remaining half for the rest of the steps.
long remainingNanos = (MILLISECONDS.toNanos(timeoutMs) / 2) - sw.elapsed(NANOSECONDS);
remainingNanos = Math.max(remainingNanos, 0);
return RetryerBuilder.<NoteDbChangeState>newBuilder()
.retryIfException(e -> (e instanceof IOException) || (e instanceof OrmException))
.withWaitStrategy(
WaitStrategies.join(
WaitStrategies.exponentialWait(250, MILLISECONDS),
WaitStrategies.randomWait(50, MILLISECONDS)))
.withStopStrategy(StopStrategies.stopAfterDelay(remainingNanos, NANOSECONDS))
.build();
}
private NoteDbChangeState ensureRebuilt(
Project.NameKey project, Change.Id id, NoteDbChangeState readOnlyState)
throws IOException, OrmException, RepositoryNotFoundException {
try (Repository changeRepo = repoManager.openRepository(project);
Repository allUsersRepo = repoManager.openRepository(allUsers)) {
if (!readOnlyState.isUpToDate(new RepoRefCache(changeRepo), new RepoRefCache(allUsersRepo))) {
NoteDbUpdateManager.Result r = rebuilder.rebuildEvenIfReadOnly(db(), id);
checkState(
r.newState().getReadOnlyUntil().equals(readOnlyState.getReadOnlyUntil()),
"state after rebuilding has different read-only lease: %s != %s",
r.newState(),
readOnlyState);
readOnlyState = r.newState();
}
}
return readOnlyState;
}
private void setPrimaryStorageNoteDb(Change.Id id, NoteDbChangeState expectedState)
throws OrmException {
db().changes()
.atomicUpdate(
id,
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
NoteDbChangeState state = NoteDbChangeState.parse(change);
if (!Objects.equals(state, expectedState)) {
throw new OrmRuntimeException(badState(state, expectedState));
}
Timestamp until = state.getReadOnlyUntil().get();
if (TimeUtil.nowTs().after(until)) {
throw new OrmRuntimeException(
"read-only lease on change " + id + " expired at " + until);
}
change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
return change;
}
});
}
private ReviewDb db() {
return ReviewDbUtil.unwrapDb(db.get());
}
private String badState(NoteDbChangeState actual, NoteDbChangeState expected) {
return "state changed unexpectedly: " + actual + " != " + expected;
}
public void migrateToReviewDbPrimary(Change.Id id, @Nullable Project.NameKey project)
throws OrmException, IOException {
// Migrating back to ReviewDb primary is much simpler than the original migration to NoteDb
// primary, because when NoteDb is primary, each write only goes to one storage location rather
// than both. We only need to consider whether a concurrent writer (OR) conflicts with the first
// setReadOnlyInNoteDb step (MR) in this method.
//
// If OR wins, then either:
// * MR will set read-only after OR is completed, which is not a concurrent write.
// * MR will fail to set read-only with a lock failure. The caller will have to retry, but the
// change is not in a read-only state, so behavior is not degraded in the meantime.
//
// If MR wins, then either:
// * OR will fail with a read-only exception (via AbstractChangeNotes#apply).
// * OR will fail with a lock failure.
//
// In all of these scenarios, the change is read-only if and only if MR succeeds.
//
// There will be no concurrent writes to ReviewDb for this change until
// setPrimaryStorageReviewDb completes, because ReviewDb writes are not attempted when primary
// storage is NoteDb. After the primary storage changes back, it is possible for subsequent
// NoteDb writes to conflict with the releaseReadOnlyLeaseInNoteDb step, but at this point,
// since ReviewDb is primary, we are back to ignoring them.
Stopwatch sw = Stopwatch.createStarted();
if (project == null) {
project = getProject(id);
}
ObjectId newMetaId = setReadOnlyInNoteDb(project, id);
rebuilder.rebuildReviewDb(db(), project, id);
setPrimaryStorageReviewDb(id, newMetaId);
releaseReadOnlyLeaseInNoteDb(project, id);
logger.atFine().log(
"Migrated change %s to ReviewDb primary in %sms", id, sw.elapsed(MILLISECONDS));
}
private ObjectId setReadOnlyInNoteDb(Project.NameKey project, Change.Id id)
throws OrmException, IOException {
Timestamp now = TimeUtil.nowTs();
Timestamp until = new Timestamp(now.getTime() + timeoutMs);
ChangeUpdate update =
updateFactory.create(
changeNotesFactory.createChecked(db.get(), project, id), internalUserFactory.create());
update.setReadOnlyUntil(until);
return update.commit();
}
private void setPrimaryStorageReviewDb(Change.Id id, ObjectId newMetaId)
throws OrmException, IOException {
ImmutableMap.Builder<Account.Id, ObjectId> draftIds = ImmutableMap.builder();
try (Repository repo = repoManager.openRepository(allUsers)) {
for (Ref draftRef :
repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(id))) {
Account.Id accountId = Account.Id.fromRef(draftRef.getName());
if (accountId != null) {
draftIds.put(accountId, draftRef.getObjectId().copy());
}
}
}
NoteDbChangeState newState =
new NoteDbChangeState(
id,
PrimaryStorage.REVIEW_DB,
Optional.of(RefState.create(newMetaId, draftIds.build())),
Optional.empty());
db().changes()
.atomicUpdate(
id,
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (PrimaryStorage.of(change) != PrimaryStorage.NOTE_DB) {
throw new OrmRuntimeException(
"change " + id + " is not NoteDb primary: " + change.getNoteDbState());
}
change.setNoteDbState(newState.toString());
return change;
}
});
}
private void releaseReadOnlyLeaseInNoteDb(Project.NameKey project, Change.Id id)
throws OrmException {
// Use a BatchUpdate since ReviewDb is primary at this point, so it needs to reflect the update.
// (In practice retrying won't happen, since we aren't using fused updates at this point.)
try {
retryHelper.execute(
updateFactory -> {
try (BatchUpdate bu =
updateFactory.create(
db.get(), project, internalUserFactory.create(), TimeUtil.nowTs())) {
bu.addOp(
id,
new BatchUpdateOp() {
@Override
public boolean updateChange(ChangeContext ctx) {
ctx.getUpdate(ctx.getChange().currentPatchSetId())
.setReadOnlyUntil(new Timestamp(0));
return true;
}
});
bu.execute();
return null;
}
});
} catch (RestApiException | UpdateException e) {
throw new OrmException(e);
}
}
private Project.NameKey getProject(Change.Id id) throws OrmException {
List<ChangeData> cds =
queryProvider.get().setRequestedFields(ChangeField.PROJECT).byLegacyChangeId(id);
Set<Project.NameKey> projects = new TreeSet<>();
for (ChangeData cd : cds) {
projects.add(cd.project());
}
if (projects.size() != 1) {
throw new OrmException(
"zero or multiple projects found for change "
+ id
+ ", must specify project explicitly: "
+ projects);
}
return projects.iterator().next();
}
}

View File

@@ -1,108 +0,0 @@
// Copyright (C) 2017 The Android Open Source Project
//
// 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 com.google.gerrit.server.notedb.rebuild;
import com.google.common.base.Stopwatch;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.OnlineUpgrader;
import com.google.gerrit.server.index.VersionManager;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
@Singleton
public class OnlineNoteDbMigrator implements LifecycleListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String TRIAL = "OnlineNoteDbMigrator/trial";
public static class Module extends LifecycleModule {
private final boolean trial;
public Module(boolean trial) {
this.trial = trial;
}
@Override
public void configure() {
listener().to(OnlineNoteDbMigrator.class);
bindConstant().annotatedWith(Names.named(TRIAL)).to(trial);
}
}
private final GcAllUsers gcAllUsers;
private final OnlineUpgrader indexUpgrader;
private final Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
private final boolean upgradeIndex;
private final boolean trial;
@Inject
OnlineNoteDbMigrator(
@GerritServerConfig Config cfg,
GcAllUsers gcAllUsers,
OnlineUpgrader indexUpgrader,
Provider<NoteDbMigrator.Builder> migratorBuilderProvider,
@Named(TRIAL) boolean trial) {
this.gcAllUsers = gcAllUsers;
this.indexUpgrader = indexUpgrader;
this.migratorBuilderProvider = migratorBuilderProvider;
this.upgradeIndex = VersionManager.getOnlineUpgrade(cfg);
this.trial = trial || NoteDbMigrator.getTrialMode(cfg);
}
@Override
public void start() {
Thread t = new Thread(this::migrate);
t.setDaemon(true);
t.setName(getClass().getSimpleName());
t.start();
}
private void migrate() {
logger.atInfo().log("Starting online NoteDb migration");
if (upgradeIndex) {
logger.atInfo().log(
"Online index schema upgrades will be deferred until NoteDb migration is complete");
}
Stopwatch sw = Stopwatch.createStarted();
// TODO(dborowitz): Tune threads, maybe expose a progress monitor somewhere.
try (NoteDbMigrator migrator =
migratorBuilderProvider.get().setAutoMigrate(true).setTrialMode(trial).build()) {
migrator.migrate();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Error in online NoteDb migration");
}
gcAllUsers.runWithLogger();
logger.atInfo().log("Online NoteDb migration completed in %ss", sw.elapsed(TimeUnit.SECONDS));
if (upgradeIndex) {
logger.atInfo().log("Starting deferred index schema upgrades");
indexUpgrader.start();
}
}
@Override
public void stop() {
// Do nothing; upgrade process uses daemon threads and knows how to recover from failures on
// next attempt.
}
}

View File

@@ -1,363 +0,0 @@
// Copyright (C) 2014 The Android Open Source Project
//
// 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 com.google.gerrit.acceptance.pgm;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.StandaloneSiteTest;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
import com.google.gerrit.server.index.GerritIndexStatus;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.notedb.NoteDbChangeState;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
import com.google.gerrit.server.notedb.NotesMigrationState;
import com.google.gerrit.server.schema.ReviewDbFactory;
import com.google.gerrit.testing.NoteDbMode;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for NoteDb migrations where the entry point is through a program, {@code
* migrate-to-note-db} or {@code daemon}.
*
* <p><strong>Note:</strong> These tests are very slow due to the repeated daemon startup. Prefer
* adding tests to {@link com.google.gerrit.acceptance.server.notedb.OnlineNoteDbMigrationIT} if
* possible.
*/
@NoHttpd
public class StandaloneNoteDbMigrationIT extends StandaloneSiteTest {
private StoredConfig gerritConfig;
private StoredConfig noteDbConfig;
private Project.NameKey project;
private Change.Id changeId;
@Before
public void setUp() throws Exception {
assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.OFF);
gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
// Unlike in the running server, for tests, we don't stack notedb.config on gerrit.config.
noteDbConfig = new FileBasedConfig(sitePaths.notedb_config.toFile(), FS.detect());
// Set gc.pruneExpire=now so GC prunes all unreachable objects from All-Users, which allows us
// to reliably test that it behaves as expected.
Path cfgPath = sitePaths.site_path.resolve("git").resolve("All-Users.git").resolve("config");
assertWithMessage("Expected All-Users config at %s", cfgPath)
.that(Files.isRegularFile(cfgPath))
.isTrue();
FileBasedConfig cfg = new FileBasedConfig(cfgPath.toFile(), FS.detect());
cfg.setString("gc", null, "pruneExpire", "now");
cfg.save();
}
@Test
public void rebuildOneChangeTrialMode() throws Exception {
assertNoAutoMigrateConfig(gerritConfig);
assertNoAutoMigrateConfig(noteDbConfig);
assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
setUpOneChange();
migrate("--trial");
assertNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE);
try (ServerContext ctx = startServer()) {
GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
ObjectId metaId;
try (Repository repo = repoManager.openRepository(project)) {
Ref ref = repo.exactRef(RefNames.changeMetaRef(changeId));
assertThat(ref).isNotNull();
metaId = ref.getObjectId();
}
try (ReviewDb db = openUnderlyingReviewDb(ctx)) {
Change c = db.changes().get(changeId);
assertThat(c).isNotNull();
NoteDbChangeState state = NoteDbChangeState.parse(c);
assertThat(state).isNotNull();
assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
assertThat(state.getRefState()).hasValue(RefState.create(metaId, ImmutableMap.of()));
}
}
}
@Test
public void migrateOneChange() throws Exception {
assertNoAutoMigrateConfig(gerritConfig);
assertNoAutoMigrateConfig(noteDbConfig);
assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
setUpOneChange();
migrate();
assertNotesMigrationState(NotesMigrationState.NOTE_DB);
File allUsersDir;
try (ServerContext ctx = startServer()) {
GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
try (Repository repo = repoManager.openRepository(project)) {
assertThat(repo.exactRef(RefNames.changeMetaRef(changeId))).isNotNull();
}
assertThat(repoManager).isInstanceOf(LocalDiskRepositoryManager.class);
try (Repository repo =
repoManager.openRepository(ctx.getInjector().getInstance(AllUsersName.class))) {
allUsersDir = repo.getDirectory();
}
try (ReviewDb db = openUnderlyingReviewDb(ctx)) {
Change c = db.changes().get(changeId);
assertThat(c).isNotNull();
NoteDbChangeState state = NoteDbChangeState.parse(c);
assertThat(state).isNotNull();
assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.NOTE_DB);
assertThat(state.getRefState()).isEmpty();
ChangeInput in = new ChangeInput(project.get(), "master", "NoteDb-only change");
in.newBranch = true;
GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
Change.Id id2 = new Change.Id(gApi.changes().create(in).info()._number);
assertThat(db.changes().get(id2)).isNull();
}
}
assertNoAutoMigrateConfig(gerritConfig);
assertAutoMigrateConfig(noteDbConfig, false);
try (FileRepository repo = new FileRepository(allUsersDir)) {
try (Stream<Path> paths = Files.walk(repo.getObjectsDirectory().toPath())) {
assertThat(paths.filter(p -> !p.toString().contains("pack") && Files.isRegularFile(p)))
.named("loose object files in All-Users")
.isEmpty();
}
assertThat(repo.getObjectDatabase().getPacks()).named("packfiles in All-Users").hasSize(1);
}
}
@Test
public void migrationWithReindex() throws Exception {
assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
setUpOneChange();
int version = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
GerritIndexStatus status = new GerritIndexStatus(sitePaths);
assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue();
status.setReady(ChangeSchemaDefinitions.NAME, version, false);
status.save();
assertServerStartupFails();
migrate();
assertNotesMigrationState(NotesMigrationState.NOTE_DB);
status = new GerritIndexStatus(sitePaths);
assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue();
}
@Test
public void onlineMigrationViaDaemon() throws Exception {
assertNoAutoMigrateConfig(gerritConfig);
assertNoAutoMigrateConfig(noteDbConfig);
testOnlineMigration(u -> startServer(u.module(), "--migrate-to-note-db", "true"));
assertNoAutoMigrateConfig(gerritConfig);
assertAutoMigrateConfig(noteDbConfig, false);
}
@Test
public void onlineMigrationViaConfig() throws Exception {
assertNoAutoMigrateConfig(gerritConfig);
assertNoAutoMigrateConfig(noteDbConfig);
testOnlineMigration(
u -> {
gerritConfig.setBoolean("noteDb", "changes", "autoMigrate", true);
gerritConfig.save();
return startServer(u.module());
});
// Auto-migration is turned off in notedb.config, which takes precedence, but is still on in
// gerrit.config. This means Puppet can continue overwriting gerrit.config without turning
// auto-migration back on.
assertAutoMigrateConfig(gerritConfig, true);
assertAutoMigrateConfig(noteDbConfig, false);
}
@Test
public void onlineMigrationTrialModeViaFlag() throws Exception {
assertNoAutoMigrateConfig(gerritConfig);
assertNoTrialConfig(gerritConfig);
assertNoAutoMigrateConfig(noteDbConfig);
assertNoTrialConfig(noteDbConfig);
testOnlineMigration(
u -> startServer(u.module(), "--migrate-to-note-db", "--trial"),
NotesMigrationState.READ_WRITE_NO_SEQUENCE);
assertNoAutoMigrateConfig(gerritConfig);
assertNoTrialConfig(gerritConfig);
assertAutoMigrateConfig(noteDbConfig, true);
assertTrialConfig(noteDbConfig, true);
}
@Test
public void onlineMigrationTrialModeViaConfig() throws Exception {
assertNoAutoMigrateConfig(gerritConfig);
assertNoTrialConfig(gerritConfig);
assertNoAutoMigrateConfig(noteDbConfig);
assertNoTrialConfig(noteDbConfig);
testOnlineMigration(
u -> {
gerritConfig.setBoolean("noteDb", "changes", "autoMigrate", true);
gerritConfig.setBoolean("noteDb", "changes", "trial", true);
gerritConfig.save();
return startServer(u.module());
},
NotesMigrationState.READ_WRITE_NO_SEQUENCE);
assertAutoMigrateConfig(gerritConfig, true);
assertTrialConfig(gerritConfig, true);
assertAutoMigrateConfig(noteDbConfig, true);
assertTrialConfig(noteDbConfig, true);
}
@FunctionalInterface
private interface StartServerWithMigration {
ServerContext start(IndexUpgradeController u) throws Exception;
}
private void testOnlineMigration(StartServerWithMigration start) throws Exception {
testOnlineMigration(start, NotesMigrationState.NOTE_DB);
}
private void testOnlineMigration(
StartServerWithMigration start, NotesMigrationState expectedEndState) throws Exception {
assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
// Before storing any changes, switch back to the previous version.
GerritIndexStatus status = new GerritIndexStatus(sitePaths);
status.setReady(ChangeSchemaDefinitions.NAME, currVersion, false);
status.setReady(ChangeSchemaDefinitions.NAME, prevVersion, true);
status.save();
setOnlineUpgradeConfig(false);
setUpOneChange();
setOnlineUpgradeConfig(true);
IndexUpgradeController u = new IndexUpgradeController(1);
try (ServerContext ctx = start.start(u)) {
ChangeIndexCollection indexes = ctx.getInjector().getInstance(ChangeIndexCollection.class);
assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(prevVersion);
// Index schema upgrades happen after NoteDb migration, so waiting for those to complete
// should be sufficient.
u.runUpgrades();
assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(currVersion);
assertNotesMigrationState(expectedEndState);
}
}
private void setUpOneChange() throws Exception {
project = new Project.NameKey("project");
try (ServerContext ctx = startServer()) {
GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
gApi.projects().create("project");
ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
in.newBranch = true;
changeId = new Change.Id(gApi.changes().create(in).info()._number);
}
}
private void migrate(String... additionalArgs) throws Exception {
runGerrit(
ImmutableList.of(
"migrate-to-note-db", "-d", sitePaths.site_path.toString(), "--show-stack-trace"),
ImmutableList.copyOf(additionalArgs));
}
private void assertNotesMigrationState(NotesMigrationState expected) throws Exception {
noteDbConfig.load();
assertThat(NotesMigrationState.forConfig(noteDbConfig)).hasValue(expected);
}
private ReviewDb openUnderlyingReviewDb(ServerContext ctx) throws Exception {
return ctx.getInjector()
.getInstance(Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}, ReviewDbFactory.class))
.open();
}
private static void assertNoAutoMigrateConfig(StoredConfig cfg) throws Exception {
cfg.load();
assertThat(cfg.getString("noteDb", "changes", "autoMigrate")).isNull();
}
private static void assertAutoMigrateConfig(StoredConfig cfg, boolean expected) throws Exception {
cfg.load();
assertThat(cfg.getString("noteDb", "changes", "autoMigrate")).isNotNull();
assertThat(cfg.getBoolean("noteDb", "changes", "autoMigrate", false)).isEqualTo(expected);
}
private static void assertNoTrialConfig(StoredConfig cfg) throws Exception {
cfg.load();
assertThat(cfg.getString("noteDb", "changes", "trial")).isNull();
}
private static void assertTrialConfig(StoredConfig cfg, boolean expected) throws Exception {
cfg.load();
assertThat(cfg.getString("noteDb", "changes", "trial")).isNotNull();
assertThat(cfg.getBoolean("noteDb", "changes", "trial", false)).isEqualTo(expected);
}
private void setOnlineUpgradeConfig(boolean enable) throws Exception {
gerritConfig.load();
gerritConfig.setBoolean("index", null, "onlineUpgrade", enable);
gerritConfig.save();
}
}

View File

@@ -1,524 +0,0 @@
// Copyright (C) 2016 The Android Open Source Project
//
// 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 com.google.gerrit.acceptance.server.notedb;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
import static com.google.gerrit.server.notedb.NoteDbUtil.formatTime;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.git.RepoRefCache;
import com.google.gerrit.server.notedb.ChangeBundle;
import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.NoteDbChangeState;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.NoteDbMode;
import com.google.gerrit.testing.TestTimeUtil;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.OrmRuntimeException;
import com.google.inject.Inject;
import com.google.inject.util.Providers;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@NoHttpd
public class NoteDbPrimaryIT extends AbstractDaemonTest {
@ConfigSuite.Default
public static Config defaultConfig() {
Config cfg = new Config();
cfg.setString("notedb", null, "concurrentWriterTimeout", "0s");
cfg.setString("notedb", null, "primaryStorageMigrationTimeout", "1d");
cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true);
return cfg;
}
@Inject private ChangeBundleReader bundleReader;
@Inject private CommentsUtil commentsUtil;
@Inject private TestChangeRebuilderWrapper rebuilderWrapper;
@Inject private ChangeNotes.Factory changeNotesFactory;
@Inject private ChangeUpdate.Factory updateFactory;
@Inject private InternalUser.Factory internalUserFactory;
@Inject private RetryHelper retryHelper;
private PrimaryStorageMigrator migrator;
@Before
public void setUp() throws Exception {
assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.READ_WRITE);
db = ReviewDbUtil.unwrapDb(db);
TestTimeUtil.resetWithClockStep(1, SECONDS);
migrator = newMigrator(null);
}
private PrimaryStorageMigrator newMigrator(
@Nullable Retryer<NoteDbChangeState> ensureRebuiltRetryer) {
return new PrimaryStorageMigrator(
cfg,
Providers.of(db),
repoManager,
allUsers,
rebuilderWrapper,
ensureRebuiltRetryer,
changeNotesFactory,
queryProvider,
updateFactory,
internalUserFactory,
retryHelper);
}
@After
public void tearDown() {
TestTimeUtil.useSystemTime();
}
@Test
public void updateChange() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
setNoteDbPrimary(id);
gApi.changes().id(id.get()).current().review(ReviewInput.approve());
gApi.changes().id(id.get()).current().submit();
ChangeInfo info = gApi.changes().id(id.get()).get();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
ApprovalInfo approval = Iterables.getOnlyElement(info.labels.get("Code-Review").all);
assertThat(approval._accountId).isEqualTo(admin.id.get());
assertThat(approval.value).isEqualTo(2);
assertThat(info.messages).hasSize(3);
assertThat(Iterables.getLast(info.messages).message)
.isEqualTo("Change has been successfully merged by " + admin.fullName);
ChangeNotes notes = notesFactory.create(db, project, id);
assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
assertThat(notes.getChange().getNoteDbState())
.isEqualTo(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
// Writes weren't reflected in ReviewDb.
assertThat(db.changes().get(id).getStatus()).isEqualTo(Change.Status.NEW);
assertThat(db.patchSetApprovals().byChange(id)).isEmpty();
assertThat(db.changeMessages().byChange(id)).hasSize(1);
}
@Test
public void deleteDraftComment() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
setNoteDbPrimary(id);
DraftInput din = new DraftInput();
din.path = PushOneCommit.FILE_NAME;
din.line = 1;
din.message = "A comment";
gApi.changes().id(id.get()).current().createDraft(din);
CommentInfo di =
Iterables.getOnlyElement(
gApi.changes().id(id.get()).current().drafts().get(PushOneCommit.FILE_NAME));
assertThat(di.message).isEqualTo(din.message);
assertThat(db.patchComments().draftByChangeFileAuthor(id, din.path, admin.id)).isEmpty();
gApi.changes().id(id.get()).current().draft(di.id).delete();
assertThat(gApi.changes().id(id.get()).current().drafts()).isEmpty();
}
@Test
public void deleteVote() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
setNoteDbPrimary(id);
gApi.changes().id(id.get()).current().review(ReviewInput.approve());
List<ApprovalInfo> approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
assertThat(approvals).hasSize(1);
assertThat(approvals.get(0).value).isEqualTo(2);
gApi.changes().id(id.get()).reviewer(admin.id.toString()).deleteVote("Code-Review");
approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
assertThat(approvals).hasSize(1);
assertThat(approvals.get(0).value).isEqualTo(0);
}
@Test
public void deleteVoteViaReview() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
setNoteDbPrimary(id);
gApi.changes().id(id.get()).current().review(ReviewInput.approve());
List<ApprovalInfo> approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
assertThat(approvals).hasSize(1);
assertThat(approvals.get(0).value).isEqualTo(2);
gApi.changes().id(id.get()).current().review(ReviewInput.noScore());
approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
assertThat(approvals).hasSize(1);
assertThat(approvals.get(0).value).isEqualTo(0);
}
@Test
public void deleteReviewer() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
setNoteDbPrimary(id);
gApi.changes().id(id.get()).addReviewer(user.id.toString());
assertThat(getReviewers(id)).containsExactly(user.id);
gApi.changes().id(id.get()).reviewer(user.id.toString()).remove();
assertThat(getReviewers(id)).isEmpty();
}
@Test
public void readOnlyReviewDb() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
testReadOnly(id);
}
@Test
public void readOnlyNoteDb() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
setNoteDbPrimary(id);
testReadOnly(id);
}
private void testReadOnly(Change.Id id) throws Exception {
Timestamp before = TimeUtil.nowTs();
Timestamp until = new Timestamp(before.getTime() + 1000 * 3600);
// Set read-only.
Change c = db.changes().get(id);
assertThat(c).named("change " + id).isNotNull();
NoteDbChangeState state = NoteDbChangeState.parse(c);
state = state.withReadOnlyUntil(until);
c.setNoteDbState(state.toString());
db.changes().update(Collections.singleton(c));
assertThat(gApi.changes().id(id.get()).get().subject).isEqualTo(PushOneCommit.SUBJECT);
assertThat(gApi.changes().id(id.get()).get().topic).isNull();
try {
gApi.changes().id(id.get()).topic("a-topic");
fail("expected read-only exception");
} catch (RestApiException e) {
Optional<Throwable> oe =
Throwables.getCausalChain(e)
.stream()
.filter(x -> x instanceof OrmRuntimeException)
.findFirst();
assertThat(oe).named("OrmRuntimeException in causal chain of " + e).isPresent();
assertThat(oe.get().getMessage()).contains("read-only");
}
assertThat(gApi.changes().id(id.get()).get().topic).isNull();
TestTimeUtil.setClock(new Timestamp(until.getTime() + 1000));
assertThat(gApi.changes().id(id.get()).get().subject).isEqualTo(PushOneCommit.SUBJECT);
gApi.changes().id(id.get()).topic("a-topic");
assertThat(gApi.changes().id(id.get()).get().topic).isEqualTo("a-topic");
}
@Test
public void migrateToNoteDb() throws Exception {
testMigrateToNoteDb(createChange().getChange().getId());
}
@Test
public void migrateToNoteDbWithRebuildingFirst() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
Change c = db.changes().get(id);
c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
db.changes().update(Collections.singleton(c));
testMigrateToNoteDb(id);
}
private void testMigrateToNoteDb(Change.Id id) throws Exception {
assertThat(PrimaryStorage.of(db.changes().get(id))).isEqualTo(PrimaryStorage.REVIEW_DB);
migrator.migrateToNoteDbPrimary(id);
assertNoteDbPrimary(id);
gApi.changes().id(id.get()).topic("a-topic");
assertThat(gApi.changes().id(id.get()).get().topic).isEqualTo("a-topic");
assertThat(db.changes().get(id).getTopic()).isNull();
}
@Test
public void migrateToNoteDbFailsRebuildingOnceAndRetries() throws Exception {
Change.Id id = createChange().getChange().getId();
Change c = db.changes().get(id);
c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
db.changes().update(Collections.singleton(c));
rebuilderWrapper.failNextUpdate();
migrator =
newMigrator(
RetryerBuilder.<NoteDbChangeState>newBuilder()
.retryIfException()
.withStopStrategy(StopStrategies.neverStop())
.build());
migrator.migrateToNoteDbPrimary(id);
assertNoteDbPrimary(id);
}
@Test
public void migrateToNoteDbFailsRebuildingAndStops() throws Exception {
Change.Id id = createChange().getChange().getId();
Change c = db.changes().get(id);
c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
db.changes().update(Collections.singleton(c));
rebuilderWrapper.failNextUpdate();
migrator =
newMigrator(
RetryerBuilder.<NoteDbChangeState>newBuilder()
.retryIfException()
.withStopStrategy(StopStrategies.stopAfterAttempt(1))
.build());
exception.expect(OrmException.class);
exception.expectMessage("Retrying failed");
migrator.migrateToNoteDbPrimary(id);
}
@Test
public void migrateToNoteDbMissingOldState() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
Change c = db.changes().get(id);
c.setNoteDbState(null);
db.changes().update(Collections.singleton(c));
exception.expect(PrimaryStorageMigrator.NoNoteDbStateException.class);
exception.expectMessage("no note_db_state");
migrator.migrateToNoteDbPrimary(id);
}
@Test
public void migrateToNoteDbLeaseExpires() throws Exception {
TestTimeUtil.resetWithClockStep(2, DAYS);
exception.expect(OrmRuntimeException.class);
exception.expectMessage("read-only lease");
migrator.migrateToNoteDbPrimary(createChange().getChange().getId());
}
@Test
public void migrateToNoteDbAlreadyReadOnly() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
Change c = db.changes().get(id);
NoteDbChangeState state = NoteDbChangeState.parse(c);
Timestamp until = new Timestamp(TimeUtil.nowMs() + MILLISECONDS.convert(1, DAYS));
state = state.withReadOnlyUntil(until);
c.setNoteDbState(state.toString());
db.changes().update(Collections.singleton(c));
exception.expect(OrmRuntimeException.class);
exception.expectMessage("read-only until " + until);
migrator.migrateToNoteDbPrimary(id);
}
@Test
public void migrateToNoteDbAlreadyMigrated() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
assertThat(PrimaryStorage.of(db.changes().get(id))).isEqualTo(PrimaryStorage.REVIEW_DB);
migrator.migrateToNoteDbPrimary(id);
assertNoteDbPrimary(id);
migrator.migrateToNoteDbPrimary(id);
assertNoteDbPrimary(id);
}
@Test
public void rebuildReviewDb() throws Exception {
Change c = createChange().getChange().change();
Change.Id id = c.getId();
CommentInput cin = new CommentInput();
cin.line = 1;
cin.message = "Published comment";
ReviewInput rin = ReviewInput.approve();
rin.comments = ImmutableMap.of(PushOneCommit.FILE_NAME, ImmutableList.of(cin));
gApi.changes().id(id.get()).current().review(ReviewInput.approve());
DraftInput din = new DraftInput();
din.path = PushOneCommit.FILE_NAME;
din.line = 1;
din.message = "Draft comment";
gApi.changes().id(id.get()).current().createDraft(din);
gApi.changes().id(id.get()).current().review(ReviewInput.approve());
gApi.changes().id(id.get()).current().createDraft(din);
assertThat(db.changeMessages().byChange(id)).isNotEmpty();
assertThat(db.patchSets().byChange(id)).isNotEmpty();
assertThat(db.patchSetApprovals().byChange(id)).isNotEmpty();
assertThat(db.patchComments().byChange(id)).isNotEmpty();
ChangeBundle noteDbBundle =
ChangeBundle.fromNotes(commentsUtil, notesFactory.create(db, project, id));
setNoteDbPrimary(id);
db.changeMessages().delete(db.changeMessages().byChange(id));
db.patchSets().delete(db.patchSets().byChange(id));
db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
db.patchComments().delete(db.patchComments().byChange(id));
ChangeMessage bogusMessage =
ChangeMessagesUtil.newMessage(
c.currentPatchSetId(),
identifiedUserFactory.create(admin.getId()),
TimeUtil.nowTs(),
"some message",
null);
db.changeMessages().insert(Collections.singleton(bogusMessage));
rebuilderWrapper.rebuildReviewDb(db, project, id);
assertThat(db.changeMessages().byChange(id)).isNotEmpty();
assertThat(db.patchSets().byChange(id)).isNotEmpty();
assertThat(db.patchSetApprovals().byChange(id)).isNotEmpty();
assertThat(db.patchComments().byChange(id)).isNotEmpty();
ChangeBundle reviewDbBundle = bundleReader.fromReviewDb(ReviewDbUtil.unwrapDb(db), id);
assertThat(reviewDbBundle.differencesFrom(noteDbBundle)).isEmpty();
}
@Test
public void migrateBackToReviewDbPrimary() throws Exception {
Change c = createChange().getChange().change();
Change.Id id = c.getId();
migrator.migrateToNoteDbPrimary(id);
assertNoteDbPrimary(id);
gApi.changes().id(id.get()).topic("new-topic");
assertThat(gApi.changes().id(id.get()).topic()).isEqualTo("new-topic");
assertThat(db.changes().get(id).getTopic()).isNotEqualTo("new-topic");
migrator.migrateToReviewDbPrimary(id, null);
ObjectId metaId;
try (Repository repo = repoManager.openRepository(c.getProject());
RevWalk rw = new RevWalk(repo)) {
metaId = repo.exactRef(RefNames.changeMetaRef(id)).getObjectId();
RevCommit commit = rw.parseCommit(metaId);
rw.parseBody(commit);
assertThat(commit.getFullMessage())
.contains("Read-only-until: " + formatTime(serverIdent.get(), new Timestamp(0)));
}
NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
assertThat(state.getChangeMetaId()).isEqualTo(metaId);
assertThat(gApi.changes().id(id.get()).topic()).isEqualTo("new-topic");
assertThat(db.changes().get(id).getTopic()).isEqualTo("new-topic");
ChangeNotes notes = notesFactory.create(db, project, id);
assertThat(notes.getRevision()).isEqualTo(metaId); // No rebuilding, change was up to date.
assertThat(notes.getReadOnlyUntil()).isNotNull();
gApi.changes().id(id.get()).topic("reviewdb-topic");
assertThat(db.changes().get(id).getTopic()).isEqualTo("reviewdb-topic");
}
private void setNoteDbPrimary(Change.Id id) throws Exception {
Change c = db.changes().get(id);
assertThat(c).named("change " + id).isNotNull();
NoteDbChangeState state = NoteDbChangeState.parse(c);
assertThat(state.getPrimaryStorage()).named("storage of " + id).isEqualTo(REVIEW_DB);
try (Repository changeRepo = repoManager.openRepository(c.getProject());
Repository allUsersRepo = repoManager.openRepository(allUsers)) {
assertThat(state.isUpToDate(new RepoRefCache(changeRepo), new RepoRefCache(allUsersRepo)))
.named("change " + id + " up to date")
.isTrue();
}
c.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
db.changes().update(Collections.singleton(c));
}
private void assertNoteDbPrimary(Change.Id id) throws Exception {
assertThat(PrimaryStorage.of(db.changes().get(id))).isEqualTo(PrimaryStorage.NOTE_DB);
}
private List<Account.Id> getReviewers(Change.Id id) throws Exception {
return gApi.changes()
.id(id.get())
.get()
.reviewers
.values()
.stream()
.flatMap(Collection::stream)
.map(a -> new Account.Id(a._accountId))
.collect(toList());
}
}

View File

@@ -1,629 +0,0 @@
// Copyright (C) 2017 The Android Open Source Project
//
// 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 com.google.gerrit.acceptance.server.notedb;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.server.notedb.NoteDbChangeState.NOTE_DB_PRIMARY_STATE;
import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB;
import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY;
import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY;
import static com.google.gerrit.server.notedb.NotesMigrationState.REVIEW_DB;
import static com.google.gerrit.server.notedb.NotesMigrationState.WRITE;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.naturalOrder;
import static org.easymock.EasyMock.createStrictMock;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.notedb.ChangeBundle;
import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.NoteDbChangeState;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
import com.google.gerrit.server.notedb.NotesMigrationState;
import com.google.gerrit.server.notedb.rebuild.MigrationException;
import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
import com.google.gerrit.server.notedb.rebuild.NotesMigrationStateListener;
import com.google.gerrit.server.schema.ReviewDbFactory;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.NoteDbMode;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@Sandboxed
@UseLocalDisk
@NoHttpd
public class OnlineNoteDbMigrationIT extends AbstractDaemonTest {
private static final String INVALID_STATE = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
@Inject private ProjectOperations projectOperations;
@ConfigSuite.Default
public static Config defaultConfig() {
Config cfg = new Config();
cfg.setInt("noteDb", "changes", "sequenceBatchSize", 10);
cfg.setInt("noteDb", "changes", "initialSequenceGap", 500);
return cfg;
}
// Tests in this class are generally interested in the actual ReviewDb contents, but the shifting
// migration state may result in various kinds of wrappers showing up unexpectedly.
@Inject @ReviewDbFactory private SchemaFactory<ReviewDb> schemaFactory;
@Inject private ChangeBundleReader changeBundleReader;
@Inject private CommentsUtil commentsUtil;
@Inject private DynamicSet<NotesMigrationStateListener> listeners;
@Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
@Inject private Sequences sequences;
@Inject private SitePaths sitePaths;
private FileBasedConfig noteDbConfig;
private List<RegistrationHandle> addedListeners;
@Before
public void setUp() throws Exception {
assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.OFF);
// Unlike in the running server, for tests, we don't stack notedb.config on gerrit.config.
noteDbConfig = new FileBasedConfig(sitePaths.notedb_config.toFile(), FS.detect());
assertNotesMigrationState(REVIEW_DB, false, false);
addedListeners = new ArrayList<>();
}
@After
public void tearDown() throws Exception {
if (addedListeners != null) {
addedListeners.forEach(RegistrationHandle::remove);
addedListeners = null;
}
}
@Test
public void preconditionsFail() throws Exception {
List<Change.Id> cs = ImmutableList.of(new Change.Id(1));
List<Project.NameKey> ps = ImmutableList.of(new Project.NameKey("p"));
assertMigrationException(
"Cannot rebuild without noteDb.changes.write=true", b -> b, NoteDbMigrator::rebuild);
assertMigrationException(
"Cannot set both changes and projects", b -> b.setChanges(cs).setProjects(ps), m -> {});
assertMigrationException(
"Cannot set changes or projects during full migration",
b -> b.setChanges(cs),
NoteDbMigrator::migrate);
assertMigrationException(
"Cannot set changes or projects during full migration",
b -> b.setProjects(ps),
NoteDbMigrator::migrate);
setNotesMigrationState(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY);
assertMigrationException(
"Migration has already progressed past the endpoint of the \"trial mode\" state",
b -> b.setTrialMode(true),
NoteDbMigrator::migrate);
setNotesMigrationState(READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
assertMigrationException(
"Cannot force rebuild changes; NoteDb is already the primary storage for some changes",
b -> b.setForceRebuild(true),
NoteDbMigrator::migrate);
}
@Test
@GerritConfig(name = "noteDb.changes.initialSequenceGap", value = "-7")
public void initialSequenceGapMustBeNonNegative() throws Exception {
setNotesMigrationState(READ_WRITE_NO_SEQUENCE);
assertMigrationException("Sequence gap must be non-negative: -7", b -> b, m -> {});
}
@Test
public void rebuildOneChangeTrialModeAndForceRebuild() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
migrate(b -> b.setTrialMode(true));
assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
ObjectId oldMetaId;
try (Repository repo = repoManager.openRepository(project);
ReviewDb db = schemaFactory.open()) {
Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
assertThat(ref).isNotNull();
oldMetaId = ref.getObjectId();
Change c = db.changes().get(id);
assertThat(c).isNotNull();
NoteDbChangeState state = NoteDbChangeState.parse(c);
assertThat(state).isNotNull();
assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
assertThat(state.getRefState()).hasValue(RefState.create(oldMetaId, ImmutableMap.of()));
// Force change to be out of date, and change topic so it will get rebuilt as something other
// than oldMetaId.
c.setNoteDbState(INVALID_STATE);
c.setTopic(name("a-new-topic"));
db.changes().update(ImmutableList.of(c));
}
migrate(b -> b.setTrialMode(true));
assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
try (Repository repo = repoManager.openRepository(project);
ReviewDb db = schemaFactory.open()) {
// Change is out of date, but was not rebuilt without forceRebuild.
assertThat(repo.exactRef(RefNames.changeMetaRef(id)).getObjectId()).isEqualTo(oldMetaId);
Change c = db.changes().get(id);
assertThat(c.getNoteDbState()).isEqualTo(INVALID_STATE);
}
migrate(b -> b.setTrialMode(true).setForceRebuild(true));
assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
try (Repository repo = repoManager.openRepository(project);
ReviewDb db = schemaFactory.open()) {
Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
assertThat(ref).isNotNull();
ObjectId newMetaId = ref.getObjectId();
assertThat(newMetaId).isNotEqualTo(oldMetaId);
NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
assertThat(state).isNotNull();
assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
assertThat(state.getRefState()).hasValue(RefState.create(newMetaId, ImmutableMap.of()));
}
}
@Test
public void autoMigrateTrialMode() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
migrate(b -> b.setAutoMigrate(true).setTrialMode(true).setStopAtStateForTesting(WRITE));
assertNotesMigrationState(WRITE, true, true);
migrate(b -> b);
// autoMigrate is still enabled so that we can continue the migration by only unsetting trial.
assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, true, true);
ObjectId metaId;
try (Repository repo = repoManager.openRepository(project);
ReviewDb db = schemaFactory.open()) {
Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
assertThat(ref).isNotNull();
metaId = ref.getObjectId();
NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
assertThat(state).isNotNull();
assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
assertThat(state.getRefState()).hasValue(RefState.create(metaId, ImmutableMap.of()));
}
// Unset trial mode and the next migration runs to completion.
noteDbConfig.load();
NoteDbMigrator.setTrialMode(noteDbConfig, false);
noteDbConfig.save();
migrate(b -> b);
assertNotesMigrationState(NOTE_DB, false, false);
try (Repository repo = repoManager.openRepository(project);
ReviewDb db = schemaFactory.open()) {
Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
assertThat(ref).isNotNull();
assertThat(ref.getObjectId()).isEqualTo(metaId);
NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
assertThat(state).isNotNull();
assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.NOTE_DB);
}
}
@Test
public void rebuildSubsetOfChanges() throws Exception {
setNotesMigrationState(WRITE);
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
Change.Id id1 = r1.getChange().getId();
Change.Id id2 = r2.getChange().getId();
try (ReviewDb db = schemaFactory.open()) {
Change c1 = db.changes().get(id1);
c1.setNoteDbState(INVALID_STATE);
Change c2 = db.changes().get(id2);
c2.setNoteDbState(INVALID_STATE);
db.changes().update(ImmutableList.of(c1, c2));
}
migrate(b -> b.setChanges(ImmutableList.of(id2)), NoteDbMigrator::rebuild);
try (ReviewDb db = schemaFactory.open()) {
NoteDbChangeState s1 = NoteDbChangeState.parse(db.changes().get(id1));
assertThat(s1.getChangeMetaId().name()).isEqualTo(INVALID_STATE);
NoteDbChangeState s2 = NoteDbChangeState.parse(db.changes().get(id2));
assertThat(s2.getChangeMetaId().name()).isNotEqualTo(INVALID_STATE);
}
}
@Test
public void rebuildSubsetOfProjects() throws Exception {
setNotesMigrationState(WRITE);
Project.NameKey p2 = projectOperations.newProject().create();
TestRepository<?> tr2 = cloneProject(p2, admin);
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = pushFactory.create(db, admin.getIdent(), tr2).to("refs/for/master");
Change.Id id1 = r1.getChange().getId();
Change.Id id2 = r2.getChange().getId();
String invalidState = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
try (ReviewDb db = schemaFactory.open()) {
Change c1 = db.changes().get(id1);
c1.setNoteDbState(invalidState);
Change c2 = db.changes().get(id2);
c2.setNoteDbState(invalidState);
db.changes().update(ImmutableList.of(c1, c2));
}
migrate(b -> b.setProjects(ImmutableList.of(p2)), NoteDbMigrator::rebuild);
try (ReviewDb db = schemaFactory.open()) {
NoteDbChangeState s1 = NoteDbChangeState.parse(db.changes().get(id1));
assertThat(s1.getChangeMetaId().name()).isEqualTo(invalidState);
NoteDbChangeState s2 = NoteDbChangeState.parse(db.changes().get(id2));
assertThat(s2.getChangeMetaId().name()).isNotEqualTo(invalidState);
}
}
@Test
public void enableSequencesNoGap() throws Exception {
testEnableSequences(0, 3, "13");
}
@Test
public void enableSequencesWithGap() throws Exception {
testEnableSequences(-1, 502, "512");
}
private void testEnableSequences(int builderOption, int expectedFirstId, String expectedRefValue)
throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
assertThat(id.get()).isEqualTo(1);
migrate(
b ->
b.setSequenceGap(builderOption)
.setStopAtStateForTesting(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY));
assertThat(sequences.nextChangeId()).isEqualTo(expectedFirstId);
assertThat(sequences.nextChangeId()).isEqualTo(expectedFirstId + 1);
try (Repository repo = repoManager.openRepository(allProjects);
ObjectReader reader = repo.newObjectReader()) {
Ref ref = repo.exactRef("refs/sequences/changes");
assertThat(ref).isNotNull();
ObjectLoader loader = reader.open(ref.getObjectId());
assertThat(loader.getType()).isEqualTo(Constants.OBJ_BLOB);
// Acquired a block of 10 to serve the first nextChangeId call after migration.
assertThat(new String(loader.getCachedBytes(), UTF_8)).isEqualTo(expectedRefValue);
}
try (ReviewDb db = schemaFactory.open()) {
// Underlying, unused ReviewDb is still on its own sequence.
@SuppressWarnings("deprecation")
int nextFromReviewDb = db.nextChangeId();
assertThat(nextFromReviewDb).isEqualTo(3);
}
}
@Test
public void fullMigrationSameThread() throws Exception {
testFullMigration(1);
}
@Test
public void fullMigrationMultipleThreads() throws Exception {
testFullMigration(2);
}
private void testFullMigration(int threads) throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
Change.Id id1 = r1.getChange().getId();
Change.Id id2 = r2.getChange().getId();
Set<String> objectFiles = getObjectFiles(project);
assertThat(objectFiles).isNotEmpty();
migrate(b -> b.setThreads(threads));
assertNotesMigrationState(NOTE_DB, false, false);
assertThat(sequences.nextChangeId()).isEqualTo(503);
assertThat(getObjectFiles(project)).containsExactlyElementsIn(objectFiles);
ObjectId oldMetaId = null;
int rowVersion = 0;
try (ReviewDb db = schemaFactory.open();
Repository repo = repoManager.openRepository(project)) {
for (Change.Id id : ImmutableList.of(id1, id2)) {
String refName = RefNames.changeMetaRef(id);
Ref ref = repo.exactRef(refName);
assertThat(ref).named(refName).isNotNull();
Change c = db.changes().get(id);
assertThat(c.getTopic()).named("topic of change %s", id).isNull();
NoteDbChangeState s = NoteDbChangeState.parse(c);
assertThat(s.getPrimaryStorage())
.named("primary storage of change %s", id)
.isEqualTo(PrimaryStorage.NOTE_DB);
assertThat(s.getRefState()).named("ref state of change %s").isEmpty();
if (id.equals(id1)) {
oldMetaId = ref.getObjectId();
rowVersion = c.getRowVersion();
}
}
}
// Do not open a new context, to simulate races with other threads that opened a context earlier
// in the migration process; this needs to work.
gApi.changes().id(id1.get()).topic(name("a-topic"));
// Of course, it should also work with a new context.
resetCurrentApiUser();
gApi.changes().id(id1.get()).topic(name("another-topic"));
try (ReviewDb db = schemaFactory.open();
Repository repo = repoManager.openRepository(project)) {
assertThat(repo.exactRef(RefNames.changeMetaRef(id1)).getObjectId()).isNotEqualTo(oldMetaId);
Change c = db.changes().get(id1);
assertThat(c.getTopic()).isNull();
assertThat(c.getRowVersion()).isEqualTo(rowVersion);
}
}
@Test
public void fullMigrationOneChangeWithNoPatchSets() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
Change.Id id1 = r1.getChange().getId();
Change.Id id2 = r2.getChange().getId();
db.changes().beginTransaction(id2);
try {
db.patchSets().delete(db.patchSets().byChange(id2));
db.commit();
} finally {
db.rollback();
}
migrate(b -> b);
assertNotesMigrationState(NOTE_DB, false, false);
try (ReviewDb db = schemaFactory.open();
Repository repo = repoManager.openRepository(project)) {
assertThat(repo.exactRef(RefNames.changeMetaRef(id1))).isNotNull();
assertThat(db.changes().get(id1).getNoteDbState()).isEqualTo(NOTE_DB_PRIMARY_STATE);
// A change with no patch sets is so corrupt that it is completely skipped by the migration
// process.
assertThat(repo.exactRef(RefNames.changeMetaRef(id2))).isNull();
assertThat(db.changes().get(id2).getNoteDbState()).isNull();
}
}
@Test
public void fullMigrationMissingPatchSetRefs() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
try (Repository repo = repoManager.openRepository(project)) {
RefUpdate u = repo.updateRef(new PatchSet.Id(id, 1).toRefName());
u.setForceUpdate(true);
assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
ChangeBundle reviewDbBundle;
try (ReviewDb db = schemaFactory.open()) {
reviewDbBundle = changeBundleReader.fromReviewDb(db, id);
}
migrate(b -> b);
assertNotesMigrationState(NOTE_DB, false, false);
try (ReviewDb db = schemaFactory.open();
Repository repo = repoManager.openRepository(project)) {
// Change migrated successfully even though it was missing patch set refs.
assertThat(repo.exactRef(RefNames.changeMetaRef(id))).isNotNull();
assertThat(db.changes().get(id).getNoteDbState()).isEqualTo(NOTE_DB_PRIMARY_STATE);
ChangeBundle noteDbBundle =
ChangeBundle.fromNotes(commentsUtil, notesFactory.createChecked(db, project, id));
assertThat(noteDbBundle.differencesFrom(reviewDbBundle)).isEmpty();
}
}
@Test
public void autoMigrationConfig() throws Exception {
createChange();
migrate(b -> b.setStopAtStateForTesting(WRITE));
assertNotesMigrationState(WRITE, false, false);
migrate(b -> b.setAutoMigrate(true).setStopAtStateForTesting(READ_WRITE_NO_SEQUENCE));
assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, true, false);
migrate(b -> b);
assertNotesMigrationState(NOTE_DB, false, false);
}
@Test
public void notesMigrationStateListener() throws Exception {
NotesMigrationStateListener listener = createStrictMock(NotesMigrationStateListener.class);
listener.preStateChange(REVIEW_DB, WRITE);
expectLastCall();
listener.preStateChange(WRITE, READ_WRITE_NO_SEQUENCE);
expectLastCall();
listener.preStateChange(READ_WRITE_NO_SEQUENCE, READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY);
expectLastCall();
listener.preStateChange(
READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY, READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
listener.preStateChange(READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY, NOTE_DB);
expectLastCall();
replay(listener);
addListener(listener);
createChange();
migrate(b -> b);
assertNotesMigrationState(NOTE_DB, false, false);
verify(listener);
}
@Test
public void notesMigrationStateListenerFails() throws Exception {
NotesMigrationStateListener listener = createStrictMock(NotesMigrationStateListener.class);
listener.preStateChange(REVIEW_DB, WRITE);
expectLastCall();
listener.preStateChange(WRITE, READ_WRITE_NO_SEQUENCE);
IOException listenerException = new IOException("Listener failed");
expectLastCall().andThrow(listenerException);
replay(listener);
addListener(listener);
createChange();
try {
migrate(b -> b);
fail("expected IOException");
} catch (IOException e) {
assertThat(e).isSameAs(listenerException);
}
assertNotesMigrationState(WRITE, false, false);
verify(listener);
}
private void assertNotesMigrationState(
NotesMigrationState expected, boolean autoMigrate, boolean trialMode) throws Exception {
assertThat(NotesMigrationState.forNotesMigration(notesMigration)).hasValue(expected);
noteDbConfig.load();
assertThat(NotesMigrationState.forConfig(noteDbConfig)).hasValue(expected);
assertThat(NoteDbMigrator.getAutoMigrate(noteDbConfig))
.named("noteDb.changes.autoMigrate")
.isEqualTo(autoMigrate);
assertThat(NoteDbMigrator.getTrialMode(noteDbConfig))
.named("noteDb.changes.trial")
.isEqualTo(trialMode);
}
private void setNotesMigrationState(NotesMigrationState state) throws Exception {
noteDbConfig.load();
state.setConfigValues(noteDbConfig);
noteDbConfig.save();
notesMigration.setFrom(state);
}
@FunctionalInterface
interface PrepareBuilder {
NoteDbMigrator.Builder prepare(NoteDbMigrator.Builder b) throws Exception;
}
@FunctionalInterface
interface RunMigration {
void run(NoteDbMigrator m) throws Exception;
}
private void migrate(PrepareBuilder b) throws Exception {
migrate(b, NoteDbMigrator::migrate);
}
private void migrate(PrepareBuilder b, RunMigration m) throws Exception {
try (NoteDbMigrator migrator = b.prepare(migratorBuilderProvider.get()).build()) {
m.run(migrator);
}
}
private void assertMigrationException(
String expectMessageContains, PrepareBuilder b, RunMigration m) throws Exception {
try {
migrate(b, m);
fail("expected MigrationException");
} catch (MigrationException e) {
assertThat(e).hasMessageThat().contains(expectMessageContains);
}
}
private void addListener(NotesMigrationStateListener listener) {
addedListeners.add(listeners.add("gerrit", listener));
}
private ImmutableSortedSet<String> getObjectFiles(Project.NameKey project) throws Exception {
try (Repository repo = repoManager.openRepository(project);
Stream<Path> paths =
Files.walk(((FileRepository) repo).getObjectDatabase().getDirectory().toPath())) {
return paths
.filter(path -> !Files.isDirectory(path))
.map(Path::toString)
.filter(name -> !name.endsWith(".pack") && !name.endsWith(".idx"))
.collect(toImmutableSortedSet(naturalOrder()));
}
}
}