Add warning with the stack trace of the caller upon failures

When a global-refdb operation has failed, the consequences could be
very serious. Log the event at warning level and with an associated
call stack.

Change-Id: Ib5012383bf3eca32c56df125cd7b8c93945d97e4
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
index 93427a7..e533d5a 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
@@ -27,7 +27,9 @@
 import com.google.gerrit.metrics.Timer0.Context;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.Arrays;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 
@@ -71,7 +73,10 @@
   @Override
   public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException {
     return trackFailingOperation(
-        () -> sharedRefDb().isUpToDate(project, ref), metrics::startIsUpToDateExecutionTime);
+        () -> sharedRefDb().isUpToDate(project, ref),
+        metrics::startIsUpToDateExecutionTime,
+        () ->
+            toString(project, project::get) + ":" + toString(ref, ref::getName) + " is up-to-date");
   }
 
   /** {@inheritDoc}. The operation is logged upon success. */
@@ -87,7 +92,18 @@
           }
           return succeeded;
         },
-        metrics::startCompareAndPutExecutionTime);
+        metrics::startCompareAndPutExecutionTime,
+        () ->
+            "compare "
+                + toString(project, project::get)
+                + ":"
+                + toString(currRef, currRef::getName)
+                + ":"
+                + toString(
+                    currRef,
+                    () -> toString(currRef.getObjectId(), () -> currRef.getObjectId().name()))
+                + " and put "
+                + toString(newRefValue, newRefValue::name));
   }
 
   /** {@inheritDoc} the operation is logged upon success. */
@@ -103,7 +119,16 @@
           }
           return succeeded;
         },
-        metrics::startCompareAndPutExecutionTime);
+        metrics::startCompareAndPutExecutionTime,
+        () ->
+            "compare "
+                + toString(project, project::get)
+                + ":"
+                + toString(refName)
+                + ":"
+                + toString(currValue)
+                + " and put "
+                + toString(newValue));
   }
 
   @Override
@@ -120,7 +145,14 @@
           sharedRefLogger.logRefUpdate(project.get(), refName, newValue);
           return null;
         },
-        metrics::startSetExecutionTime);
+        metrics::startSetExecutionTime,
+        () ->
+            "Put "
+                + toString(project, project::get)
+                + ":"
+                + toString(refName)
+                + " = "
+                + toString(newValue));
   }
 
   public boolean isSetOperationSupported() {
@@ -137,13 +169,16 @@
           sharedRefLogger.logLockAcquisition(project.get(), refName);
           return locker;
         },
-        metrics::startLockRefExecutionTime);
+        metrics::startLockRefExecutionTime,
+        () -> "Lock " + toString(project, project::get) + ":" + toString(refName));
   }
 
   @Override
   public boolean exists(Project.NameKey project, String refName) {
     return trackFailingOperation(
-        () -> sharedRefDb().exists(project, refName), metrics::startExistsExecutionTime);
+        () -> sharedRefDb().exists(project, refName),
+        metrics::startExistsExecutionTime,
+        () -> toString(project, project::get) + ":" + toString(refName) + " exists");
   }
 
   /** {@inheritDoc}. The operation is logged. */
@@ -155,14 +190,23 @@
           sharedRefLogger.logProjectDelete(project.get());
           return null;
         },
-        metrics::startRemoveExecutionTime);
+        metrics::startRemoveExecutionTime,
+        () -> "Remove " + toString(project, project::get));
   }
 
   @Override
   public <T> Optional<T> get(Project.NameKey nameKey, String s, Class<T> clazz)
       throws GlobalRefDbSystemError {
     return trackFailingOperation(
-        () -> sharedRefDb().get(nameKey, s, clazz), metrics::startGetExecutionTime);
+        () -> sharedRefDb().get(nameKey, s, clazz),
+        metrics::startGetExecutionTime,
+        () ->
+            "Get "
+                + toString(nameKey, nameKey::get)
+                + ":"
+                + toString(s)
+                + " of type "
+                + clazz.getSimpleName());
   }
 
   boolean isNoop() {
@@ -189,7 +233,10 @@
   }
 
   private <T, E extends Exception> T trackFailingOperation(
-      ThrowingSupplier<T, E> operation, Supplier<Context> metricTimer) throws E {
+      ThrowingSupplier<T, E> operation,
+      Supplier<Context> metricTimer,
+      Supplier<String> operationDetails)
+      throws E {
 
     boolean completedWithoutExceptions = false;
     try (Context ignore = metricTimer.get()) {
@@ -198,8 +245,23 @@
       return result;
     } finally {
       if (!completedWithoutExceptions) {
+        String callStack =
+            Arrays.stream(Thread.currentThread().getStackTrace())
+                .skip(2) /* Skip the getStackTrace() and trackFailingOperation() call stacks */
+                .map(StackTraceElement::toString)
+                .collect(Collectors.joining("\n   at "));
+        log.atWarning().log(
+            "Global-refdb operation '%s' failed\n%s", operationDetails.get(), callStack);
         metrics.incrementOperationFailures();
       }
     }
   }
+
+  private static <T> String toString(T value) {
+    return String.valueOf(value);
+  }
+
+  private static <O, T> String toString(O object, Supplier<T> valueSupplier) {
+    return toString(object == null ? null : valueSupplier.get());
+  }
 }