LCOV - code coverage report
Current view: top level - server/update - RetryHelper.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 150 168 89.3 %
Date: 2022-11-19 15:00:39 Functions: 30 33 90.9 %

          Line data    Source code
       1             : // Copyright (C) 2017 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.update;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static java.util.concurrent.TimeUnit.MILLISECONDS;
      19             : import static java.util.concurrent.TimeUnit.SECONDS;
      20             : 
      21             : import com.github.rholder.retry.Attempt;
      22             : import com.github.rholder.retry.RetryException;
      23             : import com.github.rholder.retry.RetryListener;
      24             : import com.github.rholder.retry.Retryer;
      25             : import com.github.rholder.retry.RetryerBuilder;
      26             : import com.github.rholder.retry.StopStrategies;
      27             : import com.github.rholder.retry.WaitStrategies;
      28             : import com.github.rholder.retry.WaitStrategy;
      29             : import com.google.auto.value.AutoValue;
      30             : import com.google.common.annotations.VisibleForTesting;
      31             : import com.google.common.base.Throwables;
      32             : import com.google.common.flogger.FluentLogger;
      33             : import com.google.gerrit.common.Nullable;
      34             : import com.google.gerrit.common.UsedAt;
      35             : import com.google.gerrit.exceptions.StorageException;
      36             : import com.google.gerrit.metrics.Counter3;
      37             : import com.google.gerrit.metrics.Description;
      38             : import com.google.gerrit.metrics.Field;
      39             : import com.google.gerrit.metrics.MetricMaker;
      40             : import com.google.gerrit.server.ExceptionHook;
      41             : import com.google.gerrit.server.config.GerritServerConfig;
      42             : import com.google.gerrit.server.logging.Metadata;
      43             : import com.google.gerrit.server.logging.RequestId;
      44             : import com.google.gerrit.server.logging.TraceContext;
      45             : import com.google.gerrit.server.plugincontext.PluginSetContext;
      46             : import com.google.gerrit.server.query.account.InternalAccountQuery;
      47             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      48             : import com.google.gerrit.server.update.RetryableAction.Action;
      49             : import com.google.gerrit.server.update.RetryableAction.ActionType;
      50             : import com.google.gerrit.server.update.RetryableChangeAction.ChangeAction;
      51             : import com.google.gerrit.server.update.RetryableIndexQueryAction.IndexQueryAction;
      52             : import com.google.inject.Inject;
      53             : import com.google.inject.Provider;
      54             : import com.google.inject.Singleton;
      55             : import java.time.Duration;
      56             : import java.util.Arrays;
      57             : import java.util.HashMap;
      58             : import java.util.Map;
      59             : import java.util.Optional;
      60             : import java.util.concurrent.ExecutionException;
      61             : import java.util.function.Consumer;
      62             : import java.util.function.Predicate;
      63             : import org.eclipse.jgit.lib.Config;
      64             : 
      65             : @Singleton
      66             : public class RetryHelper {
      67         151 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      68             : 
      69             :   /**
      70             :    * Options for retrying a single operation.
      71             :    *
      72             :    * <p>This class is similar in function to upstream's {@link RetryerBuilder}, but it exists as its
      73             :    * own class in Gerrit for several reasons:
      74             :    *
      75             :    * <ul>
      76             :    *   <li>Gerrit needs to support defaults for some of the options, such as a default timeout.
      77             :    *       {@code RetryerBuilder} doesn't support calling the same setter multiple times, so doing
      78             :    *       this with {@code RetryerBuilder} directly would not be easy.
      79             :    *   <li>Gerrit explicitly does not want callers to have full control over all possible options,
      80             :    *       so this class exposes a curated subset.
      81             :    * </ul>
      82             :    */
      83             :   @AutoValue
      84         151 :   public abstract static class Options {
      85             :     @Nullable
      86             :     abstract RetryListener listener();
      87             : 
      88             :     @Nullable
      89             :     abstract Duration timeout();
      90             : 
      91             :     abstract Optional<String> actionName();
      92             : 
      93             :     abstract Optional<Predicate<Throwable>> retryWithTrace();
      94             : 
      95             :     abstract Optional<Consumer<String>> onAutoTrace();
      96             : 
      97             :     @AutoValue.Builder
      98         151 :     public abstract static class Builder {
      99             :       public abstract Builder listener(RetryListener listener);
     100             : 
     101             :       public abstract Builder timeout(Duration timeout);
     102             : 
     103             :       public abstract Builder actionName(String caller);
     104             : 
     105             :       public abstract Builder retryWithTrace(Predicate<Throwable> exceptionPredicate);
     106             : 
     107             :       public abstract Builder onAutoTrace(Consumer<String> traceIdConsumer);
     108             : 
     109             :       public abstract Options build();
     110             :     }
     111             :   }
     112             : 
     113             :   @VisibleForTesting
     114             :   @Singleton
     115             :   public static class Metrics {
     116             :     final Counter3<String, String, String> attemptCounts;
     117             :     final Counter3<String, String, String> timeoutCount;
     118             :     final Counter3<String, String, String> autoRetryCount;
     119             :     final Counter3<String, String, String> failuresOnAutoRetryCount;
     120             : 
     121             :     @Inject
     122         151 :     Metrics(MetricMaker metricMaker) {
     123         151 :       Field<String> actionTypeField =
     124         151 :           Field.ofString("action_type", Metadata.Builder::actionType)
     125         151 :               .description("The type of the action that was retried.")
     126         151 :               .build();
     127         151 :       Field<String> operationNameField =
     128         151 :           Field.ofString("operation_name", Metadata.Builder::operationName)
     129         151 :               .description("The name of the operation that was retried.")
     130         151 :               .build();
     131         151 :       Field<String> originalCauseField =
     132         151 :           Field.ofString("cause", Metadata.Builder::cause)
     133         151 :               .description("The original cause that triggered the retry.")
     134         151 :               .build();
     135         151 :       Field<String> causeField =
     136         151 :           Field.ofString("cause", Metadata.Builder::cause)
     137         151 :               .description("The cause for the retry.")
     138         151 :               .build();
     139             : 
     140         151 :       attemptCounts =
     141         151 :           metricMaker.newCounter(
     142             :               "action/retry_attempt_count",
     143             :               new Description(
     144             :                       "Number of retry attempts made by RetryHelper to execute an action"
     145             :                           + " (0 == single attempt, no retry)")
     146         151 :                   .setCumulative()
     147         151 :                   .setUnit("attempts"),
     148             :               actionTypeField,
     149             :               operationNameField,
     150             :               originalCauseField);
     151         151 :       timeoutCount =
     152         151 :           metricMaker.newCounter(
     153             :               "action/retry_timeout_count",
     154             :               new Description(
     155             :                       "Number of action executions of RetryHelper that ultimately timed out")
     156         151 :                   .setCumulative()
     157         151 :                   .setUnit("timeouts"),
     158             :               actionTypeField,
     159             :               operationNameField,
     160             :               originalCauseField);
     161         151 :       autoRetryCount =
     162         151 :           metricMaker.newCounter(
     163             :               "action/auto_retry_count",
     164             :               new Description("Number of automatic retries with tracing")
     165         151 :                   .setCumulative()
     166         151 :                   .setUnit("retries"),
     167             :               actionTypeField,
     168             :               operationNameField,
     169             :               causeField);
     170         151 :       failuresOnAutoRetryCount =
     171         151 :           metricMaker.newCounter(
     172             :               "action/failures_on_auto_retry_count",
     173             :               new Description("Number of failures on auto retry")
     174         151 :                   .setCumulative()
     175         151 :                   .setUnit("failures"),
     176             :               actionTypeField,
     177             :               operationNameField,
     178             :               causeField);
     179         151 :     }
     180             :   }
     181             : 
     182             :   public static Options.Builder options() {
     183         151 :     return new AutoValue_RetryHelper_Options.Builder();
     184             :   }
     185             : 
     186             :   private final Config cfg;
     187             :   private final Metrics metrics;
     188             :   private final BatchUpdate.Factory updateFactory;
     189             :   private final Provider<InternalAccountQuery> internalAccountQuery;
     190             :   private final Provider<InternalChangeQuery> internalChangeQuery;
     191             :   private final PluginSetContext<ExceptionHook> exceptionHooks;
     192             :   private final Duration defaultTimeout;
     193             :   private final Map<String, Duration> defaultTimeouts;
     194             :   private final WaitStrategy waitStrategy;
     195             :   @Nullable private final Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup;
     196             :   private final boolean retryWithTraceOnFailure;
     197             : 
     198             :   @Inject
     199             :   RetryHelper(
     200             :       @GerritServerConfig Config cfg,
     201             :       Metrics metrics,
     202             :       PluginSetContext<ExceptionHook> exceptionHooks,
     203             :       BatchUpdate.Factory updateFactory,
     204             :       Provider<InternalAccountQuery> internalAccountQuery,
     205             :       Provider<InternalChangeQuery> internalChangeQuery) {
     206         151 :     this(
     207             :         cfg,
     208             :         metrics,
     209             :         updateFactory,
     210             :         internalAccountQuery,
     211             :         internalChangeQuery,
     212             :         exceptionHooks,
     213             :         null);
     214         151 :   }
     215             : 
     216             :   @VisibleForTesting
     217             :   public RetryHelper(
     218             :       @GerritServerConfig Config cfg,
     219             :       Metrics metrics,
     220             :       BatchUpdate.Factory updateFactory,
     221             :       Provider<InternalAccountQuery> internalAccountQuery,
     222             :       Provider<InternalChangeQuery> internalChangeQuery,
     223             :       PluginSetContext<ExceptionHook> exceptionHooks,
     224         151 :       @Nullable Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup) {
     225         151 :     this.cfg = cfg;
     226         151 :     this.metrics = metrics;
     227         151 :     this.updateFactory = updateFactory;
     228         151 :     this.internalAccountQuery = internalAccountQuery;
     229         151 :     this.internalChangeQuery = internalChangeQuery;
     230         151 :     this.exceptionHooks = exceptionHooks;
     231         151 :     this.defaultTimeout =
     232         151 :         Duration.ofMillis(
     233         151 :             cfg.getTimeUnit("retry", null, "timeout", SECONDS.toMillis(20), MILLISECONDS));
     234         151 :     this.defaultTimeouts = new HashMap<>();
     235         151 :     Arrays.stream(ActionType.values())
     236         151 :         .forEach(
     237             :             at ->
     238         151 :                 defaultTimeouts.put(
     239         151 :                     at.name(),
     240         151 :                     Duration.ofMillis(
     241         151 :                         cfg.getTimeUnit(
     242             :                             "retry",
     243         151 :                             at.name(),
     244             :                             "timeout",
     245         151 :                             SECONDS.toMillis(defaultTimeout.getSeconds()),
     246             :                             MILLISECONDS))));
     247             : 
     248         151 :     this.waitStrategy =
     249         151 :         WaitStrategies.join(
     250         151 :             WaitStrategies.exponentialWait(
     251         151 :                 cfg.getTimeUnit("retry", null, "maxWait", SECONDS.toMillis(5), MILLISECONDS),
     252             :                 MILLISECONDS),
     253         151 :             WaitStrategies.randomWait(50, MILLISECONDS));
     254         151 :     this.overwriteDefaultRetryerStrategySetup = overwriteDefaultRetryerStrategySetup;
     255         151 :     this.retryWithTraceOnFailure = cfg.getBoolean("retry", "retryWithTraceOnFailure", false);
     256         151 :   }
     257             : 
     258             :   /**
     259             :    * Creates an action that is executed with retrying when called.
     260             :    *
     261             :    * <p>This method allows to use a custom action type. If the action type is one of {@link
     262             :    * ActionType} the usage of {@link #action(ActionType, String, Action)} is preferred.
     263             :    *
     264             :    * <p>The action type is used as metric bucket and decides which default timeout is used.
     265             :    *
     266             :    * @param actionType the type of the action, used as metric bucket
     267             :    * @param actionName the name of the action, used as metric bucket
     268             :    * @param action the action that should be executed
     269             :    * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute
     270             :    *     the action
     271             :    */
     272             :   @UsedAt(UsedAt.Project.GOOGLE)
     273             :   public <T> RetryableAction<T> action(String actionType, String actionName, Action<T> action) {
     274           0 :     return new RetryableAction<>(this, actionType, actionName, action);
     275             :   }
     276             : 
     277             :   /**
     278             :    * Creates an action that is executed with retrying when called.
     279             :    *
     280             :    * @param actionType the type of the action, used as metric bucket
     281             :    * @param actionName the name of the action, used as metric bucket
     282             :    * @param action the action that should be executed
     283             :    * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute
     284             :    *     the action
     285             :    */
     286             :   public <T> RetryableAction<T> action(ActionType actionType, String actionName, Action<T> action) {
     287         114 :     return new RetryableAction<>(this, actionType, actionName, action);
     288             :   }
     289             : 
     290             :   /**
     291             :    * Creates an action for updating an account that is executed with retrying when called.
     292             :    *
     293             :    * @param actionName the name of the action, used as metric bucket
     294             :    * @param action the action that should be executed
     295             :    * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute
     296             :    *     the action
     297             :    */
     298             :   public <T> RetryableAction<T> accountUpdate(String actionName, Action<T> action) {
     299         151 :     return new RetryableAction<>(this, ActionType.ACCOUNT_UPDATE, actionName, action);
     300             :   }
     301             : 
     302             :   /**
     303             :    * Creates an action for updating a change that is executed with retrying when called.
     304             :    *
     305             :    * @param actionName the name of the action, used as metric bucket
     306             :    * @param action the action that should be executed
     307             :    * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute
     308             :    *     the action
     309             :    */
     310             :   public <T> RetryableAction<T> changeUpdate(String actionName, Action<T> action) {
     311           1 :     return new RetryableAction<>(this, ActionType.CHANGE_UPDATE, actionName, action);
     312             :   }
     313             : 
     314             :   /**
     315             :    * Creates an action for updating a change that is executed with retrying when called.
     316             :    *
     317             :    * <p>The change action gets a {@link BatchUpdate.Factory} provided that can be used to update the
     318             :    * change.
     319             :    *
     320             :    * @param actionName the name of the action, used as metric bucket
     321             :    * @param changeAction the action that should be executed
     322             :    * @return the retryable action, callers need to call {@link RetryableChangeAction#call()} to
     323             :    *     execute the action
     324             :    */
     325             :   public <T> RetryableChangeAction<T> changeUpdate(
     326             :       String actionName, ChangeAction<T> changeAction) {
     327          70 :     return new RetryableChangeAction<>(this, updateFactory, actionName, changeAction);
     328             :   }
     329             : 
     330             :   /**
     331             :    * Creates an action for updating a group that is executed with retrying when called.
     332             :    *
     333             :    * @param actionName the name of the action, used as metric bucket
     334             :    * @param action the action that should be executed
     335             :    * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute
     336             :    *     the action
     337             :    */
     338             :   public <T> RetryableAction<T> groupUpdate(String actionName, Action<T> action) {
     339         151 :     return new RetryableAction<>(this, ActionType.GROUP_UPDATE, actionName, action);
     340             :   }
     341             : 
     342             :   /**
     343             :    * Creates an action for updating of plugin-specific data that is executed with retrying when
     344             :    * called.
     345             :    *
     346             :    * @param actionName the name of the action, used as metric bucket
     347             :    * @param action the action that should be executed
     348             :    * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute
     349             :    *     the action
     350             :    */
     351             :   public <T> RetryableAction<T> pluginUpdate(String actionName, Action<T> action) {
     352           0 :     return new RetryableAction<>(this, ActionType.PLUGIN_UPDATE, actionName, action);
     353             :   }
     354             : 
     355             :   /**
     356             :    * Creates an action for querying the account index that is executed with retrying when called.
     357             :    *
     358             :    * <p>The index query action gets a {@link InternalAccountQuery} provided that can be used to
     359             :    * query the account index.
     360             :    *
     361             :    * @param actionName the name of the action, used as metric bucket
     362             :    * @param indexQueryAction the action that should be executed
     363             :    * @return the retryable action, callers need to call {@link RetryableIndexQueryAction#call()} to
     364             :    *     execute the action
     365             :    */
     366             :   public <T> RetryableIndexQueryAction<InternalAccountQuery, T> accountIndexQuery(
     367             :       String actionName, IndexQueryAction<T, InternalAccountQuery> indexQueryAction) {
     368          36 :     return new RetryableIndexQueryAction<>(
     369             :         this, internalAccountQuery, actionName, indexQueryAction);
     370             :   }
     371             : 
     372             :   /**
     373             :    * Creates an action for querying the change index that is executed with retrying when called.
     374             :    *
     375             :    * <p>The index query action gets a {@link InternalChangeQuery} provided that can be used to query
     376             :    * the change index.
     377             :    *
     378             :    * @param actionName the name of the action, used as metric bucket
     379             :    * @param indexQueryAction the action that should be executed
     380             :    * @return the retryable action, callers need to call {@link RetryableIndexQueryAction#call()} to
     381             :    *     execute the action
     382             :    */
     383             :   public <T> RetryableIndexQueryAction<InternalChangeQuery, T> changeIndexQuery(
     384             :       String actionName, IndexQueryAction<T, InternalChangeQuery> indexQueryAction) {
     385          48 :     return new RetryableIndexQueryAction<>(this, internalChangeQuery, actionName, indexQueryAction);
     386             :   }
     387             : 
     388             :   /**
     389             :    * Returns the default timeout for an action type.
     390             :    *
     391             :    * <p>The default timeout for an action type is defined by the 'retry.<action-type>.timeout'
     392             :    * parameter in gerrit.config. If this parameter is not set the value from the 'retry.timeout'
     393             :    * parameter is used (if this is also not set we fall back to to a hard-coded timeout of 20s).
     394             :    *
     395             :    * <p>Callers can overwrite the default timeout by setting another timeout in the {@link Options},
     396             :    * see {@link Options#timeout()}.
     397             :    *
     398             :    * @param actionType the action type for which the default timeout should be retrieved
     399             :    * @return the default timeout for the given action type
     400             :    */
     401             :   Duration getDefaultTimeout(String actionType) {
     402         151 :     Duration timeout = defaultTimeouts.get(actionType);
     403         151 :     if (timeout != null) {
     404         151 :       return timeout;
     405             :     }
     406           0 :     return readDefaultTimeoutFromConfig(actionType);
     407             :   }
     408             : 
     409             :   /**
     410             :    * Thread-safe method to read and cache a default timeout from gerrit.config.
     411             :    *
     412             :    * <p>After reading the default timeout from gerrit.config it is cached in the {@link
     413             :    * #defaultTimeouts} map, so that it's read only once.
     414             :    *
     415             :    * @param actionType the action type for which the default timeout should be retrieved
     416             :    * @return the default timeout for the given action type
     417             :    */
     418             :   private synchronized Duration readDefaultTimeoutFromConfig(String actionType) {
     419           0 :     Duration timeout = defaultTimeouts.get(actionType);
     420           0 :     if (timeout != null) {
     421             :       // some other thread has read the default timeout from the config in the meantime
     422           0 :       return timeout;
     423             :     }
     424           0 :     timeout =
     425           0 :         Duration.ofMillis(
     426           0 :             cfg.getTimeUnit(
     427             :                 "retry",
     428             :                 actionType,
     429             :                 "timeout",
     430           0 :                 SECONDS.toMillis(defaultTimeout.getSeconds()),
     431             :                 MILLISECONDS));
     432           0 :     defaultTimeouts.put(actionType, timeout);
     433           0 :     return timeout;
     434             :   }
     435             : 
     436             :   /**
     437             :    * Executes an action and records the number of attempts and the timeout as metrics.
     438             :    *
     439             :    * @param actionType the type of the action
     440             :    * @param action the action which should be executed and retried on failure
     441             :    * @param opts options for retrying the action on failure
     442             :    * @param exceptionPredicate predicate to control on which exception the action should be retried
     443             :    * @return the result of executing the action
     444             :    * @throws Exception any error or exception that made the action fail, callers are expected to
     445             :    *     catch and inspect this Throwable to decide carefully whether it should be re-thrown
     446             :    */
     447             :   <T> T execute(
     448             :       String actionType, Action<T> action, Options opts, Predicate<Throwable> exceptionPredicate)
     449             :       throws Exception {
     450         151 :     MetricListener listener = new MetricListener();
     451         151 :     try (TraceContext traceContext = TraceContext.open()) {
     452         151 :       RetryerBuilder<T> retryerBuilder =
     453         151 :           createRetryerBuilder(
     454             :               actionType,
     455             :               opts,
     456             :               t -> {
     457             :                 // exceptionPredicate checks for temporary errors for which the operation should be
     458             :                 // retried (e.g. LockFailure). The retry has good chances to succeed.
     459          38 :                 if (exceptionPredicate.test(t)) {
     460           2 :                   return true;
     461             :                 }
     462             : 
     463          38 :                 String actionName = opts.actionName().orElse("N/A");
     464             : 
     465             :                 // Exception hooks may identify additional exceptions for retry.
     466          38 :                 if (exceptionHooks.stream()
     467          38 :                     .anyMatch(h -> h.shouldRetry(actionType, actionName, t))) {
     468          11 :                   return true;
     469             :                 }
     470             : 
     471             :                 // A non-recoverable failure occurred. Check if we should retry to capture a trace
     472             :                 // of the failure. If a trace was already done there is no need to retry.
     473          38 :                 if (retryWithTraceOnFailure
     474           1 :                     && opts.retryWithTrace().isPresent()
     475           1 :                     && opts.retryWithTrace().get().test(t)) {
     476             :                   // Exception hooks may identify exceptions for which retrying with trace should be
     477             :                   // skipped.
     478           1 :                   if (exceptionHooks.stream()
     479           1 :                       .anyMatch(h -> h.skipRetryWithTrace(actionType, actionName, t))) {
     480           0 :                     return false;
     481             :                   }
     482             : 
     483           1 :                   String cause = formatCause(t);
     484           1 :                   if (!TraceContext.isTracing()) {
     485           1 :                     String traceId = "retry-on-failure-" + new RequestId();
     486           1 :                     traceContext.addTag(RequestId.Type.TRACE_ID, traceId).forceLogging();
     487           1 :                     logger.atWarning().withCause(t).log(
     488             :                         "AutoRetry: %s failed, retry with tracing enabled (cause = %s)",
     489             :                         actionName, cause);
     490           1 :                     opts.onAutoTrace().ifPresent(c -> c.accept(traceId));
     491           1 :                     metrics.autoRetryCount.increment(actionType, actionName, cause);
     492           1 :                     return true;
     493             :                   }
     494             : 
     495             :                   // A non-recoverable failure occurred. We retried the operation with tracing
     496             :                   // enabled and it failed again. Log the failure so that admin can see if it
     497             :                   // differs from the failure that triggered the retry.
     498           0 :                   logger.atWarning().withCause(t).log(
     499             :                       "AutoRetry: auto-retry of %s has failed (cause = %s)", actionName, cause);
     500           0 :                   metrics.failuresOnAutoRetryCount.increment(actionType, actionName, cause);
     501           0 :                   return false;
     502             :                 }
     503             : 
     504          38 :                 return false;
     505             :               });
     506         151 :       retryerBuilder.withRetryListener(listener);
     507         151 :       return executeWithTimeoutCount(actionType, action, opts, retryerBuilder.build(), listener);
     508             :     } finally {
     509         151 :       if (listener.getAttemptCount() > 1) {
     510          12 :         logger.atWarning().log("%s was attempted %d times", actionType, listener.getAttemptCount());
     511          12 :         metrics.attemptCounts.incrementBy(
     512             :             actionType,
     513          12 :             opts.actionName().orElse("N/A"),
     514          12 :             listener.getOriginalCause().map(this::formatCause).orElse("_unknown"),
     515          12 :             listener.getAttemptCount() - 1);
     516             :       }
     517             :     }
     518             :   }
     519             : 
     520             :   public String formatCause(Throwable t) {
     521          30 :     while ((t instanceof UpdateException
     522             :             || t instanceof StorageException
     523             :             || t instanceof ExecutionException)
     524          10 :         && t.getCause() != null) {
     525          10 :       t = t.getCause();
     526             :     }
     527             : 
     528          30 :     Optional<String> formattedCause = getFormattedCauseFromHooks(t);
     529          30 :     if (formattedCause.isPresent()) {
     530          12 :       return formattedCause.get();
     531             :     }
     532             : 
     533          20 :     return t.getClass().getSimpleName();
     534             :   }
     535             : 
     536             :   private Optional<String> getFormattedCauseFromHooks(Throwable t) {
     537          30 :     return exceptionHooks.stream()
     538          30 :         .map(h -> h.formatCause(t))
     539          30 :         .filter(Optional::isPresent)
     540          30 :         .map(Optional::get)
     541          30 :         .findFirst();
     542             :   }
     543             : 
     544             :   /**
     545             :    * Executes an action and records the timeout as metric.
     546             :    *
     547             :    * @param actionType the type of the action
     548             :    * @param action the action which should be executed and retried on failure
     549             :    * @param opts options for retrying the action on failure
     550             :    * @param retryer the retryer
     551             :    * @param listener metric listener
     552             :    * @return the result of executing the action
     553             :    * @throws Exception any exception that made the action fail, callers are expected to catch and
     554             :    *     inspect this exception to decide carefully whether it should be re-thrown
     555             :    */
     556             :   private <T> T executeWithTimeoutCount(
     557             :       String actionType,
     558             :       Action<T> action,
     559             :       Options opts,
     560             :       Retryer<T> retryer,
     561             :       MetricListener listener)
     562             :       throws Exception {
     563             :     try {
     564         151 :       return retryer.call(action::call);
     565          38 :     } catch (ExecutionException | RetryException e) {
     566          38 :       if (e instanceof RetryException) {
     567           3 :         metrics.timeoutCount.increment(
     568             :             actionType,
     569           3 :             opts.actionName().orElse("N/A"),
     570           3 :             listener.getOriginalCause().map(this::formatCause).orElse("_unknown"));
     571             :       }
     572          38 :       if (e.getCause() != null) {
     573          36 :         Throwables.throwIfUnchecked(e.getCause());
     574           0 :         Throwables.throwIfInstanceOf(e.getCause(), Exception.class);
     575             :       }
     576           0 :       throw e;
     577             :     }
     578             :   }
     579             : 
     580             :   private <O> RetryerBuilder<O> createRetryerBuilder(
     581             :       String actionType, Options opts, Predicate<Throwable> exceptionPredicate) {
     582             :     RetryerBuilder<O> retryerBuilder =
     583         151 :         RetryerBuilder.<O>newBuilder().retryIfException(exceptionPredicate::test);
     584         151 :     if (opts.listener() != null) {
     585          53 :       retryerBuilder.withRetryListener(opts.listener());
     586             :     }
     587             : 
     588         151 :     if (overwriteDefaultRetryerStrategySetup != null) {
     589           1 :       overwriteDefaultRetryerStrategySetup.accept(retryerBuilder);
     590           1 :       return retryerBuilder;
     591             :     }
     592             : 
     593         151 :     return retryerBuilder
     594         151 :         .withStopStrategy(
     595         151 :             StopStrategies.stopAfterDelay(
     596         151 :                 firstNonNull(opts.timeout(), getDefaultTimeout(actionType)).toMillis(),
     597             :                 MILLISECONDS))
     598         151 :         .withWaitStrategy(waitStrategy);
     599             :   }
     600             : 
     601             :   private static class MetricListener implements RetryListener {
     602             :     private long attemptCount;
     603             :     private Optional<Throwable> originalCause;
     604             : 
     605         151 :     MetricListener() {
     606         151 :       attemptCount = 1;
     607         151 :       originalCause = Optional.empty();
     608         151 :     }
     609             : 
     610             :     @Override
     611             :     public <V> void onRetry(Attempt<V> attempt) {
     612         151 :       attemptCount = attempt.getAttemptNumber();
     613         151 :       if (attemptCount == 1 && attempt.hasException()) {
     614          38 :         originalCause = Optional.of(attempt.getExceptionCause());
     615             :       }
     616         151 :     }
     617             : 
     618             :     long getAttemptCount() {
     619         151 :       return attemptCount;
     620             :     }
     621             : 
     622             :     Optional<Throwable> getOriginalCause() {
     623          12 :       return originalCause;
     624             :     }
     625             :   }
     626             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750