Factor out common code from RepoSequence
IntBlob is a simple value class encapsulating the results of reading an
integer blob from a given object ID in a repo. It includes factory
methods for reading/writing blobs to/from the repo, in an interface that
provides everything RepoSequence needs.
In the write case, RepoSequence needs two methods:
* A tryStore method which returns a completed RefUpdate, which may or
may not have succeeded. This return value is essentially an
encapsulation of the RefUpdate.Result and the ref name; the latter
is necessary for producing detailed error messages.
* A checked store method which throws an exception if the result of
tryStore was not success. This in turn delegates to a new
RefUpdateUtil#checkResult method paralleling the existing
checkResults.
Change-Id: Ibbe99972697cb4cb837673f19d7a6fac8b90fce0
diff --git a/java/com/google/gerrit/server/notedb/IntBlob.java b/java/com/google/gerrit/server/notedb/IntBlob.java
new file mode 100644
index 0000000..edef5cc
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/IntBlob.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.CharMatcher;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.update.RefUpdateUtil;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+@AutoValue
+public abstract class IntBlob {
+ public static Optional<IntBlob> parse(Repository repo, String refName)
+ throws IOException, OrmException {
+ try (ObjectReader or = repo.newObjectReader()) {
+ return parse(repo, refName, or);
+ }
+ }
+
+ public static Optional<IntBlob> parse(Repository repo, String refName, RevWalk rw)
+ throws IOException, OrmException {
+ return parse(repo, refName, rw.getObjectReader());
+ }
+
+ private static Optional<IntBlob> parse(Repository repo, String refName, ObjectReader or)
+ throws IOException, OrmException {
+ Ref ref = repo.exactRef(refName);
+ if (ref == null) {
+ return Optional.empty();
+ }
+ ObjectId id = ref.getObjectId();
+ ObjectLoader ol = or.open(id, OBJ_BLOB);
+ if (ol.getType() != OBJ_BLOB) {
+ // In theory this should be thrown by open but not all implementations may do it properly
+ // (certainly InMemoryRepository doesn't).
+ throw new IncorrectObjectTypeException(id, OBJ_BLOB);
+ }
+ String str = CharMatcher.whitespace().trimFrom(new String(ol.getCachedBytes(), UTF_8));
+ Integer value = Ints.tryParse(str);
+ if (value == null) {
+ throw new OrmException("invalid value in " + refName + " blob at " + id.name());
+ }
+ return Optional.of(IntBlob.create(id, value));
+ }
+
+ public static RefUpdate tryStore(
+ Repository repo,
+ RevWalk rw,
+ Project.NameKey projectName,
+ String refName,
+ @Nullable ObjectId oldId,
+ int val,
+ GitReferenceUpdated gitRefUpdated)
+ throws IOException {
+ ObjectId newId;
+ try (ObjectInserter ins = repo.newObjectInserter()) {
+ newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
+ ins.flush();
+ }
+ RefUpdate ru = repo.updateRef(refName);
+ if (oldId != null) {
+ ru.setExpectedOldObjectId(oldId);
+ }
+ ru.disableRefLog();
+ ru.setNewObjectId(newId);
+ ru.setForceUpdate(true); // Required for non-commitish updates.
+ RefUpdate.Result result = ru.update(rw);
+ if (refUpdated(result)) {
+ gitRefUpdated.fire(projectName, ru, null);
+ }
+ return ru;
+ }
+
+ public static void store(
+ Repository repo,
+ RevWalk rw,
+ Project.NameKey projectName,
+ String refName,
+ @Nullable ObjectId oldId,
+ int val,
+ GitReferenceUpdated gitRefUpdated)
+ throws IOException {
+ RefUpdateUtil.checkResult(tryStore(repo, rw, projectName, refName, oldId, val, gitRefUpdated));
+ }
+
+ private static boolean refUpdated(RefUpdate.Result result) {
+ return result == RefUpdate.Result.NEW || result == RefUpdate.Result.FORCED;
+ }
+
+ private static IntBlob create(ObjectId id, int value) {
+ return new AutoValue_IntBlob(id, value);
+ }
+
+ public abstract ObjectId id();
+
+ public abstract int value();
+}
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 4c497ac..859b561 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -27,33 +27,27 @@
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Predicates;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
-import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.Runnables;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gwtorm.server.OrmException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
@@ -75,9 +69,9 @@
}
@VisibleForTesting
- static RetryerBuilder<RefUpdate.Result> retryerBuilder() {
- return RetryerBuilder.<RefUpdate.Result>newBuilder()
- .retryIfResult(Predicates.equalTo(RefUpdate.Result.LOCK_FAILURE))
+ static RetryerBuilder<RefUpdate> retryerBuilder() {
+ return RetryerBuilder.<RefUpdate>newBuilder()
+ .retryIfResult(ru -> ru != null && RefUpdate.Result.LOCK_FAILURE.equals(ru.getResult()))
.withWaitStrategy(
WaitStrategies.join(
WaitStrategies.exponentialWait(5, TimeUnit.SECONDS),
@@ -85,7 +79,7 @@
.withStopStrategy(StopStrategies.stopAfterDelay(30, TimeUnit.SECONDS));
}
- private static final Retryer<RefUpdate.Result> RETRYER = retryerBuilder().build();
+ private static final Retryer<RefUpdate> RETRYER = retryerBuilder().build();
private final GitRepositoryManager repoManager;
private final GitReferenceUpdated gitRefUpdated;
@@ -95,7 +89,7 @@
private final int floor;
private final int batchSize;
private final Runnable afterReadRef;
- private final Retryer<RefUpdate.Result> retryer;
+ private final Retryer<RefUpdate> retryer;
// Protects all non-final fields.
private final Lock counterLock;
@@ -153,7 +147,7 @@
Seed seed,
int batchSize,
Runnable afterReadRef,
- Retryer<RefUpdate.Result> retryer) {
+ Retryer<RefUpdate> retryer) {
this(repoManager, gitRefUpdated, projectName, name, seed, batchSize, afterReadRef, retryer, 0);
}
@@ -165,7 +159,7 @@
Seed seed,
int batchSize,
Runnable afterReadRef,
- Retryer<RefUpdate.Result> retryer,
+ Retryer<RefUpdate> retryer,
int floor) {
this.repoManager = requireNonNull(repoManager, "repoManager");
this.gitRefUpdated = requireNonNull(gitRefUpdated, "gitRefUpdated");
@@ -234,7 +228,7 @@
try {
try (Repository repo = repoManager.openRepository(projectName);
RevWalk rw = new RevWalk(repo)) {
- checkResult(store(repo, rw, null, val));
+ IntBlob.store(repo, rw, projectName, refName, null, val, gitRefUpdated);
counter = limit;
} catch (IOException e) {
throw new OrmException(e);
@@ -250,7 +244,11 @@
try (Repository repo = repoManager.openRepository(projectName);
RevWalk rw = new RevWalk(repo)) {
TryIncreaseTo attempt = new TryIncreaseTo(repo, rw, val);
- checkResult(retryer.call(attempt));
+ RefUpdate ru = retryer.call(attempt);
+ // Null update is a sentinel meaning nothing to do.
+ if (ru != null) {
+ RefUpdateUtil.checkResult(ru);
+ }
counter = limit;
} catch (ExecutionException | RetryException e) {
if (e.getCause() != null) {
@@ -269,7 +267,7 @@
try (Repository repo = repoManager.openRepository(projectName);
RevWalk rw = new RevWalk(repo)) {
TryAcquire attempt = new TryAcquire(repo, rw, count);
- checkResult(retryer.call(attempt));
+ RefUpdateUtil.checkResult(retryer.call(attempt));
counter = attempt.next;
limit = counter + count;
acquireCount++;
@@ -283,17 +281,7 @@
}
}
- private void checkResult(RefUpdate.Result result) throws OrmException {
- if (!refUpdated(result) && result != Result.NO_CHANGE) {
- throw new OrmException("failed to update " + refName + ": " + result);
- }
- }
-
- private boolean refUpdated(RefUpdate.Result result) {
- return result == RefUpdate.Result.NEW || result == RefUpdate.Result.FORCED;
- }
-
- private class TryAcquire implements Callable<RefUpdate.Result> {
+ private class TryAcquire implements Callable<RefUpdate> {
private final Repository repo;
private final RevWalk rw;
private final int count;
@@ -307,23 +295,23 @@
}
@Override
- public RefUpdate.Result call() throws Exception {
- Ref ref = repo.exactRef(refName);
+ public RefUpdate call() throws Exception {
+ Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
afterReadRef.run();
ObjectId oldId;
- if (ref == null) {
+ if (!blob.isPresent()) {
oldId = ObjectId.zeroId();
next = seed.get();
} else {
- oldId = ref.getObjectId();
- next = parse(rw, oldId);
+ oldId = blob.get().id();
+ next = blob.get().value();
}
next = Math.max(floor, next);
- return store(repo, rw, oldId, next + count);
+ return IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
}
}
- private class TryIncreaseTo implements Callable<RefUpdate.Result> {
+ private class TryIncreaseTo implements Callable<RefUpdate> {
private final Repository repo;
private final RevWalk rw;
private final int value;
@@ -335,60 +323,26 @@
}
@Override
- public RefUpdate.Result call() throws Exception {
- Ref ref = repo.exactRef(refName);
+ public RefUpdate call() throws Exception {
+ Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
afterReadRef.run();
ObjectId oldId;
- if (ref == null) {
+ if (!blob.isPresent()) {
oldId = ObjectId.zeroId();
} else {
- oldId = ref.getObjectId();
- int next = parse(rw, oldId);
+ oldId = blob.get().id();
+ int next = blob.get().value();
if (next >= value) {
- // a concurrent write updated the ref already to this or a higher value
- return RefUpdate.Result.NO_CHANGE;
+ // A concurrent write updated the ref already to this or a higher value; return null as a
+ // sentinel meaning nothing to do. Returning RefUpdate doesn't give us the flexibility to
+ // return any other kind of sentinel, since it's a fairly thick object.
+ return null;
}
}
- return store(repo, rw, oldId, value);
+ return IntBlob.tryStore(repo, rw, projectName, refName, oldId, value, gitRefUpdated);
}
}
- private int parse(RevWalk rw, ObjectId id) throws IOException, OrmException {
- ObjectLoader ol = rw.getObjectReader().open(id, OBJ_BLOB);
- if (ol.getType() != OBJ_BLOB) {
- // In theory this should be thrown by open but not all implementations
- // may do it properly (certainly InMemoryRepository doesn't).
- throw new IncorrectObjectTypeException(id, OBJ_BLOB);
- }
- String str = CharMatcher.whitespace().trimFrom(new String(ol.getCachedBytes(), UTF_8));
- Integer val = Ints.tryParse(str);
- if (val == null) {
- throw new OrmException("invalid value in " + refName + " blob at " + id.name());
- }
- return val;
- }
-
- private RefUpdate.Result store(Repository repo, RevWalk rw, @Nullable ObjectId oldId, int val)
- throws IOException {
- ObjectId newId;
- try (ObjectInserter ins = repo.newObjectInserter()) {
- newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
- ins.flush();
- }
- RefUpdate ru = repo.updateRef(refName);
- if (oldId != null) {
- ru.setExpectedOldObjectId(oldId);
- }
- ru.disableRefLog();
- ru.setNewObjectId(newId);
- ru.setForceUpdate(true); // Required for non-commitish updates.
- RefUpdate.Result result = ru.update(rw);
- if (refUpdated(result)) {
- gitRefUpdated.fire(projectName, ru, null);
- }
- return result;
- }
-
public static ReceiveCommand storeNew(ObjectInserter ins, String name, int val)
throws IOException {
ObjectId newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
diff --git a/java/com/google/gerrit/server/update/RefUpdateUtil.java b/java/com/google/gerrit/server/update/RefUpdateUtil.java
index 3e33677..514d86a 100644
--- a/java/com/google/gerrit/server/update/RefUpdateUtil.java
+++ b/java/com/google/gerrit/server/update/RefUpdateUtil.java
@@ -105,6 +105,32 @@
}
/**
+ * Check results of a single ref update, throwing an exception if there was a failure.
+ *
+ * @param ru ref update; must already have been executed.
+ * @throws IllegalArgumentException if the result was {@code NOT_ATTEMPTED}.
+ * @throws LockFailureException if the result was {@code LOCK_FAILURE}.
+ * @throws IOException if the result failed for another reason.
+ */
+ public static void checkResult(RefUpdate ru) throws IOException {
+ RefUpdate.Result result = ru.getResult();
+ switch (result) {
+ case NOT_ATTEMPTED:
+ throw new IllegalArgumentException("Not attempted: " + ru.getName());
+ case NEW:
+ case FORCED:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ case RENAMED:
+ return;
+ case LOCK_FAILURE:
+ throw new LockFailureException("Failed to update " + ru.getName() + ": " + result, ru);
+ default:
+ throw new IOException("Failed to update " + ru.getName() + ": " + ru.getResult());
+ }
+ }
+
+ /**
* Delete a single ref, throwing a checked exception on failure.
*
* <p>Does not require that the ref have any particular old value. Succeeds as a no-op if the ref
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index a21f5ba..64fd875 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -46,7 +46,7 @@
public class RepoSequenceTest {
// Don't sleep in tests.
- private static final Retryer<RefUpdate.Result> RETRYER =
+ private static final Retryer<RefUpdate> RETRYER =
RepoSequence.retryerBuilder().withBlockStrategy(t -> {}).build();
@Rule public ExpectedException exception = ExpectedException.none();
@@ -200,11 +200,11 @@
1,
10,
() -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
- RetryerBuilder.<RefUpdate.Result>newBuilder()
+ RetryerBuilder.<RefUpdate>newBuilder()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build());
exception.expect(OrmException.class);
- exception.expectMessage("failed to update refs/sequences/id: LOCK_FAILURE");
+ exception.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
s.next();
}
@@ -335,11 +335,11 @@
1,
10,
() -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
- RetryerBuilder.<RefUpdate.Result>newBuilder()
+ RetryerBuilder.<RefUpdate>newBuilder()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build());
exception.expect(OrmException.class);
- exception.expectMessage("failed to update refs/sequences/id: LOCK_FAILURE");
+ exception.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
s.increaseTo(2);
}
@@ -352,7 +352,7 @@
final int start,
int batchSize,
Runnable afterReadRef,
- Retryer<RefUpdate.Result> retryer) {
+ Retryer<RefUpdate> retryer) {
return new RepoSequence(
repoManager,
GitReferenceUpdated.DISABLED,