File Coverage

blib/lib/AnyEvent/RetryTimer.pm
Criterion Covered Total %
statement 41 45 91.1
branch 15 20 75.0
condition 7 15 46.6
subroutine 8 9 88.8
pod 4 4 100.0
total 75 93 80.6


line stmt bran cond sub pod time code
1             package AnyEvent::RetryTimer;
2 1     1   8746 use common::sense;
  1         9  
  1         6  
3 1     1   61 use Scalar::Util qw/weaken/;
  1         2  
  1         142  
4 1     1   6 use AnyEvent;
  1         5  
  1         848  
5              
6             our $VERSION = '0.1';
7              
8             =head1 NAME
9              
10             AnyEvent::RetryTimer - Retry timers for AnyEvent
11              
12             =head1 VERSION
13              
14             0.1
15              
16             =head1 SYNOPSIS
17              
18             use AnyEvent::RetryTimer;
19              
20             my $con =
21             Something::Connection->new;
22              
23             my $timer;
24              
25             $con->on_disconnect (sub {
26             $timer ||=
27             AnyEvent::RetryTimer->new (
28             on_retry => sub {
29             $con->connect;
30             });
31              
32             $timer->retry;
33              
34             my $secs = $timer->current_interval;
35              
36             warn "Lost connection, reconnecting in $secs seconds!";
37             });
38              
39             $con->on_connect (sub {
40             warn "Connected successfully!";
41              
42             $timer->success;
43             undef $timer;
44             });
45              
46             =head1 DESCRIPTION
47              
48             This is a small helper utility to manage timed retries.
49              
50             This is a pattern I often stumble across when managing network connections.
51             And I'm tired to reimplement it again and again. So I wrote this module.
52              
53             At the moment it only implements a simple exponential back off retry mechanism
54             (with configurable multiplier) using L<AnyEvent> timers. If there are
55             other back off strategies you find useful you are free to send a
56             feature request or even better a patch!
57              
58             =head1 METHODS
59              
60             =over 4
61              
62             =item my $timer = AnyEvent::RetryTimer->new (%args)
63              
64             This is the constructor, it constructs the object.
65              
66             At the end of the objects lifetime, when you get rid of the last reference to
67             C<$timer>, it will stop and running timeouts and not call any of the configured
68             callbacks again.
69              
70             C<%args> can contain these keys:
71              
72             =over 4
73              
74             =item on_retry => $retry_cb->($timer)
75              
76             C<$retry_cb> is the callback that will be called for (re)tries.
77              
78             When this constructor is called and no C<no_first_try> is given,
79             an initial retry interval of the length 0 is started, which counts as the
80             first try.
81              
82             Later it is also called after a retry interval has passed, which was initiated
83             by a call to the C<retry> method.
84              
85             The first argument is the C<$timer> object itself.
86              
87             =item no_first_try => $bool
88              
89             This parameter defines whether the C<$retry_cb> will be called when the
90             L<AnyEvent::RetryTimer> object is created or not. If C<$bool> is true
91             C<$retry_cb> will not be called.
92              
93             The default is false.
94              
95             =item backoff => 'exponential'
96              
97             This is the back off algorithm that is used. Currently
98             only C<exponential> is implemented and is the default.
99              
100             =item max_retries => $max_retry_cnt
101              
102             This is the maximum number of retries that are done
103             between the first call to C<retry> and the finishing
104             call to C<success>.
105              
106             If the number of retries is exceeded by a call to C<retry>
107             the C<on_max_retries> callback is called (see below).
108              
109             Please note that a call to C<success> will of course reset the internal count
110             of calls to C<retry>.
111              
112             Default for this option is C<0> (disabled).
113              
114             =item on_max_retries => $max_retry_cb->($timer)
115              
116             After C<max_retries> the C<$max_retry_cb> callback will be
117             called with the C<$timer> as first argument.
118              
119             It is usually called when a call to C<retry> would exceed
120             C<max_retries>.
121              
122             =back
123              
124             And then there are keys that are specific to the C<backoff>
125             method used:
126              
127             =over 4
128              
129             =item B<exponential>
130              
131             =over 4
132              
133             =item start_interval => $secs
134              
135             This is the length of the first interval. Given in seconds.
136              
137             Default is C<10>.
138              
139             =item multiplier => $float
140              
141             This is the multiplier for the retry intervals. Each time
142             a C<retry> is done the previous (if any) interval will be
143             multiplied with C<$float> and used for the next interval.
144              
145             Default is C<1.5>.
146              
147             =item max_interval => $max_interval_secs
148              
149             As exponential back off intervals can increase quite a lot
150             you can give the maximum time to wait in C<$max_interval_secs>.
151              
152             Default is C<3600 * 4>, which is 4 hours.
153              
154             =back
155              
156             =back
157              
158             =cut
159              
160             sub new {
161 1     1 1 5332 my $this = shift;
162 1   33     10 my $class = ref ($this) || $this;
163 1         11 my $self = {
164             backoff => 'exponential',
165             multiplier => 1.5,
166             max_interval => 3600 * 4, # 6 hours
167             max_retries => 0, # infinite
168             start_interval => 10,
169             @_
170             };
171 1         4 bless $self, $class;
172              
173 1         2 my $rself = $self;
174              
175 1         7 weaken $self;
176              
177             $self->{timer} = AE::timer 0, 0, sub {
178 1     1   1390 delete $self->{timer};
179 1 50       8 $self->{on_retry}->($self) if $self;
180 1         31 };
181              
182 1         4 return $rself
183             }
184              
185             =item $timer->retry
186              
187             This method initiates or continues retries. If already a retry interval
188             is installed (eg. by the constructor or another previous unfinished call
189             to C<retry>), the call will be a nop.
190              
191             That means you can call C<retry> directly after you created this object and
192             will not cause the initial try to be "retried".
193              
194             If you are interested in the length of the current interval (after a
195             call to this method), you can call the C<current_interval> method.
196              
197             =cut
198              
199             sub retry {
200 6     6 1 502745 my ($self) = @_;
201              
202 6         41 weaken $self;
203              
204 6 100       230 return if $self->{timer};
205              
206 5 50       39 if ($self->{backoff} eq 'exponential') {
207 5         16 my $r;
208              
209             # layout of $r = [$interval, $retry_cnt]
210 5 100       32 if ($r = $self->{r}) {
211              
212 4 100 33     141 if ($self->{max_retries}
      66        
213             && $self->{on_max_retries}
214             && $r->[1] >= $self->{max_retries})
215             {
216 1         8 delete $self->{r};
217 1         11 $self->{on_max_retries}->($self);
218 1         63 return;
219             }
220              
221 3         16 $r->[0] *= $self->{multiplier};
222 3 100       20 $r->[0] =
223             $r->[0] > $self->{max_interval}
224             ? $self->{max_interval}
225             : $r->[0];
226              
227             } else {
228 1         7 $r = $self->{r} = [$self->{start_interval}];
229             }
230              
231             $self->{timer} = AE::timer $r->[0], 0, sub {
232 4     4   1299398 $r->[1]++;
233 4         33 delete $self->{timer};
234 4 50 33     125 $self->{on_retry}->($self)
235             if $self && $self->{on_retry};
236 4         80 };
237             }
238             }
239              
240             =item $timer->success
241              
242             This signals that the last retry was successful and it will
243             reset any state or intervals to the initial settings given
244             to the constructor.
245              
246             You can reuse the C<$timer> object after a call to C<success>.
247              
248             =cut
249              
250             sub success {
251 0     0 1 0 my ($self) = @_;
252 0         0 delete $self->{r}; # reset timer & wait counter
253 0         0 delete $self->{timer};
254             }
255              
256             =item my $secs = $timer->current_interval
257              
258             Returns the length of the current interval to the
259             next call to the C<$retry_cb>.
260              
261             =cut
262              
263             sub current_interval {
264 5     5 1 36 my ($self) = @_;
265              
266             # specialcase: first call
267 5 50 66     46 return 0 if $self->{timer} && not $self->{r};
268              
269 5 50       2665 if ($self->{backoff} eq 'exponential') {
270 5 100       86 return unless $self->{r};
271 4         91 return $self->{r}->[0];
272             }
273              
274             undef
275 0           }
276              
277             =back
278              
279             =head1 AUTHOR
280              
281             Robin Redeker, C<< <elmex@ta-sa.org> >>
282              
283             =head1 SEE ALSO
284              
285             L<AnyEvent>
286              
287             =head1 COPYRIGHT & LICENSE
288              
289             Copyright 2009 Robin Redeker, all rights reserved.
290              
291             This program is free software; you can redistribute it and/or modify it
292             under the same terms as Perl itself.
293              
294             =cut
295              
296             1;