File Coverage

blib/lib/Algorithm/Retry.pm
Criterion Covered Total %
statement 57 57 100.0
branch 21 26 80.7
condition 20 23 86.9
subroutine 10 10 100.0
pod 3 3 100.0
total 111 119 93.2


line stmt bran cond sub pod time code
1             package Algorithm::Retry;
2              
3             our $DATE = '2019-04-10'; # DATE
4             our $VERSION = '0.002'; # VERSION
5              
6 4     4   1428 use 5.010001;
  4         11  
7 4     4   18 use strict 'subs', 'vars';
  4         6  
  4         92  
8 4     4   18 use warnings;
  4         12  
  4         90  
9              
10 4     4   1685 use Time::HiRes qw(time);
  4         4445  
  4         27  
11              
12             our %SPEC;
13              
14             our %attr_consider_actual_delay = (
15             consider_actual_delay => {
16             summary => 'Whether to consider actual delay',
17             schema => ['bool*'],
18             default => 0,
19             description => <<'_',
20              
21             If set to true, will take into account the actual delay (timestamp difference).
22             For example, when using the Constant strategy of delay=2, you log failure()
23             again right after the previous failure() (i.e. specify the same timestamp).
24             failure() will then return ~2+2 = 4 seconds. On the other hand, if you waited 2
25             seconds before calling failure() again (i.e. specify the timestamp that is 2
26             seconds larger than the previous timestamp), failure() will return 2 seconds.
27             And if you waited 4 seconds or more, failure() will return 0.
28              
29             _
30             },
31             );
32              
33             our %attr_max_attempts = (
34             max_attempts => {
35             summary => 'Maximum number consecutive failures before giving up',
36             schema => 'uint*',
37             default => 0,
38             description => <<'_',
39              
40             0 means to retry endlessly without ever giving up. 1 means to give up after a
41             single failure (i.e. no retry attempts). 2 means to retry once after a failure.
42             Note that after a success, the number of attempts is reset (as expected). So if
43             max_attempts is 3, and if you fail twice then succeed, then on the next failure
44             the algorithm will retry again for a maximum of 3 times.
45              
46             _
47             },
48             );
49              
50             our %attr_jitter_factor = (
51             jitter_factor => {
52             summary => 'How much to add randomness',
53             schema => ['float*', between=>[0, 0.5]],
54             description => <<'_',
55              
56             If you set this to a value larger than 0, the actual delay will be between a
57             random number between original_delay * (1-jitter_factor) and original_delay *
58             (1+jitter_factor). Jitters are usually added to avoid so-called "thundering
59             herd" problem.
60              
61             _
62             },
63             );
64              
65             our %attr_delay_on_success = (
66             delay_on_success => {
67             summary => 'Number of seconds to wait after a success',
68             schema => 'ufloat*',
69             default => 0,
70             },
71             );
72              
73             our %attr_max_delay = (
74             max_delay => {
75             summary => 'Maximum delay time, in seconds',
76             schema => 'ufloat*',
77             },
78             );
79              
80             $SPEC{new} = {
81             v => 1.1,
82             is_class_meth => 1,
83             is_func => 0,
84             args => {
85             %attr_max_attempts,
86             %attr_jitter_factor,
87             },
88             result_naked => 1,
89             result => {
90             schema => 'obj*',
91             },
92             };
93             sub new {
94 11     11 1 31492 my ($class, %args) = @_;
95              
96 11         63 my $attrspec = ${"$class\::SPEC"}{new}{args};
  11         50  
97              
98             # check known attributes
99 11         34 for my $arg (keys %args) {
100 26 50       64 $attrspec->{$arg} or die "$class: Unknown attribute '$arg'";
101             }
102             # check required attributes and set default
103 11         35 for my $attr (keys %$attrspec) {
104 61 100       110 if ($attrspec->{$attr}{req}) {
105 12 50       26 exists($args{$attr})
106             or die "$class: Missing required attribute '$attr'";
107             }
108 61 100       160 if (exists $attrspec->{$attr}{default}) {
109 35   100     100 $args{$attr} //= $attrspec->{$attr}{default};
110             }
111             }
112 11         23 $args{_attempts} = 0;
113 11         52 bless \%args, $class;
114             }
115              
116             sub _success_or_failure {
117 58     58   674 my ($self, $is_success, $timestamp) = @_;
118              
119 58   66     138 $self->{_last_timestamp} //= $timestamp;
120             $timestamp >= $self->{_last_timestamp} or
121 58 100       114 die ref($self).": Decreasing timestamp ".
122             "($self->{_last_timestamp} -> $timestamp)";
123 57 100       164 my $delay = $is_success ?
124             $self->_success($timestamp) : $self->_failure($timestamp);
125             $delay = $self->{max_delay}
126 57 100 100     218 if defined $self->{max_delay} && $delay > $self->{max_delay};
127 57         83 $delay;
128             }
129              
130             sub _consider_actual_delay {
131 5     5   9 my ($self, $delay, $timestamp) = @_;
132              
133 5   100     10 $self->{_last_delay} //= 0;
134 5         9 my $actual_delay = $timestamp - $self->{_last_timestamp};
135 5         6 my $new_delay = $delay + $self->{_last_delay} - $actual_delay;
136 5         8 $self->{_last_delay} = $new_delay;
137 5         7 $new_delay;
138             }
139              
140             sub success {
141 18     18 1 6953 my ($self, $timestamp) = @_;
142              
143 18   66     78 $timestamp //= time();
144              
145 18         28 $self->{_attempts} = 0;
146              
147 18         32 my $delay = $self->_success_or_failure(1, $timestamp);
148             $delay = $self->_consider_actual_delay($delay, $timestamp)
149 17 50       32 if $self->{consider_actual_delay};
150 17         23 $self->{_last_timestamp} = $timestamp;
151 17 50       27 return 0 if $delay < 0;
152              
153 17         32 $self->_add_jitter($delay);
154             }
155              
156             sub failure {
157 43     43 1 225 my ($self, $timestamp) = @_;
158              
159 43   66     101 $timestamp //= time();
160              
161 43         1772 $self->{_attempts}++;
162             return -1 if $self->{max_attempts} &&
163 43 100 100     115 $self->{_attempts} >= $self->{max_attempts};
164              
165 40         78 my $delay = $self->_success_or_failure(0, $timestamp);
166             $delay = $self->_consider_actual_delay($delay, $timestamp)
167 40 100       74 if $self->{consider_actual_delay};
168 40         52 $self->{_last_timestamp} = $timestamp;
169 40 50       59 return 0 if $delay < 0;
170              
171 40         74 $self->_add_jitter($delay);
172             }
173              
174             sub _add_jitter {
175 57     57   75 my ($self, $delay) = @_;
176 57 100 100     274 return $delay unless $delay && $self->{jitter_factor};
177 20         32 my $min = $delay * (1-$self->{jitter_factor});
178 20         26 my $max = $delay * (1+$self->{jitter_factor});
179 20         74 $min + ($max-$min)*rand();
180             }
181              
182             1;
183             # ABSTRACT: Various retry/backoff strategies
184              
185             __END__