Class: Symphony::Metronome::ScheduledEvent

Inherits:
Object
  • Object
show all
Extended by:
Configurability, Loggability
Includes:
Comparable
Defined in:
lib/symphony/metronome/scheduledevent.rb

Overview

A class the represents the relationship between an interval and an event.

Constant Summary collapse

CONFIG_DEFAULTS =

Configure defaults.

{
  :db    => 'sqlite:///tmp/metronome.db',
  :splay => 0
}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(row) ⇒ ScheduledEvent

Create a new ScheduledEvent object.



97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/symphony/metronome/scheduledevent.rb', line 97

def initialize( row )
  @event    = Symphony::Metronome::IntervalExpression.parse( row[:expression], row[:created] )
  @options  = row.delete( :options )
  @id       = row.delete( :id )
  @ds       = self.class.db[ :metronome ].filter( :id => self.id )

  self.reset_runtime

  unless self.class.splay.zero?
    splay = Range.new( - self.class.splay, self.class.splay )
    @runtime = self.runtime + rand( splay )
  end
end

Class Attribute Details

.dbObject (readonly)

A Sequel-style DB connection URI.



33
34
35
# File 'lib/symphony/metronome/scheduledevent.rb', line 33

def db
  @db
end

.splayObject (readonly)

Adjust recurring intervals by a random window.



36
37
38
# File 'lib/symphony/metronome/scheduledevent.rb', line 36

def splay
  @splay
end

Instance Attribute Details

#dsObject (readonly)

The sequel dataset representing this event.



112
113
114
# File 'lib/symphony/metronome/scheduledevent.rb', line 112

def ds
  @ds
end

#eventObject (readonly)

The parsed interval expression.



115
116
117
# File 'lib/symphony/metronome/scheduledevent.rb', line 115

def event
  @event
end

#idObject (readonly)

The unique ID number of the scheduled event.



118
119
120
# File 'lib/symphony/metronome/scheduledevent.rb', line 118

def id
  @id
end

#optionsObject (readonly)

The options hash attached to this event.



121
122
123
# File 'lib/symphony/metronome/scheduledevent.rb', line 121

def options
  @options
end

#runtimeObject (readonly)

The exact time that this event will run.



124
125
126
# File 'lib/symphony/metronome/scheduledevent.rb', line 124

def runtime
  @runtime
end

Class Method Details

.configure(config = nil) ⇒ Object

Configurability API.



46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/symphony/metronome/scheduledevent.rb', line 46

def self::configure( config=nil )
  config = self.defaults.merge( config || {} )
  @db    = Sequel.connect( config.delete(:db) )
  @splay = config.delete( :splay )

  # Ensure the database is current.
  #
  migrations_dir = Symphony::Metronome::DATADIR + 'migrations'
  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 )
  end
end

.loadObject

Return a set of all known events, sorted by date of execution. Delete any rows that are invalid expressions.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/symphony/metronome/scheduledevent.rb', line 64

def self::load
  now    = Time.now
  events = SortedSet.new

  # Force reset the DB handle.
  self.db.disconnect

  self.log.debug "Parsing/loading all actions."
  self.db[ :metronome ].each do |event|
    begin
      event = new( event )
      events << event
    rescue ArgumentError, Symphony::Metronome::TimeParseError => err
      self.log.error "%p while parsing \"%s\": %s" % [
        err.class,
        event[:expression],
        err.message
      ]
      self.log.debug "  " + err.backtrace.join( "\n  " )
      self.db[ :metronome ].filter( :id => event[:id] ).delete
    end
  end

  return events
end

Instance Method Details

#<=>(other) ⇒ Object

Comparable interface, order by next run time, soonest first.



212
213
214
# File 'lib/symphony/metronome/scheduledevent.rb', line 212

def <=>( other )
  return self.runtime <=> other.runtime
end

#deleteObject

Permanently remove this event from the database.



204
205
206
207
# File 'lib/symphony/metronome/scheduledevent.rb', line 204

def delete
  self.log.debug "Removing action %p" % [ self.id ]
  self.ds.delete
end

#fireObject

Perform the action attached to the event. Yields the deserialized options, the action ID to the supplied block if 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.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/symphony/metronome/scheduledevent.rb', line 169

def fire
  rv = self.event.fire?

  # Just based on the expression parser, is this event ready to fire?
  #
  if rv
    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
      row = self.ds.first

      if row
        last = row[ :lastrun ]
        return false if last && now - last < self.event.interval
      end

      # Mark the time this recurring event was fired.
      self.ds.update( :lastrun => Time.now )
    end

    yield opts, self.id
  end

  self.delete if rv.nil?
  return rv
end

#reset_runtimeObject

Set the datetime that this event should fire next.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/symphony/metronome/scheduledevent.rb', line 129

def reset_runtime
  now = Time.now

  # Start time is in the future, so it's sufficent to be considered the run time.
  #
  if self.event.starting >= now
    @runtime = self.event.starting
    return
  end

  # Otherwise, the event should already be running (start time has already
  # elapsed), so schedule it forward on it's next interval iteration.
  #
  # 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