|
11 | 11 | #include <cstdint> |
12 | 12 | #include <random> |
13 | 13 |
|
| 14 | +#include "astarte_device_sdk/exceptions.hpp" |
| 15 | + |
14 | 16 | namespace AstarteDeviceSdk { |
15 | 17 |
|
16 | 18 | class ExponentialBackoff { |
17 | 19 | public: |
18 | 20 | /** |
19 | 21 | * @brief Construct an ExponentialBackoff instance. |
20 | | - * @param initial_delay The value for the first backoff delay. |
21 | | - * @param max_delay The upper bound for all the backoff delays. |
| 22 | + * |
| 23 | + * @details The exponential backoff will will compute an exponential delay using |
| 24 | + * 2 as the base for the power operation and @p mul_coeff as the multiplier coefficient. |
| 25 | + * The values returned by calls to getNextDelay will follow the formula: |
| 26 | + * min( @p mul_coeff * 2 ^ ( number of calls ) , @p cutoff_coeff ) + random jitter |
| 27 | + * The random jitter will be in the range [ - @p mul_coeff , + @p mul_coeff ] |
| 28 | + * |
| 29 | + * @note The jitter will be applied also once the @p cutoff_coeff has been reached. Effectively |
| 30 | + * the maximum delay produced will be @p cutoff_coeff + @p mul_coeff. |
| 31 | + * |
| 32 | + * @param mul_coeff Multiplier coefficient used in the exponential delay calculation. |
| 33 | + * @param cutoff_coeff The cut-off coefficient, an upper bound for the exponential curve. |
22 | 34 | */ |
23 | | - ExponentialBackoff(std::chrono::milliseconds initial_delay, std::chrono::milliseconds max_delay) |
24 | | - : initial_delay_(initial_delay), max_delay_(max_delay) {} |
| 35 | + ExponentialBackoff(std::chrono::milliseconds mul_coeff, std::chrono::milliseconds cutoff_coeff) |
| 36 | + : mul_coeff_(mul_coeff), cutoff_coeff_(cutoff_coeff) { |
| 37 | + if ((mul_coeff <= std::chrono::milliseconds::zero()) || |
| 38 | + (cutoff_coeff <= std::chrono::milliseconds::zero())) { |
| 39 | + throw AstarteInvalidInputException("Received zero or negative coefficients."); |
| 40 | + } |
| 41 | + if (cutoff_coeff < mul_coeff) { |
| 42 | + throw AstarteInvalidInputException( |
| 43 | + "The multiplier coefficient is larger than the cuttoff coefficient"); |
| 44 | + } |
| 45 | + } |
25 | 46 |
|
26 | 47 | /** |
27 | 48 | * @brief Calculate and returns the next backoff delay. |
28 | | - * @details Computes the appropriate delay for the current backoff generation and increments the |
29 | | - * internal generation counter for the next call. |
| 49 | + * @details See the documentation of the constructor of this class for an explanation on how |
| 50 | + * this delay is computed. |
30 | 51 | * @return The calculated delay duration. |
31 | 52 | */ |
32 | 53 | auto getNextDelay() -> std::chrono::milliseconds { |
33 | | - constexpr double BACKOFF_FACTOR = 2.0; |
| 54 | + const ChronoMillisRep mul_coeff = mul_coeff_.count(); |
| 55 | + const ChronoMillisRep max_milliseconds = std::chrono::milliseconds::max().count(); |
| 56 | + const ChronoMillisRep max_allowed_final_delay = max_milliseconds - mul_coeff; |
34 | 57 |
|
35 | | - const auto initial_delay_ms = static_cast<double>(initial_delay_.count()); |
| 58 | + // Update last delay value with the new value |
| 59 | + ChronoMillisRep delay = 0; |
| 60 | + if (prev_delay_ == 0) { |
| 61 | + delay = mul_coeff; |
| 62 | + } else if (prev_delay_ <= (max_allowed_final_delay / 2)) { |
| 63 | + delay = 2 * prev_delay_; |
| 64 | + } else { |
| 65 | + delay = max_allowed_final_delay; |
| 66 | + } |
36 | 67 |
|
37 | | - const auto delay_ms = initial_delay_ms * std::pow(BACKOFF_FACTOR, generated_delays_); |
38 | | - // Apply a positive jitter (a random value between 0 and initial_delay_) |
39 | | - const auto jitter = dist_(gen_) * initial_delay_ms; |
| 68 | + // Bound the delay to the maximum |
| 69 | + ChronoMillisRep bounded_delay = std::min(delay, cutoff_coeff_.count()); |
40 | 70 |
|
41 | | - const auto total_delay_ms = static_cast<int64_t>(delay_ms + jitter); |
42 | | - const auto jittery_delay = std::chrono::milliseconds(total_delay_ms); |
| 71 | + // Store the new delay before jitter application |
| 72 | + prev_delay_ = bounded_delay; |
43 | 73 |
|
44 | | - generated_delays_++; |
| 74 | + // Insert some jitter |
| 75 | + ChronoMillisRep jitter_minimum = -mul_coeff; |
| 76 | + if (bounded_delay - mul_coeff < 0) { |
| 77 | + jitter_minimum = 0; |
| 78 | + } |
| 79 | + ChronoMillisRep jitter_maximum = mul_coeff; |
| 80 | + if (bounded_delay > max_milliseconds - mul_coeff) { |
| 81 | + jitter_maximum = max_milliseconds - bounded_delay; |
| 82 | + } |
| 83 | + std::uniform_int_distribution<ChronoMillisRep> dist(jitter_minimum, jitter_maximum); |
| 84 | + ChronoMillisRep jittered_delay = bounded_delay + dist(gen_); |
45 | 85 |
|
46 | | - return std::min(jittery_delay, max_delay_); |
| 86 | + // Convert to a chrono object |
| 87 | + return std::chrono::milliseconds(jittered_delay); |
47 | 88 | } |
48 | 89 |
|
49 | 90 | /** @brief Reset the backoff generator. */ |
50 | | - void reset() { generated_delays_ = 0; } |
| 91 | + void reset() { prev_delay_ = 0; } |
51 | 92 |
|
52 | 93 | private: |
53 | | - std::chrono::milliseconds initial_delay_; |
54 | | - std::chrono::milliseconds max_delay_; |
55 | | - int generated_delays_{0}; |
| 94 | + using ChronoMillisRep = std::chrono::milliseconds::rep; |
| 95 | + |
| 96 | + std::chrono::milliseconds mul_coeff_; |
| 97 | + std::chrono::milliseconds cutoff_coeff_; |
56 | 98 | std::random_device rd_; |
57 | 99 | std::mt19937 gen_{rd_()}; |
58 | | - std::uniform_real_distribution<> dist_{0.0, 1.0}; |
| 100 | + ChronoMillisRep prev_delay_{0}; |
59 | 101 | }; |
60 | 102 |
|
61 | 103 | } // namespace AstarteDeviceSdk |
|
0 commit comments