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   96098 use strict;
  4         8  
  4         119  
9 4     4   23 use warnings;
  4         9  
  4         113  
10 4     4   23 use base qw( IO::Async::Timer );
  4         9  
  4         1954  
11              
12             our $VERSION = '0.801';
13              
14 4     4   30 use Carp;
  4         8  
  4         3199  
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   33 my $self = shift;
118 15         92 $self->SUPER::_init( @_ );
119              
120 15         67 $self->{reschedule} = "hard";
121             }
122              
123             sub configure
124             {
125 33     33 1 177 my $self = shift;
126 33         89 my %params = @_;
127              
128 33 100       117 if( exists $params{on_tick} ) {
129 5         18 my $on_tick = delete $params{on_tick};
130 5 50       35 ref $on_tick or croak "Expected 'on_tick' as a reference";
131              
132 5         15 $self->{on_tick} = $on_tick;
133 5         16 undef $self->{cb}; # Will be lazily constructed when needed
134             }
135              
136 33 100       137 if( exists $params{interval} ) {
137 25 100       269 $self->is_running and croak "Cannot configure 'interval' of a running timer\n";
138              
139 24         88 my $interval = delete $params{interval};
140 24 50       131 $interval > 0 or croak "Expected a 'interval' as a positive number";
141              
142 24         67 $self->{interval} = $interval;
143             }
144              
145 32 100       101 if( exists $params{first_interval} ) {
146 1 50       4 $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       4 $first_interval >= 0 or croak "Expected a 'first_interval' as a non-negative number";
150              
151 1         4 $self->{first_interval} = $first_interval;
152             }
153              
154 32 100       89 if( exists $params{reschedule} ) {
155 2   50     10 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         6 $self->{reschedule} = $resched;
160             }
161              
162 32 50       134 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         138 $self->SUPER::configure( %params );
167             }
168              
169             sub _reschedule
170             {
171 44     44   128 my $self = shift;
172              
173 44         194 my $now = $self->loop->time;
174 44         137 my $resched = $self->{reschedule};
175              
176             my $next_interval = $self->{is_first} && defined $self->{first_interval}
177 44 100 100     263 ? $self->{first_interval} : $self->{interval};
178 44         133 delete $self->{is_first};
179              
180 44 100       228 if( !defined $self->{next_time} ) {
    100          
    100          
    50          
181 17         101 $self->{next_time} = $now + $next_interval;
182             }
183             elsif( $resched eq "hard" ) {
184 23         80 $self->{next_time} += $next_interval;
185             }
186             elsif( $resched eq "skip" ) {
187             # How many ticks are needed?
188 2         50 my $ticks = POSIX::ceil( ( $now - $self->{next_time} ) / $next_interval );
189             # $self->{last_ticks} = $ticks;
190 2         14 $self->{next_time} += $next_interval * $ticks;
191             }
192             elsif( $resched eq "drift" ) {
193 2         13 $self->{next_time} = $now + $next_interval;
194             }
195              
196 44         278 $self->SUPER::start;
197             }
198              
199             sub start
200             {
201 40     40 1 925 my $self = shift;
202              
203 40         91 $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       110 if( $self->loop ) {
209 17         78 $self->_reschedule;
210             }
211             else {
212 23         75 $self->SUPER::start;
213             }
214             }
215              
216             sub stop
217             {
218 27     27 1 69 my $self = shift;
219 27         166 $self->SUPER::stop;
220              
221 27         77 undef $self->{next_time};
222             }
223              
224             sub _make_cb
225             {
226 15     15   38 my $self = shift;
227              
228             return $self->_capture_weakself( sub {
229 28 50   28   173 my $self = shift or return;
230              
231 28         121 undef $self->{id};
232              
233 28 100       149 my $ok = eval { $self->invoke_event( on_tick => ); 1 } or
  28         294  
  26         204  
234             my $e = $@;
235              
236             # detect ->stop
237 28 100       292 $self->_reschedule if defined $self->{next_time};
238              
239 28 100       151 die $e if !$ok;
240 15         156 } );
241             }
242              
243             sub _make_enqueueargs
244             {
245 44     44   113 my $self = shift;
246              
247 44         353 return at => $self->{next_time};
248             }
249              
250             =head1 AUTHOR
251              
252             Paul Evans
253              
254             =cut
255              
256             0x55AA;