Merge "Log caller for debug logs when reading external ID notes map"
diff --git a/WORKSPACE b/WORKSPACE
index 9901c1d..f160f64 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -749,8 +749,8 @@
 # Keep this version of Soy synchronized with the version used in Gitiles.
 maven_jar(
     name = "soy",
-    artifact = "com.google.template:soy:2019-03-11",
-    sha1 = "119ac4b3eb0e2c638526ca99374013965c727097",
+    artifact = "com.google.template:soy:2019-04-18",
+    sha1 = "5750208855562d74f29eee39ee497d5cf6df1490",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 1c33a68..4d96565 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -29,8 +29,10 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -40,7 +42,6 @@
 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;
@@ -69,9 +70,12 @@
   }
 
   @VisibleForTesting
-  static RetryerBuilder<RefUpdate> retryerBuilder() {
-    return RetryerBuilder.<RefUpdate>newBuilder()
-        .retryIfResult(ru -> ru != null && RefUpdate.Result.LOCK_FAILURE.equals(ru.getResult()))
+  static RetryerBuilder<ImmutableList<Integer>> retryerBuilder() {
+    return RetryerBuilder.<ImmutableList<Integer>>newBuilder()
+        .retryIfException(
+            t ->
+                t instanceof StorageException
+                    && ((StorageException) t).getCause() instanceof LockFailureException)
         .withWaitStrategy(
             WaitStrategies.join(
                 WaitStrategies.exponentialWait(5, TimeUnit.SECONDS),
@@ -79,7 +83,7 @@
         .withStopStrategy(StopStrategies.stopAfterDelay(30, TimeUnit.SECONDS));
   }
 
-  private static final Retryer<RefUpdate> RETRYER = retryerBuilder().build();
+  private static final Retryer<ImmutableList<Integer>> RETRYER = retryerBuilder().build();
 
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
@@ -89,7 +93,7 @@
   private final int floor;
   private final int batchSize;
   private final Runnable afterReadRef;
-  private final Retryer<RefUpdate> retryer;
+  private final Retryer<ImmutableList<Integer>> retryer;
 
   // Protects all non-final fields.
   private final Lock counterLock;
@@ -147,7 +151,7 @@
       Seed seed,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate> retryer) {
+      Retryer<ImmutableList<Integer>> retryer) {
     this(repoManager, gitRefUpdated, projectName, name, seed, batchSize, afterReadRef, retryer, 0);
   }
 
@@ -159,7 +163,7 @@
       Seed seed,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate> retryer,
+      Retryer<ImmutableList<Integer>> retryer,
       int floor) {
     this.repoManager = requireNonNull(repoManager, "repoManager");
     this.gitRefUpdated = requireNonNull(gitRefUpdated, "gitRefUpdated");
@@ -184,78 +188,85 @@
     counterLock = new ReentrantLock(true);
   }
 
+  /**
+   * Retrieves the next available sequence number.
+   *
+   * <p>This method is thread-safe.
+   *
+   * @return the next available sequence number
+   */
   public int next() {
-    counterLock.lock();
-    try {
-      if (counter >= limit) {
-        acquire(batchSize);
-      }
-      return counter++;
-    } finally {
-      counterLock.unlock();
-    }
+    return Iterables.getOnlyElement(next(1));
   }
 
+  /**
+   * Retrieves the next N available sequence number.
+   *
+   * <p>This method is thread-safe.
+   *
+   * @param count the number of sequence numbers which should be returned
+   * @return the next N available sequence numbers
+   */
   public ImmutableList<Integer> next(int count) {
     if (count == 0) {
       return ImmutableList.of();
     }
     checkArgument(count > 0, "count is negative: %s", count);
-    counterLock.lock();
-    try {
-      List<Integer> ids = new ArrayList<>(count);
-      while (counter < limit) {
-        ids.add(counter++);
-        if (ids.size() == count) {
-          return ImmutableList.copyOf(ids);
-        }
-      }
-      acquire(Math.max(count - ids.size(), batchSize));
-      while (ids.size() < count) {
-        ids.add(counter++);
-      }
-      return ImmutableList.copyOf(ids);
-    } finally {
-      counterLock.unlock();
-    }
-  }
 
-  private void acquire(int count) {
-    try (Repository repo = repoManager.openRepository(projectName);
-        RevWalk rw = new RevWalk(repo)) {
-      TryAcquire attempt = new TryAcquire(repo, rw, count);
-      RefUpdateUtil.checkResult(retryer.call(attempt));
-      counter = attempt.next;
-      limit = counter + count;
-      acquireCount++;
+    try {
+      return retryer.call(
+          () -> {
+            counterLock.lock();
+            try {
+              if (count == 1) {
+                if (counter >= limit) {
+                  acquire(batchSize);
+                }
+                return ImmutableList.of(counter++);
+              }
+
+              List<Integer> ids = new ArrayList<>(count);
+              while (counter < limit) {
+                ids.add(counter++);
+                if (ids.size() == count) {
+                  return ImmutableList.copyOf(ids);
+                }
+              }
+              acquire(Math.max(count - ids.size(), batchSize));
+              while (ids.size() < count) {
+                ids.add(counter++);
+              }
+              return ImmutableList.copyOf(ids);
+            } finally {
+              counterLock.unlock();
+            }
+          });
     } catch (ExecutionException | RetryException e) {
       if (e.getCause() != null) {
         Throwables.throwIfInstanceOf(e.getCause(), StorageException.class);
       }
       throw new StorageException(e);
-    } catch (IOException e) {
-      throw new StorageException(e);
     }
   }
 
-  private class TryAcquire implements Callable<RefUpdate> {
-    private final Repository repo;
-    private final RevWalk rw;
-    private final int count;
-
-    private int next;
-
-    private TryAcquire(Repository repo, RevWalk rw, int count) {
-      this.repo = repo;
-      this.rw = rw;
-      this.count = count;
-    }
-
-    @Override
-    public RefUpdate call() throws Exception {
+  /**
+   * Updates the next available sequence number in NoteDb in order to have a batch of sequence
+   * numbers available that can be handed out. {@link #counter} stores the next sequence number that
+   * can be handed out. When {@link #limit} is reached a new batch of sequence numbers needs to be
+   * retrieved by calling this method.
+   *
+   * <p><strong>Note:</strong> Callers are required to acquire the {@link #counterLock} before
+   * calling this method.
+   *
+   * @param count the number of sequence numbers which should be retrieved
+   */
+  private void acquire(int count) {
+    try (Repository repo = repoManager.openRepository(projectName);
+        RevWalk rw = new RevWalk(repo)) {
       Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
       afterReadRef.run();
       ObjectId oldId;
+      int next;
       if (!blob.isPresent()) {
         oldId = ObjectId.zeroId();
         next = seed.get();
@@ -264,7 +275,14 @@
         next = blob.get().value();
       }
       next = Math.max(floor, next);
-      return IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
+      RefUpdate refUpdate =
+          IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
+      RefUpdateUtil.checkResult(refUpdate);
+      counter = next;
+      limit = counter + count;
+      acquireCount++;
+    } catch (IOException e) {
+      throw new StorageException(e);
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index f088a79..6baa3e4 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -20,9 +20,12 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
+import com.github.rholder.retry.BlockStrategy;
 import com.github.rholder.retry.Retryer;
 import com.github.rholder.retry.RetryerBuilder;
 import com.github.rholder.retry.StopStrategies;
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Expect;
 import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Project;
@@ -30,7 +33,9 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import java.io.IOException;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -41,11 +46,14 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 public class RepoSequenceTest {
+  @Rule public final Expect expect = Expect.create();
+
   // Don't sleep in tests.
-  private static final Retryer<RefUpdate> RETRYER =
+  private static final Retryer<ImmutableList<Integer>> RETRYER =
       RepoSequence.retryerBuilder().withBlockStrategy(t -> {}).build();
 
   private InMemoryRepositoryManager repoManager;
@@ -160,7 +168,7 @@
     RepoSequence s = newSequence("id", 1, 10, bgUpdate, RETRYER);
     assertThat(doneBgUpdate.get()).isFalse();
     assertThat(s.next()).isEqualTo(1234);
-    // Single acquire call that results in 2 ref reads.
+    // Two acquire calls, but only one successful.
     assertThat(s.acquireCount).isEqualTo(1);
     assertThat(doneBgUpdate.get()).isTrue();
   }
@@ -182,8 +190,7 @@
       tr.branch(RefNames.REFS_SEQUENCES + "id").commit().create();
       StorageException e =
           assertThrows(StorageException.class, () -> newSequence("id", 1, 3).next());
-      assertThat(e.getCause()).isInstanceOf(ExecutionException.class);
-      assertThat(e.getCause().getCause()).isInstanceOf(IncorrectObjectTypeException.class);
+      assertThat(e.getCause()).isInstanceOf(IncorrectObjectTypeException.class);
     }
   }
 
@@ -196,7 +203,7 @@
             1,
             10,
             () -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
-            RetryerBuilder.<RefUpdate>newBuilder()
+            RetryerBuilder.<ImmutableList<Integer>>newBuilder()
                 .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                 .build());
     StorageException thrown = assertThrows(StorageException.class, () -> s.next());
@@ -206,6 +213,77 @@
   }
 
   @Test
+  public void idCanBeRetrievedFromOtherThreadWhileWaitingToRetry() throws Exception {
+    // Seed existing ref value.
+    writeBlob("id", "1");
+
+    // Let the first update of the sequence fail with LOCK_FAILURE, so that the update is retried.
+    CountDownLatch lockFailure = new CountDownLatch(1);
+    CountDownLatch parallelSuccessfulSequenceGeneration = new CountDownLatch(1);
+    AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
+    Runnable bgUpdate =
+        () -> {
+          if (!doneBgUpdate.getAndSet(true)) {
+            writeBlob("id", "1234");
+          }
+        };
+
+    BlockStrategy blockStrategy =
+        t -> {
+          // Keep blocking until we verified that another thread can retrieve a sequence number
+          // while we are blocking here.
+          lockFailure.countDown();
+          parallelSuccessfulSequenceGeneration.await();
+        };
+
+    // Use batch size = 1 to make each call go to NoteDb.
+    RepoSequence s =
+        newSequence(
+            "id",
+            1,
+            1,
+            bgUpdate,
+            RepoSequence.retryerBuilder().withBlockStrategy(blockStrategy).build());
+
+    assertThat(doneBgUpdate.get()).isFalse();
+
+    // Start a thread to get a sequence number. This thread needs to update the sequence in NoteDb,
+    // but due to the background update (see bgUpdate) the first attempt to update NoteDb fails
+    // with LOCK_FAILURE. RepoSequence uses a retryer to retry the NoteDb update on LOCK_FAILURE,
+    // but our block strategy ensures that this retry only happens after isBlocking was set to
+    // false.
+    Future<?> future =
+        Executors.newFixedThreadPool(1)
+            .submit(
+                () -> {
+                  // The background update sets the next available sequence number to 1234. Then the
+                  // test thread retrieves one sequence number, so that the next available sequence
+                  // number for this thread is 1235.
+                  expect.that(s.next()).isEqualTo(1235);
+                });
+
+    // Wait until the LOCK_FAILURE has happened and the block strategy was entered.
+    lockFailure.await();
+
+    // Verify that the background update was done now.
+    assertThat(doneBgUpdate.get()).isTrue();
+
+    // Verify that we can retrieve a sequence number while the other thread is blocked. If the
+    // s.next() call hangs it means that the RepoSequence.counterLock was not released before the
+    // background thread started to block for retry. In this case the test would time out.
+    assertThat(s.next()).isEqualTo(1234);
+
+    // Stop blocking the retry of the background thread (and verify that it was still blocked).
+    parallelSuccessfulSequenceGeneration.countDown();
+
+    // Wait until the background thread is done.
+    future.get();
+
+    // Two successful acquire calls (because batch size == 1).
+    assertThat(s.acquireCount).isEqualTo(2);
+  }
+
+  @Test
   public void nextWithCountOneCaller() throws Exception {
     RepoSequence s = newSequence("id", 1, 3);
     assertThat(s.next(2)).containsExactly(1, 2).inOrder();
@@ -260,7 +338,7 @@
       final int start,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate> retryer) {
+      Retryer<ImmutableList<Integer>> retryer) {
     return new RepoSequence(
         repoManager,
         GitReferenceUpdated.DISABLED,
diff --git a/lib/guava.bzl b/lib/guava.bzl
index c36bf14..9f0ff32 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "27.1-jre"
+GUAVA_VERSION = "28.0-jre"
 
-GUAVA_BIN_SHA1 = "e47b59c893079b87743cdcfb6f17ca95c08c592c"
+GUAVA_BIN_SHA1 = "54fed371b4b8a8cce1e94a9abd9620982d3aa54b"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"