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 com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
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.base.Function;
|
||||||
import com.google.common.collect.ImmutableList;
|
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;
|
||||||
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
|
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
|
||||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
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.Account;
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
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;
|
||||||
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
|
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
|
||||||
import com.google.gerrit.server.git.RepoRefCache;
|
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.ChangeBundle;
|
||||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||||
import com.google.gerrit.server.notedb.NoteDbChangeState;
|
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.notedb.TestChangeRebuilderWrapper;
|
||||||
import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
|
import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
|
||||||
import com.google.gerrit.testutil.ConfigSuite;
|
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 {
|
private void setInvalidNoteDbState(Change.Id id) throws Exception {
|
||||||
ReviewDb db = unwrapDb();
|
ReviewDb db = unwrapDb();
|
||||||
Change c = db.changes().get(id);
|
Change c = db.changes().get(id);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public class Rebuild implements RestModifyView<ChangeResource, Input> {
|
|||||||
public Response<?> apply(ChangeResource rsrc, Input input)
|
public Response<?> apply(ChangeResource rsrc, Input input)
|
||||||
throws ResourceNotFoundException, IOException, OrmException,
|
throws ResourceNotFoundException, IOException, OrmException,
|
||||||
ConfigInvalidException {
|
ConfigInvalidException {
|
||||||
if (!migration.writeChanges()) {
|
if (!migration.commitChangeWrites()) {
|
||||||
throw new ResourceNotFoundException();
|
throw new ResourceNotFoundException();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -636,6 +636,11 @@ public class BatchUpdate implements AutoCloseable {
|
|||||||
|
|
||||||
List<ChangeTask> tasks = new ArrayList<>(ops.keySet().size());
|
List<ChangeTask> tasks = new ArrayList<>(ops.keySet().size());
|
||||||
try {
|
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());
|
List<ListenableFuture<?>> futures = new ArrayList<>(ops.keySet().size());
|
||||||
for (Map.Entry<Change.Id, Collection<Op>> e : ops.asMap().entrySet()) {
|
for (Map.Entry<Change.Id, Collection<Op>> e : ops.asMap().entrySet()) {
|
||||||
ChangeTask task =
|
ChangeTask task =
|
||||||
@@ -645,13 +650,15 @@ public class BatchUpdate implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
Futures.allAsList(futures).get();
|
Futures.allAsList(futures).get();
|
||||||
|
|
||||||
if (notesMigration.writeChanges()) {
|
if (notesMigration.commitChangeWrites()) {
|
||||||
executeNoteDbUpdates(tasks);
|
executeNoteDbUpdates(tasks);
|
||||||
}
|
}
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
Throwables.propagateIfInstanceOf(e.getCause(), UpdateException.class);
|
Throwables.propagateIfInstanceOf(e.getCause(), UpdateException.class);
|
||||||
Throwables.propagateIfInstanceOf(e.getCause(), RestApiException.class);
|
Throwables.propagateIfInstanceOf(e.getCause(), RestApiException.class);
|
||||||
throw new UpdateException(e);
|
throw new UpdateException(e);
|
||||||
|
} catch (OrmException e) {
|
||||||
|
throw new UpdateException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reindex changes.
|
// Reindex changes.
|
||||||
@@ -802,7 +809,7 @@ public class BatchUpdate implements AutoCloseable {
|
|||||||
deleted = ctx.deleted;
|
deleted = ctx.deleted;
|
||||||
|
|
||||||
// Stage the NoteDb update and store its state in the Change.
|
// Stage the NoteDb update and store its state in the Change.
|
||||||
if (notesMigration.writeChanges()) {
|
if (notesMigration.commitChangeWrites()) {
|
||||||
updateManager = stageNoteDbUpdate(ctx, deleted);
|
updateManager = stageNoteDbUpdate(ctx, deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,7 +828,7 @@ public class BatchUpdate implements AutoCloseable {
|
|||||||
db.rollback();
|
db.rollback();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notesMigration.writeChanges()) {
|
if (notesMigration.commitChangeWrites()) {
|
||||||
try {
|
try {
|
||||||
// Do not execute the NoteDbUpdateManager, as we don't want too much
|
// Do not execute the NoteDbUpdateManager, as we don't want too much
|
||||||
// contention on the underlying repo, and we would rather use a
|
// 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)
|
public void index(ReviewDb db, Change change)
|
||||||
throws IOException, OrmException {
|
throws IOException, OrmException {
|
||||||
ChangeData cd;
|
ChangeData cd;
|
||||||
if (notesMigration.readChanges()) {
|
if (notesMigration.commitChangeWrites()) {
|
||||||
cd = changeDataFactory.create(db, change);
|
|
||||||
} else if (notesMigration.writeChanges()) {
|
|
||||||
// Auto-rebuilding when NoteDb reads are disabled just increases
|
// Auto-rebuilding when NoteDb reads are disabled just increases
|
||||||
// contention on the meta ref from a background indexing thread with
|
// contention on the meta ref from a background indexing thread with
|
||||||
// little benefit. The next actual write to the entity may still incur a
|
// little benefit. The next actual write to the entity may still incur a
|
||||||
|
|||||||
@@ -192,6 +192,11 @@ public abstract class AbstractChangeUpdate {
|
|||||||
if (isEmpty()) {
|
if (isEmpty()) {
|
||||||
return null;
|
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);
|
checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
|
||||||
ObjectId z = ObjectId.zeroId();
|
ObjectId z = ObjectId.zeroId();
|
||||||
CommitBuilder cb = applyImpl(rw, ins, curr);
|
CommitBuilder cb = applyImpl(rw, ins, curr);
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
|||||||
private final ChangeNoteUtil changeNoteUtil;
|
private final ChangeNoteUtil changeNoteUtil;
|
||||||
private final ChangeUpdate.Factory updateFactory;
|
private final ChangeUpdate.Factory updateFactory;
|
||||||
private final NoteDbUpdateManager.Factory updateManagerFactory;
|
private final NoteDbUpdateManager.Factory updateManagerFactory;
|
||||||
|
private final NotesMigration migration;
|
||||||
private final PatchListCache patchListCache;
|
private final PatchListCache patchListCache;
|
||||||
private final PersonIdent serverIdent;
|
private final PersonIdent serverIdent;
|
||||||
private final ProjectCache projectCache;
|
private final ProjectCache projectCache;
|
||||||
@@ -134,6 +135,7 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
|||||||
ChangeNoteUtil changeNoteUtil,
|
ChangeNoteUtil changeNoteUtil,
|
||||||
ChangeUpdate.Factory updateFactory,
|
ChangeUpdate.Factory updateFactory,
|
||||||
NoteDbUpdateManager.Factory updateManagerFactory,
|
NoteDbUpdateManager.Factory updateManagerFactory,
|
||||||
|
NotesMigration migration,
|
||||||
PatchListCache patchListCache,
|
PatchListCache patchListCache,
|
||||||
@GerritPersonIdent PersonIdent serverIdent,
|
@GerritPersonIdent PersonIdent serverIdent,
|
||||||
@Nullable ProjectCache projectCache,
|
@Nullable ProjectCache projectCache,
|
||||||
@@ -144,6 +146,7 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
|||||||
this.changeNoteUtil = changeNoteUtil;
|
this.changeNoteUtil = changeNoteUtil;
|
||||||
this.updateFactory = updateFactory;
|
this.updateFactory = updateFactory;
|
||||||
this.updateManagerFactory = updateManagerFactory;
|
this.updateManagerFactory = updateManagerFactory;
|
||||||
|
this.migration = migration;
|
||||||
this.patchListCache = patchListCache;
|
this.patchListCache = patchListCache;
|
||||||
this.serverIdent = serverIdent;
|
this.serverIdent = serverIdent;
|
||||||
this.projectCache = projectCache;
|
this.projectCache = projectCache;
|
||||||
@@ -221,7 +224,14 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
|||||||
return change;
|
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) {
|
} catch (AbortUpdateException e) {
|
||||||
// Drop this rebuild; another thread completed it.
|
// Drop this rebuild; another thread completed it.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,6 @@ public class ConfigNotesMigration extends NotesMigration {
|
|||||||
checkArgument(lk.equals(WRITE) || lk.equals(READ),
|
checkArgument(lk.equals(WRITE) || lk.equals(READ),
|
||||||
"invalid NoteDb key: %s.%s", t, key);
|
"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
|
@Override
|
||||||
public boolean writeChanges() {
|
protected boolean writeChanges() {
|
||||||
return writeChanges;
|
return writeChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ import java.util.Set;
|
|||||||
* of updates, use {@link #stage()}.
|
* of updates, use {@link #stage()}.
|
||||||
*/
|
*/
|
||||||
public class NoteDbUpdateManager {
|
public class NoteDbUpdateManager {
|
||||||
|
public static String CHANGES_READ_ONLY = "NoteDb changes are read-only";
|
||||||
|
|
||||||
public interface Factory {
|
public interface Factory {
|
||||||
NoteDbUpdateManager create(Project.NameKey projectName);
|
NoteDbUpdateManager create(Project.NameKey projectName);
|
||||||
}
|
}
|
||||||
@@ -249,7 +251,7 @@ public class NoteDbUpdateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEmpty() {
|
private boolean isEmpty() {
|
||||||
if (!migration.writeChanges()) {
|
if (!migration.commitChangeWrites()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return changeUpdates.isEmpty()
|
return changeUpdates.isEmpty()
|
||||||
@@ -382,6 +384,10 @@ public class NoteDbUpdateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void execute() throws OrmException, IOException {
|
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()) {
|
if (isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,33 @@ package com.google.gerrit.server.notedb;
|
|||||||
* these reasons, the options remain undocumented.
|
* these reasons, the options remain undocumented.
|
||||||
*/
|
*/
|
||||||
public abstract class NotesMigration {
|
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 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();
|
public abstract boolean readAccounts();
|
||||||
|
|
||||||
@@ -51,6 +75,24 @@ public abstract class NotesMigration {
|
|||||||
return false;
|
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() {
|
public boolean enabled() {
|
||||||
return writeChanges() || readChanges()
|
return writeChanges() || readChanges()
|
||||||
|| writeAccounts() || readAccounts();
|
|| writeAccounts() || readAccounts();
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ public class TestNotesMigration extends NotesMigration {
|
|||||||
return readChanges;
|
return readChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Increase visbility from superclass, as tests may want to check whether
|
||||||
|
// NoteDb data is written in specific migration scenarios.
|
||||||
@Override
|
@Override
|
||||||
public boolean writeChanges() {
|
public boolean writeChanges() {
|
||||||
return writeChanges;
|
return writeChanges;
|
||||||
|
|||||||
Reference in New Issue
Block a user