File Coverage

blib/lib/Try/ALRM.pm
Criterion Covered Total %
statement 115 118 97.4
branch 39 44 88.6
condition 25 31 80.6
subroutine 20 20 100.0
pod 6 6 100.0
total 205 219 93.6


line stmt bran cond sub pod time code
1 7     7   778861 use strict;
  7         12  
  7         260  
2 7     7   61 use warnings;
  7         22  
  7         690  
3              
4             package Try::ALRM;
5              
6             # ABSTRACT: Structured retry and timeout handling using CORE::alarm
7              
8             our $VERSION = q{1.03};
9              
10 7     7   78 use Exporter qw/import/;
  7         31  
  7         269  
11 7     7   34 use Scalar::Util qw/refaddr/;
  7         26  
  7         440  
12 7     7   53 use Time::HiRes qw/time/;
  7         29  
  7         81  
13              
14             our @EXPORT = qw(try_once retry ALRM finally timeout tries);
15             our @EXPORT_OK = qw(try_once retry ALRM finally timeout tries);
16              
17             our $TIMEOUT = 60;
18             our $TRIES = 3;
19              
20             my @ALARM_STACK;
21              
22             sub timeout (;$) {
23 53     53 1 940013 my $timeout = shift;
24 53 100       182 if ( defined $timeout ) {
25 5         23 _assert_timeout($timeout);
26 5         8 $TIMEOUT = $timeout;
27             }
28 53         386 return $TIMEOUT;
29             }
30              
31             sub tries (;$) {
32 45     45 1 111 my $tries = shift;
33 45 100       142 if ( defined $tries ) {
34 1         5 _assert_tries($tries);
35 1         19 $TRIES = $tries;
36             }
37 45         328 return $TRIES;
38             }
39              
40             sub try_once (&;@) {
41 11     11 1 34 my $block = shift;
42 11         37 &retry( $block, @_, tries => 1 ); # bypass prototype intentionally
43             }
44              
45             sub retry (&;@) {
46 32     32 1 7723 my $block = shift;
47              
48 32         95 my $spec = _parse_retry_args(@_);
49              
50 30         58 my $retry_block = $block;
51 30         78 my $alarm_block = $spec->{ALRM};
52 30   66 7   108 my $finally_block = $spec->{finally} || sub { };
53              
54 30 100       84 my $timeout = exists $spec->{timeout} ? $spec->{timeout} : $TIMEOUT;
55 30 100       79 my $tries = exists $spec->{tries} ? $spec->{tries} : $TRIES;
56              
57 30         112 _assert_timeout($timeout);
58 28         88 _assert_tries($tries);
59              
60 26         55 local $TIMEOUT = $timeout;
61 26         52 local $TRIES = $tries;
62              
63 26         62 my $attempts = 0;
64 26         49 my $succeeded = 0;
65 26         35 my $error;
66              
67             ATTEMPT:
68 26         75 for my $attempt ( 1 .. $tries ) {
69 33         49 $attempts = $attempt;
70              
71 33         149 my $alarm_token = bless \( my $token = "Try::ALRM timeout" ),
72             'Try::ALRM::_Timeout';
73              
74 33         408 local $SIG{ALRM} = \&_dispatch_alarm;
75              
76 33         109 my $frame = _push_alarm_frame(
77             timeout => $timeout,
78             attempt => $attempt,
79             handler => $alarm_block,
80             token => $alarm_token,
81             );
82              
83 33         58 my $ok = eval {
84 33         141 $retry_block->($attempt);
85 22         5991283 _pop_alarm_frame($frame);
86 7         9 1;
87             };
88              
89 33         98 my $eval_error = $@;
90              
91 33         120 _pop_alarm_frame($frame);
92              
93 33 100       98 if ($ok) {
94 7         11 $succeeded = 1;
95 7         69 last ATTEMPT;
96             }
97              
98 26 100 66     272 if (
      100        
99             ref($eval_error)
100             && ref($eval_error) eq 'Try::ALRM::_Timeout'
101             && refaddr($eval_error) == refaddr($alarm_token)
102             ) {
103 23         402 next ATTEMPT;
104             }
105              
106 3   50     13 $error = $eval_error || 'Unknown error';
107 3         39 last ATTEMPT;
108             }
109              
110 26         58 my $finally_error;
111             eval {
112 26         168 $finally_block->( $attempts, $succeeded );
113 24         3785 1;
114 26 100       62 } or do {
115 2   50     20 $finally_error = $@ || 'Unknown error';
116             };
117              
118 26 100       109 die $error if defined $error;
119 23 100       114 die $finally_error if defined $finally_error;
120              
121 22         265 return;
122             }
123              
124             sub ALRM (&;@) {
125 17     17 1 77 return ALRM => @_;
126             }
127              
128             sub finally (&;@) {
129 19     19 1 328378 return finally => @_;
130             }
131              
132             sub _push_alarm_frame {
133 33     33   148 my %frame = @_;
134              
135 33         158 $frame{deadline} = time() + $frame{timeout};
136              
137 33         69 push @ALARM_STACK, \%frame;
138 33         83 _reset_alarm();
139              
140 33         57 return \%frame;
141             }
142              
143             sub _pop_alarm_frame {
144 40     40   79 my $frame = shift;
145              
146 40 50       198 return unless $frame;
147              
148             @ALARM_STACK = grep {
149 40         88 refaddr($_) != refaddr($frame)
  40         128  
150             } @ALARM_STACK;
151              
152 40         133 _reset_alarm();
153              
154 40         62 return;
155             }
156              
157             sub _dispatch_alarm {
158 23     23   8002360 my $now = time();
159              
160             my @expired = sort {
161             $a->{deadline} <=> $b->{deadline}
162 0         0 } grep {
163 23         131 $_->{deadline} <= $now
  26         215  
164             } @ALARM_STACK;
165              
166 23 50       163 unless (@expired) {
167 0         0 _reset_alarm();
168 0         0 return;
169             }
170              
171 23         49 my $frame = $expired[0];
172              
173 23 100       111 if ( ref( $frame->{handler} ) eq 'CODE' ) {
174 17         121 $frame->{handler}->( $frame->{attempt} );
175             }
176              
177 23         7774 die $frame->{token};
178             }
179              
180             sub _reset_alarm {
181 73     73   294 CORE::alarm(0);
182              
183 73 100       195 return unless @ALARM_STACK;
184              
185             my ($next) = sort {
186 40         124 $a->{deadline} <=> $b->{deadline}
187 5         37 } @ALARM_STACK;
188              
189 40         117 my $remaining = $next->{deadline} - time();
190              
191 40 100       109 if ( $remaining <= 0 ) {
192 1         5 CORE::alarm(1);
193 1         2 return;
194             }
195              
196 39         103 CORE::alarm( _ceil($remaining) );
197              
198 39         89 return;
199             }
200              
201             sub _ceil {
202 39     39   56 my $value = shift;
203              
204 39 50       238 return int($value) == $value ? int($value) : int($value) + 1;
205             }
206              
207             sub _parse_retry_args {
208 32     32   91 my @args = @_;
209              
210 32 50       119 die "Odd number of arguments to retry\n" if @args % 2;
211              
212 32         51 my %spec;
213              
214 32         73 while (@args) {
215 92         224 my ( $key, $value ) = splice @args, 0, 2;
216              
217 92 100 100     496 die "Unknown retry argument '$key'\n"
      100        
      100        
218             unless $key eq 'ALRM'
219             || $key eq 'finally'
220             || $key eq 'timeout'
221             || $key eq 'tries';
222              
223             die "Duplicate retry argument '$key'\n"
224 91 100       184 if exists $spec{$key};
225              
226 90 100 100     275 if ( $key eq 'ALRM' || $key eq 'finally' ) {
227 36 50       88 die "$key must be a CODE reference\n"
228             unless ref($value) eq 'CODE';
229             }
230              
231 90         229 $spec{$key} = $value;
232             }
233              
234 30         64 return \%spec;
235             }
236              
237             sub _assert_timeout {
238 35     35   53 my $timeout = shift;
239              
240 35 100 66     315 die qq{timeout must be an integer >= 1!\n}
241             unless defined $timeout
242             && $timeout =~ /\A[1-9][0-9]*\z/;
243             }
244              
245             sub _assert_tries {
246 29     29   42 my $tries = shift;
247              
248 29 100 66     208 die qq{tries must be an integer >= 1!\n}
249             unless defined $tries
250             && $tries =~ /\A[1-9][0-9]*\z/;
251             }
252              
253             __PACKAGE__;
254              
255             __END__