Merge "Update the intro Gerrit walkthrough with rebase"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index a91a138..223ec71 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2752,6 +2752,9 @@
 this interface can be used to retry the request instead of failing it
 immediately.
 
+It also allows implementors to group exceptions that have the same
+cause into one metric bucket.
+
 [[mail-soy-template-provider]]
 == MailSoyTemplateProvider
 
diff --git a/java/com/google/gerrit/extensions/restapi/Response.java b/java/com/google/gerrit/extensions/restapi/Response.java
index bec7fc3..bf363d8 100644
--- a/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/java/com/google/gerrit/extensions/restapi/Response.java
@@ -14,10 +14,6 @@
 
 package com.google.gerrit.extensions.restapi;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.gerrit.common.Nullable;
-import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 /** Special return value to mean specific HTTP status codes in a REST API. */
@@ -66,48 +62,24 @@
     return new Redirect(location);
   }
 
-  /**
-   * HTTP 500 Internal Server Error: failure due to an unexpected exception.
-   *
-   * <p>Can be returned from REST endpoints, instead of throwing the exception, if additional
-   * properties (e.g. a traceId) should be set on the response.
-   *
-   * @param cause the exception that caused the request to fail, must not be a {@link
-   *     RestApiException} because such an exception would result in a 4XX response code
-   */
-  public static <T> InternalServerError<T> internalServerError(Exception cause) {
-    return new InternalServerError<>(cause);
-  }
-
   /** Arbitrary status code with wrapped result. */
   public static <T> Response<T> withStatusCode(int statusCode, T value) {
     return new Impl<>(statusCode, value);
   }
 
   @SuppressWarnings({"unchecked", "rawtypes"})
-  public static <T> T unwrap(T obj) throws Exception {
+  public static <T> T unwrap(T obj) {
     while (obj instanceof Response) {
       obj = (T) ((Response) obj).value();
     }
     return obj;
   }
 
-  private String traceId;
-
-  public Response<T> traceId(@Nullable String traceId) {
-    this.traceId = traceId;
-    return this;
-  }
-
-  public Optional<String> traceId() {
-    return Optional.ofNullable(traceId);
-  }
-
   public abstract boolean isNone();
 
   public abstract int statusCode();
 
-  public abstract T value() throws Exception;
+  public abstract T value();
 
   public abstract CacheControl caching();
 
@@ -297,57 +269,4 @@
       return String.format("[202 Accepted] %s", location);
     }
   }
-
-  public static final class InternalServerError<T> extends Response<T> {
-    private final Exception cause;
-
-    private InternalServerError(Exception cause) {
-      checkArgument(!(cause instanceof RestApiException), "cause must not be a RestApiException");
-      this.cause = cause;
-    }
-
-    @Override
-    public boolean isNone() {
-      return false;
-    }
-
-    @Override
-    public int statusCode() {
-      return 500;
-    }
-
-    @Override
-    public T value() throws Exception {
-      throw cause();
-    }
-
-    @Override
-    public CacheControl caching() {
-      return CacheControl.NONE;
-    }
-
-    @Override
-    public Response<T> caching(CacheControl c) {
-      throw new UnsupportedOperationException();
-    }
-
-    public Exception cause() {
-      return cause;
-    }
-
-    @Override
-    public int hashCode() {
-      return cause.hashCode();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      return o instanceof InternalServerError && ((InternalServerError<?>) o).cause.equals(cause);
-    }
-
-    @Override
-    public String toString() {
-      return String.format("[500 Internal Server Error] %s", cause.getClass());
-    }
-  }
 }
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java
index 519c400..2cce480 100644
--- a/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -21,6 +21,7 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.io.ByteStreams;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.ObjectIds;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -387,9 +388,10 @@
         toAdd.clear();
         toRemove.clear();
         break;
+      case LOCK_FAILURE:
+        throw new LockFailureException("Failed to store public keys", ru);
       case FORCED:
       case IO_FAILURE:
-      case LOCK_FAILURE:
       case NOT_ATTEMPTED:
       case REJECTED:
       case REJECTED_CURRENT_BRANCH:
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
index fc099a6..a378fa4 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.restapi;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.httpd.restapi.RestApiServlet.ViewData;
 import com.google.gerrit.metrics.Counter1;
@@ -79,16 +80,19 @@
   }
 
   String view(ViewData viewData) {
-    String impl = viewData.view.getClass().getName().replace('$', '.');
+    return view(viewData.view.getClass(), viewData.pluginName);
+  }
+
+  String view(Class<?> clazz, @Nullable String pluginName) {
+    String impl = clazz.getName().replace('$', '.');
     for (String p : PKGS) {
       if (impl.startsWith(p)) {
         impl = impl.substring(p.length());
         break;
       }
     }
-    if (!Strings.isNullOrEmpty(viewData.pluginName)
-        && !PluginName.GERRIT.equals(viewData.pluginName)) {
-      impl = viewData.pluginName + '-' + impl;
+    if (!Strings.isNullOrEmpty(pluginName) && !PluginName.GERRIT.equals(pluginName)) {
+      impl = pluginName + '-' + impl;
     }
     return impl;
   }
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 61fa4f9..7700740 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -123,6 +123,9 @@
 import com.google.gerrit.server.quota.QuotaException;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.Action;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.util.http.CacheHeaders;
@@ -170,6 +173,7 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
 import java.util.zip.GZIPOutputStream;
@@ -241,6 +245,7 @@
     final Config config;
     final DynamicSet<PerformanceLogger> performanceLoggers;
     final ChangeFinder changeFinder;
+    final RetryHelper retryHelper;
 
     @Inject
     Globals(
@@ -254,7 +259,8 @@
         RestApiQuotaEnforcer quotaChecker,
         @GerritServerConfig Config config,
         DynamicSet<PerformanceLogger> performanceLoggers,
-        ChangeFinder changeFinder) {
+        ChangeFinder changeFinder,
+        RetryHelper retryHelper) {
       this.currentUser = currentUser;
       this.webSession = webSession;
       this.paramParser = paramParser;
@@ -266,6 +272,7 @@
       this.config = config;
       this.performanceLoggers = performanceLoggers;
       this.changeFinder = changeFinder;
+      this.retryHelper = retryHelper;
       allowOrigin = makeAllowOrigin(config);
     }
 
@@ -326,12 +333,7 @@
         // TraceIT#performanceLoggingForRestCall()).
         try (PerformanceLogContext performanceLogContext =
             new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
-          logger.atFinest().log(
-              "Received REST request: %s %s (parameters: %s)",
-              req.getMethod(), req.getRequestURI(), getParameterNames(req));
-          logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
-          logger.atFinest().log(
-              "Groups: %s", globals.currentUser.get().getEffectiveGroups().getKnownGroups());
+          traceRequestData(req);
 
           if (isCorsPreflight(req)) {
             doCorsPreflight(req, res);
@@ -376,7 +378,7 @@
           } else {
             IdString id = path.remove(0);
             try {
-              rsrc = rc.parse(rsrc, id);
+              rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, rc, rsrc, id);
               globals.quotaChecker.enforce(rsrc, req);
               if (path.isEmpty()) {
                 checkPreconditions(req);
@@ -449,7 +451,7 @@
             }
             IdString id = path.remove(0);
             try {
-              rsrc = c.parse(rsrc, id);
+              rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, c, rsrc, id);
               checkPreconditions(req);
               viewData = new ViewData(null, null);
             } catch (ResourceNotFoundException e) {
@@ -495,7 +497,9 @@
           }
 
           if (viewData.view instanceof RestReadView<?> && isRead(req)) {
-            response = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
+            response =
+                invokeRestReadViewWithRetry(
+                    req, traceContext, viewData, (RestReadView<RestResource>) viewData.view, rsrc);
           } else if (viewData.view instanceof RestModifyView<?, ?>) {
             @SuppressWarnings("unchecked")
             RestModifyView<RestResource, Object> m =
@@ -503,7 +507,10 @@
 
             Type type = inputType(m);
             inputRequestBody = parseRequest(req, type);
-            response = m.apply(rsrc, inputRequestBody);
+            response =
+                invokeRestModifyViewWithRetry(
+                    req, traceContext, viewData, m, rsrc, inputRequestBody);
+
             if (inputRequestBody instanceof RawInput) {
               try (InputStream is = req.getInputStream()) {
                 ServletUtils.consumeRequestBody(is);
@@ -516,7 +523,9 @@
 
             Type type = inputType(m);
             inputRequestBody = parseRequest(req, type);
-            response = m.apply(rsrc, path.get(0), inputRequestBody);
+            response =
+                invokeRestCollectionCreateViewWithRetry(
+                    req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
             if (inputRequestBody instanceof RawInput) {
               try (InputStream is = req.getInputStream()) {
                 ServletUtils.consumeRequestBody(is);
@@ -529,7 +538,9 @@
 
             Type type = inputType(m);
             inputRequestBody = parseRequest(req, type);
-            response = m.apply(rsrc, path.get(0), inputRequestBody);
+            response =
+                invokeRestCollectionDeleteMissingViewWithRetry(
+                    req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
             if (inputRequestBody instanceof RawInput) {
               try (InputStream is = req.getInputStream()) {
                 ServletUtils.consumeRequestBody(is);
@@ -542,7 +553,9 @@
 
             Type type = inputType(m);
             inputRequestBody = parseRequest(req, type);
-            response = m.apply(rsrc, inputRequestBody);
+            response =
+                invokeRestCollectionModifyViewWithRetry(
+                    req, traceContext, viewData, m, rsrc, inputRequestBody);
             if (inputRequestBody instanceof RawInput) {
               try (InputStream is = req.getInputStream()) {
                 ServletUtils.consumeRequestBody(is);
@@ -552,9 +565,6 @@
             throw new ResourceNotFoundException();
           }
 
-          traceId = response.traceId();
-          traceId.ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
-
           if (response instanceof Response.Redirect) {
             CacheHeaders.setNotCacheable(res);
             String location = ((Response.Redirect) response).location();
@@ -567,12 +577,6 @@
             res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) response).location());
             logger.atFinest().log("REST call succeeded: %d", response.statusCode());
             return;
-          } else if (response instanceof Response.InternalServerError) {
-            // Rethrow the exception to have exactly the same error handling as if the REST endpoint
-            // would have thrown the exception directly, instead of returning
-            // Response.InternalServerError.
-            Exception cause = ((Response.InternalServerError<?>) response).cause();
-            throw cause;
           }
 
           status = response.statusCode();
@@ -661,8 +665,7 @@
         status = SC_INTERNAL_SERVER_ERROR;
         responseBytes = handleException(e, req, res);
       } finally {
-        String metric =
-            viewData != null && viewData.view != null ? globals.metrics.view(viewData) : "_unknown";
+        String metric = getViewName(viewData);
         globals.metrics.count.increment(metric);
         if (status >= SC_BAD_REQUEST) {
           globals.metrics.errorCount.increment(metric, status);
@@ -688,6 +691,159 @@
     }
   }
 
+  private RestResource parseResourceWithRetry(
+      HttpServletRequest req,
+      TraceContext traceContext,
+      @Nullable String pluginName,
+      RestCollection<RestResource, RestResource> restCollection,
+      RestResource parentResource,
+      IdString id)
+      throws Exception {
+    return invokeRestEndpointWithRetry(
+        req,
+        traceContext,
+        globals.metrics.view(restCollection.getClass(), pluginName) + "#parse",
+        ActionType.REST_READ_REQUEST,
+        () -> restCollection.parse(parentResource, id),
+        noRetry());
+  }
+
+  private Response<?> invokeRestReadViewWithRetry(
+      HttpServletRequest req,
+      TraceContext traceContext,
+      ViewData viewData,
+      RestReadView<RestResource> view,
+      RestResource rsrc)
+      throws Exception {
+    return invokeRestEndpointWithRetry(
+        req,
+        traceContext,
+        getViewName(viewData),
+        ActionType.REST_READ_REQUEST,
+        () -> view.apply(rsrc),
+        noRetry());
+  }
+
+  private Response<?> invokeRestModifyViewWithRetry(
+      HttpServletRequest req,
+      TraceContext traceContext,
+      ViewData viewData,
+      RestModifyView<RestResource, Object> view,
+      RestResource rsrc,
+      Object inputRequestBody)
+      throws Exception {
+    return invokeRestEndpointWithRetry(
+        req,
+        traceContext,
+        getViewName(viewData),
+        ActionType.REST_WRITE_REQUEST,
+        () -> view.apply(rsrc, inputRequestBody),
+        retryOnLockFailure());
+  }
+
+  private Response<?> invokeRestCollectionCreateViewWithRetry(
+      HttpServletRequest req,
+      TraceContext traceContext,
+      ViewData viewData,
+      RestCollectionCreateView<RestResource, RestResource, Object> view,
+      RestResource rsrc,
+      IdString path,
+      Object inputRequestBody)
+      throws Exception {
+    return invokeRestEndpointWithRetry(
+        req,
+        traceContext,
+        getViewName(viewData),
+        ActionType.REST_WRITE_REQUEST,
+        () -> view.apply(rsrc, path, inputRequestBody),
+        retryOnLockFailure());
+  }
+
+  private Response<?> invokeRestCollectionDeleteMissingViewWithRetry(
+      HttpServletRequest req,
+      TraceContext traceContext,
+      ViewData viewData,
+      RestCollectionDeleteMissingView<RestResource, RestResource, Object> view,
+      RestResource rsrc,
+      IdString path,
+      Object inputRequestBody)
+      throws Exception {
+    return invokeRestEndpointWithRetry(
+        req,
+        traceContext,
+        getViewName(viewData),
+        ActionType.REST_WRITE_REQUEST,
+        () -> view.apply(rsrc, path, inputRequestBody),
+        retryOnLockFailure());
+  }
+
+  private Response<?> invokeRestCollectionModifyViewWithRetry(
+      HttpServletRequest req,
+      TraceContext traceContext,
+      ViewData viewData,
+      RestCollectionModifyView<RestResource, RestResource, Object> view,
+      RestResource rsrc,
+      Object inputRequestBody)
+      throws Exception {
+    return invokeRestEndpointWithRetry(
+        req,
+        traceContext,
+        getViewName(viewData),
+        ActionType.REST_WRITE_REQUEST,
+        () -> view.apply(rsrc, inputRequestBody),
+        retryOnLockFailure());
+  }
+
+  private <T> T invokeRestEndpointWithRetry(
+      HttpServletRequest req,
+      TraceContext traceContext,
+      String caller,
+      ActionType actionType,
+      Action<T> action,
+      Predicate<Throwable> retryExceptionPredicate)
+      throws Exception {
+    RetryHelper.Options.Builder retryOptionsBuilder = RetryHelper.options().caller(caller);
+    if (!traceContext.isTracing()) {
+      // enable automatic retry with tracing in case of non-recoverable failure
+      retryOptionsBuilder =
+          retryOptionsBuilder
+              .retryWithTrace(t -> !(t instanceof RestApiException))
+              .onAutoTrace(
+                  autoTraceId -> {
+                    traceId = Optional.of(autoTraceId);
+
+                    // Include details of the request into the trace.
+                    traceRequestData(req);
+                  });
+    }
+    try {
+      return globals.retryHelper.execute(
+          actionType, action, retryOptionsBuilder.build(), retryExceptionPredicate);
+    } finally {
+      // If auto-tracing got triggered due to a non-recoverable failure, also trace the rest of
+      // this request. This means logging is forced for all further log statements and the logs are
+      // associated with the same trace ID.
+      traceId.ifPresent(tid -> traceContext.addTag(RequestId.Type.TRACE_ID, tid).forceLogging());
+    }
+  }
+
+  private static Predicate<Throwable> noRetry() {
+    return t -> false;
+  }
+
+  private static Predicate<Throwable> retryOnLockFailure() {
+    return t -> {
+      if (t instanceof UpdateException) {
+        t = t.getCause();
+      }
+      return t instanceof LockFailureException;
+    };
+  }
+
+  private String getViewName(ViewData viewData) {
+    return viewData != null && viewData.view != null ? globals.metrics.view(viewData) : "_unknown";
+  }
+
   private static HttpServletRequest applyXdOverrides(HttpServletRequest req, QueryParams qp)
       throws BadRequestException {
     if (!isPost(req)) {
@@ -1451,6 +1607,15 @@
     return requestInfo.build();
   }
 
+  private void traceRequestData(HttpServletRequest req) {
+    logger.atFinest().log(
+        "Received REST request: %s %s (parameters: %s)",
+        req.getMethod(), req.getRequestURI(), getParameterNames(req));
+    logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
+    logger.atFinest().log(
+        "Groups: %s", globals.currentUser.get().getEffectiveGroups().getKnownGroups());
+  }
+
   private boolean isDelete(HttpServletRequest req) {
     return "DELETE".equals(req.getMethod());
   }
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java
index ea76330..6f05814 100644
--- a/java/com/google/gerrit/server/ExceptionHook.java
+++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.Optional;
 
 /**
  * Allows implementors to control how certain exceptions should be handled.
@@ -39,4 +40,17 @@
   default boolean shouldRetry(Throwable throwable) {
     return false;
   }
+
+  /**
+   * Formats the cause of an exception for use in metrics.
+   *
+   * <p>This method allows implementors to group exceptions that have the same cause into one metric
+   * bucket.
+   *
+   * @param throwable the exception cause
+   * @return formatted cause or {@link Optional#empty()} if no formatting was done
+   */
+  default Optional<String> formatCause(Throwable throwable) {
+    return Optional.empty();
+  }
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 5824240..93cf0de 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -479,8 +479,10 @@
         case FAST_FORWARD:
           gitRefUpdated.fire(allUsers, u, null);
           return;
-        case IO_FAILURE:
         case LOCK_FAILURE:
+          throw new LockFailureException(
+              String.format("Update star labels on ref %s failed", refName), u);
+        case IO_FAILURE:
         case NOT_ATTEMPTED:
         case REJECTED:
         case REJECTED_CURRENT_BRANCH:
@@ -513,11 +515,12 @@
         case FORCED:
           gitRefUpdated.fire(allUsers, u, null);
           return;
+        case LOCK_FAILURE:
+          throw new LockFailureException(String.format("Delete star ref %s failed", refName), u);
         case NEW:
         case NO_CHANGE:
         case FAST_FORWARD:
         case IO_FAILURE:
-        case LOCK_FAILURE:
         case NOT_ATTEMPTED:
         case REJECTED:
         case REJECTED_CURRENT_BRANCH:
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 6ba30bf..926e0d5 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -248,9 +249,10 @@
       case NEW:
       case NO_CHANGE:
         break;
+      case LOCK_FAILURE:
+        throw new LockFailureException(String.format("Failed to delete ref %s", refName), ru);
       case FAST_FORWARD:
       case IO_FAILURE:
-      case LOCK_FAILURE:
       case NOT_ATTEMPTED:
       case REJECTED:
       case REJECTED_CURRENT_BRANCH:
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 60e41ed..64d156f 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -59,6 +59,9 @@
   // The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.).
   public abstract Optional<String> changeIdType();
 
+  // The cause of an error.
+  public abstract Optional<String> cause();
+
   // The type of an event.
   public abstract Optional<String> eventType();
 
@@ -158,12 +161,12 @@
    * Metadata{accountId=Optional.empty, actionType=Optional.empty, authDomainName=Optional.empty,
    * branchName=Optional.empty, cacheKey=Optional.empty, cacheName=Optional.empty,
    * className=Optional.empty, changeId=Optional[9212550], changeIdType=Optional.empty,
-   * eventType=Optional.empty, exportValue=Optional.empty, filePath=Optional.empty,
-   * garbageCollectorName=Optional.empty, gitOperation=Optional.empty, groupId=Optional.empty,
-   * groupName=Optional.empty, groupUuid=Optional.empty, httpStatus=Optional.empty,
-   * indexName=Optional.empty, indexVersion=Optional[0], methodName=Optional.empty,
-   * multiple=Optional.empty, operationName=Optional.empty, partial=Optional.empty,
-   * noteDbFilePath=Optional.empty, noteDbRefName=Optional.empty,
+   * cause=Optional.empty, eventType=Optional.empty, exportValue=Optional.empty,
+   * filePath=Optional.empty, garbageCollectorName=Optional.empty, gitOperation=Optional.empty,
+   * groupId=Optional.empty, groupName=Optional.empty, groupUuid=Optional.empty,
+   * httpStatus=Optional.empty, indexName=Optional.empty, indexVersion=Optional[0],
+   * methodName=Optional.empty, multiple=Optional.empty, operationName=Optional.empty,
+   * partial=Optional.empty, noteDbFilePath=Optional.empty, noteDbRefName=Optional.empty,
    * noteDbSequenceType=Optional.empty, noteDbTable=Optional.empty, patchSetId=Optional.empty,
    * pluginMetadata=[], pluginName=Optional.empty, projectName=Optional.empty,
    * pushType=Optional.empty, resourceCount=Optional.empty, restViewName=Optional.empty,
@@ -276,6 +279,8 @@
 
     public abstract Builder changeIdType(@Nullable String changeIdType);
 
+    public abstract Builder cause(@Nullable String cause);
+
     public abstract Builder eventType(@Nullable String eventType);
 
     public abstract Builder exportValue(@Nullable String exportValue);
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
index 58a33c8..396e29b 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -20,7 +20,6 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import java.sql.Timestamp;
 import java.util.Optional;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -34,8 +33,7 @@
   private static final ImmutableList<String> PACKAGE_PREFIXES =
       ImmutableList.of("com.google.gerrit.server.", "com.google.gerrit.");
   private static final ImmutableSet<String> SERVLET_NAMES =
-      ImmutableSet.of(
-          "com.google.gerrit.httpd.restapi.RestApiServlet", RetryingRestModifyView.class.getName());
+      ImmutableSet.of("com.google.gerrit.httpd.restapi.RestApiServlet");
 
   /** Returns an AccountId for the given email address. */
   public static Optional<Account.Id> parseIdent(PersonIdent ident) {
@@ -74,13 +72,16 @@
     StackTraceElement[] trace = Thread.currentThread().getStackTrace();
     int i = findRestApiServlet(trace);
     if (i < 0) {
+      i = findApiImpl(trace);
+    }
+    if (i < 0) {
       return null;
     }
     try {
       for (i--; i >= 0; i--) {
         String cn = trace[i].getClassName();
         Class<?> cls = Class.forName(cn);
-        if (RestModifyView.class.isAssignableFrom(cls) && cls != RetryingRestModifyView.class) {
+        if (RestModifyView.class.isAssignableFrom(cls)) {
           return viewName(cn);
         }
       }
@@ -110,6 +111,16 @@
     return -1;
   }
 
+  private static int findApiImpl(StackTraceElement[] trace) {
+    for (int i = 0; i < trace.length; i++) {
+      String clazz = trace[i].getClassName();
+      if (clazz.startsWith("com.google.gerrit.server.api.") && clazz.endsWith("ApiImpl")) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
   private static String viewName(String cn) {
     String impl = cn.replace('$', '.');
     for (String p : PACKAGE_PREFIXES) {
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index 2d746d8..04d0859 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
@@ -200,10 +201,11 @@
             referenceUpdated.fire(
                 project, ru, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
             break;
+          case LOCK_FAILURE:
+            throw new LockFailureException(String.format("Failed to create ref \"%s\"", ref), ru);
           case FAST_FORWARD:
           case FORCED:
           case IO_FAILURE:
-          case LOCK_FAILURE:
           case NOT_ATTEMPTED:
           case NO_CHANGE:
           case REJECTED:
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java
index cdaa99d..c27bdd8 100644
--- a/java/com/google/gerrit/server/restapi/account/Stars.java
+++ b/java/com/google/gerrit/server/restapi/account/Stars.java
@@ -98,7 +98,8 @@
 
     @Override
     @SuppressWarnings("unchecked")
-    public Response<List<ChangeInfo>> apply(AccountResource rsrc) throws Exception {
+    public Response<List<ChangeInfo>> apply(AccountResource rsrc)
+        throws RestApiException, PermissionBackendException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to list stars of another account");
       }
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index df3b58e..ae69ccd 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -34,8 +35,6 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -44,10 +43,11 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
-public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo>
-    implements UiAction<ChangeResource> {
+public class Abandon
+    implements RestModifyView<ChangeResource, AbandonInput>, UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeJson.Factory json;
   private final AbandonOp.Factory abandonOpFactory;
   private final NotifyResolver notifyResolver;
@@ -55,12 +55,12 @@
 
   @Inject
   Abandon(
+      BatchUpdate.Factory updateFactory,
       ChangeJson.Factory json,
-      RetryHelper retryHelper,
       AbandonOp.Factory abandonOpFactory,
       NotifyResolver notifyResolver,
       PatchSetUtil patchSetUtil) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.json = json;
     this.abandonOpFactory = abandonOpFactory;
     this.notifyResolver = notifyResolver;
@@ -68,8 +68,7 @@
   }
 
   @Override
-  protected Response<ChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, AbandonInput input)
+  public Response<ChangeInfo> apply(ChangeResource rsrc, AbandonInput input)
       throws RestApiException, UpdateException, PermissionBackendException, IOException,
           ConfigInvalidException {
     // Not allowed to abandon if the current patch set is locked.
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index 1a89935..725defc 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.RevisionResource;
@@ -39,8 +40,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.submit.IntegrationException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -49,11 +48,11 @@
 
 @Singleton
 public class CherryPick
-    extends RetryingRestModifyView<RevisionResource, CherryPickInput, CherryPickChangeInfo>
-    implements UiAction<RevisionResource> {
+    implements RestModifyView<RevisionResource, CherryPickInput>, UiAction<RevisionResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory updateFactory;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
   private final ContributorAgreementsChecker contributorAgreements;
@@ -62,13 +61,13 @@
   @Inject
   CherryPick(
       PermissionBackend permissionBackend,
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       CherryPickChange cherryPickChange,
       ChangeJson.Factory json,
       ContributorAgreementsChecker contributorAgreements,
       ProjectCache projectCache) {
-    super(retryHelper);
     this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
     this.contributorAgreements = contributorAgreements;
@@ -76,8 +75,7 @@
   }
 
   @Override
-  public Response<CherryPickChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input)
+  public Response<CherryPickChangeInfo> apply(RevisionResource rsrc, CherryPickInput input)
       throws IOException, UpdateException, RestApiException, PermissionBackendException,
           ConfigInvalidException, NoSuchProjectException {
     input.parent = input.parent == null ? 1 : input.parent;
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index a3c8a97..729e32d 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -35,8 +36,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.submit.IntegrationException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -45,9 +44,9 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
-public class CherryPickCommit
-    extends RetryingRestModifyView<CommitResource, CherryPickInput, CherryPickChangeInfo> {
+public class CherryPickCommit implements RestModifyView<CommitResource, CherryPickInput> {
   private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory updateFactory;
   private final Provider<CurrentUser> user;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
@@ -55,14 +54,14 @@
 
   @Inject
   CherryPickCommit(
-      RetryHelper retryHelper,
+      PermissionBackend permissionBackend,
+      BatchUpdate.Factory updateFactory,
       Provider<CurrentUser> user,
       CherryPickChange cherryPickChange,
       ChangeJson.Factory json,
-      PermissionBackend permissionBackend,
       ContributorAgreementsChecker contributorAgreements) {
-    super(retryHelper);
     this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
     this.user = user;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
@@ -70,8 +69,7 @@
   }
 
   @Override
-  public Response<CherryPickChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
+  public Response<CherryPickChangeInfo> apply(CommitResource rsrc, CherryPickInput input)
       throws IOException, UpdateException, RestApiException, PermissionBackendException,
           ConfigInvalidException, NoSuchProjectException {
     String destination = Strings.nullToEmpty(input.destination).trim();
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index acc6465..f4a8b75 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.CurrentUser;
@@ -69,8 +70,6 @@
 import com.google.gerrit.server.restapi.project.CommitsCollection;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestCollectionModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -103,8 +102,8 @@
 
 @Singleton
 public class CreateChange
-    extends RetryingRestCollectionModifyView<
-        TopLevelResource, ChangeResource, ChangeInput, ChangeInfo> {
+    implements RestCollectionModifyView<TopLevelResource, ChangeResource, ChangeInput> {
+  private final BatchUpdate.Factory updateFactory;
   private final String anonymousCowardName;
   private final GitRepositoryManager gitManager;
   private final Sequences seq;
@@ -126,6 +125,7 @@
 
   @Inject
   CreateChange(
+      BatchUpdate.Factory updateFactory,
       @AnonymousCowardName String anonymousCowardName,
       GitRepositoryManager gitManager,
       Sequences seq,
@@ -138,13 +138,12 @@
       ChangeJson.Factory json,
       ChangeFinder changeFinder,
       Provider<InternalChangeQuery> queryProvider,
-      RetryHelper retryHelper,
       PatchSetUtil psUtil,
       @GerritServerConfig Config config,
       MergeUtil.Factory mergeUtilFactory,
       NotifyResolver notifyResolver,
       ContributorAgreementsChecker contributorAgreements) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.anonymousCowardName = anonymousCowardName;
     this.gitManager = gitManager;
     this.seq = seq;
@@ -166,8 +165,7 @@
   }
 
   @Override
-  protected Response<ChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
+  public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInput input)
       throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
           PermissionBackendException, ConfigInvalidException {
     if (!user.get().isIdentifiedUser()) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index f434e31..5b7245d 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.CommentsUtil;
@@ -36,8 +37,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -46,8 +45,8 @@
 import java.util.Collections;
 
 @Singleton
-public class CreateDraftComment
-    extends RetryingRestModifyView<RevisionResource, DraftInput, CommentInfo> {
+public class CreateDraftComment implements RestModifyView<RevisionResource, DraftInput> {
+  private final BatchUpdate.Factory updateFactory;
   private final Provider<CommentJson> commentJson;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
@@ -55,12 +54,12 @@
 
   @Inject
   CreateDraftComment(
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       Provider<CommentJson> commentJson,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
       PatchListCache patchListCache) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
@@ -68,8 +67,7 @@
   }
 
   @Override
-  protected Response<CommentInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, RevisionResource rsrc, DraftInput in)
+  public Response<CommentInfo> apply(RevisionResource rsrc, DraftInput in)
       throws RestApiException, UpdateException, PermissionBackendException {
     if (Strings.isNullOrEmpty(in.path)) {
       throw new BadRequestException("path must be non-empty");
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index b84ac12..8d50b86 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -53,8 +54,6 @@
 import com.google.gerrit.server.restapi.project.CommitsCollection;
 import com.google.gerrit.server.submit.MergeIdenticalTreeException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -75,8 +74,8 @@
 import org.eclipse.jgit.util.ChangeIdUtil;
 
 @Singleton
-public class CreateMergePatchSet
-    extends RetryingRestModifyView<ChangeResource, MergePatchSetInput, ChangeInfo> {
+public class CreateMergePatchSet implements RestModifyView<ChangeResource, MergePatchSetInput> {
+  private final BatchUpdate.Factory updateFactory;
   private final GitRepositoryManager gitManager;
   private final CommitsCollection commits;
   private final TimeZone serverTimeZone;
@@ -91,6 +90,7 @@
 
   @Inject
   CreateMergePatchSet(
+      BatchUpdate.Factory updateFactory,
       GitRepositoryManager gitManager,
       CommitsCollection commits,
       @GerritPersonIdent PersonIdent myIdent,
@@ -98,12 +98,11 @@
       ChangeJson.Factory json,
       PatchSetUtil psUtil,
       MergeUtil.Factory mergeUtilFactory,
-      RetryHelper retryHelper,
       PatchSetInserter.Factory patchSetInserterFactory,
       ProjectCache projectCache,
       ChangeFinder changeFinder,
       PermissionBackend permissionBackend) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.gitManager = gitManager;
     this.commits = commits;
     this.serverTimeZone = myIdent.getTimeZone();
@@ -118,8 +117,7 @@
   }
 
   @Override
-  protected Response<ChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, MergePatchSetInput in)
+  public Response<ChangeInfo> apply(ChangeResource rsrc, MergePatchSetInput in)
       throws IOException, RestApiException, UpdateException, PermissionBackendException {
     // Not allowed to create a new patch set if the current patch set is locked.
     psUtil.checkPatchSetNotLocked(rsrc.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index 834782f..20fd675 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountLoader;
@@ -34,16 +35,14 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-public class DeleteAssignee extends RetryingRestModifyView<ChangeResource, Input, AccountInfo> {
-
+public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeMessagesUtil cmUtil;
   private final AssigneeChanged assigneeChanged;
   private final IdentifiedUser.GenericFactory userFactory;
@@ -51,12 +50,12 @@
 
   @Inject
   DeleteAssignee(
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       ChangeMessagesUtil cmUtil,
       AssigneeChanged assigneeChanged,
       IdentifiedUser.GenericFactory userFactory,
       AccountLoader.Factory accountLoaderFactory) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.cmUtil = cmUtil;
     this.assigneeChanged = assigneeChanged;
     this.userFactory = userFactory;
@@ -64,8 +63,7 @@
   }
 
   @Override
-  protected Response<AccountInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
+  public Response<AccountInfo> apply(ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index aa4dcf0..3ca5463 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.DeleteChangeOp;
@@ -28,28 +29,25 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-public class DeleteChange extends RetryingRestModifyView<ChangeResource, Input, Object>
-    implements UiAction<ChangeResource> {
-
+public class DeleteChange
+    implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
+  private final BatchUpdate.Factory updateFactory;
   private final DeleteChangeOp.Factory opFactory;
 
   @Inject
-  public DeleteChange(RetryHelper retryHelper, DeleteChangeOp.Factory opFactory) {
-    super(retryHelper);
+  public DeleteChange(BatchUpdate.Factory updateFactory, DeleteChangeOp.Factory opFactory) {
+    this.updateFactory = updateFactory;
     this.opFactory = opFactory;
   }
 
   @Override
-  protected Response<Object> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
+  public Response<Object> apply(ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
     if (!isChangeDeletable(rsrc)) {
       throw new MethodNotAllowedException("delete not permitted");
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index 63f5bbe..f79209d 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -40,8 +40,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -53,11 +51,11 @@
 /** Deletes a change message by rewriting history. */
 @Singleton
 public class DeleteChangeMessage
-    extends RetryingRestModifyView<
-        ChangeMessageResource, DeleteChangeMessageInput, ChangeMessageInfo> {
+    implements RestModifyView<ChangeMessageResource, DeleteChangeMessageInput> {
 
   private final Provider<CurrentUser> userProvider;
   private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final AccountLoader.Factory accountLoaderFactory;
   private final ChangeNotes.Factory notesFactory;
@@ -66,23 +64,21 @@
   public DeleteChangeMessage(
       Provider<CurrentUser> userProvider,
       PermissionBackend permissionBackend,
+      BatchUpdate.Factory updateFactory,
       ChangeMessagesUtil changeMessagesUtil,
       AccountLoader.Factory accountLoaderFactory,
-      ChangeNotes.Factory notesFactory,
-      RetryHelper retryHelper) {
-    super(retryHelper);
+      ChangeNotes.Factory notesFactory) {
     this.userProvider = userProvider;
     this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
     this.changeMessagesUtil = changeMessagesUtil;
     this.accountLoaderFactory = accountLoaderFactory;
     this.notesFactory = notesFactory;
   }
 
   @Override
-  public Response<ChangeMessageInfo> applyImpl(
-      BatchUpdate.Factory updateFactory,
-      ChangeMessageResource resource,
-      DeleteChangeMessageInput input)
+  public Response<ChangeMessageInfo> apply(
+      ChangeMessageResource resource, DeleteChangeMessageInput input)
       throws RestApiException, PermissionBackendException, UpdateException, IOException {
     CurrentUser user = userProvider.get();
     permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
@@ -157,7 +153,7 @@
 
     @Override
     public Response<ChangeMessageInfo> apply(ChangeMessageResource resource, Input input)
-        throws RestApiException {
+        throws RestApiException, PermissionBackendException, UpdateException, IOException {
       return deleteChangeMessage.apply(resource, new DeleteChangeMessageInput());
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteComment.java b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
index 95479a6..f915728 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.CommentResource;
@@ -33,8 +34,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -46,11 +45,11 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
-public class DeleteComment
-    extends RetryingRestModifyView<CommentResource, DeleteCommentInput, CommentInfo> {
+public class DeleteComment implements RestModifyView<CommentResource, DeleteCommentInput> {
 
   private final Provider<CurrentUser> userProvider;
   private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory updateFactory;
   private final CommentsUtil commentsUtil;
   private final Provider<CommentJson> commentJson;
   private final ChangeNotes.Factory notesFactory;
@@ -59,21 +58,20 @@
   public DeleteComment(
       Provider<CurrentUser> userProvider,
       PermissionBackend permissionBackend,
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       CommentsUtil commentsUtil,
       Provider<CommentJson> commentJson,
       ChangeNotes.Factory notesFactory) {
-    super(retryHelper);
     this.userProvider = userProvider;
     this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
     this.commentsUtil = commentsUtil;
     this.commentJson = commentJson;
     this.notesFactory = notesFactory;
   }
 
   @Override
-  public Response<CommentInfo> applyImpl(
-      BatchUpdate.Factory batchUpdateFactory, CommentResource rsrc, DeleteCommentInput input)
+  public Response<CommentInfo> apply(CommentResource rsrc, DeleteCommentInput input)
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException,
           UpdateException {
     CurrentUser user = userProvider.get();
@@ -86,8 +84,7 @@
     String newMessage = getCommentNewMessage(user.asIdentifiedUser().getName(), input.reason);
     DeleteCommentOp deleteCommentOp = new DeleteCommentOp(rsrc, newMessage);
     try (BatchUpdate batchUpdate =
-        batchUpdateFactory.create(
-            rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) {
       batchUpdate.addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp).execute();
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index 9296988..89fc3b7 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DraftCommentResource;
@@ -31,8 +32,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -41,28 +40,26 @@
 import java.util.Optional;
 
 @Singleton
-public class DeleteDraftComment
-    extends RetryingRestModifyView<DraftCommentResource, Input, CommentInfo> {
-
+public class DeleteDraftComment implements RestModifyView<DraftCommentResource, Input> {
+  private final BatchUpdate.Factory updateFactory;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
   private final PatchListCache patchListCache;
 
   @Inject
   DeleteDraftComment(
+      BatchUpdate.Factory updateFactory,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
-      RetryHelper retryHelper,
       PatchListCache patchListCache) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
     this.patchListCache = patchListCache;
   }
 
   @Override
-  protected Response<CommentInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, Input input)
+  public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input)
       throws RestApiException, UpdateException {
     try (BatchUpdate bu =
         updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
index 8478bb5..16b7136 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -23,37 +23,35 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-public class DeletePrivate
-    extends RetryingRestModifyView<ChangeResource, InputWithMessage, String> {
+public class DeletePrivate implements RestModifyView<ChangeResource, InputWithMessage> {
   private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory updateFactory;
   private final SetPrivateOp.Factory setPrivateOpFactory;
 
   @Inject
   DeletePrivate(
-      RetryHelper retryHelper,
       PermissionBackend permissionBackend,
+      BatchUpdate.Factory updateFactory,
       SetPrivateOp.Factory setPrivateOpFactory) {
-    super(retryHelper);
     this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
     this.setPrivateOpFactory = setPrivateOpFactory;
   }
 
   @Override
-  protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, @Nullable InputWithMessage input)
+  public Response<String> apply(ChangeResource rsrc, @Nullable InputWithMessage input)
       throws RestApiException, UpdateException {
     if (!canDeletePrivate(rsrc).value()) {
       throw new AuthException("not allowed to unmark private");
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java b/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
index c86d0ca..10feb63 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.BatchUpdate;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -28,10 +28,10 @@
 public class DeletePrivateByPost extends DeletePrivate implements UiAction<ChangeResource> {
   @Inject
   DeletePrivateByPost(
-      RetryHelper retryHelper,
       PermissionBackend permissionBackend,
+      BatchUpdate.Factory updateFactory,
       SetPrivateOp.Factory setPrivateOpFactory) {
-    super(retryHelper, permissionBackend, setPrivateOpFactory);
+    super(permissionBackend, updateFactory, setPrivateOpFactory);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
index b98bb3b..3e4a483 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -19,39 +19,36 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.DeleteReviewerByEmailOp;
 import com.google.gerrit.server.change.DeleteReviewerOp;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-public class DeleteReviewer
-    extends RetryingRestModifyView<ReviewerResource, DeleteReviewerInput, Object> {
-
+public class DeleteReviewer implements RestModifyView<ReviewerResource, DeleteReviewerInput> {
+  private final BatchUpdate.Factory updateFactory;
   private final DeleteReviewerOp.Factory deleteReviewerOpFactory;
   private final DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory;
 
   @Inject
   DeleteReviewer(
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       DeleteReviewerOp.Factory deleteReviewerOpFactory,
       DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.deleteReviewerOpFactory = deleteReviewerOpFactory;
     this.deleteReviewerByEmailOpFactory = deleteReviewerByEmailOpFactory;
   }
 
   @Override
-  protected Response<Object> applyImpl(
-      BatchUpdate.Factory updateFactory, ReviewerResource rsrc, DeleteReviewerInput input)
+  public Response<Object> apply(ReviewerResource rsrc, DeleteReviewerInput input)
       throws RestApiException, UpdateException {
     if (input == null) {
       input = new DeleteReviewerInput();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 1193ad6..bdbf3f7 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -51,8 +52,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -64,9 +63,10 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
-public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Object> {
+public class DeleteVote implements RestModifyView<VoteResource, DeleteVoteInput> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final BatchUpdate.Factory updateFactory;
   private final ApprovalsUtil approvalsUtil;
   private final PatchSetUtil psUtil;
   private final ChangeMessagesUtil cmUtil;
@@ -79,7 +79,7 @@
 
   @Inject
   DeleteVote(
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
       PatchSetUtil psUtil,
       ChangeMessagesUtil cmUtil,
@@ -89,7 +89,7 @@
       NotifyResolver notifyResolver,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
     this.cmUtil = cmUtil;
@@ -102,8 +102,7 @@
   }
 
   @Override
-  protected Response<Object> applyImpl(
-      BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input)
+  public Response<Object> apply(VoteResource rsrc, DeleteVoteInput input)
       throws RestApiException, UpdateException, IOException, ConfigInvalidException {
     if (input == null) {
       input = new DeleteVoteInput();
diff --git a/java/com/google/gerrit/server/restapi/change/Index.java b/java/com/google/gerrit/server/restapi/change/Index.java
index 5a17c07..5e17ae8 100644
--- a/java/com/google/gerrit/server/restapi/change/Index.java
+++ b/java/com/google/gerrit/server/restapi/change/Index.java
@@ -17,33 +17,29 @@
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
 
 @Singleton
-public class Index extends RetryingRestModifyView<ChangeResource, Input, Object> {
+public class Index implements RestModifyView<ChangeResource, Input> {
   private final PermissionBackend permissionBackend;
   private final ChangeIndexer indexer;
 
   @Inject
-  Index(RetryHelper retryHelper, PermissionBackend permissionBackend, ChangeIndexer indexer) {
-    super(retryHelper);
+  Index(PermissionBackend permissionBackend, ChangeIndexer indexer) {
     this.permissionBackend = permissionBackend;
     this.indexer = indexer;
   }
 
   @Override
-  protected Response<Object> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
+  public Response<Object> apply(ChangeResource rsrc, Input input)
       throws IOException, AuthException, PermissionBackendException {
     permissionBackend.currentUser().check(GlobalPermission.MAINTAIN_SERVER);
     indexer.index(rsrc.getChange());
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 7d4c4d1..51c512f 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -59,8 +60,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -76,11 +75,11 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
-public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo>
-    implements UiAction<ChangeResource> {
+public class Move implements RestModifyView<ChangeResource, MoveInput>, UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeJson.Factory json;
   private final GitRepositoryManager repoManager;
   private final Provider<InternalChangeQuery> queryProvider;
@@ -93,17 +92,17 @@
   @Inject
   Move(
       PermissionBackend permissionBackend,
+      BatchUpdate.Factory updateFactory,
       ChangeJson.Factory json,
       GitRepositoryManager repoManager,
       Provider<InternalChangeQuery> queryProvider,
       ChangeMessagesUtil cmUtil,
-      RetryHelper retryHelper,
       PatchSetUtil psUtil,
       ApprovalsUtil approvalsUtil,
       ProjectCache projectCache,
       @GerritServerConfig Config gerritConfig) {
-    super(retryHelper);
     this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
     this.json = json;
     this.repoManager = repoManager;
     this.queryProvider = queryProvider;
@@ -115,8 +114,7 @@
   }
 
   @Override
-  protected Response<ChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input)
+  public Response<ChangeInfo> apply(ChangeResource rsrc, MoveInput input)
       throws RestApiException, UpdateException, PermissionBackendException, IOException {
     if (!moveEnabled) {
       // This will be removed with the above config once we reach consensus for the move change
diff --git a/java/com/google/gerrit/server/restapi/change/PostHashtags.java b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
index 516dead..c1a6a13 100644
--- a/java/com/google/gerrit/server/restapi/change/PostHashtags.java
+++ b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
@@ -18,14 +18,13 @@
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetHashtagsOp;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -33,19 +32,18 @@
 
 @Singleton
 public class PostHashtags
-    extends RetryingRestModifyView<ChangeResource, HashtagsInput, ImmutableSortedSet<String>>
-    implements UiAction<ChangeResource> {
+    implements RestModifyView<ChangeResource, HashtagsInput>, UiAction<ChangeResource> {
+  private final BatchUpdate.Factory updateFactory;
   private final SetHashtagsOp.Factory hashtagsFactory;
 
   @Inject
-  PostHashtags(RetryHelper retryHelper, SetHashtagsOp.Factory hashtagsFactory) {
-    super(retryHelper);
+  PostHashtags(BatchUpdate.Factory updateFactory, SetHashtagsOp.Factory hashtagsFactory) {
+    this.updateFactory = updateFactory;
     this.hashtagsFactory = hashtagsFactory;
   }
 
   @Override
-  protected Response<ImmutableSortedSet<String>> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource req, HashtagsInput input)
+  public Response<ImmutableSortedSet<String>> apply(ChangeResource req, HashtagsInput input)
       throws RestApiException, UpdateException, PermissionBackendException {
     req.permissions().check(ChangePermission.EDIT_HASHTAGS);
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
index c9ad049..f774457 100644
--- a/java/com/google/gerrit/server/restapi/change/PostPrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.SetPrivateOp;
@@ -31,8 +32,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -40,27 +39,27 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-public class PostPrivate extends RetryingRestModifyView<ChangeResource, InputWithMessage, String>
-    implements UiAction<ChangeResource> {
+public class PostPrivate
+    implements RestModifyView<ChangeResource, InputWithMessage>, UiAction<ChangeResource> {
   private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory updateFactory;
   private final SetPrivateOp.Factory setPrivateOpFactory;
   private final boolean disablePrivateChanges;
 
   @Inject
   PostPrivate(
-      RetryHelper retryHelper,
       PermissionBackend permissionBackend,
+      BatchUpdate.Factory updateFactory,
       SetPrivateOp.Factory setPrivateOpFactory,
       @GerritServerConfig Config config) {
-    super(retryHelper);
     this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
     this.setPrivateOpFactory = setPrivateOpFactory;
     this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
   }
 
   @Override
-  public Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, InputWithMessage input)
+  public Response<String> apply(ChangeResource rsrc, InputWithMessage input)
       throws RestApiException, UpdateException {
     if (disablePrivateChanges) {
       throw new MethodNotAllowedException("private changes are disabled");
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 602ab19..6f10839 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -73,6 +73,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.extensions.validators.CommentForValidation;
@@ -120,8 +121,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.CommentsRejectedException;
 import com.google.gerrit.server.update.Context;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -150,8 +149,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
-public class PostReview
-    extends RetryingRestModifyView<RevisionResource, ReviewInput, ReviewResult> {
+public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String ERROR_ADDING_REVIEWER = "error adding reviewer";
@@ -163,6 +161,7 @@
   private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
   private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
 
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeResource.Factory changeResourceFactory;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
@@ -186,7 +185,7 @@
 
   @Inject
   PostReview(
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       ChangeResource.Factory changeResourceFactory,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
@@ -206,7 +205,7 @@
       ProjectCache projectCache,
       PermissionBackend permissionBackend,
       PluginSetContext<CommentValidator> commentValidators) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.changeResourceFactory = changeResourceFactory;
     this.changeDataFactory = changeDataFactory;
     this.commentsUtil = commentsUtil;
@@ -230,15 +229,13 @@
   }
 
   @Override
-  protected Response<ReviewResult> applyImpl(
-      BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input)
+  public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input)
       throws RestApiException, UpdateException, IOException, PermissionBackendException,
           ConfigInvalidException, PatchListNotAvailableException {
-    return apply(updateFactory, revision, input, TimeUtil.nowTs());
+    return apply(revision, input, TimeUtil.nowTs());
   }
 
-  public Response<ReviewResult> apply(
-      BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input, Timestamp ts)
+  public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Timestamp ts)
       throws RestApiException, UpdateException, IOException, PermissionBackendException,
           ConfigInvalidException, PatchListNotAvailableException {
     // Respect timestamp, but truncate at change created-on time.
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index f74643c..e6a87e9 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerAdder;
@@ -29,8 +30,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestCollectionModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -40,28 +39,26 @@
 
 @Singleton
 public class PostReviewers
-    extends RetryingRestCollectionModifyView<
-        ChangeResource, ReviewerResource, AddReviewerInput, AddReviewerResult> {
-
+    implements RestCollectionModifyView<ChangeResource, ReviewerResource, AddReviewerInput> {
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeData.Factory changeDataFactory;
   private final NotifyResolver notifyResolver;
   private final ReviewerAdder reviewerAdder;
 
   @Inject
   PostReviewers(
+      BatchUpdate.Factory updateFactory,
       ChangeData.Factory changeDataFactory,
-      RetryHelper retryHelper,
       NotifyResolver notifyResolver,
       ReviewerAdder reviewerAdder) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.changeDataFactory = changeDataFactory;
     this.notifyResolver = notifyResolver;
     this.reviewerAdder = reviewerAdder;
   }
 
   @Override
-  protected Response<AddReviewerResult> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input)
+  public Response<AddReviewerResult> apply(ChangeResource rsrc, AddReviewerInput input)
       throws IOException, RestApiException, UpdateException, PermissionBackendException,
           ConfigInvalidException {
     if (input.reviewer == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
index 44f35a0..d76e53a 100644
--- a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.edit.ChangeEdit;
@@ -28,8 +29,6 @@
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -38,27 +37,26 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
-public class PublishChangeEdit
-    extends RetryingRestModifyView<ChangeResource, PublishChangeEditInput, Object> {
+public class PublishChangeEdit implements RestModifyView<ChangeResource, PublishChangeEditInput> {
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeEditUtil editUtil;
   private final NotifyResolver notifyResolver;
   private final ContributorAgreementsChecker contributorAgreementsChecker;
 
   @Inject
   PublishChangeEdit(
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       ChangeEditUtil editUtil,
       NotifyResolver notifyResolver,
       ContributorAgreementsChecker contributorAgreementsChecker) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.editUtil = editUtil;
     this.notifyResolver = notifyResolver;
     this.contributorAgreementsChecker = contributorAgreementsChecker;
   }
 
   @Override
-  protected Response<Object> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in)
+  public Response<Object> apply(ChangeResource rsrc, PublishChangeEditInput in)
       throws IOException, RestApiException, UpdateException, ConfigInvalidException,
           NoSuchProjectException {
     contributorAgreementsChecker.check(rsrc.getProject(), rsrc.getUser());
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index dd84624..dc1adfa 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -38,8 +39,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -48,9 +47,10 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
-public class PutAssignee extends RetryingRestModifyView<ChangeResource, AssigneeInput, AccountInfo>
-    implements UiAction<ChangeResource> {
+public class PutAssignee
+    implements RestModifyView<ChangeResource, AssigneeInput>, UiAction<ChangeResource> {
 
+  private final BatchUpdate.Factory updateFactory;
   private final AccountResolver accountResolver;
   private final SetAssigneeOp.Factory assigneeFactory;
   private final ReviewerAdder reviewerAdder;
@@ -60,14 +60,14 @@
 
   @Inject
   PutAssignee(
+      BatchUpdate.Factory updateFactory,
       AccountResolver accountResolver,
       SetAssigneeOp.Factory assigneeFactory,
-      RetryHelper retryHelper,
       ReviewerAdder reviewerAdder,
       AccountLoader.Factory accountLoaderFactory,
       PermissionBackend permissionBackend,
       ApprovalsUtil approvalsUtil) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.accountResolver = accountResolver;
     this.assigneeFactory = assigneeFactory;
     this.reviewerAdder = reviewerAdder;
@@ -77,8 +77,7 @@
   }
 
   @Override
-  protected Response<AccountInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input)
+  public Response<AccountInfo> apply(ChangeResource rsrc, AssigneeInput input)
       throws RestApiException, UpdateException, IOException, PermissionBackendException,
           ConfigInvalidException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
diff --git a/java/com/google/gerrit/server/restapi/change/PutDescription.java b/java/com/google/gerrit/server/restapi/change/PutDescription.java
index 451d010..d84ab3e 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDescription.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.common.DescriptionInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.PatchSetUtil;
@@ -30,8 +31,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -39,21 +38,21 @@
 
 @Singleton
 public class PutDescription
-    extends RetryingRestModifyView<RevisionResource, DescriptionInput, String>
-    implements UiAction<RevisionResource> {
+    implements RestModifyView<RevisionResource, DescriptionInput>, UiAction<RevisionResource> {
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
 
   @Inject
-  PutDescription(ChangeMessagesUtil cmUtil, RetryHelper retryHelper, PatchSetUtil psUtil) {
-    super(retryHelper);
+  PutDescription(
+      BatchUpdate.Factory updateFactory, ChangeMessagesUtil cmUtil, PatchSetUtil psUtil) {
+    this.updateFactory = updateFactory;
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
   }
 
   @Override
-  protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, RevisionResource rsrc, DescriptionInput input)
+  public Response<String> apply(RevisionResource rsrc, DescriptionInput input)
       throws UpdateException, RestApiException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_DESCRIPTION);
 
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 5696fcb..63cd7a3 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
@@ -35,8 +36,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -47,9 +46,8 @@
 import java.util.Optional;
 
 @Singleton
-public class PutDraftComment
-    extends RetryingRestModifyView<DraftCommentResource, DraftInput, CommentInfo> {
-
+public class PutDraftComment implements RestModifyView<DraftCommentResource, DraftInput> {
+  private final BatchUpdate.Factory updateFactory;
   private final DeleteDraftComment delete;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
@@ -58,13 +56,13 @@
 
   @Inject
   PutDraftComment(
+      BatchUpdate.Factory updateFactory,
       DeleteDraftComment delete,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
-      RetryHelper retryHelper,
       Provider<CommentJson> commentJson,
       PatchListCache patchListCache) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.delete = delete;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
@@ -73,11 +71,10 @@
   }
 
   @Override
-  protected Response<CommentInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, DraftInput in)
+  public Response<CommentInfo> apply(DraftCommentResource rsrc, DraftInput in)
       throws RestApiException, UpdateException, PermissionBackendException {
     if (in == null || in.message == null || in.message.trim().isEmpty()) {
-      return delete.applyImpl(updateFactory, rsrc, null);
+      return delete.apply(rsrc, null);
     } else if (in.id != null && !rsrc.getId().equals(in.id)) {
       throw new BadRequestException("id must match URL");
     } else if (in.line != null && in.line < 0) {
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index acda547..4761d0c3 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -38,8 +39,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -61,8 +60,9 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
-public class PutMessage extends RetryingRestModifyView<ChangeResource, CommitMessageInput, String> {
+public class PutMessage implements RestModifyView<ChangeResource, CommitMessageInput> {
 
+  private final BatchUpdate.Factory updateFactory;
   private final GitRepositoryManager repositoryManager;
   private final Provider<CurrentUser> userProvider;
   private final TimeZone tz;
@@ -74,7 +74,7 @@
 
   @Inject
   PutMessage(
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       GitRepositoryManager repositoryManager,
       Provider<CurrentUser> userProvider,
       PatchSetInserter.Factory psInserterFactory,
@@ -83,7 +83,7 @@
       PatchSetUtil psUtil,
       NotifyResolver notifyResolver,
       ProjectCache projectCache) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.repositoryManager = repositoryManager;
     this.userProvider = userProvider;
     this.psInserterFactory = psInserterFactory;
@@ -95,8 +95,7 @@
   }
 
   @Override
-  protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource resource, CommitMessageInput input)
+  public Response<String> apply(ChangeResource resource, CommitMessageInput input)
       throws IOException, RestApiException, UpdateException, PermissionBackendException,
           ConfigInvalidException {
     PatchSet ps = psUtil.current(resource.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/change/PutTopic.java b/java/com/google/gerrit/server/restapi/change/PutTopic.java
index cfeb884..f673bfc 100644
--- a/java/com/google/gerrit/server/restapi/change/PutTopic.java
+++ b/java/com/google/gerrit/server/restapi/change/PutTopic.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -33,29 +34,27 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-public class PutTopic extends RetryingRestModifyView<ChangeResource, TopicInput, String>
-    implements UiAction<ChangeResource> {
+public class PutTopic
+    implements RestModifyView<ChangeResource, TopicInput>, UiAction<ChangeResource> {
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeMessagesUtil cmUtil;
   private final TopicEdited topicEdited;
 
   @Inject
-  PutTopic(ChangeMessagesUtil cmUtil, RetryHelper retryHelper, TopicEdited topicEdited) {
-    super(retryHelper);
+  PutTopic(BatchUpdate.Factory updateFactory, ChangeMessagesUtil cmUtil, TopicEdited topicEdited) {
+    this.updateFactory = updateFactory;
     this.cmUtil = cmUtil;
     this.topicEdited = topicEdited;
   }
 
   @Override
-  protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource req, TopicInput input)
+  public Response<String> apply(ChangeResource req, TopicInput input)
       throws UpdateException, RestApiException, PermissionBackendException {
     req.permissions().check(ChangePermission.EDIT_TOPIC_NAME);
 
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 50d1ad0..7a9136b 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -47,8 +47,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -63,13 +61,14 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
-public class Rebase extends RetryingRestModifyView<RevisionResource, RebaseInput, ChangeInfo>
+public class Rebase
     implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final ImmutableSet<ListChangesOption> OPTIONS =
       Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
 
+  private final BatchUpdate.Factory updateFactory;
   private final GitRepositoryManager repoManager;
   private final RebaseChangeOp.Factory rebaseFactory;
   private final RebaseUtil rebaseUtil;
@@ -80,7 +79,7 @@
 
   @Inject
   public Rebase(
-      RetryHelper retryHelper,
+      BatchUpdate.Factory updateFactory,
       GitRepositoryManager repoManager,
       RebaseChangeOp.Factory rebaseFactory,
       RebaseUtil rebaseUtil,
@@ -88,7 +87,7 @@
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       PatchSetUtil patchSetUtil) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.repoManager = repoManager;
     this.rebaseFactory = rebaseFactory;
     this.rebaseUtil = rebaseUtil;
@@ -99,8 +98,7 @@
   }
 
   @Override
-  protected Response<ChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, RevisionResource rsrc, RebaseInput input)
+  public Response<ChangeInfo> apply(RevisionResource rsrc, RebaseInput input)
       throws UpdateException, RestApiException, IOException, PermissionBackendException {
     // Not allowed to rebase if the current patch set is locked.
     patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
@@ -271,7 +269,8 @@
     }
 
     @Override
-    public Response<ChangeInfo> apply(ChangeResource rsrc, RebaseInput input) throws Exception {
+    public Response<ChangeInfo> apply(ChangeResource rsrc, RebaseInput input)
+        throws RestApiException, UpdateException, IOException, PermissionBackendException {
       PatchSet ps = psUtil.current(rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
index 7be8765..9fb8de8 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
@@ -19,37 +19,30 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.lib.Repository;
 
 @Singleton
-public class RebaseChangeEdit extends RetryingRestModifyView<ChangeResource, Input, Object> {
+public class RebaseChangeEdit implements RestModifyView<ChangeResource, Input> {
   private final GitRepositoryManager repositoryManager;
   private final ChangeEditModifier editModifier;
 
   @Inject
-  RebaseChangeEdit(
-      RetryHelper retryHelper,
-      GitRepositoryManager repositoryManager,
-      ChangeEditModifier editModifier) {
-    super(retryHelper);
+  RebaseChangeEdit(GitRepositoryManager repositoryManager, ChangeEditModifier editModifier) {
     this.repositoryManager = repositoryManager;
     this.editModifier = editModifier;
   }
 
   @Override
-  protected Response<Object> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input in)
+  public Response<Object> apply(ChangeResource rsrc, Input in)
       throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     Project.NameKey project = rsrc.getProject();
     try (Repository repository = repositoryManager.openRepository(project)) {
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index 679d4f8..54575bb 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -43,8 +44,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -52,10 +51,11 @@
 import java.io.IOException;
 
 @Singleton
-public class Restore extends RetryingRestModifyView<ChangeResource, RestoreInput, ChangeInfo>
-    implements UiAction<ChangeResource> {
+public class Restore
+    implements RestModifyView<ChangeResource, RestoreInput>, UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final BatchUpdate.Factory updateFactory;
   private final RestoredSender.Factory restoredSenderFactory;
   private final ChangeJson.Factory json;
   private final ChangeMessagesUtil cmUtil;
@@ -65,14 +65,14 @@
 
   @Inject
   Restore(
+      BatchUpdate.Factory updateFactory,
       RestoredSender.Factory restoredSenderFactory,
       ChangeJson.Factory json,
       ChangeMessagesUtil cmUtil,
       PatchSetUtil psUtil,
-      RetryHelper retryHelper,
       ChangeRestored changeRestored,
       ProjectCache projectCache) {
-    super(retryHelper);
+    this.updateFactory = updateFactory;
     this.restoredSenderFactory = restoredSenderFactory;
     this.json = json;
     this.cmUtil = cmUtil;
@@ -82,8 +82,7 @@
   }
 
   @Override
-  protected Response<ChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, RestoreInput input)
+  public Response<ChangeInfo> apply(ChangeResource rsrc, RestoreInput input)
       throws RestApiException, UpdateException, PermissionBackendException, IOException {
     // Not allowed to restore if the current patch set is locked.
     psUtil.checkPatchSetNotLocked(rsrc.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index dad87e5..d0b2562 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -59,8 +60,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -79,11 +78,12 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
-public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput, ChangeInfo>
-    implements UiAction<ChangeResource> {
+public class Revert
+    implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory updateFactory;
   private final GitRepositoryManager repoManager;
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeMessagesUtil cmUtil;
@@ -101,10 +101,10 @@
   @Inject
   Revert(
       PermissionBackend permissionBackend,
+      BatchUpdate.Factory updateFactory,
       GitRepositoryManager repoManager,
       ChangeInserter.Factory changeInserterFactory,
       ChangeMessagesUtil cmUtil,
-      RetryHelper retryHelper,
       Sequences seq,
       PatchSetUtil psUtil,
       RevertedSender.Factory revertedSenderFactory,
@@ -115,8 +115,8 @@
       ProjectCache projectCache,
       NotifyResolver notifyResolver,
       CommitUtil commitUtil) {
-    super(retryHelper);
     this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
     this.repoManager = repoManager;
     this.changeInserterFactory = changeInserterFactory;
     this.cmUtil = cmUtil;
@@ -133,8 +133,7 @@
   }
 
   @Override
-  public Response<ChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, RevertInput input)
+  public Response<ChangeInfo> apply(ChangeResource rsrc, RevertInput input)
       throws IOException, RestApiException, UpdateException, NoSuchChangeException,
           PermissionBackendException, NoSuchProjectException, ConfigInvalidException {
     Change change = rsrc.getChange();
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 105ffa2..7ba9b98 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -25,6 +25,8 @@
 import com.google.gerrit.extensions.common.RevertSubmissionInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -32,13 +34,14 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -46,11 +49,11 @@
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.commons.lang.RandomStringUtils;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class RevertSubmission
-    extends RetryingRestModifyView<ChangeResource, RevertInput, RevertSubmissionInfo>
-    implements UiAction<ChangeResource> {
+    implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Revert revert;
@@ -64,7 +67,6 @@
 
   @Inject
   RevertSubmission(
-      RetryHelper retryHelper,
       Revert revert,
       Provider<InternalChangeQuery> queryProvider,
       ChangeResource.Factory changeResourceFactory,
@@ -73,7 +75,6 @@
       ProjectCache projectCache,
       PatchSetUtil psUtil,
       ContributorAgreementsChecker contributorAgreements) {
-    super(retryHelper);
     this.revert = revert;
     this.queryProvider = queryProvider;
     this.changeResourceFactory = changeResourceFactory;
@@ -85,9 +86,9 @@
   }
 
   @Override
-  public Response<RevertSubmissionInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource changeResource, RevertInput input)
-      throws Exception {
+  public Response<RevertSubmissionInfo> apply(ChangeResource changeResource, RevertInput input)
+      throws RestApiException, NoSuchChangeException, IOException, UpdateException,
+          PermissionBackendException, NoSuchProjectException, ConfigInvalidException {
 
     if (!changeResource.getChange().isMerged()) {
       throw new ResourceConflictException(
@@ -123,7 +124,9 @@
   }
 
   private RevertSubmissionInfo revertSubmission(
-      List<ChangeData> changeDatas, RevertInput input, String submissionId) throws Exception {
+      List<ChangeData> changeDatas, RevertInput input, String submissionId)
+      throws RestApiException, NoSuchChangeException, IOException, UpdateException,
+          PermissionBackendException, NoSuchProjectException, ConfigInvalidException {
     List<ChangeInfo> results;
     results = new ArrayList<>();
     if (input.topic == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index 8470742..8362e95 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.ChangeResource;
@@ -31,27 +32,25 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, Input, String>
-    implements UiAction<ChangeResource> {
+public class SetReadyForReview
+    implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
+  private final BatchUpdate.Factory updateFactory;
   private final WorkInProgressOp.Factory opFactory;
 
   @Inject
-  SetReadyForReview(RetryHelper retryHelper, WorkInProgressOp.Factory opFactory) {
-    super(retryHelper);
+  SetReadyForReview(BatchUpdate.Factory updateFactory, WorkInProgressOp.Factory opFactory) {
+    this.updateFactory = updateFactory;
     this.opFactory = opFactory;
   }
 
   @Override
-  protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
+  public Response<String> apply(ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
 
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index 60884c9..fdaad9d 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.ChangeResource;
@@ -31,27 +32,25 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, Input, String>
-    implements UiAction<ChangeResource> {
+public class SetWorkInProgress
+    implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
+  private final BatchUpdate.Factory updateFactory;
   private final WorkInProgressOp.Factory opFactory;
 
   @Inject
-  SetWorkInProgress(WorkInProgressOp.Factory opFactory, RetryHelper retryHelper) {
-    super(retryHelper);
+  SetWorkInProgress(BatchUpdate.Factory updateFactory, WorkInProgressOp.Factory opFactory) {
+    this.updateFactory = updateFactory;
     this.opFactory = opFactory;
   }
 
   @Override
-  protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
+  public Response<String> apply(ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
 
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 56dee3f..287c2bc 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -19,7 +19,6 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
-import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
@@ -190,7 +189,8 @@
   @UsedAt(UsedAt.Project.GOOGLE)
   public Response<Output> mergeChange(
       RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, UpdateException, ConfigInvalidException,
+          PermissionBackendException {
     Change change = rsrc.getChange();
     if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -207,13 +207,7 @@
     try (MergeOp op = mergeOpProvider.get()) {
       Change updatedChange;
 
-      try {
-        updatedChange = op.merge(change, submitter, true, input, false);
-      } catch (Exception e) {
-        Throwables.throwIfInstanceOf(e, RestApiException.class);
-        return Response.<Output>internalServerError(e).traceId(op.getTraceId().orElse(null));
-      }
-
+      updatedChange = op.merge(change, submitter, true, input, false);
       if (updatedChange.isMerged()) {
         return Response.ok(new Output(change));
       }
@@ -471,12 +465,6 @@
       }
 
       Response<Output> response = submit.apply(new RevisionResource(rsrc, ps), input);
-      if (response instanceof Response.InternalServerError) {
-        Response.InternalServerError<?> ise = (Response.InternalServerError<?>) response;
-        return Response.<ChangeInfo>internalServerError(ise.cause())
-            .traceId(ise.traceId().orElse(null));
-      }
-
       return Response.ok(json.noOptions().format(response.value().change));
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index 86d1283..b60d78e 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -222,7 +223,8 @@
 
     @Override
     public Response<AccountInfo> apply(GroupResource resource, IdString id, Input input)
-        throws Exception {
+        throws RestApiException, NotInternalGroupException, IOException, ConfigInvalidException,
+            PermissionBackendException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = id.get();
       try {
diff --git a/java/com/google/gerrit/server/restapi/project/BanCommit.java b/java/com/google/gerrit/server/restapi/project/BanCommit.java
index 64e38b0..a20d462 100644
--- a/java/com/google/gerrit/server/restapi/project/BanCommit.java
+++ b/java/com/google/gerrit/server/restapi/project/BanCommit.java
@@ -18,14 +18,11 @@
 import com.google.gerrit.extensions.api.projects.BanCommitInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.git.BanCommitResult;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.BanCommit.BanResultInfo;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -35,19 +32,16 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
-public class BanCommit
-    extends RetryingRestModifyView<ProjectResource, BanCommitInput, BanResultInfo> {
+public class BanCommit implements RestModifyView<ProjectResource, BanCommitInput> {
   private final com.google.gerrit.server.git.BanCommit banCommit;
 
   @Inject
-  BanCommit(RetryHelper retryHelper, com.google.gerrit.server.git.BanCommit banCommit) {
-    super(retryHelper);
+  BanCommit(com.google.gerrit.server.git.BanCommit banCommit) {
     this.banCommit = banCommit;
   }
 
   @Override
-  protected Response<BanResultInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ProjectResource rsrc, BanCommitInput input)
+  public Response<BanResultInfo> apply(ProjectResource rsrc, BanCommitInput input)
       throws RestApiException, UpdateException, IOException, PermissionBackendException {
     BanResultInfo r = new BanResultInfo();
     if (input != null && input.commits != null && !input.commits.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index c036c78..56948c1 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -159,8 +160,7 @@
               }
               refPrefix = RefUtil.getRefPrefix(refPrefix);
             }
-            // fall through
-            // $FALL-THROUGH$
+            throw new LockFailureException(String.format("Failed to create %s", ref), u);
           case FORCED:
           case IO_FAILURE:
           case NOT_ATTEMPTED:
@@ -170,9 +170,7 @@
           case REJECTED_MISSING_OBJECT:
           case REJECTED_OTHER_REASON:
           default:
-            {
-              throw new IOException(result.name());
-            }
+            throw new IOException(String.format("Failed to create %s: %s", ref, result.name()));
         }
 
         BranchInfo info = new BranchInfo();
diff --git a/java/com/google/gerrit/server/restapi/project/CreateDashboard.java b/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
index 314df73..9904b1f 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
@@ -19,12 +19,15 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.DashboardResource;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import org.kohsuke.args4j.Option;
 
 @Singleton
@@ -42,7 +45,7 @@
 
   @Override
   public Response<DashboardInfo> apply(ProjectResource parent, IdString id, SetDashboardInput input)
-      throws Exception {
+      throws RestApiException, IOException, PermissionBackendException {
     parent.getProjectState().checkStatePermitsWrite();
     if (!DashboardsCollection.isDefaultDashboard(id)) {
       throw new ResourceNotFoundException(id);
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 1979d61..2395bdd 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -43,7 +44,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
@@ -58,9 +58,6 @@
 public class DeleteRef {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private static final int MAX_LOCK_FAILURE_CALLS = 10;
-  private static final long SLEEP_ON_LOCK_FAILURE_MS = 15;
-
   private final Provider<IdentifiedUser> identifiedUser;
   private final PermissionBackend permissionBackend;
   private final GitRepositoryManager repoManager;
@@ -126,23 +123,7 @@
       u.setNewObjectId(ObjectId.zeroId());
       u.setForceUpdate(true);
       refDeletionValidator.validateRefOperation(projectState.getName(), identifiedUser.get(), u);
-      int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-      for (; ; ) {
-        try {
-          result = u.delete();
-        } catch (LockFailedException e) {
-          result = RefUpdate.Result.LOCK_FAILURE;
-        }
-        if (result == RefUpdate.Result.LOCK_FAILURE && --remainingLockFailureCalls > 0) {
-          try {
-            Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
-          } catch (InterruptedException ie) {
-            // ignore
-          }
-        } else {
-          break;
-        }
-      }
+      result = u.delete();
 
       switch (result) {
         case NEW:
@@ -160,8 +141,9 @@
           logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name());
           throw new ResourceConflictException("cannot delete current branch");
 
-        case IO_FAILURE:
         case LOCK_FAILURE:
+          throw new LockFailureException(String.format("Cannot delete %s", ref), u);
+        case IO_FAILURE:
         case NOT_ATTEMPTED:
         case REJECTED:
         case RENAMED:
diff --git a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
index bac71de..8879fae 100644
--- a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
@@ -24,9 +24,11 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.DashboardResource;
 import com.google.gerrit.server.project.ProjectCache;
@@ -34,6 +36,7 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Option;
@@ -67,7 +70,7 @@
 
   @Override
   public Response<DashboardInfo> apply(DashboardResource rsrc, SetDashboardInput input)
-      throws Exception {
+      throws RestApiException, IOException, PermissionBackendException {
     if (input == null) {
       input = new SetDashboardInput(); // Delete would set input to null.
     }
diff --git a/java/com/google/gerrit/server/restapi/project/SetHead.java b/java/com/google/gerrit/server/restapi/project/SetHead.java
index 42c0559..946695e 100644
--- a/java/com/google/gerrit/server/restapi/project/SetHead.java
+++ b/java/com/google/gerrit/server/restapi/project/SetHead.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -96,9 +97,10 @@
           case FORCED:
           case NEW:
             break;
+          case LOCK_FAILURE:
+            throw new LockFailureException("Setting HEAD failed", u);
           case FAST_FORWARD:
           case IO_FAILURE:
-          case LOCK_FAILURE:
           case NOT_ATTEMPTED:
           case REJECTED:
           case REJECTED_CURRENT_BRANCH:
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 406c0a1..75ae62d 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -91,7 +91,6 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -248,7 +247,6 @@
   private Set<Project.NameKey> allProjects;
   private boolean dryrun;
   private TopicMetrics topicMetrics;
-  private String traceId;
 
   @Inject
   MergeOp(
@@ -518,9 +516,6 @@
                     retryHelper
                         .getDefaultTimeout(ActionType.CHANGE_UPDATE)
                         .multipliedBy(cs.projects().size()))
-                .caller(getClass())
-                .retryWithTrace(t -> !(t instanceof RestApiException))
-                .onAutoTrace(traceId -> this.traceId = traceId)
                 .build());
 
         if (projects > 1) {
@@ -541,10 +536,6 @@
     }
   }
 
-  public Optional<String> getTraceId() {
-    return Optional.ofNullable(traceId);
-  }
-
   private void openRepoManager() {
     if (orm != null) {
       orm.close();
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index bea3867..18e47f9 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -32,10 +32,12 @@
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
@@ -55,6 +57,7 @@
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.RefUpdate;
 
 @Singleton
 public class RetryHelper {
@@ -75,7 +78,9 @@
     CHANGE_UPDATE,
     GROUP_UPDATE,
     INDEX_QUERY,
-    PLUGIN_UPDATE
+    PLUGIN_UPDATE,
+    REST_READ_REQUEST,
+    REST_WRITE_REQUEST,
   }
 
   /**
@@ -100,7 +105,7 @@
     @Nullable
     abstract Duration timeout();
 
-    abstract Optional<Class<?>> caller();
+    abstract Optional<String> caller();
 
     abstract Optional<Predicate<Throwable>> retryWithTrace();
 
@@ -112,7 +117,7 @@
 
       public abstract Builder timeout(Duration timeout);
 
-      public abstract Builder caller(Class<?> caller);
+      public abstract Builder caller(String caller);
 
       public abstract Builder retryWithTrace(Predicate<Throwable> exceptionPredicate);
 
@@ -125,10 +130,10 @@
   @VisibleForTesting
   @Singleton
   public static class Metrics {
-    final Counter1<ActionType> attemptCounts;
+    final Counter2<ActionType, String> attemptCounts;
     final Counter1<ActionType> timeoutCount;
-    final Counter2<ActionType, String> autoRetryCount;
-    final Counter2<ActionType, String> failuresOnAutoRetryCount;
+    final Counter3<ActionType, String, String> autoRetryCount;
+    final Counter3<ActionType, String, String> failuresOnAutoRetryCount;
 
     @Inject
     Metrics(MetricMaker metricMaker) {
@@ -142,7 +147,10 @@
                           + " (0 == single attempt, no retry)")
                   .setCumulative()
                   .setUnit("attempts"),
-              actionTypeField);
+              actionTypeField,
+              Field.ofString("cause", Metadata.Builder::cause)
+                  .description("The cause for the last attempt.")
+                  .build());
       timeoutCount =
           metricMaker.newCounter(
               "action/retry_timeout_count",
@@ -160,6 +168,9 @@
               actionTypeField,
               Field.ofString("operation_name", Metadata.Builder::operationName)
                   .description("The name of the operation that was retried.")
+                  .build(),
+              Field.ofString("cause", Metadata.Builder::cause)
+                  .description("The cause for the retry.")
                   .build());
       failuresOnAutoRetryCount =
           metricMaker.newCounter(
@@ -170,6 +181,9 @@
               actionTypeField,
               Field.ofString("operation_name", Metadata.Builder::operationName)
                   .description("The name of the operation that was retried.")
+                  .build(),
+              Field.ofString("cause", Metadata.Builder::cause)
+                  .description("The cause for the retry.")
                   .build());
     }
   }
@@ -327,14 +341,15 @@
                 if (retryWithTraceOnFailure
                     && opts.retryWithTrace().isPresent()
                     && opts.retryWithTrace().get().test(t)) {
-                  String caller = opts.caller().map(Class::getSimpleName).orElse("N/A");
+                  String caller = opts.caller().orElse("N/A");
+                  String cause = formatCause(t);
                   if (!traceContext.isTracing()) {
                     String traceId = "retry-on-failure-" + new RequestId();
                     traceContext.addTag(RequestId.Type.TRACE_ID, traceId).forceLogging();
                     opts.onAutoTrace().ifPresent(c -> c.accept(traceId));
                     logger.atFine().withCause(t).log(
                         "AutoRetry: %s failed, retry with tracing enabled", caller);
-                    metrics.autoRetryCount.increment(actionType, caller);
+                    metrics.autoRetryCount.increment(actionType, caller, cause);
                     return true;
                   }
 
@@ -343,7 +358,7 @@
                   // differs from the failure that triggered the retry.
                   logger.atFine().withCause(t).log(
                       "AutoRetry: auto-retry of %s has failed", caller);
-                  metrics.failuresOnAutoRetryCount.increment(actionType, caller);
+                  metrics.failuresOnAutoRetryCount.increment(actionType, caller, cause);
                   return false;
                 }
 
@@ -354,11 +369,38 @@
     } finally {
       if (listener.getAttemptCount() > 1) {
         logger.atFine().log("%s was attempted %d times", actionType, listener.getAttemptCount());
-        metrics.attemptCounts.incrementBy(actionType, listener.getAttemptCount() - 1);
+        metrics.attemptCounts.incrementBy(
+            actionType,
+            listener.getCause().map(this::formatCause).orElse("_unknown"),
+            listener.getAttemptCount() - 1);
       }
     }
   }
 
+  private String formatCause(Throwable t) {
+    if (t instanceof UpdateException || t instanceof StorageException) {
+      t = t.getCause();
+    }
+
+    Optional<String> formattedCause = getFormattedCauseFromHooks(t);
+    if (formattedCause.isPresent()) {
+      return formattedCause.get();
+    }
+
+    if (t instanceof LockFailureException) {
+      return RefUpdate.Result.LOCK_FAILURE.name();
+    }
+    return t.getClass().getSimpleName();
+  }
+
+  private Optional<String> getFormattedCauseFromHooks(Throwable t) {
+    return exceptionHooks.stream()
+        .map(h -> h.formatCause(t))
+        .filter(Optional::isPresent)
+        .map(Optional::get)
+        .findFirst();
+  }
+
   /**
    * Executes an action and records the timeout as metric.
    *
@@ -407,18 +449,27 @@
 
   private static class MetricListener implements RetryListener {
     private long attemptCount;
+    private Optional<Throwable> cause;
 
     MetricListener() {
       attemptCount = 1;
+      cause = Optional.empty();
     }
 
     @Override
     public <V> void onRetry(Attempt<V> attempt) {
       attemptCount = attempt.getAttemptNumber();
+      if (attempt.hasException()) {
+        cause = Optional.of(attempt.getExceptionCause());
+      }
     }
 
     long getAttemptCount() {
       return attemptCount;
     }
+
+    Optional<Throwable> getCause() {
+      return cause;
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java b/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
deleted file mode 100644
index 96c2ed3..0000000
--- a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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.update;
-
-import com.google.common.base.Throwables;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
-import com.google.gerrit.extensions.restapi.RestResource;
-import java.util.concurrent.atomic.AtomicReference;
-
-public abstract class RetryingRestCollectionModifyView<
-        P extends RestResource, C extends RestResource, I, O>
-    implements RestCollectionModifyView<P, C, I> {
-  private final RetryHelper retryHelper;
-
-  protected RetryingRestCollectionModifyView(RetryHelper retryHelper) {
-    this.retryHelper = retryHelper;
-  }
-
-  @Override
-  public final Response<O> apply(P parentResource, I input)
-      throws AuthException, BadRequestException, ResourceConflictException, Exception {
-    AtomicReference<String> traceId = new AtomicReference<>(null);
-    try {
-      RetryHelper.Options retryOptions =
-          RetryHelper.options()
-              .caller(getClass())
-              .retryWithTrace(t -> !(t instanceof RestApiException))
-              .onAutoTrace(traceId::set)
-              .build();
-      return retryHelper
-          .execute((updateFactory) -> applyImpl(updateFactory, parentResource, input), retryOptions)
-          .traceId(traceId.get());
-    } catch (Exception e) {
-      Throwables.throwIfInstanceOf(e, RestApiException.class);
-      return Response.<O>internalServerError(e).traceId(traceId.get());
-    }
-  }
-
-  protected abstract Response<O> applyImpl(
-      BatchUpdate.Factory updateFactory, P parentResource, I input) throws Exception;
-}
diff --git a/java/com/google/gerrit/server/update/RetryingRestModifyView.java b/java/com/google/gerrit/server/update/RetryingRestModifyView.java
deleted file mode 100644
index 275dc55..0000000
--- a/java/com/google/gerrit/server/update/RetryingRestModifyView.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2017 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.update;
-
-import com.google.common.base.Throwables;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.RestResource;
-import java.util.concurrent.atomic.AtomicReference;
-
-public abstract class RetryingRestModifyView<R extends RestResource, I, O>
-    implements RestModifyView<R, I> {
-  private final RetryHelper retryHelper;
-
-  protected RetryingRestModifyView(RetryHelper retryHelper) {
-    this.retryHelper = retryHelper;
-  }
-
-  @Override
-  public final Response<O> apply(R resource, I input) throws RestApiException {
-    AtomicReference<String> traceId = new AtomicReference<>(null);
-    try {
-      RetryHelper.Options retryOptions =
-          RetryHelper.options()
-              .caller(getClass())
-              .retryWithTrace(t -> !(t instanceof RestApiException))
-              .onAutoTrace(traceId::set)
-              .build();
-      return retryHelper
-          .execute((updateFactory) -> applyImpl(updateFactory, resource, input), retryOptions)
-          .traceId(traceId.get());
-    } catch (Exception e) {
-      Throwables.throwIfInstanceOf(e, RestApiException.class);
-      return Response.<O>internalServerError(e).traceId(traceId.get());
-    }
-  }
-
-  protected abstract Response<O> applyImpl(BatchUpdate.Factory updateFactory, R resource, I input)
-      throws Exception;
-}
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 9f420ed..17f80c0 100644
--- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -119,7 +119,8 @@
     }
   }
 
-  private GroupResource createGroup() throws Exception {
+  private GroupResource createGroup()
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     GroupInput input = new GroupInput();
     input.description = groupDescription;
     input.visibleToAll = visibleToAll;
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index db0a481..7e0439f 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -197,7 +197,7 @@
     stdout.flush();
   }
 
-  private Collection<CacheInfo> getCaches() throws Exception {
+  private Collection<CacheInfo> getCaches() {
     @SuppressWarnings("unchecked")
     Map<String, CacheInfo> caches =
         (Map<String, CacheInfo>) listCaches.apply(new ConfigResource()).value();
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index bf2b915..d85a07b 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -445,7 +445,7 @@
       ChangeResource changeRsrc =
           changes.get().parse(TopLevelResource.INSTANCE, IdString.fromDecoded(changeId));
       RevisionResource revRsrc = revisions.parse(changeRsrc, IdString.fromDecoded(revId));
-      postReview.get().apply(batchUpdateFactory, revRsrc, input, timestamp);
+      postReview.get().apply(revRsrc, input, timestamp);
       Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
       assertThat(result).isNotEmpty();
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
index 206fedb..8ad261c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -103,7 +103,8 @@
       .robotActions {
         display: flex;
         justify-content: flex-start;
-        padding-top: 0;
+        padding-top: var(--spacing-m);
+        border-top: 1px solid var(--border-color);
       }
       .robotActions .action {
         /* Keep button text lined up with output text */