Delete NoteDb migration code
Change-Id: I31b79a49f46cd60671db8de5b2378d43c6eed9cd
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 016ad1a..e7e8ea3 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -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.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 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 @@
@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 @@
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 @@
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
diff --git a/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
deleted file mode 100644
index 21799be..0000000
--- a/java/com/google/gerrit/pgm/MigrateToNoteDb.java
+++ /dev/null
@@ -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();
- }
- }
- }
-}
diff --git a/java/com/google/gerrit/server/config/GerritServerIdProvider.java b/java/com/google/gerrit/server/config/GerritServerIdProvider.java
index c609cc4..4898f55 100644
--- a/java/com/google/gerrit/server/config/GerritServerIdProvider.java
+++ b/java/com/google/gerrit/server/config/GerritServerIdProvider.java
@@ -48,9 +48,7 @@
// 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);
diff --git a/java/com/google/gerrit/server/notedb/NotesMigration.java b/java/com/google/gerrit/server/notedb/NotesMigration.java
index c7acaf1..28754a6 100644
--- a/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -46,8 +46,7 @@
* <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.
diff --git a/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java b/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
deleted file mode 100644
index 7b427b4..0000000
--- a/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
deleted file mode 100644
index f60744f..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ /dev/null
@@ -1,1035 +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 static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-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.WRITE;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Comparator.comparing;
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.SetMultimap;
-import com.google.common.collect.Streams;
-import com.google.common.flogger.FluentLogger;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.FormatUtil;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.git.LockFailureException;
-import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.InternalUser;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerConfigProvider;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.MutableNotesMigration;
-import com.google.gerrit.server.notedb.NoteDbSchemaVersionManager;
-import com.google.gerrit.server.notedb.NoteDbTable;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.notedb.NotesMigrationState;
-import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
-import com.google.gerrit.server.notedb.PrimaryStorageMigrator.NoNoteDbStateException;
-import com.google.gerrit.server.notedb.RepoSequence;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
-import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.update.ChainedReceiveCommands;
-import com.google.gerrit.server.util.ManualRequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.internal.storage.file.PackInserter;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.io.NullOutputStream;
-
-/** One stop shop for migrating a site's change storage from ReviewDb to NoteDb. */
-public class NoteDbMigrator implements AutoCloseable {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- private static final String AUTO_MIGRATE = "autoMigrate";
- private static final String TRIAL = "trial";
-
- public static boolean getAutoMigrate(Config cfg) {
- return cfg.getBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, false);
- }
-
- private static void setAutoMigrate(Config cfg, boolean autoMigrate) {
- cfg.setBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, autoMigrate);
- }
-
- public static boolean getTrialMode(Config cfg) {
- return cfg.getBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), TRIAL, false);
- }
-
- public static void setTrialMode(Config cfg, boolean trial) {
- cfg.setBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), TRIAL, trial);
- }
-
- public static class Builder {
- private final Config cfg;
- private final SitePaths sitePaths;
- private final Provider<PersonIdent> serverIdent;
- private final AllUsersName allUsers;
- private final SchemaFactory<ReviewDb> schemaFactory;
- private final GitRepositoryManager repoManager;
- private final NoteDbUpdateManager.Factory updateManagerFactory;
- private final ChangeBundleReader bundleReader;
- private final AllProjectsName allProjects;
- private final InternalUser.Factory userFactory;
- private final ThreadLocalRequestContext requestContext;
- private final ChangeRebuilderImpl rebuilder;
- private final WorkQueue workQueue;
- private final MutableNotesMigration globalNotesMigration;
- private final PrimaryStorageMigrator primaryStorageMigrator;
- private final PluginSetContext<NotesMigrationStateListener> listeners;
- private final NoteDbSchemaVersionManager versionManager;
-
- private int threads;
- private ImmutableList<Project.NameKey> projects = ImmutableList.of();
- private ImmutableList<Change.Id> changes = ImmutableList.of();
- private OutputStream progressOut = NullOutputStream.INSTANCE;
- private NotesMigrationState stopAtState;
- private boolean trial;
- private boolean forceRebuild;
- private int sequenceGap = -1;
- private boolean autoMigrate;
-
- @Inject
- Builder(
- GerritServerConfigProvider configProvider,
- SitePaths sitePaths,
- @GerritPersonIdent Provider<PersonIdent> serverIdent,
- AllUsersName allUsers,
- SchemaFactory<ReviewDb> schemaFactory,
- GitRepositoryManager repoManager,
- NoteDbUpdateManager.Factory updateManagerFactory,
- ChangeBundleReader bundleReader,
- AllProjectsName allProjects,
- ThreadLocalRequestContext requestContext,
- InternalUser.Factory userFactory,
- ChangeRebuilderImpl rebuilder,
- WorkQueue workQueue,
- MutableNotesMigration globalNotesMigration,
- PrimaryStorageMigrator primaryStorageMigrator,
- PluginSetContext<NotesMigrationStateListener> listeners,
- NoteDbSchemaVersionManager versionManager) {
- // Reload gerrit.config/notedb.config on each migrator invocation, in case a previous
- // migration in the same process modified the on-disk contents. This ensures the defaults for
- // trial/autoMigrate get set correctly below.
- this.cfg = configProvider.loadConfig();
- this.sitePaths = sitePaths;
- this.serverIdent = serverIdent;
- this.allUsers = allUsers;
- this.schemaFactory = schemaFactory;
- this.repoManager = repoManager;
- this.updateManagerFactory = updateManagerFactory;
- this.bundleReader = bundleReader;
- this.allProjects = allProjects;
- this.requestContext = requestContext;
- this.userFactory = userFactory;
- this.rebuilder = rebuilder;
- this.workQueue = workQueue;
- this.globalNotesMigration = globalNotesMigration;
- this.primaryStorageMigrator = primaryStorageMigrator;
- this.listeners = listeners;
- this.versionManager = versionManager;
- this.trial = getTrialMode(cfg);
- this.autoMigrate = getAutoMigrate(cfg);
- }
-
- /**
- * Set the number of threads used by parallelizable phases of the migration, such as rebuilding
- * all changes.
- *
- * <p>Not all phases are parallelizable, and calling {@link #rebuild()} directly will do
- * substantial work in the calling thread regardless of the number of threads configured.
- *
- * <p>By default, all work is done in the calling thread.
- *
- * @param threads thread count; if less than 2, all work happens in the calling thread.
- * @return this.
- */
- public Builder setThreads(int threads) {
- this.threads = threads;
- return this;
- }
-
- /**
- * Limit the set of projects that are processed.
- *
- * <p>Incompatible with {@link #setChanges(Collection)}.
- *
- * <p>By default, all projects will be processed.
- *
- * @param projects set of projects; if null or empty, all projects will be processed.
- * @return this.
- */
- public Builder setProjects(@Nullable Collection<Project.NameKey> projects) {
- this.projects = projects != null ? ImmutableList.copyOf(projects) : ImmutableList.of();
- return this;
- }
-
- /**
- * Limit the set of changes that are processed.
- *
- * <p>Incompatible with {@link #setProjects(Collection)}.
- *
- * <p>By default, all changes will be processed.
- *
- * @param changes set of changes; if null or empty, all changes will be processed.
- * @return this.
- */
- public Builder setChanges(@Nullable Collection<Change.Id> changes) {
- this.changes = changes != null ? ImmutableList.copyOf(changes) : ImmutableList.of();
- return this;
- }
-
- /**
- * Set output stream for progress monitors.
- *
- * <p>By default, there is no progress monitor output (although there may be other logs).
- *
- * @param progressOut output stream.
- * @return this.
- */
- public Builder setProgressOut(OutputStream progressOut) {
- this.progressOut = requireNonNull(progressOut);
- return this;
- }
-
- /**
- * Stop at a specific migration state, for testing only.
- *
- * @param stopAtState state to stop at.
- * @return this.
- */
- @VisibleForTesting
- public Builder setStopAtStateForTesting(NotesMigrationState stopAtState) {
- this.stopAtState = stopAtState;
- return this;
- }
-
- /**
- * Rebuild in "trial mode": configure Gerrit to write to and read from NoteDb, but leave
- * ReviewDb as the source of truth for all changes.
- *
- * <p>By default, trial mode is off, and NoteDb is the source of truth for all changes following
- * the migration.
- *
- * @param trial whether to rebuild in trial mode.
- * @return this.
- */
- public Builder setTrialMode(boolean trial) {
- this.trial = trial;
- return this;
- }
-
- /**
- * Rebuild all changes in NoteDb from ReviewDb, even if Gerrit is currently configured to read
- * from NoteDb.
- *
- * <p>Only supported if ReviewDb is still the source of truth for all changes.
- *
- * <p>By default, force rebuilding is off.
- *
- * @param forceRebuild whether to force rebuilding.
- * @return this.
- */
- public Builder setForceRebuild(boolean forceRebuild) {
- this.forceRebuild = forceRebuild;
- return this;
- }
-
- /**
- * Gap between ReviewDb change sequence numbers and NoteDb.
- *
- * <p>If NoteDb sequences are enabled in a running server, there is a race between the migration
- * step that calls {@code nextChangeId()} to seed the ref, and other threads that call {@code
- * nextChangeId()} to create new changes. In order to prevent these operations stepping on one
- * another, we use this value to skip some predefined sequence numbers. This is strongly
- * recommended in a running server.
- *
- * <p>If the migration takes place offline, there is no race with other threads, and this option
- * may be set to 0. However, admins may still choose to use a gap, for example to make it easier
- * to distinguish changes that were created before and after the NoteDb migration.
- *
- * <p>By default, uses the value from {@code noteDb.changes.initialSequenceGap} in {@code
- * gerrit.config}, which defaults to 1000.
- *
- * @param sequenceGap sequence gap size; if negative, use the default.
- * @return this.
- */
- public Builder setSequenceGap(int sequenceGap) {
- this.sequenceGap = sequenceGap;
- return this;
- }
-
- /**
- * Enable auto-migration on subsequent daemon launches.
- *
- * <p>If true, prior to running any migration steps, sets the necessary configuration in {@code
- * gerrit.config} to make {@code gerrit.war daemon} retry the migration on next startup, if it
- * fails.
- *
- * @param autoMigrate whether to set auto-migration config.
- * @return this.
- */
- public Builder setAutoMigrate(boolean autoMigrate) {
- this.autoMigrate = autoMigrate;
- return this;
- }
-
- public NoteDbMigrator build() throws MigrationException {
- return new NoteDbMigrator(
- sitePaths,
- schemaFactory,
- serverIdent,
- allUsers,
- repoManager,
- updateManagerFactory,
- bundleReader,
- allProjects,
- requestContext,
- userFactory,
- rebuilder,
- globalNotesMigration,
- primaryStorageMigrator,
- listeners,
- versionManager,
- threads > 1
- ? MoreExecutors.listeningDecorator(
- workQueue.createQueue(threads, "RebuildChange", true))
- : MoreExecutors.newDirectExecutorService(),
- projects,
- changes,
- progressOut,
- stopAtState,
- trial,
- forceRebuild,
- sequenceGap >= 0 ? sequenceGap : Sequences.getChangeSequenceGap(cfg),
- autoMigrate);
- }
- }
-
- private final FileBasedConfig gerritConfig;
- private final FileBasedConfig noteDbConfig;
- private final SchemaFactory<ReviewDb> schemaFactory;
- private final Provider<PersonIdent> serverIdent;
- private final AllUsersName allUsers;
- private final GitRepositoryManager repoManager;
- private final NoteDbUpdateManager.Factory updateManagerFactory;
- private final ChangeBundleReader bundleReader;
- private final AllProjectsName allProjects;
- private final ThreadLocalRequestContext requestContext;
- private final InternalUser.Factory userFactory;
- private final ChangeRebuilderImpl rebuilder;
- private final MutableNotesMigration globalNotesMigration;
- private final PrimaryStorageMigrator primaryStorageMigrator;
- private final PluginSetContext<NotesMigrationStateListener> listeners;
- private final NoteDbSchemaVersionManager versionManager;
-
- private final ListeningExecutorService executor;
- private final ImmutableList<Project.NameKey> projects;
- private final ImmutableList<Change.Id> changes;
- private final OutputStream progressOut;
- private final NotesMigrationState stopAtState;
- private final boolean trial;
- private final boolean forceRebuild;
- private final int sequenceGap;
- private final boolean autoMigrate;
-
- private NoteDbMigrator(
- SitePaths sitePaths,
- SchemaFactory<ReviewDb> schemaFactory,
- Provider<PersonIdent> serverIdent,
- AllUsersName allUsers,
- GitRepositoryManager repoManager,
- NoteDbUpdateManager.Factory updateManagerFactory,
- ChangeBundleReader bundleReader,
- AllProjectsName allProjects,
- ThreadLocalRequestContext requestContext,
- InternalUser.Factory userFactory,
- ChangeRebuilderImpl rebuilder,
- MutableNotesMigration globalNotesMigration,
- PrimaryStorageMigrator primaryStorageMigrator,
- PluginSetContext<NotesMigrationStateListener> listeners,
- NoteDbSchemaVersionManager versionManager,
- ListeningExecutorService executor,
- ImmutableList<Project.NameKey> projects,
- ImmutableList<Change.Id> changes,
- OutputStream progressOut,
- NotesMigrationState stopAtState,
- boolean trial,
- boolean forceRebuild,
- int sequenceGap,
- boolean autoMigrate)
- throws MigrationException {
- if (!changes.isEmpty() && !projects.isEmpty()) {
- throw new MigrationException("Cannot set both changes and projects");
- }
- if (sequenceGap < 0) {
- throw new MigrationException("Sequence gap must be non-negative: " + sequenceGap);
- }
-
- this.schemaFactory = schemaFactory;
- this.serverIdent = serverIdent;
- this.allUsers = allUsers;
- this.rebuilder = rebuilder;
- this.repoManager = repoManager;
- this.updateManagerFactory = updateManagerFactory;
- this.bundleReader = bundleReader;
- this.allProjects = allProjects;
- this.requestContext = requestContext;
- this.userFactory = userFactory;
- this.globalNotesMigration = globalNotesMigration;
- this.primaryStorageMigrator = primaryStorageMigrator;
- this.listeners = listeners;
- this.versionManager = versionManager;
- this.executor = executor;
- this.projects = projects;
- this.changes = changes;
- this.progressOut = progressOut;
- this.stopAtState = stopAtState;
- this.trial = trial;
- this.forceRebuild = forceRebuild;
- this.sequenceGap = sequenceGap;
- this.autoMigrate = autoMigrate;
-
- // Stack notedb.config over gerrit.config, in the same way as GerritServerConfigProvider.
- this.gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
- this.noteDbConfig =
- new FileBasedConfig(gerritConfig, sitePaths.notedb_config.toFile(), FS.detect());
- }
-
- @Override
- public void close() {
- executor.shutdownNow();
- }
-
- public void migrate() throws OrmException, IOException {
- if (!changes.isEmpty() || !projects.isEmpty()) {
- throw new MigrationException(
- "Cannot set changes or projects during full migration; call rebuild() instead");
- }
- Optional<NotesMigrationState> maybeState = loadState();
- if (!maybeState.isPresent()) {
- throw new MigrationException("Could not determine initial migration state");
- }
-
- NotesMigrationState state = maybeState.get();
- if (trial && state.compareTo(READ_WRITE_NO_SEQUENCE) > 0) {
- throw new MigrationException(
- "Migration has already progressed past the endpoint of the \"trial mode\" state;"
- + " NoteDb is already the primary storage for some changes");
- }
- if (forceRebuild && state.compareTo(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY) > 0) {
- throw new MigrationException(
- "Cannot force rebuild changes; NoteDb is already the primary storage for some changes");
- }
- setControlFlags();
-
- boolean rebuilt = false;
- while (state.compareTo(NOTE_DB) < 0) {
- if (state.equals(stopAtState)) {
- return;
- }
- boolean stillNeedsRebuild = forceRebuild && !rebuilt;
- if (trial && state.compareTo(READ_WRITE_NO_SEQUENCE) >= 0) {
- if (stillNeedsRebuild && state == READ_WRITE_NO_SEQUENCE) {
- // We're at the end state of trial mode, but still need a rebuild due to forceRebuild. Let
- // the loop go one more time.
- } else {
- return;
- }
- }
- switch (state) {
- case REVIEW_DB:
- state = turnOnWrites(state);
- break;
- case WRITE:
- state = rebuildAndEnableReads(state);
- rebuilt = true;
- break;
- case READ_WRITE_NO_SEQUENCE:
- if (stillNeedsRebuild) {
- state = rebuildAndEnableReads(state);
- rebuilt = true;
- } else {
- state = enableSequences(state);
- }
- break;
- case READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY:
- if (stillNeedsRebuild) {
- state = rebuildAndEnableReads(state);
- rebuilt = true;
- } else {
- state = setNoteDbPrimary(state);
- }
- break;
- case READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY:
- // The only way we can get here is if there was a failure on a previous run of
- // setNoteDbPrimary, since that method moves to NOTE_DB if it completes
- // successfully. Assume that not all changes were converted and re-run the step.
- // migrateToNoteDbPrimary is a relatively fast no-op for already-migrated changes, so this
- // isn't actually repeating work.
- state = setNoteDbPrimary(state);
- break;
- case NOTE_DB:
- // Done!
- break;
- default:
- throw new MigrationException(
- "Migration out of the following state is not supported:\n" + state.toText());
- }
- }
- }
-
- private NotesMigrationState turnOnWrites(NotesMigrationState prev)
- throws OrmException, IOException {
- versionManager.init();
- return saveState(prev, WRITE);
- }
-
- private NotesMigrationState rebuildAndEnableReads(NotesMigrationState prev)
- throws OrmException, IOException {
- rebuild();
- return saveState(prev, READ_WRITE_NO_SEQUENCE);
- }
-
- private NotesMigrationState enableSequences(NotesMigrationState prev)
- throws OrmException, IOException {
- try (ReviewDb db = schemaFactory.open()) {
- @SuppressWarnings("deprecation")
- final int nextChangeId = db.nextChangeId();
-
- RepoSequence seq =
- new RepoSequence(
- repoManager,
- GitReferenceUpdated.DISABLED,
- allProjects,
- Sequences.NAME_CHANGES,
- // If sequenceGap is 0, this writes into the sequence ref the same ID that is returned
- // by the call to seq.next() below. If we actually used this as a change ID, that
- // would be a problem, but we just discard it, so this is safe.
- () -> nextChangeId + sequenceGap - 1,
- 1,
- nextChangeId);
- seq.next();
- }
- return saveState(prev, READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY);
- }
-
- private NotesMigrationState setNoteDbPrimary(NotesMigrationState prev)
- throws MigrationException, OrmException, IOException {
- checkState(
- projects.isEmpty() && changes.isEmpty(),
- "Should not have attempted setNoteDbPrimary with a subset of changes");
- checkState(
- prev == READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY
- || prev == READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY,
- "Unexpected start state for setNoteDbPrimary: %s",
- prev);
-
- // Before changing the primary storage of old changes, ensure new changes are created with
- // NoteDb primary.
- prev = saveState(prev, READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
-
- Stopwatch sw = Stopwatch.createStarted();
- logger.atInfo().log("Setting primary storage to NoteDb");
- List<Change.Id> allChanges;
- try (ReviewDb db = unwrapDb(schemaFactory.open())) {
- allChanges = Streams.stream(db.changes().all()).map(Change::getId).collect(toList());
- }
-
- try (ContextHelper contextHelper = new ContextHelper()) {
- List<ListenableFuture<Boolean>> futures =
- allChanges
- .stream()
- .map(
- id ->
- executor.submit(
- () -> {
- try (ManualRequestContext ctx = contextHelper.open()) {
- try {
- primaryStorageMigrator.migrateToNoteDbPrimary(id);
- } catch (NoNoteDbStateException e) {
- if (canSkipPrimaryStorageMigration(
- ctx.getReviewDbProvider().get(), id)) {
- logger.atWarning().withCause(e).log(
- "Change %s previously failed to rebuild;"
- + " skipping primary storage migration",
- id);
- } else {
- throw e;
- }
- }
- return true;
- } catch (Exception e) {
- logger.atSevere().withCause(e).log(
- "Error migrating primary storage for %s", id);
- return false;
- }
- }))
- .collect(toList());
-
- boolean ok = futuresToBoolean(futures, "Error migrating primary storage");
- double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
- logger.atInfo().log(
- "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n",
- allChanges.size(), t, allChanges.size() / t);
- if (!ok) {
- throw new MigrationException("Migrating primary storage for some changes failed, see log");
- }
- }
-
- return disableReviewDb(prev);
- }
-
- /**
- * Checks whether a change is so corrupt that it can be completely skipped by the primary storage
- * migration step.
- *
- * <p>To get to the point where this method is called from {@link #setNoteDbPrimary}, it means we
- * attempted to rebuild it, and encountered an error that was then caught in {@link
- * #rebuildProject} and skipped. As a result, there is no {@code noteDbState} field in the change
- * by the time we get to {@link #setNoteDbPrimary}, so {@code migrateToNoteDbPrimary} throws an
- * exception.
- *
- * <p>We have to do this hacky double-checking because we don't have a way for the rebuilding
- * phase to communicate to the primary storage migration phase that the change is skippable. It
- * would be possible to store this info in some field in this class, but there is no guarantee
- * that the rebuild and primary storage migration phases are run in the same JVM invocation.
- *
- * <p>In an ideal world, we could do this through the {@link
- * com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage} enum, having a separate value
- * for errors. However, that would be an invasive change touching many non-migration-related parts
- * of the NoteDb migration code, which is too risky to attempt in the stable branch where this bug
- * had to be fixed.
- *
- * <p>As of this writing, the only case where this happens is when a change has no patch sets.
- */
- private static boolean canSkipPrimaryStorageMigration(ReviewDb db, Change.Id id) {
- try {
- return Iterables.isEmpty(unwrapDb(db).patchSets().byChange(id));
- } catch (Exception e) {
- logger.atSevere().withCause(e).log(
- "Error checking if change %s can be skipped, assuming no", id);
- return false;
- }
- }
-
- private NotesMigrationState disableReviewDb(NotesMigrationState prev) throws IOException {
- return saveState(prev, NOTE_DB, c -> setAutoMigrate(c, false));
- }
-
- private Optional<NotesMigrationState> loadState() throws IOException {
- try {
- gerritConfig.load();
- noteDbConfig.load();
- return NotesMigrationState.forConfig(noteDbConfig);
- } catch (ConfigInvalidException | IllegalArgumentException e) {
- logger.atWarning().withCause(e).log(
- "error reading NoteDb migration options from %s", noteDbConfig.getFile());
- return Optional.empty();
- }
- }
-
- private NotesMigrationState saveState(
- NotesMigrationState expectedOldState, NotesMigrationState newState) throws IOException {
- return saveState(expectedOldState, newState, c -> {});
- }
-
- private NotesMigrationState saveState(
- NotesMigrationState expectedOldState,
- NotesMigrationState newState,
- Consumer<Config> additionalUpdates)
- throws IOException {
- synchronized (globalNotesMigration) {
- // This read-modify-write is racy. We're counting on the fact that no other Gerrit operation
- // modifies gerrit.config, and hoping that admins don't either.
- Optional<NotesMigrationState> actualOldState = loadState();
- if (!actualOldState.equals(Optional.of(expectedOldState))) {
- throw new MigrationException(
- "Cannot move to new state:\n"
- + newState.toText()
- + "\n\n"
- + "Expected this state in gerrit.config:\n"
- + expectedOldState.toText()
- + "\n\n"
- + (actualOldState.isPresent()
- ? "But found this state:\n" + actualOldState.get().toText()
- : "But could not parse the current state"));
- }
-
- preStateChange(expectedOldState, newState);
-
- newState.setConfigValues(noteDbConfig);
- additionalUpdates.accept(noteDbConfig);
- noteDbConfig.save();
-
- // Only set in-memory state once it's been persisted to storage.
- globalNotesMigration.setFrom(newState);
- logger.atInfo().log("Migration state: %s => %s", expectedOldState, newState);
-
- return newState;
- }
- }
-
- private void preStateChange(NotesMigrationState oldState, NotesMigrationState newState)
- throws IOException {
- listeners.runEach(l -> l.preStateChange(oldState, newState), IOException.class);
- }
-
- private void setControlFlags() throws MigrationException {
- synchronized (globalNotesMigration) {
- try {
- noteDbConfig.load();
- setAutoMigrate(noteDbConfig, autoMigrate);
- setTrialMode(noteDbConfig, trial);
- noteDbConfig.save();
- } catch (ConfigInvalidException | IOException e) {
- throw new MigrationException("Error saving auto-migration config", e);
- }
- }
- }
-
- public void rebuild() throws MigrationException, OrmException {
- if (!globalNotesMigration.commitChangeWrites()) {
- throw new MigrationException("Cannot rebuild without noteDb.changes.write=true");
- }
- Stopwatch sw = Stopwatch.createStarted();
- logger.atInfo().log("Rebuilding changes in NoteDb");
-
- ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
- List<ListenableFuture<Boolean>> futures = new ArrayList<>();
- try (ContextHelper contextHelper = new ContextHelper()) {
- List<Project.NameKey> projectNames =
- Ordering.usingToString().sortedCopy(changesByProject.keySet());
- for (Project.NameKey project : projectNames) {
- ListenableFuture<Boolean> future =
- executor.submit(
- () -> {
- try {
- return rebuildProject(contextHelper.getReviewDb(), changesByProject, project);
- } catch (Exception e) {
- logger.atSevere().withCause(e).log("Error rebuilding project %s", project);
- return false;
- }
- });
- futures.add(future);
- }
-
- boolean ok = futuresToBoolean(futures, "Error rebuilding projects");
- double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
- logger.atInfo().log(
- "Rebuilt %d changes in %.01fs (%.01f/s)\n",
- changesByProject.size(), t, changesByProject.size() / t);
- if (!ok) {
- throw new MigrationException("Rebuilding some changes failed, see log");
- }
- }
- }
-
- private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject()
- throws OrmException {
- // Memoize all changes so we can close the db connection and allow other threads to use the full
- // connection pool.
- SetMultimap<Project.NameKey, Change.Id> out =
- MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
- .treeSetValues(comparing(Change.Id::get))
- .build();
- try (ReviewDb db = unwrapDb(schemaFactory.open())) {
- if (!projects.isEmpty()) {
- return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
- }
- if (!changes.isEmpty()) {
- return byProject(db.changes().get(changes), c -> true, out);
- }
- return byProject(db.changes().all(), c -> true, out);
- }
- }
-
- private static ImmutableListMultimap<Project.NameKey, Change.Id> byProject(
- Iterable<Change> changes,
- Predicate<Change> pred,
- SetMultimap<Project.NameKey, Change.Id> out) {
- Streams.stream(changes).filter(pred).forEach(c -> out.put(c.getProject(), c.getId()));
- return ImmutableListMultimap.copyOf(out);
- }
-
- private static ObjectInserter newPackInserter(Repository repo) {
- if (!(repo instanceof FileRepository)) {
- return repo.newObjectInserter();
- }
- PackInserter ins = ((FileRepository) repo).getObjectDatabase().newPackInserter();
- ins.checkExisting(false);
- return ins;
- }
-
- private boolean rebuildProject(
- ReviewDb db,
- ImmutableListMultimap<Project.NameKey, Change.Id> allChanges,
- Project.NameKey project) {
- checkArgument(allChanges.containsKey(project));
- boolean ok = true;
- ProgressMonitor pm =
- new TextProgressMonitor(
- new PrintWriter(new BufferedWriter(new OutputStreamWriter(progressOut, UTF_8))));
- try (Repository changeRepo = repoManager.openRepository(project);
- // Only use a PackInserter for the change repo, not All-Users.
- //
- // It's not possible to share a single inserter for All-Users across all project tasks, and
- // we don't want to add one pack per project to All-Users. Adding many loose objects is
- // preferable to many packs.
- //
- // Anyway, the number of objects inserted into All-Users is proportional to the number
- // of pending draft comments, which should not be high (relative to the total number of
- // changes), so the number of loose objects shouldn't be too unreasonable.
- ObjectInserter changeIns = newPackInserter(changeRepo);
- ObjectReader changeReader = changeIns.newReader();
- RevWalk changeRw = new RevWalk(changeReader);
- Repository allUsersRepo = repoManager.openRepository(allUsers);
- ObjectInserter allUsersIns = allUsersRepo.newObjectInserter();
- ObjectReader allUsersReader = allUsersIns.newReader();
- RevWalk allUsersRw = new RevWalk(allUsersReader)) {
- ChainedReceiveCommands changeCmds = new ChainedReceiveCommands(changeRepo);
- ChainedReceiveCommands allUsersCmds = new ChainedReceiveCommands(allUsersRepo);
-
- Collection<Change.Id> changes = allChanges.get(project);
- pm.beginTask(FormatUtil.elide("Rebuilding " + project.get(), 50), changes.size());
- int toSave = 0;
- try {
- for (Change.Id changeId : changes) {
- // NoteDbUpdateManager assumes that all commands in its OpenRepo were added by itself, so
- // we can't share the top-level ChainedReceiveCommands. Use a new set of commands sharing
- // the same underlying repo, and copy commands back to the top-level
- // ChainedReceiveCommands later. This also assumes that each ref in the final list of
- // commands was only modified by a single NoteDbUpdateManager; since we use one manager
- // per change, and each ref corresponds to exactly one change, this assumption should be
- // safe.
- ChainedReceiveCommands tmpChangeCmds =
- new ChainedReceiveCommands(changeCmds.getRepoRefCache());
- ChainedReceiveCommands tmpAllUsersCmds =
- new ChainedReceiveCommands(allUsersCmds.getRepoRefCache());
-
- try (NoteDbUpdateManager manager =
- updateManagerFactory
- .create(project)
- .setAtomicRefUpdates(false)
- .setSaveObjects(false)
- .setChangeRepo(changeRepo, changeRw, changeIns, tmpChangeCmds)
- .setAllUsersRepo(allUsersRepo, allUsersRw, allUsersIns, tmpAllUsersCmds)) {
- rebuild(db, changeId, manager);
-
- // Executing with dryRun=true writes all objects to the underlying inserters and adds
- // commands to the ChainedReceiveCommands. Afterwards, we can discard the manager, so we
- // don't keep using any memory beyond what may be buffered in the PackInserter.
- manager.execute(true);
-
- tmpChangeCmds.getCommands().values().forEach(c -> addCommand(changeCmds, c));
- tmpAllUsersCmds.getCommands().values().forEach(c -> addCommand(allUsersCmds, c));
-
- toSave++;
- } catch (NoPatchSetsException e) {
- logger.atWarning().log(e.getMessage());
- } catch (ConflictingUpdateException ex) {
- logger.atWarning().log(
- "Rebuilding detected a conflicting ReviewDb update for change %s;"
- + " will be auto-rebuilt at runtime",
- changeId);
- } catch (Throwable t) {
- logger.atSevere().withCause(t).log("Failed to rebuild change %s", changeId);
- ok = false;
- }
- pm.update(1);
- }
- } finally {
- pm.endTask();
- }
-
- pm.beginTask(FormatUtil.elide("Saving " + project.get(), 50), ProgressMonitor.UNKNOWN);
- try {
- save(changeRepo, changeRw, changeIns, changeCmds);
- save(allUsersRepo, allUsersRw, allUsersIns, allUsersCmds);
- // This isn't really useful progress. If we passed a real ProgressMonitor to
- // BatchRefUpdate#execute we might get something more incremental, but that doesn't allow us
- // to specify the repo name in the task text.
- pm.update(toSave);
- } catch (LockFailureException e) {
- logger.atWarning().log(
- "Rebuilding detected a conflicting NoteDb update for the following refs, which will"
- + " be auto-rebuilt at runtime: %s",
- e.getFailedRefs().stream().distinct().sorted().collect(joining(", ")));
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Failed to save NoteDb state for %s", project);
- } finally {
- pm.endTask();
- }
- } catch (RepositoryNotFoundException e) {
- logger.atWarning().log("Repository %s not found", project);
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Failed to rebuild project %s", project);
- }
- return ok;
- }
-
- private void rebuild(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
- throws OrmException, IOException {
- // Match ChangeRebuilderImpl#stage, but without calling manager.stage(), since that can only be
- // called after building updates for all changes.
- Change change =
- ChangeRebuilderImpl.checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId));
- if (change == null) {
- // Could log here instead, but this matches the behavior of ChangeRebuilderImpl#stage.
- throw new NoSuchChangeException(changeId);
- }
- rebuilder.buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
-
- rebuilder.execute(db, changeId, manager, true, false);
- }
-
- private static void addCommand(ChainedReceiveCommands cmds, ReceiveCommand cmd) {
- // ChainedReceiveCommands doesn't allow no-ops, but these occur when rebuilding a
- // previously-rebuilt change.
- if (!cmd.getOldId().equals(cmd.getNewId())) {
- cmds.add(cmd);
- }
- }
-
- private void save(Repository repo, RevWalk rw, ObjectInserter ins, ChainedReceiveCommands cmds)
- throws IOException {
- if (cmds.isEmpty()) {
- return;
- }
- ins.flush();
- BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
- bru.setRefLogMessage("Migrate changes to NoteDb", false);
- bru.setRefLogIdent(serverIdent.get());
- bru.setAtomic(false);
- bru.setAllowNonFastForwards(true);
- cmds.addTo(bru);
- RefUpdateUtil.executeChecked(bru, rw);
- }
-
- private static boolean futuresToBoolean(List<ListenableFuture<Boolean>> futures, String errMsg) {
- try {
- return Futures.allAsList(futures).get().stream().allMatch(b -> b);
- } catch (InterruptedException | ExecutionException e) {
- logger.atSevere().withCause(e).log(errMsg);
- return false;
- }
- }
-
- private class ContextHelper implements AutoCloseable {
- private final Thread callingThread;
- private ReviewDb db;
- private Runnable closeDb;
-
- ContextHelper() {
- callingThread = Thread.currentThread();
- }
-
- ManualRequestContext open() throws OrmException {
- return new ManualRequestContext(
- userFactory.create(),
- // Reuse the same lazily-opened ReviewDb on the original calling thread, otherwise open
- // SchemaFactory in the normal way.
- Thread.currentThread().equals(callingThread) ? this::getReviewDb : schemaFactory,
- requestContext);
- }
-
- synchronized ReviewDb getReviewDb() throws OrmException {
- if (db == null) {
- ReviewDb actual = schemaFactory.open();
- closeDb = actual::close;
- db =
- new ReviewDbWrapper(unwrapDb(actual)) {
- @Override
- public void close() {
- // Closed by ContextHelper#close.
- }
- };
- }
- return db;
- }
-
- @Override
- public synchronized void close() {
- if (db != null) {
- closeDb.run();
- db = null;
- closeDb = null;
- }
- }
- }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
deleted file mode 100644
index b5a8236..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
+++ /dev/null
@@ -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.
- }
-}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
deleted file mode 100644
index 1bb23fb..0000000
--- a/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
deleted file mode 100644
index 26d5461..0000000
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
+++ /dev/null
@@ -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());
- }
-}
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
deleted file mode 100644
index 6f226c8..0000000
--- a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ /dev/null
@@ -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()));
- }
- }
-}