Skip Menu |

This queue is for tickets about the IO-Async CPAN distribution.

Report information
The Basics
Id: 77090
Status: resolved
Priority: 0/
Queue: IO-Async

People
Owner: Nobody in particular
Requestors: leonerd-cpan [...] leonerd.org.uk
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: 0.47
Fixed in: 0.48



Subject: Allow Periodic timers to skip missed ticks
Create Periodic timer for, say, 10 seconds interval. Suspend machine. Wake up several hours later. It will now fire the event lots of times. Consider a "skip" option of some kind, pass number of skipped ticks into callback. Consider attempting to resynchronise vs. just continuing where it left off. -- Paul Evans
It occurs to me there are various behaviours to take when trying to handle a missed tick: 1: run on_tick once per missing tick (current behaviour) 2: run on_tick just once, and somehow let it know how many ticks were missed - passed in, or an accessor on $self Additionally, decisions on whether to restart timing from the new time, or continue using the previous sequence after it's been caught up. Requires option names... hrmmmmm -- Paul Evans
I'm now thinking of a parameter called "reschedule", with three possible values: reschedule => "hard" The current behaviour. Next tick runs at previous + interval always, even if that's still in the past. May result in a burst of activity. reschedule => "soft" Next tick runs at interval delay after the on_tick method returns. Allows for drift. reschedule => "hard+count" Next tick runs at previous + ( $N * interval ), for the smallest value of $N that puts it in the future. During the on_tick method, the value of $N should be accessible somehow - passed in, or $self->ticks_past, or something -- Paul Evans
Went with hard/skip/drift in the end. -- Paul Evans
Subject: rt77090.patch
=== modified file 'lib/IO/Async/Timer/Periodic.pm' --- lib/IO/Async/Timer/Periodic.pm 2012-03-24 21:08:34 +0000 +++ lib/IO/Async/Timer/Periodic.pm 2012-05-18 22:04:04 +0000 @@ -1,7 +1,7 @@ # You may distribute under the terms of either the GNU General Public License # or the Artistic License (the same terms as Perl itself) # -# (C) Paul Evans, 2009-2011 -- leonerd@leonerd.org.uk +# (C) Paul Evans, 2009-2012 -- leonerd@leonerd.org.uk package IO::Async::Timer::Periodic; @@ -41,9 +41,10 @@ =head1 DESCRIPTION This subclass of L<IO::Async::Timer> implements repeating events at regular -clock intervals. The timing is not subject to how long it takes the callback -to execute, but runs at regular intervals beginning at the time the timer was -started, then adding each interval thereafter. +clock intervals. The timing may or may not be subject to how long it takes the +callback to execute. Iterations may be rescheduled runs at fixed regular +intervals beginning at the time the timer was started, or by a fixed delay +after the previous code has finished executing. For a C<Timer> object that only runs a callback once, after a given delay, see instead L<IO::Async::Timer::Countdown>. A Countdown timer can also be used to @@ -90,6 +91,24 @@ by the containing C<Loop> object, and not synchronously by the C<start> method itself. +=item reschedule => STRING + +Optional. Must be one of C<hard>, C<skip> or C<drift>. Defines the algorithm +used to reschedule the next invocation. + +C<hard> schedules each iteration at the fixed interval from the previous +iteration's schedule time, ensuring a regular repeating event. + +C<skip> schedules similarly to C<hard>, but skips over times that have already +passed. This matters if the duration is particularly short and there's a +possibility that times may be missed, or if the entire process is stopped and +resumed by C<SIGSTOP> or similar. + +C<drift> schedules each iteration at the fixed interval from the time that the +previous iteration's event handler returns. This allows it to slowly drift over +time and become desynchronised with other events of the same interval or +multiples/fractions of it. + =back Once constructed, the timer object will need to be added to the C<Loop> before @@ -97,6 +116,14 @@ =cut +sub _init +{ + my $self = shift; + $self->SUPER::_init( @_ ); + + $self->{reschedule} = "hard"; +} + sub configure { my $self = shift; @@ -128,6 +155,14 @@ $self->{first_interval} = $first_interval; } + if( exists $params{reschedule} ) { + my $resched = delete $params{reschedule} || "hard"; + grep { $_ eq $resched } qw( hard skip drift ) or + croak "Expected 'reschedule' to be one of hard, skip, drift"; + + $self->{reschedule} = $resched; + } + unless( $self->can_event( 'on_tick' ) ) { croak 'Expected either a on_tick callback or an ->on_tick method'; } @@ -150,12 +185,24 @@ # become start-pending. We'll calculate it properly when it gets added to # the Loop if( my $loop = $self->loop ) { + my $now = $loop->time; + my $resched = $self->{reschedule}; + if( !defined $self->{next_time} ) { - $self->{next_time} = $loop->time + $self->_next_interval; + $self->{next_time} = $now + $self->_next_interval; } - else { + elsif( $resched eq "hard" ) { $self->{next_time} += $self->_next_interval; } + elsif( $resched eq "skip" ) { + # How many ticks are needed? + my $ticks = POSIX::ceil( $now - $self->{next_time} ); + # $self->{last_ticks} = $ticks; + $self->{next_time} += $self->_next_interval * $ticks; + } + elsif( $resched eq "drift" ) { + $self->{next_time} = $now + $self->_next_interval; + } } $self->SUPER::start; @@ -179,9 +226,10 @@ undef $self->{first_interval}; undef $self->{id}; + + $self->invoke_event( on_tick => ); + $self->start; - - $self->invoke_event( on_tick => ); } ); } === modified file 't/22timer-periodic.t' --- t/22timer-periodic.t 2012-03-12 18:33:45 +0000 +++ t/22timer-periodic.t 2012-05-18 18:58:19 +0000 @@ -4,7 +4,7 @@ use IO::Async::Test; -use Test::More tests => 37; +use Test::More tests => 47; use Test::Fatal; use Test::Refcount; @@ -122,6 +122,52 @@ is_oneref( $timer, 'Timer has refcount 1 finally' ); } +# reschedule => "skip" +{ + my $tick = 0; + + my $timer = IO::Async::Timer::Periodic->new( + interval => 1 * AUT, + reschedule => "skip", + + on_tick => sub { $tick++ }, + ); + + $loop->add( $timer ); + $timer->start; + + time_about( sub { wait_for { $tick == 1 } }, 1, 'skip Timer works' ); + + ok( $timer->is_running, 'skip Timer is still running' ); + + time_about( sub { wait_for { $tick == 2 } }, 1, 'skip Timer ticks a second time' ); + + $loop->remove( $timer ); +} + +# reschedule => "drift" +{ + my $tick = 0; + + my $timer = IO::Async::Timer::Periodic->new( + interval => 1 * AUT, + reschedule => "drift", + + on_tick => sub { $tick++ }, + ); + + $loop->add( $timer ); + $timer->start; + + time_about( sub { wait_for { $tick == 1 } }, 1, 'drift Timer works' ); + + ok( $timer->is_running, 'drift Timer is still running' ); + + time_about( sub { wait_for { $tick == 2 } }, 1, 'drift Timer ticks a second time' ); + + $loop->remove( $timer ); +} + ## Subclass my $sub_tick = 0;
Released in 0.48 -- Paul Evans