|
| 1 | +//----------------------------------------------------------------------- |
| 2 | +// <copyright file="RetrySupport.cs" company="Akka.NET Project"> |
| 3 | +// Copyright (C) 2009-2020 Lightbend Inc. <http://www.lightbend.com> |
| 4 | +// Copyright (C) 2013-2020 .NET Foundation <https://github.com/akkadotnet/akka.net> |
| 5 | +// </copyright> |
| 6 | +//----------------------------------------------------------------------- |
| 7 | + |
| 8 | +using System; |
| 9 | +using System.Threading.Tasks; |
| 10 | +using Akka.Actor; |
| 11 | +using Akka.Util; |
| 12 | +using static Akka.Pattern.FutureTimeoutSupport; |
| 13 | + |
| 14 | +namespace Akka.Pattern |
| 15 | +{ |
| 16 | + /// <summary> |
| 17 | + /// This class provides the retry utility functions. |
| 18 | + /// </summary> |
| 19 | + public static class RetrySupport |
| 20 | + { |
| 21 | + /// <summary> |
| 22 | + /// <para> |
| 23 | + /// Given a function, returns an internally retrying Task. |
| 24 | + /// The first attempt will be made immediately, each subsequent attempt will be made immediately |
| 25 | + /// if the previous attempt failed. |
| 26 | + /// </para> |
| 27 | + /// If attempts are exhausted the returned Task is simply the result of invoking attempt. |
| 28 | + /// </summary> |
| 29 | + /// <param name="attempt">TBD</param> |
| 30 | + /// <param name="attempts">TBD</param> |
| 31 | + public static Task<T> Retry<T>(Func<Task<T>> attempt, int attempts) => |
| 32 | + Retry(attempt, attempts, attempted: 0); |
| 33 | + |
| 34 | + /// <summary> |
| 35 | + /// <para> |
| 36 | + /// Given a function, returns an internally retrying Task. |
| 37 | + /// The first attempt will be made immediately, each subsequent attempt will be made with a backoff time, |
| 38 | + /// if the previous attempt failed. |
| 39 | + /// </para> |
| 40 | + /// If attempts are exhausted the returned Task is simply the result of invoking attempt. |
| 41 | + /// </summary> |
| 42 | + /// <param name="attempt">TBD</param> |
| 43 | + /// <param name="attempts">TBD</param> |
| 44 | + /// <param name="minBackoff">minimum (initial) duration until the child actor will started again, if it is terminated.</param> |
| 45 | + /// <param name="maxBackoff">the exponential back-off is capped to this duration.</param> |
| 46 | + /// <param name="randomFactor">after calculation of the exponential back-off an additional random delay based on this factor is added, e.g. `0.2` adds up to `20%` delay. In order to skip this additional delay pass in `0`.</param> |
| 47 | + /// <param name="scheduler">The scheduler instance to use.</param> |
| 48 | + public static Task<T> Retry<T>(Func<Task<T>> attempt, int attempts, TimeSpan minBackoff, TimeSpan maxBackoff, int randomFactor, IScheduler scheduler) |
| 49 | + { |
| 50 | + if (attempt == null) throw new ArgumentNullException("Parameter attempt should not be null."); |
| 51 | + if (minBackoff <= TimeSpan.Zero) throw new ArgumentException("Parameter minBackoff must be > 0"); |
| 52 | + if (maxBackoff < minBackoff) throw new ArgumentException("Parameter maxBackoff must be >= minBackoff"); |
| 53 | + if (randomFactor < 0.0 || randomFactor > 1.0) throw new ArgumentException("RandomFactor must be between 0.0 and 1.0"); |
| 54 | + |
| 55 | + return Retry(attempt, attempts, attempted => BackoffSupervisor.CalculateDelay(attempted, minBackoff, maxBackoff, randomFactor), scheduler); |
| 56 | + } |
| 57 | + |
| 58 | + /// <summary> |
| 59 | + /// <para> |
| 60 | + /// Given a function, returns an internally retrying Task. |
| 61 | + /// The first attempt will be made immediately, each subsequent attempt will be made after 'delay'. |
| 62 | + /// A scheduler (eg Context.System.Scheduler) must be provided to delay each retry. |
| 63 | + /// </para> |
| 64 | + /// If attempts are exhausted the returned future is simply the result of invoking attempt. |
| 65 | + /// </summary> |
| 66 | + /// <param name="attempt">TBD</param> |
| 67 | + /// <param name="attempts">TBD</param> |
| 68 | + /// <param name="delay">TBD</param> |
| 69 | + /// <param name="scheduler">The scheduler instance to use.</param> |
| 70 | + public static Task<T> Retry<T>(Func<Task<T>> attempt, int attempts, TimeSpan delay, IScheduler scheduler) => |
| 71 | + Retry(attempt, attempts, _ => delay, scheduler); |
| 72 | + |
| 73 | + /// <summary> |
| 74 | + /// <para> |
| 75 | + /// Given a function, returns an internally retrying Task. |
| 76 | + /// The first attempt will be made immediately, each subsequent attempt will be made after |
| 77 | + /// the 'delay' return by `delayFunction`(the input next attempt count start from 1). |
| 78 | + /// Returns <see cref="Option{TimeSpan}.None"/> for no delay. |
| 79 | + /// A scheduler (eg Context.System.Scheduler) must be provided to delay each retry. |
| 80 | + /// You could provide a function to generate the next delay duration after first attempt, |
| 81 | + /// this function should never return `null`, otherwise an <see cref="InvalidOperationException"/> will be through. |
| 82 | + /// </para> |
| 83 | + /// If attempts are exhausted the returned Task is simply the result of invoking attempt. |
| 84 | + /// </summary> |
| 85 | + /// <param name="attempt">TBD</param> |
| 86 | + /// <param name="attempts">TBD</param> |
| 87 | + /// <param name="delayFunction">TBD</param> |
| 88 | + /// <param name="scheduler">The scheduler instance to use.</param> |
| 89 | + public static Task<T> Retry<T>(Func<Task<T>> attempt, int attempts, Func<int, Option<TimeSpan>> delayFunction, IScheduler scheduler) => |
| 90 | + Retry(attempt, attempts, delayFunction, attempted: 0, scheduler); |
| 91 | + |
| 92 | + private static Task<T> Retry<T>(Func<Task<T>> attempt, int maxAttempts, int attempted) => |
| 93 | + Retry(attempt, maxAttempts, _ => Option<TimeSpan>.None, attempted); |
| 94 | + |
| 95 | + private static Task<T> Retry<T>(Func<Task<T>> attempt, int maxAttempts, Func<int, Option<TimeSpan>> delayFunction, int attempted, IScheduler scheduler = null) |
| 96 | + { |
| 97 | + Task<T> tryAttempt() |
| 98 | + { |
| 99 | + try |
| 100 | + { |
| 101 | + return attempt(); |
| 102 | + } |
| 103 | + catch (Exception ex) |
| 104 | + { |
| 105 | + return Task.FromException<T>(ex); // in case the `attempt` function throws |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + if (maxAttempts < 0) throw new ArgumentException("Parameter maxAttempts must >= 0."); |
| 110 | + if (attempt == null) throw new ArgumentNullException(nameof(attempt), "Parameter attempt should not be null."); |
| 111 | + |
| 112 | + if (maxAttempts - attempted > 0) |
| 113 | + { |
| 114 | + return tryAttempt().ContinueWith(t => |
| 115 | + { |
| 116 | + if (t.IsFaulted) |
| 117 | + { |
| 118 | + var nextAttempt = attempted + 1; |
| 119 | + switch (delayFunction(nextAttempt)) |
| 120 | + { |
| 121 | + case Option<TimeSpan> delay when delay.HasValue: |
| 122 | + return delay.Value.Ticks < 1 |
| 123 | + ? Retry(attempt, maxAttempts, delayFunction, nextAttempt, scheduler) |
| 124 | + : After(delay.Value, scheduler, () => Retry(attempt, maxAttempts, delayFunction, nextAttempt, scheduler)); |
| 125 | + case Option<TimeSpan> _: |
| 126 | + return Retry(attempt, maxAttempts, delayFunction, nextAttempt, scheduler); |
| 127 | + default: |
| 128 | + throw new InvalidOperationException("The delayFunction of Retry should not return null."); |
| 129 | + } |
| 130 | + } |
| 131 | + return t; |
| 132 | + }).Unwrap(); |
| 133 | + } |
| 134 | + |
| 135 | + return tryAttempt(); |
| 136 | + } |
| 137 | + } |
| 138 | +} |
0 commit comments