Merge "Support read-only mode for NoteDb changes"
This commit is contained in:
@@ -20,6 +20,7 @@ import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
|
||||
import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -34,6 +35,7 @@ import com.google.gerrit.extensions.api.changes.DraftInput;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
|
||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||
@@ -52,9 +54,11 @@ import com.google.gerrit.server.config.AllUsersName;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
|
||||
import com.google.gerrit.server.git.RepoRefCache;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
import com.google.gerrit.server.notedb.ChangeBundle;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
import com.google.gerrit.server.notedb.NoteDbChangeState;
|
||||
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
|
||||
import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
|
||||
import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
|
||||
import com.google.gerrit.testutil.ConfigSuite;
|
||||
@@ -880,6 +884,71 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failWhenWritesDisabled() throws Exception {
|
||||
setNotesMigration(true, true);
|
||||
|
||||
PushOneCommit.Result r = createChange();
|
||||
Change.Id id = r.getPatchSetId().getParentKey();
|
||||
assertChangeUpToDate(true, id);
|
||||
assertThat(gApi.changes().id(id.get()).info().topic).isNull();
|
||||
|
||||
// Turning off writes causes failure.
|
||||
setNotesMigration(false, true);
|
||||
try {
|
||||
gApi.changes().id(id.get()).topic(name("a-topic"));
|
||||
fail("Expected write to fail");
|
||||
} catch (RestApiException e) {
|
||||
assertChangesReadOnly(e);
|
||||
}
|
||||
|
||||
// Update was not written.
|
||||
assertThat(gApi.changes().id(id.get()).info().topic).isNull();
|
||||
assertChangeUpToDate(true, id);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rebuildWhenWritesDisabledWorksButDoesNotWrite() throws Exception {
|
||||
setNotesMigration(true, true);
|
||||
|
||||
PushOneCommit.Result r = createChange();
|
||||
Change.Id id = r.getPatchSetId().getParentKey();
|
||||
assertChangeUpToDate(true, id);
|
||||
|
||||
// Make a ReviewDb change behind NoteDb's back and ensure it's detected.
|
||||
setNotesMigration(false, false);
|
||||
gApi.changes().id(id.get()).topic(name("a-topic"));
|
||||
setInvalidNoteDbState(id);
|
||||
assertChangeUpToDate(false, id);
|
||||
|
||||
// On next NoteDb read, change is rebuilt in-memory but not stored.
|
||||
setNotesMigration(false, true);
|
||||
assertThat(gApi.changes().id(id.get()).info().topic)
|
||||
.isEqualTo(name("a-topic"));
|
||||
assertChangeUpToDate(false, id);
|
||||
|
||||
// Attempting to write directly causes failure.
|
||||
try {
|
||||
gApi.changes().id(id.get()).topic(name("other-topic"));
|
||||
fail("Expected write to fail");
|
||||
} catch (RestApiException e) {
|
||||
assertChangesReadOnly(e);
|
||||
}
|
||||
|
||||
// Update was not written.
|
||||
assertThat(gApi.changes().id(id.get()).info().topic)
|
||||
.isEqualTo(name("a-topic"));
|
||||
assertChangeUpToDate(false, id);
|
||||
}
|
||||
|
||||
private void assertChangesReadOnly(RestApiException e) throws Exception {
|
||||
Throwable cause = e.getCause();
|
||||
assertThat(cause).isInstanceOf(UpdateException.class);
|
||||
assertThat(cause.getCause()).isInstanceOf(OrmException.class);
|
||||
assertThat(cause.getCause())
|
||||
.hasMessage(NoteDbUpdateManager.CHANGES_READ_ONLY);
|
||||
}
|
||||
|
||||
private void setInvalidNoteDbState(Change.Id id) throws Exception {
|
||||
ReviewDb db = unwrapDb();
|
||||
Change c = db.changes().get(id);
|
||||
|
||||
@@ -54,7 +54,7 @@ public class Rebuild implements RestModifyView<ChangeResource, Input> {
|
||||
public Response<?> apply(ChangeResource rsrc, Input input)
|
||||
throws ResourceNotFoundException, IOException, OrmException,
|
||||
ConfigInvalidException {
|
||||
if (!migration.writeChanges()) {
|
||||
if (!migration.commitChangeWrites()) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -636,6 +636,11 @@ public class BatchUpdate implements AutoCloseable {
|
||||
|
||||
List<ChangeTask> tasks = new ArrayList<>(ops.keySet().size());
|
||||
try {
|
||||
if (!ops.isEmpty() && notesMigration.failChangeWrites()) {
|
||||
// Fail fast before attempting any writes if changes are read-only, as
|
||||
// this is a programmer error.
|
||||
throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
|
||||
}
|
||||
List<ListenableFuture<?>> futures = new ArrayList<>(ops.keySet().size());
|
||||
for (Map.Entry<Change.Id, Collection<Op>> e : ops.asMap().entrySet()) {
|
||||
ChangeTask task =
|
||||
@@ -645,13 +650,15 @@ public class BatchUpdate implements AutoCloseable {
|
||||
}
|
||||
Futures.allAsList(futures).get();
|
||||
|
||||
if (notesMigration.writeChanges()) {
|
||||
if (notesMigration.commitChangeWrites()) {
|
||||
executeNoteDbUpdates(tasks);
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Throwables.propagateIfInstanceOf(e.getCause(), UpdateException.class);
|
||||
Throwables.propagateIfInstanceOf(e.getCause(), RestApiException.class);
|
||||
throw new UpdateException(e);
|
||||
} catch (OrmException e) {
|
||||
throw new UpdateException(e);
|
||||
}
|
||||
|
||||
// Reindex changes.
|
||||
@@ -802,7 +809,7 @@ public class BatchUpdate implements AutoCloseable {
|
||||
deleted = ctx.deleted;
|
||||
|
||||
// Stage the NoteDb update and store its state in the Change.
|
||||
if (notesMigration.writeChanges()) {
|
||||
if (notesMigration.commitChangeWrites()) {
|
||||
updateManager = stageNoteDbUpdate(ctx, deleted);
|
||||
}
|
||||
|
||||
@@ -821,7 +828,7 @@ public class BatchUpdate implements AutoCloseable {
|
||||
db.rollback();
|
||||
}
|
||||
|
||||
if (notesMigration.writeChanges()) {
|
||||
if (notesMigration.commitChangeWrites()) {
|
||||
try {
|
||||
// Do not execute the NoteDbUpdateManager, as we don't want too much
|
||||
// contention on the underlying repo, and we would rather use a
|
||||
|
||||
@@ -204,9 +204,7 @@ public class ChangeIndexer {
|
||||
public void index(ReviewDb db, Change change)
|
||||
throws IOException, OrmException {
|
||||
ChangeData cd;
|
||||
if (notesMigration.readChanges()) {
|
||||
cd = changeDataFactory.create(db, change);
|
||||
} else if (notesMigration.writeChanges()) {
|
||||
if (notesMigration.commitChangeWrites()) {
|
||||
// Auto-rebuilding when NoteDb reads are disabled just increases
|
||||
// contention on the meta ref from a background indexing thread with
|
||||
// little benefit. The next actual write to the entity may still incur a
|
||||
|
||||
@@ -192,6 +192,11 @@ public abstract class AbstractChangeUpdate {
|
||||
if (isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allow this method to proceed even if migration.failChangeWrites() = true.
|
||||
// This may be used by an auto-rebuilding step that the caller does not plan
|
||||
// to actually store.
|
||||
|
||||
checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
|
||||
ObjectId z = ObjectId.zeroId();
|
||||
CommitBuilder cb = applyImpl(rw, ins, curr);
|
||||
|
||||
@@ -122,6 +122,7 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
private final ChangeNoteUtil changeNoteUtil;
|
||||
private final ChangeUpdate.Factory updateFactory;
|
||||
private final NoteDbUpdateManager.Factory updateManagerFactory;
|
||||
private final NotesMigration migration;
|
||||
private final PatchListCache patchListCache;
|
||||
private final PersonIdent serverIdent;
|
||||
private final ProjectCache projectCache;
|
||||
@@ -134,6 +135,7 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
ChangeNoteUtil changeNoteUtil,
|
||||
ChangeUpdate.Factory updateFactory,
|
||||
NoteDbUpdateManager.Factory updateManagerFactory,
|
||||
NotesMigration migration,
|
||||
PatchListCache patchListCache,
|
||||
@GerritPersonIdent PersonIdent serverIdent,
|
||||
@Nullable ProjectCache projectCache,
|
||||
@@ -144,6 +146,7 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
this.changeNoteUtil = changeNoteUtil;
|
||||
this.updateFactory = updateFactory;
|
||||
this.updateManagerFactory = updateManagerFactory;
|
||||
this.migration = migration;
|
||||
this.patchListCache = patchListCache;
|
||||
this.serverIdent = serverIdent;
|
||||
this.projectCache = projectCache;
|
||||
@@ -221,7 +224,14 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
return change;
|
||||
}
|
||||
});
|
||||
manager.execute();
|
||||
if (!migration.failChangeWrites()) {
|
||||
manager.execute();
|
||||
} else {
|
||||
// Don't even attempt to execute if read-only, it would fail anyway. But
|
||||
// do throw an exception to the caller so they know to use the staged
|
||||
// results instead of reading from the repo.
|
||||
throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
|
||||
}
|
||||
} catch (AbortUpdateException e) {
|
||||
// Drop this rebuild; another thread completed it.
|
||||
}
|
||||
|
||||
@@ -64,10 +64,6 @@ public class ConfigNotesMigration extends NotesMigration {
|
||||
checkArgument(lk.equals(WRITE) || lk.equals(READ),
|
||||
"invalid NoteDb key: %s.%s", t, key);
|
||||
}
|
||||
boolean write = cfg.getBoolean(NOTE_DB, t, WRITE, false);
|
||||
boolean read = cfg.getBoolean(NOTE_DB, t, READ, false);
|
||||
checkArgument(!(read && !write),
|
||||
"must have write enabled when read enabled: %s", t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +91,7 @@ public class ConfigNotesMigration extends NotesMigration {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeChanges() {
|
||||
protected boolean writeChanges() {
|
||||
return writeChanges;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ import java.util.Set;
|
||||
* of updates, use {@link #stage()}.
|
||||
*/
|
||||
public class NoteDbUpdateManager {
|
||||
public static String CHANGES_READ_ONLY = "NoteDb changes are read-only";
|
||||
|
||||
public interface Factory {
|
||||
NoteDbUpdateManager create(Project.NameKey projectName);
|
||||
}
|
||||
@@ -249,7 +251,7 @@ public class NoteDbUpdateManager {
|
||||
}
|
||||
|
||||
private boolean isEmpty() {
|
||||
if (!migration.writeChanges()) {
|
||||
if (!migration.commitChangeWrites()) {
|
||||
return true;
|
||||
}
|
||||
return changeUpdates.isEmpty()
|
||||
@@ -382,6 +384,10 @@ public class NoteDbUpdateManager {
|
||||
}
|
||||
|
||||
public void execute() throws OrmException, IOException {
|
||||
// Check before even inspecting the list, as this is a programmer error.
|
||||
if (migration.failChangeWrites()) {
|
||||
throw new OrmException(CHANGES_READ_ONLY);
|
||||
}
|
||||
if (isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,9 +34,33 @@ package com.google.gerrit.server.notedb;
|
||||
* these reasons, the options remain undocumented.
|
||||
*/
|
||||
public abstract class NotesMigration {
|
||||
/**
|
||||
* Read changes from NoteDb.
|
||||
* <p>
|
||||
* Change data is read from NoteDb refs, but ReviewDb is still the source of
|
||||
* truth. If the loader determines NoteDb is out of date, the change data in
|
||||
* NoteDb will be transparently rebuilt. This means that some code paths that
|
||||
* look read-only may in fact attempt to write.
|
||||
* <p>
|
||||
* If true and {@code writeChanges() = false}, changes can still be read from
|
||||
* NoteDb, but any attempts to write will generate an error.
|
||||
*/
|
||||
public abstract boolean readChanges();
|
||||
|
||||
public abstract boolean writeChanges();
|
||||
/**
|
||||
* Write changes to NoteDb.
|
||||
* <p>
|
||||
* Updates to change data are written to NoteDb refs, but ReviewDb is still
|
||||
* the source of truth. Change data will not be written unless the NoteDb refs
|
||||
* are already up to date, and the write path will attempt to rebuild the
|
||||
* change if not.
|
||||
* <p>
|
||||
* If false, the behavior when attempting to write depends on
|
||||
* {@code readChanges()}. If {@code readChanges() = false}, writes to NoteDb
|
||||
* are simply ignored; if {@code true}, any attempts to write will generate an
|
||||
* error.
|
||||
*/
|
||||
protected abstract boolean writeChanges();
|
||||
|
||||
public abstract boolean readAccounts();
|
||||
|
||||
@@ -51,6 +75,24 @@ public abstract class NotesMigration {
|
||||
return false;
|
||||
}
|
||||
|
||||
public 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
|
||||
// write NoteDb refs.
|
||||
//
|
||||
// It is possible for commitChangeWrites() to return true and
|
||||
// failChangeWrites() to also return true, causing an error later in the
|
||||
// same codepath. This specific condition is used by the auto-rebuilding
|
||||
// path to rebuild a change and stage the results, but not commit them due
|
||||
// to failChangeWrites().
|
||||
return writeChanges() || readChanges();
|
||||
}
|
||||
|
||||
public boolean failChangeWrites() {
|
||||
return !writeChanges() && readChanges();
|
||||
}
|
||||
|
||||
public boolean enabled() {
|
||||
return writeChanges() || readChanges()
|
||||
|| writeAccounts() || readAccounts();
|
||||
|
||||
@@ -29,6 +29,8 @@ public class TestNotesMigration extends NotesMigration {
|
||||
return readChanges;
|
||||
}
|
||||
|
||||
// Increase visbility from superclass, as tests may want to check whether
|
||||
// NoteDb data is written in specific migration scenarios.
|
||||
@Override
|
||||
public boolean writeChanges() {
|
||||
return writeChanges;
|
||||
|
||||
Reference in New Issue
Block a user