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