Automatically rebuild out-of-date NoteDb drafts

Uses the same mechanism as when rebuilding changes, except checking
the state of the relevant drafts ref in All-Users.

The one place we couldn't do this easily is in
PatchLineCommentsUtil#draftsByAuthor, since it doesn't have a Change
object available for each change, and we don't want to go overboard in
reading the notes. Just bail and skip rebuilding in that particular
case, marking it as deprecated so it doesn't pick up any new users.

Change-Id: Iaeccc9e665eb6ee2ffa4583f5add8d0ba9968d70
This commit is contained in:
Dave Borowitz
2016-03-29 12:32:42 -04:00
parent a30ce68ee1
commit 029d256839
6 changed files with 129 additions and 32 deletions

View File

@@ -18,7 +18,9 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Change;
@@ -103,13 +105,13 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
// First write doesn't create the ref, but rebuilding works.
checker.assertNoChangeRef(project, id);
assertThat(db.changes().get(id).getNoteDbState()).isNull();
assertThat(unwrapDb().changes().get(id).getNoteDbState()).isNull();
checker.rebuildAndCheckChanges(id);
// Now that there is a ref, writes are "turned on" for this change, and
// NoteDb stays up to date without explicit rebuilding.
gApi.changes().id(id.get()).topic(name("new-topic"));
assertThat(db.changes().get(id).getNoteDbState()).isNotNull();
assertThat(unwrapDb().changes().get(id).getNoteDbState()).isNotNull();
checker.checkChanges(id);
}
@@ -166,26 +168,14 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
changeMetaId.name());
DraftInput in = new DraftInput();
in.line = 1;
in.message = "comment by user";
in.path = PushOneCommit.FILE_NAME;
setApiUser(user);
gApi.changes().id(id.get()).current().createDraft(in);
putDraft(user, id, 1, "comment by user");
ObjectId userDraftsId = getMetaRef(
allUsers, RefNames.refsDraftComments(user.getId(), id));
assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
changeMetaId.name()
+ "," + user.getId() + "=" + userDraftsId.name());
in = new DraftInput();
in.line = 2;
in.message = "comment by admin";
in.path = PushOneCommit.FILE_NAME;
setApiUser(admin);
gApi.changes().id(id.get()).current().createDraft(in);
putDraft(admin, id, 2, "comment by admin");
ObjectId adminDraftsId = getMetaRef(
allUsers, RefNames.refsDraftComments(admin.getId(), id));
assertThat(admin.getId().get()).isLessThan(user.getId().get());
@@ -194,9 +184,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
+ "," + admin.getId() + "=" + adminDraftsId.name()
+ "," + user.getId() + "=" + userDraftsId.name());
in.message = "revised comment by admin";
gApi.changes().id(id.get()).current().createDraft(in);
putDraft(admin, id, 2, "revised comment by admin");
adminDraftsId = getMetaRef(
allUsers, RefNames.refsDraftComments(admin.getId(), id));
assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
@@ -211,19 +199,19 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
assertUpToDate(true, id);
assertChangeUpToDate(true, id);
// Make a ReviewDb change behind NoteDb's back and ensure it's detected.
notesMigration.setAllEnabled(false);
gApi.changes().id(id.get()).topic(name("a-topic"));
setInvalidNoteDbState(id);
assertUpToDate(false, id);
assertChangeUpToDate(false, id);
// On next NoteDb read, the change is transparently rebuilt.
notesMigration.setAllEnabled(true);
assertThat(gApi.changes().id(id.get()).info().topic)
.isEqualTo(name("a-topic"));
assertUpToDate(true, id);
assertChangeUpToDate(true, id);
// Check that the bundles are equal.
ChangeBundle actual = ChangeBundle.fromNotes(
@@ -232,6 +220,29 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
assertThat(actual.differencesFrom(expected)).isEmpty();
}
@Test
public void rebuildAutomaticallyWhenDraftsOutOfDate() throws Exception {
notesMigration.setAllEnabled(true);
setApiUser(user);
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "comment");
assertDraftsUpToDate(true, id, user);
// Make a ReviewDb change behind NoteDb's back and ensure it's detected.
notesMigration.setAllEnabled(false);
putDraft(user, id, 1, "comment");
setInvalidNoteDbState(id);
assertDraftsUpToDate(false, id, user);
// On next NoteDb read, the drafts are transparently rebuilt.
notesMigration.setAllEnabled(true);
assertThat(gApi.changes().id(id.get()).current().drafts())
.containsKey(PushOneCommit.FILE_NAME);
assertDraftsUpToDate(true, id, user);
}
private void setInvalidNoteDbState(Change.Id id) throws Exception {
ReviewDb db = unwrapDb();
Change c = db.changes().get(id);
@@ -243,7 +254,8 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
db.changes().update(Collections.singleton(c));
}
private void assertUpToDate(boolean expected, Change.Id id) throws Exception {
private void assertChangeUpToDate(boolean expected, Change.Id id)
throws Exception {
try (Repository repo = repoManager.openMetadataRepository(project)) {
Change c = unwrapDb().changes().get(id);
assertThat(c).isNotNull();
@@ -253,6 +265,18 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
}
}
private void assertDraftsUpToDate(boolean expected, Change.Id changeId,
TestAccount account) throws Exception {
try (Repository repo = repoManager.openMetadataRepository(allUsers)) {
Change c = unwrapDb().changes().get(changeId);
assertThat(c).isNotNull();
assertThat(c.getNoteDbState()).isNotNull();
NoteDbChangeState state = NoteDbChangeState.parse(c);
assertThat(state.areDraftsUpToDate(repo, account.getId()))
.isEqualTo(expected);
}
}
private ObjectId getMetaRef(Project.NameKey p, String name) throws Exception {
try (Repository repo = repoManager.openMetadataRepository(p)) {
Ref ref = repo.exactRef(name);
@@ -260,6 +284,20 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
}
}
private void putDraft(TestAccount account, Change.Id id, int line, String msg)
throws Exception {
DraftInput in = new DraftInput();
in.line = line;
in.message = msg;
in.path = PushOneCommit.FILE_NAME;
AcceptanceTestRequestScope.Context old = setApiUser(account);
try {
gApi.changes().id(id.get()).current().createDraft(in);
} finally {
atrScope.set(old);
}
}
private ReviewDb unwrapDb() {
ReviewDb db = dbProvider.get();
if (db instanceof DisabledChangesReviewDbWrapper) {

View File

@@ -272,6 +272,7 @@ public class PatchLineCommentsUtil {
return sort(comments);
}
@Deprecated // To be used only by HasDraftByLegacyPredicate.
public List<PatchLineComment> draftByAuthor(ReviewDb db,
Account.Id author) throws OrmException {
if (!migration.readChanges()) {
@@ -283,8 +284,12 @@ public class PatchLineCommentsUtil {
List<PatchLineComment> comments = Lists.newArrayList();
for (String refName : refNames) {
Change.Id changeId = Change.Id.parse(refName);
comments.addAll(
draftFactory.create(changeId, author).load().getComments().values());
// Avoid loading notes for all affected changes just to be able to auto-
// rebuild. This is only used in a corner case in the search codepath, so
// returning slightly stale values is ok.
DraftCommentNotes notes =
draftFactory.createWithAutoRebuildingDisabled(changeId, author);
comments.addAll(notes.load().getComments().values());
}
return sort(comments);
}

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
import com.google.auto.value.AutoValue;
@@ -77,15 +78,21 @@ public abstract class AbstractChangeNotes<T> {
public static abstract class LoadHandle implements AutoCloseable {
public static LoadHandle create(RevWalk walk, ObjectId id) {
return new AutoValue_AbstractChangeNotes_LoadHandle(
walk, id != null ? id.copy() : null);
checkNotNull(walk), id != null ? id.copy() : null);
}
public abstract RevWalk walk();
public static LoadHandle missing() {
return new AutoValue_AbstractChangeNotes_LoadHandle(null, null);
}
@Nullable public abstract RevWalk walk();
@Nullable public abstract ObjectId id();
@Override
public void close() {
walk().close();
if (walk() != null) {
walk().close();
}
}
}

View File

@@ -491,7 +491,7 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
throws OrmException {
if (draftCommentNotes == null ||
!author.equals(draftCommentNotes.getAuthor())) {
draftCommentNotes = new DraftCommentNotes(args, getChangeId(), author);
draftCommentNotes = new DraftCommentNotes(args, change, author);
draftCommentNotes.load();
}
}

View File

@@ -24,14 +24,18 @@ import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
@@ -41,20 +45,34 @@ import java.io.IOException;
*/
public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
public interface Factory {
DraftCommentNotes create(Change.Id changeId, Account.Id accountId);
DraftCommentNotes create(Change change, Account.Id accountId);
DraftCommentNotes createWithAutoRebuildingDisabled(
Change.Id changeId, Account.Id accountId);
}
private final Change change;
private final Account.Id author;
private ImmutableListMultimap<RevId, PatchLineComment> comments;
private RevisionNoteMap revisionNoteMap;
@AssistedInject
DraftCommentNotes(
Args args,
@Assisted Change change,
@Assisted Account.Id author) {
super(args, change.getId());
this.change = change;
this.author = author;
}
@AssistedInject
DraftCommentNotes(
Args args,
@Assisted Change.Id changeId,
@Assisted Account.Id author) {
super(args, changeId);
this.change = null;
this.author = author;
}
@@ -118,6 +136,36 @@ public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
return args.allUsers;
}
@Override
protected LoadHandle openHandle(Repository repo) throws IOException {
if (change != null) {
NoteDbChangeState state = NoteDbChangeState.parse(change);
// Only check if this particular user's drafts are up to date, to avoid
// reading unnecessary refs.
if (state == null || !state.areDraftsUpToDate(repo, author)) {
return rebuildAndOpen(repo);
}
}
return super.openHandle(repo);
}
private LoadHandle rebuildAndOpen(Repository repo) throws IOException {
try {
NoteDbChangeState newState =
args.rebuilder.get().rebuild(args.db.get(), getChangeId());
if (newState == null) {
return super.openHandle(repo); // May be null in tests.
}
ObjectId draftsId = newState.getDraftIds().get(author);
repo.scanForRepoChanges();
return LoadHandle.create(new RevWalk(repo), draftsId);
} catch (NoSuchChangeException e) {
return super.openHandle(repo);
} catch (OrmException | ConfigInvalidException e) {
throw new IOException(e);
}
}
@VisibleForTesting
NoteMap getNoteMap() {
return revisionNoteMap != null ? revisionNoteMap.noteMap : null;

View File

@@ -1808,8 +1808,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
manager.execute();
// Looking at drafts directly shows the zombie comment.
DraftCommentNotes draftNotes =
draftNotesFactory.create(c.getId(), otherUserId);
DraftCommentNotes draftNotes = draftNotesFactory.create(c, otherUserId);
assertThat(draftNotes.load().getComments().get(rev1))
.containsExactly(comment1, comment2);