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:
commit
682267df7b
@ -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`.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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() {
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user