File Coverage

blib/lib/IO/Async/Timer/Periodic.pm
Criterion Covered Total %
statement 70 71 98.5
branch 34 42 80.9
condition 4 5 80.0
subroutine 12 12 100.0
pod 3 3 100.0
total 123 133 92.4


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2009-2015 -- leonerd@leonerd.org.uk
5              
6             package IO::Async::Timer::Periodic;
7              
8 4     4   82352 use strict;
  4         11  
  4         112  
9 4     4   21 use warnings;
  4         8  
  4         113  
10 4     4   19 use base qw( IO::Async::Timer );
  4         9  
  4         1457  
11              
12             our $VERSION = '0.79';
13              
14 4     4   28 use Carp;
  4         8  
  4         2856  
15              
16             =head1 NAME
17              
18             C - event callback at regular intervals
19              
20             =head1 SYNOPSIS
21              
22             use IO::Async::Timer::Periodic;
23              
24             use IO::Async::Loop;
25             my $loop = IO::Async::Loop->new;
26              
27             my $timer = IO::Async::Timer::Periodic->new(
28             interval => 60,
29              
30             on_tick => sub {
31             print "You've had a minute\n";
32             },
33             );
34              
35             $timer->start;
36              
37             $loop->add( $timer );
38              
39             $loop->run;
40              
41             =head1 DESCRIPTION
42              
43             This subclass of L implements repeating events at regular
44             clock intervals. The timing may or may not be subject to how long it takes the
45             callback to execute. Iterations may be rescheduled runs at fixed regular
46             intervals beginning at the time the timer was started, or by a fixed delay
47             after the previous code has finished executing.
48              
49             For a C object that only runs a callback once, after a given delay, see
50             instead L. A Countdown timer can also be used to
51             create repeating events that fire at a fixed delay after the previous event
52             has finished processing. See als the examples in
53             C.
54              
55             =cut
56              
57             =head1 EVENTS
58              
59             The following events are invoked, either using subclass methods or CODE
60             references in parameters:
61              
62             =head2 on_tick
63              
64             Invoked on each interval of the timer.
65              
66             =cut
67              
68             =head1 PARAMETERS
69              
70             The following named parameters may be passed to C or C:
71              
72             =head2 on_tick => CODE
73              
74             CODE reference for the C event.
75              
76             =head2 interval => NUM
77              
78             The interval in seconds between invocations of the callback or method. Cannot
79             be changed if the timer is running.
80              
81             =head2 first_interval => NUM
82              
83             Optional. If defined, the interval in seconds after calling the C
84             method before the first invocation of the callback or method. Thereafter, the
85             regular C will be used. If not supplied, the first interval will be
86             the same as the others.
87              
88             Even if this value is zero, the first invocation will be made asynchronously,
89             by the containing C object, and not synchronously by the C method
90             itself.
91              
92             =head2 reschedule => STRING
93              
94             Optional. Must be one of C, C or C. Defines the algorithm
95             used to reschedule the next invocation.
96              
97             C schedules each iteration at the fixed interval from the previous
98             iteration's schedule time, ensuring a regular repeating event.
99              
100             C schedules similarly to C, but skips over times that have already
101             passed. This matters if the duration is particularly short and there's a
102             possibility that times may be missed, or if the entire process is stopped and
103             resumed by C or similar.
104              
105             C schedules each iteration at the fixed interval from the time that the
106             previous iteration's event handler returns. This allows it to slowly drift over
107             time and become desynchronised with other events of the same interval or
108             multiples/fractions of it.
109              
110             Once constructed, the timer object will need to be added to the C before
111             it will work. It will also need to be started by the C method.
112              
113             =cut
114              
115             sub _init
116             {
117 15     15   37 my $self = shift;
118 15         82 $self->SUPER::_init( @_ );
119              
120 15         61 $self->{reschedule} = "hard";
121             }
122              
123             sub configure
124             {
125 33     33 1 175 my $self = shift;
126 33         88 my %params = @_;
127              
128 33 100       109 if( exists $params{on_tick} ) {
129 5         15 my $on_tick = delete $params{on_tick};
130 5 50       18 ref $on_tick or croak "Expected 'on_tick' as a reference";
131              
132 5         11 $self->{on_tick} = $on_tick;
133 5         12 undef $self->{cb}; # Will be lazily constructed when needed
134             }
135              
136 33 100       93 if( exists $params{interval} ) {
137 25 100       101 $self->is_running and croak "Cannot configure 'interval' of a running timer\n";
138              
139 24         65 my $interval = delete $params{interval};
140 24 50       94 $interval > 0 or croak "Expected a 'interval' as a positive number";
141              
142 24         63 $self->{interval} = $interval;
143             }
144              
145 32 100       108 if( exists $params{first_interval} ) {
146 1 50       6 $self->is_running and croak "Cannot configure 'first_interval' of a running timer\n";
147              
148 1         3 my $first_interval = delete $params{first_interval};
149 1 50       6 $first_interval >= 0 or croak "Expected a 'first_interval' as a non-negative number";
150              
151 1         3 $self->{first_interval} = $first_interval;
152             }
153              
154 32 100       78 if( exists $params{reschedule} ) {
155 2   50     9 my $resched = delete $params{reschedule} || "hard";
156 2 50       7 grep { $_ eq $resched } qw( hard skip drift ) or
  6         18  
157             croak "Expected 'reschedule' to be one of hard, skip, drift";
158              
159 2         5 $self->{reschedule} = $resched;
160             }
161              
162 32 50       110 unless( $self->can_event( 'on_tick' ) ) {
163 0         0 croak 'Expected either a on_tick callback or an ->on_tick method';
164             }
165              
166 32         134 $self->SUPER::configure( %params );
167             }
168              
169             sub _reschedule
170             {
171 44     44   89 my $self = shift;
172              
173 44         182 my $now = $self->loop->time;
174 44         119 my $resched = $self->{reschedule};
175              
176             my $next_interval = $self->{is_first} && defined $self->{first_interval}
177 44 100 100     264 ? $self->{first_interval} : $self->{interval};
178 44         117 delete $self->{is_first};
179              
180 44 100       201 if( !defined $self->{next_time} ) {
    100          
    100          
    50          
181 17         58 $self->{next_time} = $now + $next_interval;
182             }
183             elsif( $resched eq "hard" ) {
184 23         71 $self->{next_time} += $next_interval;
185             }
186             elsif( $resched eq "skip" ) {
187             # How many ticks are needed?
188 2         42 my $ticks = POSIX::ceil( ( $now - $self->{next_time} ) / $next_interval );
189             # $self->{last_ticks} = $ticks;
190 2         11 $self->{next_time} += $next_interval * $ticks;
191             }
192             elsif( $resched eq "drift" ) {
193 2         12 $self->{next_time} = $now + $next_interval;
194             }
195              
196 44         287 $self->SUPER::start;
197             }
198              
199             sub start
200             {
201 40     40 1 827 my $self = shift;
202              
203 40         80 $self->{is_first} = 1;
204              
205             # Only actually define a time if we've got a loop; otherwise it'll just
206             # become start-pending. We'll calculate it properly when it gets added to
207             # the Loop
208 40 100       109 if( $self->loop ) {
209 17         57 $self->_reschedule;
210             }
211             else {
212 23         65 $self->SUPER::start;
213             }
214             }
215              
216             sub stop
217             {
218 27     27 1 61 my $self = shift;
219 27         141 $self->SUPER::stop;
220              
221 27         82 undef $self->{next_time};
222             }
223              
224             sub _make_cb
225             {
226 15     15   33 my $self = shift;
227              
228             return $self->_capture_weakself( sub {
229 28 50   28   158 my $self = shift or return;
230              
231 28         133 undef $self->{id};
232              
233 28 100       85 my $ok = eval { $self->invoke_event( on_tick => ); 1 } or
  28         319  
  26         164  
234             my $e = $@;
235              
236             # detect ->stop
237 28 100       244 $self->_reschedule if defined $self->{next_time};
238              
239 28 100       159 die $e if !$ok;
240 15         147 } );
241             }
242              
243             sub _make_enqueueargs
244             {
245 44     44   120 my $self = shift;
246              
247 44         270 return at => $self->{next_time};
248             }
249              
250             =head1 AUTHOR
251              
252             Paul Evans
253              
254             =cut
255              
256             0x55AA;