Merge "Add maxRefsInBatch and maxRefsToUpdate options"
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index e940b1e..eabee65 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -30,6 +30,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
@@ -117,6 +118,16 @@
public boolean verifyCommits = true;
/** Whether to compute and output the diff of the commit history for the backfilled refs. */
public boolean outputDiff = true;
+
+ /** Max number of refs to update in a single {@link BatchRefUpdate}. */
+ public int maxRefsInBatch = 10000;
+ /**
+ * Max number of refs to fix by a single {@link RefsUpdate#backfillProject} run. Since second
+ * run on the same set of refs is a no-op, running with this option in a loop will eventually
+ * fix all refs. Number of executed {@link BatchRefUpdate} depends on {@link #maxRefsInBatch}
+ * option.
+ */
+ public int maxRefsToUpdate = 50000;
}
/** Result of the backfill run for a project. */
@@ -239,15 +250,24 @@
*/
public BackfillResult backfillProject(
Project.NameKey project, Repository repo, RunOptions options) {
+
+ checkState(
+ options.maxRefsInBatch > 0 && options.maxRefsToUpdate > 0,
+ "Expected maxRefsInBatch>0 && <= maxRefsToUpdate>0");
+ checkState(
+ options.maxRefsInBatch <= options.maxRefsToUpdate,
+ "Expected maxRefsInBatch(%s) <= maxRefsToUpdate(%s)",
+ options.maxRefsInBatch,
+ options.maxRefsToUpdate);
BackfillResult result = new BackfillResult();
result.ok = true;
- try (RevWalk revWalk = new RevWalk(repo);
- ObjectInserter ins = newPackInserter(repo)) {
- BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
- bru.setForceRefLog(true);
- bru.setRefLogMessage(CommitRewriter.class.getName(), false);
- bru.setAllowNonFastForwards(true);
+ int refsInUpdate = 0;
+ RefsUpdate refsUpdate = null;
+ try {
for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
+ if (result.fixedRefDiff.size() >= options.maxRefsToUpdate) {
+ return result;
+ }
Change.Id changeId = Change.Id.fromRef(ref.getName());
if (changeId == null || !ref.getName().equals(RefNames.changeMetaRef(changeId))) {
continue;
@@ -262,14 +282,26 @@
logger.atWarning().withCause(e).log("Failed to run verification on ref %s", ref);
}
}
+ if (refsUpdate == null) {
+ refsUpdate = RefsUpdate.create(repo);
+ }
ChangeFixProgress changeFixProgress =
- backfillChange(revWalk, ins, ref, accountsInChange, options);
+ backfillChange(refsUpdate, ref, accountsInChange, options);
if (changeFixProgress.anyFixesApplied) {
- bru.addCommand(
- new ReceiveCommand(ref.getObjectId(), changeFixProgress.newTipId, ref.getName()));
+ refsInUpdate++;
+ refsUpdate
+ .batchRefUpdate()
+ .addCommand(
+ new ReceiveCommand(
+ ref.getObjectId(), changeFixProgress.newTipId, ref.getName()));
result.fixedRefDiff.put(ref.getName(), changeFixProgress.commitDiffs);
}
-
+ if (refsInUpdate >= options.maxRefsInBatch
+ || result.fixedRefDiff.size() >= options.maxRefsToUpdate) {
+ processUpdate(options, refsUpdate);
+ refsUpdate = null;
+ refsInUpdate = 0;
+ }
if (!changeFixProgress.isValidAfterFix) {
result.refsStillInvalidAfterFix.add(ref.getName());
}
@@ -278,21 +310,34 @@
result.refsFailedToFix.add(ref.getName());
}
}
-
- if (!bru.getCommands().isEmpty()) {
- if (!options.dryRun) {
- ins.flush();
- RefUpdateUtil.executeChecked(bru, revWalk);
- }
- }
+ processUpdate(options, refsUpdate);
} catch (IOException e) {
- logger.atWarning().withCause(e).log("Failed to fix project %s", project.get());
+ logger.atWarning().log("Failed to fix project %s. Reason: %s", project.get(), e.getMessage());
result.ok = false;
+ } finally {
+ if (refsUpdate != null) {
+ refsUpdate.close();
+ }
}
return result;
}
+ /** Executes a single {@link RefsUpdate#batchRefUpdate}. */
+ private void processUpdate(RunOptions options, @Nullable RefsUpdate refsUpdate)
+ throws IOException {
+ if (refsUpdate == null) {
+ return;
+ }
+ if (!refsUpdate.batchRefUpdate().getCommands().isEmpty()) {
+ if (!options.dryRun) {
+ refsUpdate.inserter().flush();
+ RefUpdateUtil.executeChecked(refsUpdate.batchRefUpdate(), refsUpdate.revWalk());
+ }
+ }
+ refsUpdate.close();
+ }
+
/**
* Retrieves accounts, that are associated with a change (e.g. reviewers, commenters, etc.). These
* accounts are used to verify that commits do not contain user data. See {@link #verifyCommit}
@@ -376,8 +421,7 @@
* ChangeFixProgress#newTipId}.
*/
public ChangeFixProgress backfillChange(
- RevWalk revWalk,
- ObjectInserter inserter,
+ RefsUpdate refsUpdate,
Ref ref,
ImmutableSet<AccountState> accountsInChange,
RunOptions options)
@@ -385,17 +429,17 @@
ObjectId oldTip = ref.getObjectId();
// Walk from the first commit of the branch.
- revWalk.reset();
- revWalk.markStart(revWalk.parseCommit(oldTip));
- revWalk.sort(RevSort.TOPO);
+ refsUpdate.revWalk().reset();
+ refsUpdate.revWalk().markStart(refsUpdate.revWalk().parseCommit(oldTip));
+ refsUpdate.revWalk().sort(RevSort.TOPO);
- revWalk.sort(RevSort.REVERSE);
+ refsUpdate.revWalk().sort(RevSort.REVERSE);
RevCommit originalCommit;
boolean rewriteStarted = false;
ChangeFixProgress changeFixProgress = new ChangeFixProgress(ref.getName());
- while ((originalCommit = revWalk.next()) != null) {
+ while ((originalCommit = refsUpdate.revWalk().next()) != null) {
changeFixProgress.updateAuthorId =
parseIdent(changeFixProgress, originalCommit.getAuthorIdent());
@@ -453,7 +497,8 @@
cb.setEncoding(originalCommit.getEncoding());
byte[] newCommitContent = cb.build();
checkCommitModification(originalCommit, newCommitContent);
- changeFixProgress.newTipId = inserter.insert(Constants.OBJ_COMMIT, newCommitContent);
+ changeFixProgress.newTipId =
+ refsUpdate.inserter().insert(Constants.OBJ_COMMIT, newCommitContent);
// Only compute diff if the content of the commit was actually changed.
if (options.outputDiff && needsFix) {
String diff = computeDiff(originalCommit.getRawBuffer(), newCommitContent);
@@ -1283,4 +1328,33 @@
abstract Optional<String> email();
}
+
+ /**
+ * Objects, needed to fix Refs in a single {@link BatchRefUpdate}. Number of changes in a batch
+ * are limited by {@link RunOptions#maxRefsInBatch}.
+ */
+ @AutoValue
+ abstract static class RefsUpdate implements AutoCloseable {
+ static RefsUpdate create(Repository repo) {
+ RevWalk revWalk = new RevWalk(repo);
+ ObjectInserter inserter = newPackInserter(repo);
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ bru.setForceRefLog(true);
+ bru.setRefLogMessage(CommitRewriter.class.getName(), false);
+ bru.setAllowNonFastForwards(true);
+ return new AutoValue_CommitRewriter_RefsUpdate(bru, revWalk, inserter);
+ }
+
+ @Override
+ public void close() {
+ inserter().close();
+ revWalk().close();
+ }
+
+ abstract BatchRefUpdate batchRefUpdate();
+
+ abstract RevWalk revWalk();
+
+ abstract ObjectInserter inserter();
+ }
}
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 056c7dc..98721fd 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -24,6 +24,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.AttentionSetUpdate.Operation;
@@ -33,6 +34,7 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -48,7 +50,9 @@
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.stream.IntStream;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
@@ -56,6 +60,8 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -70,6 +76,21 @@
@Before
public void setUp() throws Exception {}
+ @After
+ public void cleanUp() throws Exception {
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ bru.setAllowNonFastForwards(true);
+ for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
+ Change.Id changeId = Change.Id.fromRef(ref.getName());
+ if (changeId == null || !ref.getName().equals(RefNames.changeMetaRef(changeId))) {
+ continue;
+ }
+ bru.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
+ }
+
+ RefUpdateUtil.executeChecked(bru, repo);
+ }
+
@Test
public void validHistoryNoOp() throws Exception {
String tag = "jenkins";
@@ -157,6 +178,138 @@
}
@Test
+ public void numRefs_greater_maxRefsToUpdate_allFixed() throws Exception {
+ int numberOfChanges = 12;
+ ImmutableMap.Builder<String, Ref> refsToOldMetaBuilder = new ImmutableMap.Builder<>();
+ for (int i = 0; i < numberOfChanges; i++) {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setChangeMessage("Change has been successfully merged by " + changeOwner.getName());
+ update.commit();
+ ChangeUpdate updateWithSubject = newUpdate(c, changeOwner);
+ updateWithSubject.setSubjectForCommit("Update with subject");
+ updateWithSubject.commit();
+ String refName = RefNames.changeMetaRef(c.getId());
+ Ref metaRefBeforeRewrite = repo.exactRef(refName);
+ refsToOldMetaBuilder.put(refName, metaRefBeforeRewrite);
+ }
+ ImmutableMap<String, Ref> refsToOldMeta = refsToOldMetaBuilder.build();
+
+ RunOptions options = new RunOptions();
+ options.dryRun = false;
+ options.outputDiff = false;
+ options.verifyCommits = false;
+ options.maxRefsInBatch = 10;
+ options.maxRefsToUpdate = 12;
+ BackfillResult backfillResult = rewriter.backfillProject(project, repo, options);
+ assertThat(backfillResult.fixedRefDiff.keySet()).isEqualTo(refsToOldMeta.keySet());
+ for (Map.Entry<String, Ref> refEntry : refsToOldMeta.entrySet()) {
+ Ref metaRefAfterRewrite = repo.exactRef(refEntry.getKey());
+ assertThat(refEntry.getValue()).isNotEqualTo(metaRefAfterRewrite);
+ }
+ }
+
+ @Test
+ public void maxRefsToUpdate_coversAllInvalid_inMultipleBatches() throws Exception {
+ testMaxRefsToUpdate(
+ /*numberOfInvalidChanges=*/ 11,
+ /*numberOfValidChanges=*/ 9,
+ /*maxRefsToUpdate=*/ 12,
+ /*maxRefsInBatch=*/ 2);
+ }
+
+ @Test
+ public void maxRefsToUpdate_coversAllInvalid_inSingleBatch() throws Exception {
+ testMaxRefsToUpdate(
+ /*numberOfInvalidChanges=*/ 11,
+ /*numberOfValidChanges=*/ 9,
+ /*maxRefsToUpdate=*/ 12,
+ /*maxRefsInBatch=*/ 12);
+ }
+
+ @Test
+ public void moreInvalidRefs_thenMaxRefsToUpdate_inMultipleBatches() throws Exception {
+ testMaxRefsToUpdate(
+ /*numberOfInvalidChanges=*/ 11,
+ /*numberOfValidChanges=*/ 9,
+ /*maxRefsToUpdate=*/ 10,
+ /*maxRefsInBatch=*/ 2);
+ }
+
+ @Test
+ public void moreInvalidRefs_thenMaxRefsToUpdate_inSingleBatch() throws Exception {
+ testMaxRefsToUpdate(
+ /*numberOfInvalidChanges=*/ 11,
+ /*numberOfValidChanges=*/ 9,
+ /*maxRefsToUpdate=*/ 10,
+ /*maxRefsInBatch=*/ 10);
+ }
+
+ private void testMaxRefsToUpdate(
+ int numberOfInvalidChanges, int numberOfValidChanges, int maxRefsToUpdate, int maxRefsInBatch)
+ throws Exception {
+ ImmutableMap.Builder<String, ObjectId> expectedFixedRefsToOldMetaBuilder =
+ new ImmutableMap.Builder<>();
+ ImmutableMap.Builder<String, ObjectId> expectedSkippedRefsToOldMetaBuilder =
+ new ImmutableMap.Builder<>();
+ for (int i = 0; i < numberOfValidChanges; i++) {
+ Change c = newChange();
+ ChangeUpdate updateWithSubject = newUpdate(c, changeOwner);
+ updateWithSubject.setSubjectForCommit("Update with subject");
+ updateWithSubject.commit();
+ String refName = RefNames.changeMetaRef(c.getId());
+ Ref metaRefBeforeRewrite = repo.exactRef(refName);
+ expectedSkippedRefsToOldMetaBuilder.put(refName, metaRefBeforeRewrite.getObjectId());
+ }
+ for (int i = 0; i < numberOfInvalidChanges; i++) {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setChangeMessage("Change has been successfully merged by " + changeOwner.getName());
+ update.commit();
+ ChangeUpdate updateWithSubject = newUpdate(c, changeOwner);
+ updateWithSubject.setSubjectForCommit("Update with subject");
+ updateWithSubject.commit();
+ String refName = RefNames.changeMetaRef(c.getId());
+ Ref metaRefBeforeRewrite = repo.exactRef(refName);
+ if (i < maxRefsToUpdate) {
+ expectedFixedRefsToOldMetaBuilder.put(refName, metaRefBeforeRewrite.getObjectId());
+ } else {
+ expectedSkippedRefsToOldMetaBuilder.put(refName, metaRefBeforeRewrite.getObjectId());
+ }
+ }
+ ImmutableMap<String, ObjectId> expectedFixedRefsToOldMeta =
+ expectedFixedRefsToOldMetaBuilder.build();
+ ImmutableMap<String, ObjectId> expectedSkippedRefsToOldMeta =
+ expectedSkippedRefsToOldMetaBuilder.build();
+ RunOptions options = new RunOptions();
+ options.dryRun = false;
+ options.outputDiff = false;
+ options.verifyCommits = false;
+ options.maxRefsInBatch = maxRefsInBatch;
+ options.maxRefsToUpdate = maxRefsToUpdate;
+ BackfillResult backfillResult = rewriter.backfillProject(project, repo, options);
+ assertThat(backfillResult.fixedRefDiff.keySet()).isEqualTo(expectedFixedRefsToOldMeta.keySet());
+ for (Map.Entry<String, ObjectId> refEntry : expectedFixedRefsToOldMeta.entrySet()) {
+ Ref metaRefAfterRewrite = repo.exactRef(refEntry.getKey());
+ assertThat(refEntry.getValue()).isNotEqualTo(metaRefAfterRewrite.getObjectId());
+ }
+ for (Map.Entry<String, ObjectId> refEntry : expectedSkippedRefsToOldMeta.entrySet()) {
+ Ref metaRefAfterRewrite = repo.exactRef(refEntry.getKey());
+ assertThat(refEntry.getValue()).isEqualTo(metaRefAfterRewrite.getObjectId());
+ }
+ RunOptions secondRunOptions = new RunOptions();
+ secondRunOptions.dryRun = false;
+ secondRunOptions.outputDiff = false;
+ secondRunOptions.verifyCommits = false;
+ secondRunOptions.maxRefsInBatch = maxRefsInBatch;
+ secondRunOptions.maxRefsToUpdate = numberOfInvalidChanges + numberOfValidChanges;
+ BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+ int expectedSecondRunResult =
+ numberOfInvalidChanges > maxRefsToUpdate ? numberOfInvalidChanges - maxRefsToUpdate : 0;
+ assertThat(secondRunResult.fixedRefDiff.keySet().size()).isEqualTo(expectedSecondRunResult);
+ }
+
+ @Test
public void fixAuthorIdent() throws Exception {
Change c = newChange();
Timestamp when = TimeUtil.nowTs();