File Coverage

blib/lib/Try/ALRM.pm
Criterion Covered Total %
statement 113 118 95.7
branch 37 44 84.0
condition 24 31 77.4
subroutine 20 20 100.0
pod 6 6 100.0
total 200 219 91.3


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