1+ /* *********************************************************************
2+ * This Original Work is copyright of 51 Degrees Mobile Experts Limited.
3+ * Copyright 2023 51 Degrees Mobile Experts Limited, Davidson House,
4+ * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
5+ *
6+ * This Original Work is licensed under the European Union Public Licence
7+ * (EUPL) v.1.2 and is subject to its terms as set out below.
8+ *
9+ * If a copy of the EUPL was not distributed with this file, You can obtain
10+ * one at https://opensource.org/licenses/EUPL-1.2.
11+ *
12+ * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
13+ * amended by the European Commission) shall be deemed incompatible for
14+ * the purposes of the Work and the provisions of the compatibility
15+ * clause in Article 5 of the EUPL shall not apply.
16+ *
17+ * If using the Work as, or as part of, a network application, by
18+ * including the attribution notice(s) required under Article 5 of the EUPL
19+ * in the end user terms of the application under an appropriate heading,
20+ * such notice(s) shall fulfill the requirements of that article.
21+ * ********************************************************************* */
22+
23+ using FiftyOne . Pipeline . Core . FailHandling . ExceptionCaching ;
24+ using System ;
25+
26+ namespace FiftyOne . Pipeline . Core . FailHandling . Recovery
27+ {
28+ /// <summary>
29+ /// Implements exponential backoff where delay doubles after each consecutive failure.
30+ /// First failure: wait initialDelay seconds
31+ /// Second failure: wait initialDelay * multiplier seconds
32+ /// Third failure: wait initialDelay * multiplier^2 seconds
33+ /// And so on, up to maxDelaySeconds.
34+ /// </summary>
35+ public class ExponentialBackoffRecoveryStrategy : IRecoveryStrategy
36+ {
37+ /// <summary>
38+ /// Initial delay in seconds for the first failure.
39+ /// </summary>
40+ public readonly double InitialDelaySeconds ;
41+
42+ /// <summary>
43+ /// Maximum delay in seconds to cap the exponential growth.
44+ /// </summary>
45+ public readonly double MaxDelaySeconds ;
46+
47+ /// <summary>
48+ /// Multiplier for exponential backoff (typically 2.0 for doubling).
49+ /// </summary>
50+ public readonly double Multiplier ;
51+
52+ /// <summary>
53+ /// Current delay in seconds based on consecutive failures.
54+ /// </summary>
55+ public double CurrentDelaySeconds { get ; private set ; }
56+
57+ private CachedException _exception = null ;
58+ private DateTime _recoveryDateTime = DateTime . MinValue ;
59+ private int _consecutiveFailures = 0 ;
60+ private readonly object _lock = new object ( ) ;
61+
62+ /// <summary>
63+ /// Constructor with default exponential backoff parameters.
64+ /// </summary>
65+ /// <param name="initialDelaySeconds">
66+ /// Initial delay in seconds (default: 2.0).
67+ /// </param>
68+ /// <param name="maxDelaySeconds">
69+ /// Maximum delay in seconds to cap growth (default: 300.0).
70+ /// </param>
71+ /// <param name="multiplier">
72+ /// Exponential multiplier (default: 2.0 for doubling).
73+ /// </param>
74+ public ExponentialBackoffRecoveryStrategy (
75+ double initialDelaySeconds = 2.0 ,
76+ double maxDelaySeconds = 300.0 ,
77+ double multiplier = 2.0 )
78+ {
79+ if ( initialDelaySeconds <= 0 )
80+ throw new ArgumentException ( "Initial delay must be positive" , nameof ( initialDelaySeconds ) ) ;
81+ if ( maxDelaySeconds <= 0 )
82+ throw new ArgumentException ( "Max delay must be positive" , nameof ( maxDelaySeconds ) ) ;
83+ if ( multiplier <= 1.0 )
84+ throw new ArgumentException ( "Multiplier must be greater than 1.0" , nameof ( multiplier ) ) ;
85+
86+ InitialDelaySeconds = initialDelaySeconds ;
87+ MaxDelaySeconds = maxDelaySeconds ;
88+ Multiplier = multiplier ;
89+ CurrentDelaySeconds = initialDelaySeconds ;
90+ }
91+
92+ /// <summary>
93+ /// Called when querying the server failed.
94+ /// Calculates the next delay using exponential backoff.
95+ /// </summary>
96+ /// <param name="cachedException">
97+ /// Timestamped exception.
98+ /// </param>
99+ public void RecordFailure ( CachedException cachedException )
100+ {
101+ lock ( _lock )
102+ {
103+ _consecutiveFailures ++ ;
104+
105+ // Calculate new delay: initialDelay * multiplier^(failures-1)
106+ // For failures=1: initialDelay * multiplier^0 = initialDelay
107+ // For failures=2: initialDelay * multiplier^1 = initialDelay * multiplier
108+ // For failures=3: initialDelay * multiplier^2, etc.
109+ CurrentDelaySeconds = Math . Min (
110+ InitialDelaySeconds * Math . Pow ( Multiplier , _consecutiveFailures - 1 ) ,
111+ MaxDelaySeconds ) ;
112+
113+ var newRecoveryTime = cachedException . DateTime . AddSeconds ( CurrentDelaySeconds ) ;
114+
115+ _exception = cachedException ;
116+ _recoveryDateTime = newRecoveryTime ;
117+ }
118+ }
119+
120+ /// <summary>
121+ /// Whether the new request may be sent already.
122+ /// </summary>
123+ /// <param name="cachedException">
124+ /// Timestamped exception that prevents new requests.
125+ /// </param>
126+ /// <returns>true -- send, false -- skip</returns>
127+ public bool MayTryNow ( out CachedException cachedException )
128+ {
129+ DateTime recoveryDateTime ;
130+ CachedException lastCachedException ;
131+
132+ lock ( _lock )
133+ {
134+ recoveryDateTime = _recoveryDateTime ;
135+ lastCachedException = _exception ;
136+ }
137+
138+ if ( recoveryDateTime < DateTime . Now )
139+ {
140+ cachedException = null ;
141+ return true ;
142+ }
143+ else
144+ {
145+ cachedException = lastCachedException ;
146+ return false ;
147+ }
148+ }
149+
150+ /// <summary>
151+ /// Called once the request succeeds (after recovery).
152+ /// Resets consecutive failures and delay back to initial value.
153+ /// </summary>
154+ public void Reset ( )
155+ {
156+ lock ( _lock )
157+ {
158+ _consecutiveFailures = 0 ;
159+ CurrentDelaySeconds = InitialDelaySeconds ;
160+ _exception = null ;
161+ _recoveryDateTime = DateTime . MinValue ;
162+ }
163+ }
164+ }
165+ }
0 commit comments