// 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.github.rholder.retry.RetryException;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Throwables;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.git.LockFailureException;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

@Singleton
public class RetryHelper {
  public interface Action<T> {
    T call(BatchUpdate.Factory updateFactory) throws Exception;
  }

  private final BatchUpdate.Factory updateFactory;

  @Inject
  RetryHelper(
      NotesMigration migration,
      ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
      FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory,
      UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory) {
    this.updateFactory =
        new BatchUpdate.Factory(
            migration,
            reviewDbBatchUpdateFactory,
            fusedNoteDbBatchUpdateFactory,
            unfusedNoteDbBatchUpdateFactory);
  }

  public <T> T execute(Action<T> action) throws RestApiException, UpdateException {
    try {
      // TODO(dborowitz): Make configurable.
      return RetryerBuilder.<T>newBuilder()
          .withStopStrategy(StopStrategies.stopAfterDelay(20, TimeUnit.SECONDS))
          .withWaitStrategy(
              WaitStrategies.join(
                  WaitStrategies.exponentialWait(5, TimeUnit.SECONDS),
                  WaitStrategies.randomWait(50, TimeUnit.MILLISECONDS)))
          .retryIfException(RetryHelper::isLockFailure)
          .build()
          .call(() -> action.call(updateFactory));
    } catch (ExecutionException | RetryException e) {
      if (e.getCause() != null) {
        Throwables.throwIfInstanceOf(e.getCause(), UpdateException.class);
        Throwables.throwIfInstanceOf(e.getCause(), RestApiException.class);
      }
      throw new UpdateException(e);
    }
  }

  private static boolean isLockFailure(Throwable t) {
    if (t instanceof UpdateException) {
      t = t.getCause();
    }
    return t instanceof LockFailureException;
  }
}

@@ -1343,24 +1343,66 @@
 ption;%0A  %7D%0A%0A
+  private final NotesMigration migration;%0A
   private fi
@@ -1440,16 +1440,16 @@
 actory;%0A
-
 %0A  @Inje
@@ -1722,24 +1722,56 @@
 eFactory) %7B%0A
+    this.migration = migration;%0A
     this.upd
@@ -2067,16 +2067,155 @@
   try %7B%0A
+      RetryerBuilder%3CT%3E builder = RetryerBuilder.newBuilder();%0A      if (migration.disableChangeReviewDb() && migration.fuseUpdates()) %7B%0A  
       //
@@ -2261,45 +2261,17 @@
     
-return RetryerBuilder.%3CT%3EnewB
+  b
 uilder
-()
 %0A   
@@ -2273,24 +2273,26 @@
 r%0A          
+  
 .withStopStr
@@ -2356,24 +2356,26 @@
 )%0A          
+  
 .withWaitStr
@@ -2381,16 +2381,18 @@
 rategy(%0A
+  
         
@@ -2418,16 +2418,18 @@
 s.join(%0A
+  
         
@@ -2501,32 +2501,34 @@
                 
+  
 WaitStrategies.r
@@ -2576,16 +2576,18 @@
         
+  
 .retryIf
@@ -2627,38 +2627,342 @@
 ure)
+;
 %0A      
-    .build()
+%7D else %7B%0A        // Either we aren't full-NoteDb, or the underlying ref storage doesn't support atomic%0A        // transactions. Either way, retrying a partially-failed operation is not idempotent, so%0A        // don't do it automatically. Let the end user decide whether they want to retry.
 %0A    
+  %7D%0A
       
+return builder.build()
 .cal

Disable retrying when updates are non-idempotent