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 : }
|