File Coverage

blib/lib/IO/Async/Timer/Periodic.pm
Criterion Covered Total %
statement 69 70 98.5
branch 34 42 80.9
condition 4 5 80.0
subroutine 12 12 100.0
pod 3 3 100.0
total 122 132 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-2024 -- leonerd@leonerd.org.uk
5              
6             package IO::Async::Timer::Periodic 0.805;
7              
8 4     4   233203 use v5.14;
  4         19  
9 4     4   21 use warnings;
  4         8  
  4         336  
10 4     4   22 use base qw( IO::Async::Timer );
  4         7  
  4         1766  
11              
12 4     4   28 use Carp;
  4         8  
  4         3865  
13              
14             =head1 NAME
15              
16             C - event callback at regular intervals
17              
18             =head1 SYNOPSIS
19              
20             =for highlighter language=perl
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   36 my $self = shift;
118 15         81 $self->SUPER::_init( @_ );
119              
120 15         74 $self->{reschedule} = "hard";
121             }
122              
123             sub configure
124             {
125 33     33 1 76 my $self = shift;
126 33         103 my %params = @_;
127              
128 33 100       100 if( exists $params{on_tick} ) {
129 5         13 my $on_tick = delete $params{on_tick};
130 5 50       80 ref $on_tick or croak "Expected 'on_tick' as a reference";
131              
132 5         15 $self->{on_tick} = $on_tick;
133 5         11 undef $self->{cb}; # Will be lazily constructed when needed
134             }
135              
136 33 100       84 if( exists $params{interval} ) {
137 25 100       92 $self->is_running and croak "Cannot configure 'interval' of a running timer\n";
138              
139 24         160 my $interval = delete $params{interval};
140 24 50       104 $interval > 0 or croak "Expected a 'interval' as a positive number";
141              
142 24         70 $self->{interval} = $interval;
143             }
144              
145 32 100       90 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       5 $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       107 if( exists $params{reschedule} ) {
155 2   50     8 my $resched = delete $params{reschedule} || "hard";
156 2 50       7 grep { $_ eq $resched } qw( hard skip drift ) or
  6         15  
157             croak "Expected 'reschedule' to be one of hard, skip, drift";
158              
159 2         6 $self->{reschedule} = $resched;
160             }
161              
162 32 50       119 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         117 $self->SUPER::configure( %params );
167             }
168              
169             sub _reschedule
170             {
171 46     46   85 my $self = shift;
172              
173 46         194 my $now = $self->loop->time;
174 46         121 my $resched = $self->{reschedule};
175              
176             my $next_interval = $self->{is_first} && defined $self->{first_interval}
177 46 100 100     270 ? $self->{first_interval} : $self->{interval};
178 46         115 delete $self->{is_first};
179              
180 46 100       312 if( !defined $self->{next_time} ) {
    100          
    100          
    50          
181 17         55 $self->{next_time} = $now + $next_interval;
182             }
183             elsif( $resched eq "hard" ) {
184 25         74 $self->{next_time} += $next_interval;
185             }
186             elsif( $resched eq "skip" ) {
187             # How many ticks are needed?
188 2         22 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         8 $self->{next_time} = $now + $next_interval;
194             }
195              
196 46         260 $self->SUPER::start;
197             }
198              
199             sub start
200             {
201 40     40 1 911 my $self = shift;
202              
203 40         85 $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       107 if( $self->loop ) {
209 17         81 $self->_reschedule;
210             }
211             else {
212 23         63 $self->SUPER::start;
213             }
214             }
215              
216             sub stop
217             {
218 27     27 1 69 my $self = shift;
219 27         129 $self->SUPER::stop;
220              
221 27         89 undef $self->{next_time};
222             }
223              
224             sub _make_cb
225             {
226 15     15   29 my $self = shift;
227              
228             return $self->_capture_weakself( sub {
229 30 50   30   169 my $self = shift or return;
230              
231 30         124 undef $self->{id};
232              
233 30 100       85 my $ok = eval { $self->invoke_event( on_tick => ); 1 } or
  30         264  
  28         152  
234             my $e = $@;
235              
236             # detect ->stop
237 30 100       287 $self->_reschedule if defined $self->{next_time};
238              
239 30 100       157 die $e if !$ok;
240 15         213 } );
241             }
242              
243             sub _make_enqueueargs
244             {
245 46     46   112 my $self = shift;
246              
247 46         283 return at => $self->{next_time};
248             }
249              
250             =head1 AUTHOR
251              
252             Paul Evans
253              
254             =cut
255              
256             0x55AA;