Add 'last run' timestamps for recurring events, and take these into

account when firing events on daemon rescheduling (via HUP.)

FossilOrigin-Name: 1620e80e8bddd27741569dea0ff4bd84ba29429861ed49d17f5777c238ae4876
This commit is contained in:
mahlon@laika.com 2014-10-28 18:53:17 +00:00
parent b18647f6a5
commit 7c2954b0ad
6 changed files with 79 additions and 8 deletions

View file

@ -50,7 +50,7 @@ Symphony connection information:
== Adding Actions == Adding Actions
There are two primary components to Metronome -- getting actions into There are two primary components to Metronome -- getting schedules into
its database, and performing some task with those actions when the time its database, and performing some task with those actions when the time
is appropriate. is appropriate.
@ -76,6 +76,15 @@ starts up or receives a HUP signal, it will re-read and schedule out
upcoming work. upcoming work.
== Performing Work
Calling 'run' on the Metronome class is a blocking call, waking up upon
a scheduled event. The run method expects a ruby block, and it receives
the payload and the database ID of the scheduled event. Metronome is
unopinioned, what you do within this block is entirely up to you. See
the Synopsis section above for some examples.
== Options == Options
Metronome uses Metronome uses

View file

@ -140,4 +140,5 @@ lib/symphony/metronome/mixins.rb
lib/symphony/metronome/intervalexpression.rb lib/symphony/metronome/intervalexpression.rb
lib/symphony/metronome/scheduledevent.rb lib/symphony/metronome/scheduledevent.rb
data/symphony-metronome/migrations/20140419_initial.rb data/symphony-metronome/migrations/20140419_initial.rb
data/symphony-metronome/migrations/20141028_lastrun.rb
README.rdoc README.rdoc

View file

@ -0,0 +1,23 @@
# vim: set nosta noet ts=4 sw=4:
### Add a 'lastrun' time stamp for recurring events.
###
class Lastrun < Sequel::Migration
def initialize( db )
@db = db
end
def up
if @db.adapter_scheme == :postgres
add_column :metronome, :lastrun, timestamptz
else
add_column :metronome, :lastrun, DateTime
end
end
def down
drop_column :metronome, :lastrun
end
end

View file

@ -9,7 +9,7 @@ module Symphony::Metronome
Configurability Configurability
# Library version constant # Library version constant
VERSION = '0.1.0' VERSION = '0.2.0'
# Version-control revision constant # Version-control revision constant
REVISION = %q$Revision$ REVISION = %q$Revision$
@ -74,6 +74,5 @@ module Symphony::Metronome
return Symphony::Metronome::Scheduler.run( &block ) return Symphony::Metronome::Scheduler.run( &block )
end end
end # Symphony::Metronome end # Symphony::Metronome

View file

@ -24,8 +24,8 @@ class Symphony::Metronome::ScheduledEvent
# Configure defaults. # Configure defaults.
# #
CONFIG_DEFAULTS = { CONFIG_DEFAULTS = {
db: 'sqlite:///tmp/metronome.db', :db => 'sqlite:///tmp/metronome.db',
splay: 0 :splay => 0
} }
class << self class << self
@ -52,6 +52,7 @@ class Symphony::Metronome::ScheduledEvent
# #
migrations_dir = Symphony::Metronome::DATADIR + 'migrations' migrations_dir = Symphony::Metronome::DATADIR + 'migrations'
unless Sequel::Migrator.is_current?( self.db, migrations_dir.to_s ) unless Sequel::Migrator.is_current?( self.db, migrations_dir.to_s )
Loggability[ Symphony ].info "Installing database schema..."
Sequel::Migrator.apply( self.db, migrations_dir.to_s ) Sequel::Migrator.apply( self.db, migrations_dir.to_s )
end end
end end
@ -97,6 +98,8 @@ class Symphony::Metronome::ScheduledEvent
@event = Symphony::Metronome::IntervalExpression.parse( row[:expression], row[:created] ) @event = Symphony::Metronome::IntervalExpression.parse( row[:expression], row[:created] )
@options = row.delete( :options ) @options = row.delete( :options )
@id = row.delete( :id ) @id = row.delete( :id )
@ds = self.class.db[ :metronome ].filter( :id => self.id )
self.reset_runtime self.reset_runtime
unless self.class.splay.zero? unless self.class.splay.zero?
@ -105,6 +108,9 @@ class Symphony::Metronome::ScheduledEvent
end end
end end
# The sequel dataset representing this event.
attr_reader :ds
# The parsed interval expression. # The parsed interval expression.
attr_reader :event attr_reader :event
@ -133,7 +139,21 @@ class Symphony::Metronome::ScheduledEvent
# Otherwise, the event should already be running (start time has already # Otherwise, the event should already be running (start time has already
# elapsed), so schedule it forward on it's next interval iteration. # elapsed), so schedule it forward on it's next interval iteration.
# #
@runtime = now + self.event.interval # If it's a recurring event that has run before, consider the elapsed time
# as part of the next calculation.
#
row = self.ds.first
if self.event.recurring && row
last = row[ :lastrun ]
if last && now > last
@runtime = now + self.event.interval - ( now - last )
else
@runtime = now + self.event.interval
end
else
@runtime = now + self.event.interval
end
end end
@ -141,13 +161,32 @@ class Symphony::Metronome::ScheduledEvent
### deserialized options, the action ID to the supplied block if ### deserialized options, the action ID to the supplied block if
### this event is okay to execute. ### this event is okay to execute.
### ###
### If the event is recurring, perform additional checks against the
### last run time.
###
### Automatically remove the event if it has expired. ### Automatically remove the event if it has expired.
### ###
def fire def fire
rv = self.event.fire? rv = self.event.fire?
# Just based on the expression parser, is this event ready to fire?
#
if rv if rv
opts = Yajl.load( self.options ) opts = Yajl.load( self.options )
# Don't fire recurring events unless their interval has elapsed.
# This prevents events from triggering when the daemon receives
# a HUP.
#
if self.event.recurring
now = Time.now
last = self.ds.first[ :lastrun ]
return false if last && now - last < self.event.interval
# Mark the time this recurring event was fired.
self.ds.update( :lastrun => Time.now )
end
yield opts, self.id yield opts, self.id
end end
@ -160,7 +199,7 @@ class Symphony::Metronome::ScheduledEvent
### ###
def delete def delete
self.log.debug "Removing action %p" % [ self.id ] self.log.debug "Removing action %p" % [ self.id ]
self.class.db[ :metronome ].filter( :id => self.id ).delete self.ds.delete
end end

View file

@ -18,7 +18,7 @@ class Symphony::Metronome::Scheduler
SIGNALS = [ :HUP, :INT, :TERM ] SIGNALS = [ :HUP, :INT, :TERM ]
CONFIG_DEFAULTS = { CONFIG_DEFAULTS = {
:listen => true :listen => false
} }
class << self class << self