File Coverage

blib/lib/Algorithm/Backoff.pm
Criterion Covered Total %
statement 57 57 100.0
branch 27 30 90.0
condition 27 35 77.1
subroutine 10 10 100.0
pod 3 3 100.0
total 124 135 91.8


line stmt bran cond sub pod time code
1             package Algorithm::Backoff;
2              
3 16     16   383582 use 5.010001;
  16         89  
4 16     16   109 use strict 'subs', 'vars';
  16         30  
  16         657  
5 16     16   78 use warnings;
  16         42  
  16         983  
6              
7 16     16   4800 use Time::HiRes qw(time);
  16         11738  
  16         166  
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2024-02-24'; # DATE
11             our $DIST = 'Algorithm-Backoff'; # DIST
12             our $VERSION = '0.010'; # VERSION
13              
14             our %SPEC;
15              
16             our %attr_consider_actual_delay = (
17             consider_actual_delay => {
18             summary => 'Whether to consider actual delay',
19             schema => ['bool*'],
20             default => 0,
21             tags => ['common'],
22             description => <<'_',
23              
24             If set to true, will take into account the actual delay (timestamp difference).
25             For example, when using the Constant strategy of delay=2, you log failure()
26             again right after the previous failure() (i.e. specify the same timestamp).
27             failure() will then return ~2+2 = 4 seconds. On the other hand, if you waited 2
28             seconds before calling failure() again (i.e. specify the timestamp that is 2
29             seconds larger than the previous timestamp), failure() will return 2 seconds.
30             And if you waited 4 seconds or more, failure() will return 0.
31              
32             _
33             },
34             );
35              
36             our %attr_max_actual_duration = (
37             max_actual_duration => {
38             summary => 'Maximum number of seconds for all of the attempts (0 means unlimited)',
39             schema => ['ufloat*'],
40             default => 0,
41             tags => ['common'],
42             description => <<'_',
43              
44             If set to a positive number, will limit the number of seconds for all of the
45             attempts. This setting is used to limit the amount of time you are willing to
46             spend on a task. For example, when using the Exponential strategy of
47             initial_delay=3 and max_attempts=10, the delays will be 3, 6, 12, 24, ... If
48             failures are logged according to the suggested delays, and max_actual_duration
49             is set to 21 seconds, then the third failure() will return -1 instead of 24
50             because 3+6+12 >= 21, even though max_attempts has not been exceeded.
51              
52             _
53             },
54             );
55              
56             our %attr_max_attempts = (
57             max_attempts => {
58             summary => 'Maximum number consecutive failures before giving up',
59             schema => 'uint*',
60             default => 0,
61             tags => ['common'],
62             description => <<'_',
63              
64             0 means to retry endlessly without ever giving up. 1 means to give up after a
65             single failure (i.e. no retry attempts). 2 means to retry once after a failure.
66             Note that after a success, the number of attempts is reset (as expected). So if
67             max_attempts is 3, and if you fail twice then succeed, then on the next failure
68             the algorithm will retry again for a maximum of 3 times.
69              
70             _
71             },
72             );
73              
74             our %attr_jitter_factor = (
75             jitter_factor => {
76             summary => 'How much to add randomness',
77             schema => ['float*', between=>[0, 0.5]],
78             tags => ['common'],
79             description => <<'_',
80              
81             If you set this to a value larger than 0, the actual delay will be between a
82             random number between original_delay * (1-jitter_factor) and original_delay *
83             (1+jitter_factor). Jitters are usually added to avoid so-called "thundering
84             herd" problem.
85              
86             The jitter will be applied to delay on failure as well as on success.
87              
88             _
89             },
90             );
91              
92             our %attr_delay_on_success = (
93             delay_on_success => {
94             summary => 'Number of seconds to wait after a success',
95             schema => 'ufloat*',
96             default => 0,
97             },
98             );
99              
100             our %attr_max_delay = (
101             max_delay => {
102             summary => 'Maximum delay time, in seconds',
103             schema => 'ufloat*',
104             tags => ['common'],
105             },
106             );
107              
108             our %attr_min_delay = (
109             min_delay => {
110             summary => 'Maximum delay time, in seconds',
111             schema => 'ufloat*',
112             default => 0,
113             tags => ['common'],
114             },
115             );
116              
117             our %attr_initial_delay = (
118             initial_delay => {
119             summary => 'Initial delay for the first attempt after failure, '.
120             'in seconds',
121             schema => 'ufloat*',
122             req => 1,
123             },
124             );
125              
126             our %attr_delay_multiple_on_failure = (
127             delay_multiple_on_failure => {
128             summary => 'How much to multiple previous delay, upon failure (e.g. 1.5)',
129             schema => 'ufloat*',
130             req => 1,
131             },
132             );
133              
134             our %attr_delay_multiple_on_success = (
135             delay_multiple_on_success => {
136             summary => 'How much to multiple previous delay, upon success (e.g. 0.5)',
137             schema => 'ufloat*',
138             req => 1,
139             },
140             );
141              
142             our %attr_delay_increment_on_failure = (
143             delay_increment_on_failure => {
144             summary => 'How much to add to previous delay, in seconds, upon failure (e.g. 5)',
145             schema => 'float*',
146             req => 1,
147             },
148             );
149              
150             our %attr_delay_increment_on_success = (
151             delay_increment_on_success => {
152             summary => 'How much to add to previous delay, in seconds, upon success (e.g. -5)',
153             schema => 'float*',
154             req => 1,
155             },
156             );
157              
158             $SPEC{new} = {
159             v => 1.1,
160             is_class_meth => 1,
161             is_func => 0,
162             args => {
163             %attr_max_attempts,
164             %attr_jitter_factor,
165             },
166             result_naked => 1,
167             result => {
168             schema => 'obj*',
169             },
170             };
171             sub new {
172 19     19 1 2985713 my ($class, %args) = @_;
173              
174 19         102 my $attrspec = ${"$class\::SPEC"}{new}{args};
  19         140  
175              
176             # check known attributes
177 19         74 for my $arg (keys %args) {
178 60 100       173 $arg =~ /\A(_start_timestamp)\z/ and next;
179 59 50       157 $attrspec->{$arg} or die "$class: Unknown attribute '$arg'";
180             }
181             # check required attributes and set default
182 19         107 for my $attr (keys %$attrspec) {
183 159 100       370 if ($attrspec->{$attr}{req}) {
184 28 50       67 exists($args{$attr})
185             or die "$class: Missing required attribute '$attr'";
186             }
187 159 100       342 if (exists $attrspec->{$attr}{default}) {
188 93   100     376 $args{$attr} //= $attrspec->{$attr}{default};
189             }
190             }
191 19         62 $args{_attempts} = 0;
192 19   66     137 $args{_start_timestamp} //= time();
193 19         99 bless \%args, $class;
194             }
195              
196             sub _consider_actual_delay {
197 10     10   21 my ($self, $delay, $timestamp) = @_;
198              
199 10   100     31 $self->{_prev_delay} //= 0;
200 10         21 my $actual_delay = $timestamp - $self->{_last_timestamp};
201 10         21 my $new_delay = $delay + $self->{_prev_delay} - $actual_delay;
202 10         20 $new_delay;
203             }
204              
205             sub _add_jitter {
206 120     120   244 my ($self, $delay) = @_;
207 120 50 33     437 return $delay unless $delay && $self->{jitter_factor};
208 120         248 my $min = $delay * (1-$self->{jitter_factor});
209 120         242 my $max = $delay * (1+$self->{jitter_factor});
210 120         313 $min + ($max-$min)*rand();
211             }
212              
213             sub _success_or_failure {
214 205     205   402 my ($self, $is_success, $timestamp) = @_;
215              
216 205   66     549 $self->{_last_timestamp} //= $timestamp;
217             $timestamp >= $self->{_last_timestamp} or
218 205 100       513 die ref($self).": Decreasing timestamp ".
219             "($self->{_last_timestamp} -> $timestamp)";
220              
221 204 100       668 my $delay = $is_success ?
222             $self->_success($timestamp) : $self->_failure($timestamp);
223              
224             $delay = $self->_consider_actual_delay($delay, $timestamp)
225 204 100       498 if $self->{consider_actual_delay};
226              
227             $delay = $self->_add_jitter($delay)
228 204 100       524 if $self->{jitter_factor};
229              
230             # keep between max(0, min_delay) and max_delay
231             $delay = $self->{max_delay}
232 204 100 100     635 if defined $self->{max_delay} && $delay > $self->{max_delay};
233 204 100       457 $delay = 0 if $delay < 0;
234             $delay = $self->{min_delay}
235 204 100 66     782 if defined $self->{min_delay} && $delay < $self->{min_delay};
236              
237 204         339 $self->{_last_timestamp} = $timestamp;
238 204         361 $self->{_prev_delay} = $delay;
239 204         671 $delay;
240             }
241              
242             sub success {
243 88     88 1 13241 my ($self, $timestamp) = @_;
244              
245 88   66     218 $timestamp //= time();
246              
247 88         161 $self->{_attempts} = 0;
248              
249 88         185 $self->_success_or_failure(1, $timestamp);
250             }
251              
252             sub failure {
253 121     121 1 1107 my ($self, $timestamp) = @_;
254              
255 121   66     424 $timestamp //= time();
256              
257             return -1 if defined $self->{max_actual_duration} &&
258             $self->{max_actual_duration} > 0 &&
259 121 100 66     609 $timestamp - $self->{_start_timestamp} >= $self->{max_actual_duration};
      100        
260              
261 120         207 $self->{_attempts}++;
262             return -1 if $self->{max_attempts} &&
263 120 100 100     335 $self->{_attempts} >= $self->{max_attempts};
264              
265 117         288 $self->_success_or_failure(0, $timestamp);
266             }
267              
268             1;
269             # ABSTRACT: Various backoff strategies for retry
270              
271             __END__
272              
273             =pod
274              
275             =encoding UTF-8
276              
277             =head1 NAME
278              
279             Algorithm::Backoff - Various backoff strategies for retry
280              
281             =head1 VERSION
282              
283             This document describes version 0.010 of Algorithm::Backoff (from Perl distribution Algorithm-Backoff), released on 2024-02-24.
284              
285             =head1 SYNOPSIS
286              
287             # 1. pick a strategy and instantiate
288              
289             use Algorithm::Backoff::Constant;
290             my $ab = Algorithm::Backoff::Constant->new(
291             delay => 2, # required
292             #delay_on_success => 0, # optional, default 0
293             );
294              
295             # 2. log success/failure and get a new number of seconds to delay. if you don't
296             # want to log for the current time, you can pass a timestamp (number of seconds
297             # passed since some reference value, like a Unix epoch) as the argument, which
298             # should be monotonically increasing.
299              
300             my $secs = $ab->failure(); # => 2
301             my $secs = $ab->success(); # => 0
302             my $secs = $ab->failure(); # => 2
303              
304             =head1 DESCRIPTION
305              
306             This distribution provides several classes that implement various backoff
307             strategies for setting delay between retry attempts.
308              
309             This class (C<Algorithm::Backoff>) is a base class only.
310              
311             Algorithm::Backoff does not actually provide a function/method to retry a piece
312             of code. It only contains the backoff strategies and splits the actual delaying
313             to another module (e.g. L<Retry::Backoff>). This allows for things like
314             printing/returning all the retries and their delay amounts without actually
315             doing the delay (e.g. in L<show-backoff-delays> script).
316              
317             =head1 METHODS
318              
319              
320             =head2 new
321              
322             Usage:
323              
324             new(%args) -> obj
325              
326             This function is not exported.
327              
328             Arguments ('*' denotes required arguments):
329              
330             =over 4
331              
332             =item * B<jitter_factor> => I<float>
333              
334             How much to add randomness.
335              
336             If you set this to a value larger than 0, the actual delay will be between a
337             random number between original_delay * (1-jitter_factor) and original_delay *
338             (1+jitter_factor). Jitters are usually added to avoid so-called "thundering
339             herd" problem.
340              
341             The jitter will be applied to delay on failure as well as on success.
342              
343             =item * B<max_attempts> => I<uint> (default: 0)
344              
345             Maximum number consecutive failures before giving up.
346              
347             0 means to retry endlessly without ever giving up. 1 means to give up after a
348             single failure (i.e. no retry attempts). 2 means to retry once after a failure.
349             Note that after a success, the number of attempts is reset (as expected). So if
350             max_attempts is 3, and if you fail twice then succeed, then on the next failure
351             the algorithm will retry again for a maximum of 3 times.
352              
353              
354             =back
355              
356             Return value: (obj)
357              
358              
359             =head2 success
360              
361             Usage:
362              
363             my $secs = $obj->success([ $timestamp ]);
364              
365             Log a successful attempt. If not specified, C<$timestamp> defaults to current
366             Unix timestamp. Will return the suggested number of seconds to wait before doing
367             another attempt.
368              
369             =head2 failure
370              
371             Usage:
372              
373             my $secs = $obj->failure([ $timestamp ]);
374              
375             Log a failed attempt. If not specified, C<$timestamp> defaults to current Unix
376             timestamp. Will return the suggested number of seconds to wait before doing
377             another attempt, or -1 if it suggests that one gives up (e.g. if C<max_attempts>
378             parameter has been exceeded).
379              
380             =head1 HOMEPAGE
381              
382             Please visit the project's homepage at L<https://metacpan.org/release/Algorithm-Backoff>.
383              
384             =head1 SOURCE
385              
386             Source repository is at L<https://github.com/perlancar/perl-Algorithm-Backoff>.
387              
388             =head1 SEE ALSO
389              
390             L<Retry::Backoff> - an application of Algorithm::Backoff to retry a piece of
391             code using various backoff strategies.
392              
393             L<App::AlgorithmBackoffUtils> - various CLI's related to Algorithm::Backoff.
394              
395             L<Action::Retry> - A prior art for Algorithm::Backoff. Somehow I didn't find
396             this module before writing Algorithm::Backoff. But Algorithm::Backoff offers an
397             alternative interface (a split of actual sleep/retry vs the algorithm), and some
398             additional parameters (like delay on success and jitter factor), a lighter
399             footprint (no Moo), and a couple more strategies.
400              
401             =head1 AUTHOR
402              
403             perlancar <perlancar@cpan.org>
404              
405             =head1 CONTRIBUTORS
406              
407             =for stopwords Brendan Byrd SineSwiper
408              
409             =over 4
410              
411             =item *
412              
413             Brendan Byrd <brendan.byrd@grantstreet.com>
414              
415             =item *
416              
417             SineSwiper <GitHub@ResonatorSoft.org>
418              
419             =back
420              
421             =head1 CONTRIBUTING
422              
423              
424             To contribute, you can send patches by email/via RT, or send pull requests on
425             GitHub.
426              
427             Most of the time, you don't need to build the distribution yourself. You can
428             simply modify the code, then test via:
429              
430             % prove -l
431              
432             If you want to build the distribution (e.g. to try to install it locally on your
433             system), you can install L<Dist::Zilla>,
434             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
435             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
436             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
437             that are considered a bug and can be reported to me.
438              
439             =head1 COPYRIGHT AND LICENSE
440              
441             This software is copyright (c) 2024, 2019 by perlancar <perlancar@cpan.org>.
442              
443             This is free software; you can redistribute it and/or modify it under
444             the same terms as the Perl 5 programming language system itself.
445              
446             =head1 BUGS
447              
448             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Algorithm-Backoff>
449              
450             When submitting a bug or request, please include a test-file or a
451             patch to an existing test-file that illustrates the bug or desired
452             feature.
453              
454             =cut