File Coverage

blib/lib/Algorithm/Retry.pm
Criterion Covered Total %
statement 50 50 100.0
branch 19 22 86.3
condition 15 18 83.3
subroutine 9 9 100.0
pod 3 3 100.0
total 96 102 94.1


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