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