Merge changes from topic 'migrate-to-note-db'

* changes:
  Rename RebuildNoteDb to MigrateToNoteDb and expand flags
  Expand SiteRebuilder into a skeleton of a full-service migration
  SiteRebuilder: Build rebuilder with a builder
This commit is contained in:
Dave Borowitz 2017-06-20 12:18:19 +00:00 committed by Gerrit Code Review
commit 682267df7b
12 changed files with 626 additions and 247 deletions

View File

@ -1742,7 +1742,7 @@ Common unit suffixes of 'k', 'm', or 'g' are supported.
If `true` enable the automatic mixed mode
(see link:http://www.h2database.com/html/features.html#auto_mixed_mode[Automatic Mixed Mode]).
This enables concurrent access to the embedded H2 database from command line
utils (e.g. RebuildNoteDb).
utils (e.g. MigrateToNoteDb).
+
Default is `false`.

View File

@ -99,7 +99,7 @@ previous options, unless otherwise noted.
inaccurate results, and writing to NoteDb would compound the problem. +
Thus it is up to an admin of a previously-ReviewDb site to ensure
MigratePrimaryStorage has been run for all changes. Note that the current
implementation of the `rebuild-note-db` program does not do this. +
implementation of the `migrate-to-note-db` program does not do this. +
In this phase, it would be possible to delete the Changes tables out from
under a running server with no effect.
- `noteDb.changes.fuseUpdates=true`: Code and meta updates within a single
@ -112,25 +112,25 @@ previous options, unless otherwise noted.
== Migration
Once configuration options are set, migration to NoteDb is primarily
accomplished by running the `rebuild-note-db` program. Currently, this program
accomplished by running the `migrate-to-note-db` program. Currently, this program
bulk copies ReviewDb data into NoteDb, but leaves primary storage of these
changes in ReviewDb, so the site is runnable with
`noteDb.changes.{write,read}=true`, but ReviewDb is still required.
Eventually, `rebuild-note-db` will set primary storage to NoteDb for all
Eventually, `migrate-to-note-db` will set primary storage to NoteDb for all
changes by default, so a site will be able to stop using ReviewDb for changes
immediately after a successful run.
There is code in `PrimaryStorageMigrator.java` to migrate individual changes
from NoteDb primary to ReviewDb primary. This code is not intended to be used
except in the event of a critical bug in NoteDb primary changes in production.
It will likely never be used by `rebuild-note-db`, and in fact it's not
recommended to run `rebuild-note-db` until the code is stable enough that the
It will likely never be used by `migrate-to-note-db`, and in fact it's not
recommended to run `migrate-to-note-db` until the code is stable enough that the
reverse migration won't be necessary.
=== Zero-Downtime Multi-Master Migration
Single-master Gerrit sites can use `rebuild-note-db` on an offline site to
Single-master Gerrit sites can use `migrate-to-note-db` on an offline site to
rebuild NoteDb, but this doesn't work in a zero-downtime environment like
googlesource.com.

View File

@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.pgm;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.server.config.SitePaths;
@ -28,7 +29,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class RebuildNoteDbIT {
public class MigrateToNoteDbIT {
private String sitePath;
private StoredConfig gerritConfig;
@ -37,6 +38,7 @@ public class RebuildNoteDbIT {
SitePaths sitePaths = new SitePaths(TempFileUtil.createTempDirectory().toPath());
sitePath = sitePaths.site_path.toString();
gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
initSite();
}
@After
@ -44,11 +46,18 @@ public class RebuildNoteDbIT {
TempFileUtil.cleanup();
}
@Test
public void rebuildEmptySiteStartingWithNoteDbDisabed() throws Exception {
assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
runGerrit("MigrateToNoteDb", "-d", sitePath, "--show-stack-trace");
assertNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE);
}
@Test
public void rebuildEmptySiteStartingWithNoteDbEnabled() throws Exception {
initSite();
setNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED);
runGerrit("RebuildNoteDb", "-d", sitePath, "--show-stack-trace");
setNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE);
runGerrit("MigrateToNoteDb", "-d", sitePath, "--show-stack-trace");
assertNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE);
}
private void initSite() throws Exception {
@ -71,4 +80,10 @@ public class RebuildNoteDbIT {
ConfigNotesMigration.setConfigValues(gerritConfig, state.migration());
gerritConfig.save();
}
private void assertNotesMigrationState(NotesMigrationState expected) throws Exception {
gerritConfig.load();
assertThat(NotesMigrationState.forNotesMigration(new ConfigNotesMigration(gerritConfig)))
.hasValue(expected);
}
}

View File

@ -29,33 +29,54 @@ import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.index.DummyIndexModule;
import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.notedb.rebuild.SiteRebuilder;
import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.List;
import org.kohsuke.args4j.Option;
public class RebuildNoteDb extends SiteProgram {
public class MigrateToNoteDb extends SiteProgram {
@Option(name = "--threads", usage = "Number of threads to use for rebuilding NoteDb")
private int threads = Runtime.getRuntime().availableProcessors();
@Option(name = "--project", usage = "Projects to rebuild; recommended for debugging only")
@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 = "Individual change numbers to rebuild; recommended for debugging only"
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 mode: migrate changes and turn on reading from NoteDb, but leave ReviewDb as"
+ " the source of truth"
)
private boolean trial = true; // TODO(dborowitz): Default to false in 3.0.
private Injector dbInjector;
private Injector sysInjector;
@Inject private SiteRebuilder.Factory rebuilderFactory;
@Inject private NotesMigration notesMigration;
@Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
@Override
public int run() throws Exception {
@ -69,22 +90,27 @@ public class RebuildNoteDb extends SiteProgram {
sysInjector = createSysInjector();
sysInjector.injectMembers(this);
if (!notesMigration.enabled()) {
throw die("NoteDb is not enabled.");
}
LifecycleManager sysManager = new LifecycleManager();
sysManager.add(sysInjector);
sysManager.start();
System.out.println("Rebuilding the NoteDb");
try (SiteRebuilder rebuilder =
rebuilderFactory.create(
threads,
projects.stream().map(Project.NameKey::new).collect(toList()),
changes.stream().map(Change.Id::new).collect(toList()))) {
return rebuilder.rebuild() ? 0 : 1;
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)
.build()) {
if (!projects.isEmpty() || !changes.isEmpty()) {
migrator.rebuild();
} else {
migrator.migrate();
}
}
return 0;
}
private Injector createSysInjector() {

View File

@ -46,11 +46,11 @@ public class GerritServerIdProvider implements Provider<String> {
return;
}
// 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
// RebuildNoteDb, which otherwise would have been a reasonable place to do
// the ID generation. Fortunately, it's not much work, and it happens once.
// 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.
id = generate();
Config newCfg = readGerritConfig(sitePaths);
newCfg.setString(SECTION, null, KEY, id);

View File

@ -30,7 +30,7 @@ import org.eclipse.jgit.lib.Config;
* <p>This class controls the state of the migration according to options in {@code gerrit.config}.
* In general, any changes to these options should only be made by adventurous administrators, who
* know what they're doing, on non-production data, for the purposes of testing the NoteDb
* implementation. Changing options quite likely requires re-running {@code RebuildNoteDb}. For
* implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For
* these reasons, the options remain undocumented.
*/
@Singleton
@ -73,6 +73,12 @@ public class ConfigNotesMigration extends NotesMigration {
cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, migration.fuseUpdates());
}
public static String toText(NotesMigration migration) {
Config cfg = new Config();
setConfigValues(cfg, migration);
return cfg.toText();
}
private final boolean writeChanges;
private final boolean readChanges;
private final boolean readChangeSequence;

View File

@ -24,7 +24,6 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.gerrit.server.notedb.rebuild.SiteRebuilder;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import org.eclipse.jgit.lib.Config;
@ -55,7 +54,6 @@ public class NoteDbModule extends FactoryModule {
factory(NoteDbUpdateManager.Factory.class);
factory(RobotCommentNotes.Factory.class);
factory(RobotCommentUpdate.Factory.class);
factory(SiteRebuilder.Factory.class);
if (!useTestBindings) {
install(ChangeNotesCache.module());

View File

@ -15,6 +15,7 @@
package com.google.gerrit.server.notedb;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import java.util.Objects;
/**
* Current low-level settings of the NoteDb migration for changes.
@ -39,7 +40,7 @@ import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
* <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 RebuildNoteDb}. For
* implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For
* these reasons, the options remain undocumented.
*
* <p><strong>Note:</strong> Callers should not assume the values returned by {@code
@ -116,7 +117,7 @@ public abstract class NotesMigration {
return false;
}
public boolean commitChangeWrites() {
public final boolean commitChangeWrites() {
// It may seem odd that readChanges() without writeChanges() means we should
// attempt to commit writes. However, this method is used by callers to know
// whether or not they should short-circuit and skip attempting to read or
@ -130,11 +131,38 @@ public abstract class NotesMigration {
return rawWriteChangesSetting() || readChanges();
}
public boolean failChangeWrites() {
public final boolean failChangeWrites() {
return !rawWriteChangesSetting() && readChanges();
}
public boolean enabled() {
public final boolean enabled() {
return rawWriteChangesSetting() || readChanges();
}
@Override
public final boolean equals(Object o) {
if (!(o instanceof NotesMigration)) {
return false;
}
NotesMigration m = (NotesMigration) o;
return readChanges() == m.readChanges()
&& rawWriteChangesSetting() == m.rawWriteChangesSetting()
&& readChangeSequence() == m.readChangeSequence()
&& changePrimaryStorage() == m.changePrimaryStorage()
&& disableChangeReviewDb() == m.disableChangeReviewDb()
&& fuseUpdates() == m.fuseUpdates()
&& failOnLoad() == m.failOnLoad();
}
@Override
public final int hashCode() {
return Objects.hash(
readChanges(),
rawWriteChangesSetting(),
readChangeSequence(),
changePrimaryStorage(),
disableChangeReviewDb(),
fuseUpdates(),
failOnLoad());
}
}

View File

@ -15,6 +15,8 @@
package com.google.gerrit.server.notedb;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import java.util.Optional;
import java.util.stream.Stream;
/**
* Possible high-level states of the NoteDb migration for changes.
@ -45,6 +47,10 @@ public enum NotesMigrationState {
NOTE_DB(true, true, true, PrimaryStorage.NOTE_DB, true, true);
public static Optional<NotesMigrationState> forNotesMigration(NotesMigration migration) {
return Stream.of(values()).filter(s -> s.migration().equals(migration)).findFirst();
}
private final NotesMigration migration;
NotesMigrationState(
@ -92,4 +98,8 @@ public enum NotesMigrationState {
public NotesMigration migration() {
return migration;
}
public String toText() {
return ConfigNotesMigration.toText(migration);
}
}

View File

@ -0,0 +1,26 @@
// 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 java.io.IOException;
/** Exception thrown by {@link NoteDbMigrator} when migration fails. */
class MigrationException extends IOException {
private static final long serialVersionUID = 1L;
MigrationException(String message) {
super(message);
}
}

View File

@ -0,0 +1,475 @@
// 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;
import com.google.common.base.Predicates;
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.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.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.ConfigNotesMigration;
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.NotesMigrationState;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
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.Predicate;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.io.NullOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** One stop shop for migrating a site's change storage from ReviewDb to NoteDb. */
public class NoteDbMigrator implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(NoteDbMigrator.class);
public static class Builder {
private final SitePaths sitePaths;
private final SchemaFactory<ReviewDb> schemaFactory;
private final NoteDbUpdateManager.Factory updateManagerFactory;
private final ChangeRebuilder rebuilder;
private final ChangeBundleReader bundleReader;
private final WorkQueue workQueue;
private int threads;
private ImmutableList<Project.NameKey> projects = ImmutableList.of();
private ImmutableList<Change.Id> changes = ImmutableList.of();
private OutputStream progressOut = NullOutputStream.INSTANCE;
private boolean trial;
private boolean forceRebuild;
@Inject
Builder(
SitePaths sitePaths,
SchemaFactory<ReviewDb> schemaFactory,
NoteDbUpdateManager.Factory updateManagerFactory,
ChangeRebuilder rebuilder,
ChangeBundleReader bundleReader,
WorkQueue workQueue) {
this.sitePaths = sitePaths;
this.schemaFactory = schemaFactory;
this.updateManagerFactory = updateManagerFactory;
this.rebuilder = rebuilder;
this.bundleReader = bundleReader;
this.workQueue = workQueue;
}
/**
* 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 = checkNotNull(progressOut);
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;
}
public NoteDbMigrator build() {
return new NoteDbMigrator(
sitePaths,
schemaFactory,
updateManagerFactory,
rebuilder,
bundleReader,
threads > 1
? MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "RebuildChange"))
: MoreExecutors.newDirectExecutorService(),
projects,
changes,
progressOut,
trial,
forceRebuild);
}
}
private final FileBasedConfig gerritConfig;
private final SchemaFactory<ReviewDb> schemaFactory;
private final NoteDbUpdateManager.Factory updateManagerFactory;
private final ChangeRebuilder rebuilder;
private final ChangeBundleReader bundleReader;
private final ListeningExecutorService executor;
private final ImmutableList<Project.NameKey> projects;
private final ImmutableList<Change.Id> changes;
private final OutputStream progressOut;
private final boolean trial;
private final boolean forceRebuild;
private NoteDbMigrator(
SitePaths sitePaths,
SchemaFactory<ReviewDb> schemaFactory,
NoteDbUpdateManager.Factory updateManagerFactory,
ChangeRebuilder rebuilder,
ChangeBundleReader bundleReader,
ListeningExecutorService executor,
ImmutableList<Project.NameKey> projects,
ImmutableList<Change.Id> changes,
OutputStream progressOut,
boolean trial,
boolean forceRebuild) {
this.schemaFactory = schemaFactory;
this.updateManagerFactory = updateManagerFactory;
this.rebuilder = rebuilder;
this.bundleReader = bundleReader;
boolean hasChanges = !changes.isEmpty();
boolean hasProjects = !projects.isEmpty();
checkArgument(!(hasChanges && hasProjects), "cannot set both changes and projects");
this.gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
this.executor = executor;
this.projects = projects;
this.changes = changes;
this.progressOut = progressOut;
this.trial = trial;
this.forceRebuild = forceRebuild;
}
@Override
public void close() {
executor.shutdownNow();
}
public void migrate() throws OrmException, IOException {
checkState(
changes.isEmpty() && projects.isEmpty(),
"cannot set changes or projects during auto-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(NotesMigrationState.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");
}
boolean rebuilt = false;
while (state.compareTo(NotesMigrationState.NOTE_DB_UNFUSED) < 0) {
if (trial && state.compareTo(NotesMigrationState.READ_WRITE_NO_SEQUENCE) >= 0) {
return;
}
switch (state) {
case REVIEW_DB:
state = turnOnWrites(state);
break;
case WRITE:
state = rebuildAndEnableReads(state);
rebuilt = true;
break;
case READ_WRITE_NO_SEQUENCE:
if (forceRebuild && !rebuilt) {
state = rebuildAndEnableReads(state);
rebuilt = true;
}
state = enableSequences();
break;
case READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY:
if (forceRebuild && !rebuilt) {
state = rebuildAndEnableReads(state);
rebuilt = true;
}
state = setNoteDbPrimary();
break;
case READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY:
state = disableReviewDb();
break;
case NOTE_DB_UNFUSED:
// Done!
break;
case NOTE_DB:
// TODO(dborowitz): Allow this state once FileRepository supports fused updates.
// Until then, fallthrough and throw.
default:
throw new MigrationException(
"Migration out of the following state is not supported:\n" + state.toText());
}
}
}
private NotesMigrationState turnOnWrites(NotesMigrationState prev) throws IOException {
return saveState(prev, NotesMigrationState.WRITE);
}
private NotesMigrationState rebuildAndEnableReads(NotesMigrationState prev)
throws OrmException, IOException {
rebuild();
return saveState(prev, NotesMigrationState.READ_WRITE_NO_SEQUENCE);
}
private NotesMigrationState enableSequences() {
throw new UnsupportedOperationException("not yet implemented");
}
private NotesMigrationState setNoteDbPrimary() {
throw new UnsupportedOperationException("not yet implemented");
}
private NotesMigrationState disableReviewDb() {
throw new UnsupportedOperationException("not yet implemented");
}
private Optional<NotesMigrationState> loadState() throws IOException {
try {
gerritConfig.load();
return NotesMigrationState.forNotesMigration(new ConfigNotesMigration(gerritConfig));
} catch (ConfigInvalidException | IllegalArgumentException e) {
log.warn("error reading NoteDb migration options from " + gerritConfig.getFile(), e);
return Optional.empty();
}
}
private NotesMigrationState saveState(
NotesMigrationState expectedOldState, NotesMigrationState newState) throws IOException {
// 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"));
}
ConfigNotesMigration.setConfigValues(gerritConfig, newState.migration());
gerritConfig.save();
return newState;
}
public void rebuild() throws MigrationException, OrmException {
boolean ok;
Stopwatch sw = Stopwatch.createStarted();
log.info("Rebuilding changes in NoteDb");
List<ListenableFuture<Boolean>> futures = new ArrayList<>();
ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
List<Project.NameKey> projectNames =
Ordering.usingToString().sortedCopy(changesByProject.keySet());
for (Project.NameKey project : projectNames) {
ListenableFuture<Boolean> future =
executor.submit(
() -> {
try (ReviewDb db = unwrapDb(schemaFactory.open())) {
return rebuildProject(db, changesByProject, project);
} catch (Exception e) {
log.error("Error rebuilding project " + project, e);
return false;
}
});
futures.add(future);
}
try {
ok = Iterables.all(Futures.allAsList(futures).get(), Predicates.equalTo(true));
} catch (InterruptedException | ExecutionException e) {
log.error("Error rebuilding projects", e);
ok = false;
}
double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
log.info(
String.format(
"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.
try (ReviewDb db = unwrapDb(schemaFactory.open())) {
SetMultimap<Project.NameKey, Change.Id> out =
MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
.treeSetValues(comparing(Change.Id::get))
.build();
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 boolean rebuildProject(
ReviewDb db,
ImmutableListMultimap<Project.NameKey, Change.Id> allChanges,
Project.NameKey project)
throws IOException, OrmException {
checkArgument(allChanges.containsKey(project));
boolean ok = true;
ProgressMonitor pm =
new TextProgressMonitor(
new PrintWriter(new BufferedWriter(new OutputStreamWriter(progressOut, UTF_8))));
pm.beginTask(FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) {
for (Change.Id changeId : allChanges.get(project)) {
try {
rebuilder.buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
} catch (NoPatchSetsException e) {
log.warn(e.getMessage());
} catch (Throwable t) {
log.error("Failed to rebuild change " + changeId, t);
ok = false;
}
pm.update(1);
}
manager.execute();
} finally {
pm.endTask();
}
return ok;
}
}

View File

@ -1,205 +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 java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;
import com.google.common.base.Predicates;
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.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.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Rebuilder for all changes in a site. */
public class SiteRebuilder implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(SiteRebuilder.class);
public interface Factory {
SiteRebuilder create(
int threads,
@Nullable Collection<Project.NameKey> projects,
@Nullable Collection<Change.Id> changes);
}
private final SchemaFactory<ReviewDb> schemaFactory;
private final NoteDbUpdateManager.Factory updateManagerFactory;
private final ChangeRebuilder rebuilder;
private final ChangeBundleReader bundleReader;
private final ListeningExecutorService executor;
private final ImmutableList<Project.NameKey> projects;
private final ImmutableList<Change.Id> changes;
@Inject
SiteRebuilder(
SchemaFactory<ReviewDb> schemaFactory,
NoteDbUpdateManager.Factory updateManagerFactory,
ChangeRebuilder rebuilder,
ChangeBundleReader bundleReader,
WorkQueue workQueue,
@Assisted int threads,
@Assisted @Nullable Collection<Project.NameKey> projects,
@Assisted @Nullable Collection<Change.Id> changes) {
this.schemaFactory = schemaFactory;
this.updateManagerFactory = updateManagerFactory;
this.rebuilder = rebuilder;
this.bundleReader = bundleReader;
this.executor =
threads > 0
? MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "RebuildChange"))
: MoreExecutors.newDirectExecutorService();
this.projects = projects != null ? ImmutableList.copyOf(projects) : ImmutableList.of();
this.changes = changes != null ? ImmutableList.copyOf(changes) : ImmutableList.of();
}
@Override
public void close() {
executor.shutdownNow();
}
public boolean rebuild() throws OrmException {
boolean ok;
Stopwatch sw = Stopwatch.createStarted();
List<ListenableFuture<Boolean>> futures = new ArrayList<>();
ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
List<Project.NameKey> projectNames =
Ordering.usingToString().sortedCopy(changesByProject.keySet());
for (Project.NameKey project : projectNames) {
ListenableFuture<Boolean> future =
executor.submit(
() -> {
try (ReviewDb db = unwrapDb(schemaFactory.open())) {
return rebuildProject(db, changesByProject, project);
} catch (Exception e) {
log.error("Error rebuilding project " + project, e);
return false;
}
});
futures.add(future);
}
try {
ok = Iterables.all(Futures.allAsList(futures).get(), Predicates.equalTo(true));
} catch (InterruptedException | ExecutionException e) {
log.error("Error rebuilding projects", e);
ok = false;
}
double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
System.out.format(
"Rebuild %d changes in %.01fs (%.01f/s)\n",
changesByProject.size(), t, changesByProject.size() / t);
return ok;
}
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.
try (ReviewDb db = unwrapDb(schemaFactory.open())) {
SetMultimap<Project.NameKey, Change.Id> out =
MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
.treeSetValues(comparing(Change.Id::get))
.build();
if (!projects.isEmpty()) {
checkState(changes.isEmpty());
return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
}
if (!changes.isEmpty()) {
checkState(projects.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 boolean rebuildProject(
ReviewDb db,
ImmutableListMultimap<Project.NameKey, Change.Id> allChanges,
Project.NameKey project)
throws IOException, OrmException {
checkArgument(allChanges.containsKey(project));
boolean ok = true;
ProgressMonitor pm =
new TextProgressMonitor(
new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
pm.beginTask(FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) {
for (Change.Id changeId : allChanges.get(project)) {
try {
rebuilder.buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
} catch (NoPatchSetsException e) {
log.warn(e.getMessage());
} catch (Throwable t) {
log.error("Failed to rebuild change " + changeId, t);
ok = false;
}
pm.update(1);
}
manager.execute();
} finally {
pm.endTask();
}
return ok;
}
}