Add config option setting default primary storage for new changes
Setting to NOTE_DB means new changes will never be written to ReviewDb. Update ChangeNotes.Factory to properly detect the case of a change not existing in ReviewDb but possibly still existing in NoteDb. This config option only controls the primary storage for new changes. Old changes (that have not been migrated, which is all of them since this change predates the migration tools) keep their primary storage as REVIEW_DB. This means that a single running server needs to be able to handle a mix of NOTE_DB/REVIEW_DB changes. Thus we need to continue using a live ReviewDb instance in the server, and just avoid writing any NoteDb-primary changes to that instance. The easiest way to implement this technically is to keep all the BatchUpdate code the same but only commit the change transaction if the change requires ReviewDb. This means we don't have to change any BatchUpdate.Op implementations, they can continue unconditionally writing. Rolling back the transaction is much simpler than creating some kind of ReviewDb wrapper that drops writes on the floor, even though it does technically create some database traffic even though no writes are committed. Add a new NoteDbMode to run all tests with this option enabled, double-checking after the tests that no changes were stored in ReviewDb. Tweak tests in various ways to work with this option enabled, avoiding direct use of ReviewDb when the primary storage is NoteDb. Change-Id: I9caf13192f955c4ec90409da32609d0a6f496d96
This commit is contained in:
@@ -1049,18 +1049,45 @@ public class BatchUpdate implements AutoCloseable {
|
||||
private ChangeContext newChangeContext(ReviewDb db, Repository repo,
|
||||
RevWalk rw, Change.Id id) throws OrmException {
|
||||
Change c = newChanges.get(id);
|
||||
if (c == null) {
|
||||
boolean isNew = c != null;
|
||||
PrimaryStorage defaultStorage = notesMigration.changePrimaryStorage();
|
||||
if (isNew) {
|
||||
// New change: populate noteDbState.
|
||||
checkState(c.getNoteDbState() == null,
|
||||
"noteDbState should not be filled in by callers");
|
||||
if (defaultStorage == PrimaryStorage.NOTE_DB) {
|
||||
c.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
|
||||
}
|
||||
} else {
|
||||
// Existing change.
|
||||
c = ChangeNotes.readOneReviewDbChange(db, id);
|
||||
if (c == null) {
|
||||
logDebug("Failed to get change {} from unwrapped db", id);
|
||||
throw new NoSuchChangeException(id);
|
||||
if (defaultStorage == PrimaryStorage.REVIEW_DB) {
|
||||
logDebug("Failed to get change {} from unwrapped db", id);
|
||||
throw new NoSuchChangeException(id);
|
||||
}
|
||||
// Not in ReviewDb, but new changes are created with default primary
|
||||
// storage as NOTE_DB, so we can assume that a missing change is
|
||||
// NoteDb primary. Pass a synthetic change into ChangeNotes.Factory,
|
||||
// which lets ChangeNotes take care of the existence check.
|
||||
//
|
||||
// TODO(dborowitz): This assumption is potentially risky, because
|
||||
// it means once we turn this option on and start creating changes
|
||||
// without writing anything to ReviewDb, we can't turn this option
|
||||
// back off without making those changes inaccessible. The problem
|
||||
// is we have no way of distinguishing a change that only exists in
|
||||
// NoteDb because it only ever existed in NoteDb, from a change that
|
||||
// only exists in NoteDb because it used to exist in ReviewDb and
|
||||
// deleting from ReviewDb succeeded but deleting from NoteDb failed.
|
||||
//
|
||||
// TODO(dborowitz): We actually still have that problem anyway. Maybe
|
||||
// we need a cutoff timestamp? Or maybe we need to start leaving
|
||||
// tombstones in ReviewDb?
|
||||
c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
|
||||
}
|
||||
NoteDbChangeState.checkNotReadOnly(c, skewMs);
|
||||
}
|
||||
// Pass in preloaded change to controlFor, to avoid:
|
||||
// - reading from a db that does not belong to this update
|
||||
// - attempting to read a change that doesn't exist yet
|
||||
ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c);
|
||||
ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
|
||||
ChangeControl ctl = changeControlFactory.controlFor(notes, user);
|
||||
return new ChangeContext(ctl, new BatchUpdateReviewDb(db), repo, rw);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
|
||||
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
|
||||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
|
||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
@@ -174,7 +175,20 @@ public abstract class AbstractChangeNotes<T> {
|
||||
return ref != null ? ref.getObjectId() : null;
|
||||
}
|
||||
|
||||
protected LoadHandle openHandle(Repository repo) throws IOException {
|
||||
/**
|
||||
* Open a handle for reading this entity from a repository.
|
||||
* <p>
|
||||
* Implementations may override this method to provide auto-rebuilding
|
||||
* behavior.
|
||||
*
|
||||
* @param repo open repository.
|
||||
* @return handle for reading the entity.
|
||||
*
|
||||
* @throws NoSuchChangeException change does not exist.
|
||||
* @throws IOException a repo-level error occurred.
|
||||
*/
|
||||
protected LoadHandle openHandle(Repository repo)
|
||||
throws NoSuchChangeException, IOException {
|
||||
return openHandle(repo, readRef(repo));
|
||||
}
|
||||
|
||||
@@ -182,7 +196,7 @@ public abstract class AbstractChangeNotes<T> {
|
||||
return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), id);
|
||||
}
|
||||
|
||||
public T reload() throws OrmException {
|
||||
public T reload() throws NoSuchChangeException, OrmException {
|
||||
loaded = false;
|
||||
return load();
|
||||
}
|
||||
@@ -215,7 +229,7 @@ public abstract class AbstractChangeNotes<T> {
|
||||
|
||||
/** Set up the metadata, parsing any state from the loaded revision. */
|
||||
protected abstract void onLoad(LoadHandle handle)
|
||||
throws IOException, ConfigInvalidException;
|
||||
throws NoSuchChangeException, IOException, ConfigInvalidException;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final T self() {
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.common.data.SubmitRecord;
|
||||
import com.google.gerrit.metrics.Timer1;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||
import com.google.gerrit.reviewdb.client.Comment;
|
||||
@@ -124,7 +125,14 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
public ChangeNotes createChecked(ReviewDb db, Project.NameKey project,
|
||||
Change.Id changeId) throws OrmException {
|
||||
Change change = readOneReviewDbChange(db, changeId);
|
||||
if (change == null || !change.getProject().equals(project)) {
|
||||
if (change == null) {
|
||||
if (!args.migration.readChanges()) {
|
||||
throw new NoSuchChangeException(changeId);
|
||||
}
|
||||
// Change isn't in ReviewDb, but its primary storage might be in NoteDb.
|
||||
// Prepopulate the change exists with proper noteDbState field.
|
||||
change = newNoteDbOnlyChange(project, changeId);
|
||||
} else if (!change.getProject().equals(project)) {
|
||||
throw new NoSuchChangeException(changeId);
|
||||
}
|
||||
return new ChangeNotes(args, change).load();
|
||||
@@ -144,16 +152,33 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
return changes.get(0).notes();
|
||||
}
|
||||
|
||||
public static Change newNoteDbOnlyChange(
|
||||
Project.NameKey project, Change.Id changeId) {
|
||||
Change change = new Change(
|
||||
null, changeId, null,
|
||||
new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"),
|
||||
null);
|
||||
change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
|
||||
return change;
|
||||
}
|
||||
|
||||
private Change loadChangeFromDb(ReviewDb db, Project.NameKey project,
|
||||
Change.Id changeId) throws OrmException {
|
||||
Change change = readOneReviewDbChange(db, changeId);
|
||||
checkArgument(project != null, "project is required");
|
||||
checkNotNull(change,
|
||||
"change %s not found in ReviewDb", changeId);
|
||||
checkArgument(change.getProject().equals(project),
|
||||
"passed project %s when creating ChangeNotes for %s, but actual"
|
||||
+ " project is %s",
|
||||
project, changeId, change.getProject());
|
||||
Change change = readOneReviewDbChange(db, changeId);
|
||||
|
||||
if (change == null && args.migration.readChanges()) {
|
||||
// Change isn't in ReviewDb, but its primary storage might be in NoteDb.
|
||||
// Prepopulate the change exists with proper noteDbState field.
|
||||
change = newNoteDbOnlyChange(project, changeId);
|
||||
} else {
|
||||
checkNotNull(change, "change %s not found in ReviewDb", changeId);
|
||||
checkArgument(change.getProject().equals(project),
|
||||
"passed project %s when creating ChangeNotes for %s, but actual"
|
||||
+ " project is %s",
|
||||
project, changeId, change.getProject());
|
||||
}
|
||||
|
||||
// TODO: Throw NoSuchChangeException when the change is not found in the
|
||||
// database
|
||||
return change;
|
||||
@@ -168,7 +193,7 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
public ChangeNotes createWithAutoRebuildingDisabled(ReviewDb db,
|
||||
Project.NameKey project, Change.Id changeId) throws OrmException {
|
||||
return new ChangeNotes(
|
||||
args, loadChangeFromDb(db, project, changeId), false, null).load();
|
||||
args, loadChangeFromDb(db, project, changeId), true, false, null).load();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,13 +208,14 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
return new ChangeNotes(args, change);
|
||||
}
|
||||
|
||||
public ChangeNotes createForBatchUpdate(Change change) throws OrmException {
|
||||
return new ChangeNotes(args, change, false, null).load();
|
||||
public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist)
|
||||
throws OrmException {
|
||||
return new ChangeNotes(args, change, shouldExist, false, null).load();
|
||||
}
|
||||
|
||||
public ChangeNotes createWithAutoRebuildingDisabled(Change change,
|
||||
RefCache refs) throws OrmException {
|
||||
return new ChangeNotes(args, change, false, refs).load();
|
||||
return new ChangeNotes(args, change, true, false, refs).load();
|
||||
}
|
||||
|
||||
// TODO(ekempin): Remove when database backend is deleted
|
||||
@@ -226,7 +252,7 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
|
||||
public List<ChangeNotes> create(ReviewDb db, Project.NameKey project,
|
||||
Collection<Change.Id> changeIds, Predicate<ChangeNotes> predicate)
|
||||
throws OrmException {
|
||||
throws OrmException {
|
||||
List<ChangeNotes> notes = new ArrayList<>();
|
||||
if (args.migration.enabled()) {
|
||||
for (Change.Id cid : changeIds) {
|
||||
@@ -302,13 +328,18 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
Project.NameKey project) throws OrmException, IOException {
|
||||
Set<Change.Id> ids = scan(repo);
|
||||
List<ChangeNotes> changeNotes = new ArrayList<>(ids.size());
|
||||
PrimaryStorage defaultStorage = args.migration.changePrimaryStorage();
|
||||
for (Change.Id id : ids) {
|
||||
Change change = readOneReviewDbChange(db, id);
|
||||
if (change == null) {
|
||||
log.warn("skipping change {} found in project {} " +
|
||||
"but not in ReviewDb",
|
||||
id, project);
|
||||
continue;
|
||||
if (defaultStorage == PrimaryStorage.REVIEW_DB) {
|
||||
log.warn("skipping change {} found in project {} " +
|
||||
"but not in ReviewDb",
|
||||
id, project);
|
||||
continue;
|
||||
}
|
||||
// TODO(dborowitz): See discussion in BatchUpdate#newChangeContext.
|
||||
change = newNoteDbOnlyChange(project, id);
|
||||
} else if (!change.getProject().equals(project)) {
|
||||
log.error(
|
||||
"skipping change {} found in project {} " +
|
||||
@@ -337,6 +368,7 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
}
|
||||
}
|
||||
|
||||
private final boolean shouldExist;
|
||||
private final RefCache refs;
|
||||
|
||||
private Change change;
|
||||
@@ -358,13 +390,14 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
|
||||
@VisibleForTesting
|
||||
public ChangeNotes(Args args, Change change) {
|
||||
this(args, change, true, null);
|
||||
this(args, change, true, true, null);
|
||||
}
|
||||
|
||||
private ChangeNotes(Args args, Change change, boolean autoRebuild,
|
||||
@Nullable RefCache refs) {
|
||||
private ChangeNotes(Args args, Change change, boolean shouldExist,
|
||||
boolean autoRebuild, @Nullable RefCache refs) {
|
||||
super(args, change.getId(), PrimaryStorage.of(change), autoRebuild);
|
||||
this.change = new Change(change);
|
||||
this.shouldExist = shouldExist;
|
||||
this.refs = refs;
|
||||
}
|
||||
|
||||
@@ -555,9 +588,14 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
|
||||
@Override
|
||||
protected void onLoad(LoadHandle handle)
|
||||
throws IOException, ConfigInvalidException {
|
||||
throws NoSuchChangeException, IOException, ConfigInvalidException {
|
||||
ObjectId rev = handle.id();
|
||||
if (rev == null) {
|
||||
if (args.migration.readChanges()
|
||||
&& PrimaryStorage.of(change) == PrimaryStorage.NOTE_DB
|
||||
&& shouldExist) {
|
||||
throw new NoSuchChangeException(getChangeId());
|
||||
}
|
||||
loadDefaults();
|
||||
return;
|
||||
}
|
||||
@@ -587,12 +625,17 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LoadHandle openHandle(Repository repo) throws IOException {
|
||||
protected LoadHandle openHandle(Repository repo)
|
||||
throws NoSuchChangeException, IOException {
|
||||
if (autoRebuild) {
|
||||
NoteDbChangeState state = NoteDbChangeState.parse(change);
|
||||
ObjectId id = readRef(repo);
|
||||
if (state == null && id == null) {
|
||||
return super.openHandle(repo, id);
|
||||
if (id == null) {
|
||||
if (state == null) {
|
||||
return super.openHandle(repo, id);
|
||||
} else if (shouldExist) {
|
||||
throw new NoSuchChangeException(getChangeId());
|
||||
}
|
||||
}
|
||||
RefCache refs = this.refs != null ? this.refs : new RepoRefCache(repo);
|
||||
if (!NoteDbChangeState.isChangeUpToDate(state, refs, getChangeId())) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
@@ -49,9 +50,11 @@ public class ConfigNotesMigration extends NotesMigration {
|
||||
}
|
||||
|
||||
private static final String NOTE_DB = "noteDb";
|
||||
|
||||
private static final String PRIMARY_STORAGE = "primaryStorage";
|
||||
private static final String READ = "read";
|
||||
private static final String WRITE = "write";
|
||||
private static final String SEQUENCE = "sequence";
|
||||
private static final String WRITE = "write";
|
||||
|
||||
private static void checkConfig(Config cfg) {
|
||||
Set<String> keys = new HashSet<>();
|
||||
@@ -81,6 +84,7 @@ public class ConfigNotesMigration extends NotesMigration {
|
||||
private final boolean writeChanges;
|
||||
private final boolean readChanges;
|
||||
private final boolean readChangeSequence;
|
||||
private final PrimaryStorage changePrimaryStorage;
|
||||
|
||||
private final boolean writeAccounts;
|
||||
private final boolean readAccounts;
|
||||
@@ -98,6 +102,9 @@ public class ConfigNotesMigration extends NotesMigration {
|
||||
// NoteDb. This decision for the default may be reevaluated later.
|
||||
readChangeSequence = cfg.getBoolean(NOTE_DB, CHANGES.key(), SEQUENCE, false);
|
||||
|
||||
changePrimaryStorage = cfg.getEnum(
|
||||
NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB);
|
||||
|
||||
writeAccounts = cfg.getBoolean(NOTE_DB, ACCOUNTS.key(), WRITE, false);
|
||||
readAccounts = cfg.getBoolean(NOTE_DB, ACCOUNTS.key(), READ, false);
|
||||
}
|
||||
@@ -117,6 +124,11 @@ public class ConfigNotesMigration extends NotesMigration {
|
||||
return readChangeSequence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrimaryStorage changePrimaryStorage() {
|
||||
return changePrimaryStorage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeAccounts() {
|
||||
return writeAccounts;
|
||||
|
||||
@@ -188,7 +188,8 @@ public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LoadHandle openHandle(Repository repo) throws IOException {
|
||||
protected LoadHandle openHandle(Repository repo)
|
||||
throws NoSuchChangeException, IOException {
|
||||
if (rebuildResult != null) {
|
||||
StagedResult sr = checkNotNull(rebuildResult.staged());
|
||||
return LoadHandle.create(
|
||||
@@ -216,7 +217,8 @@ public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
|
||||
return null;
|
||||
}
|
||||
|
||||
private LoadHandle rebuildAndOpen(Repository repo) throws IOException {
|
||||
private LoadHandle rebuildAndOpen(Repository repo)
|
||||
throws NoSuchChangeException, IOException {
|
||||
Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES);
|
||||
try {
|
||||
Change.Id cid = getChangeId();
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
package com.google.gerrit.server.notedb;
|
||||
|
||||
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
|
||||
|
||||
/**
|
||||
* Holds the current state of the NoteDb migration.
|
||||
* <p>
|
||||
@@ -71,6 +73,9 @@ public abstract class NotesMigration {
|
||||
*/
|
||||
public abstract boolean readChangeSequence();
|
||||
|
||||
/** @return default primary storage for new changes. */
|
||||
public abstract PrimaryStorage changePrimaryStorage();
|
||||
|
||||
public abstract boolean readAccounts();
|
||||
|
||||
public abstract boolean writeAccounts();
|
||||
|
||||
Reference in New Issue
Block a user