* (timers) failures made more verbose; reverted timer.block()s in ircbot.rb
[rbot] / lib / rbot / timer.rb
1 # changes:
2 #  1. Timer::Timer ---> Timer
3 #  2. timer id is now the object_id of the action
4 #  3. Timer resolution removed, we're always arbitrary precision now
5 #  4. I don't see any obvious races [not that i did see any in old impl, though]
6 #  5. We're tickless now, so no need to jerk start/stop
7 #  6. We should be pretty fast now, wrt old impl
8 #  7. reschedule/remove/block now accept nil as an action id (meaning "current")
9 #  8. repeatability is ignored for 0-period repeatable timers
10 #  9. configure() method superceeds reschedule() [the latter stays as compat]
11
12 require 'thread'
13 require 'monitor'
14
15 # Timer handler, manage multiple Action objects, calling them when required.
16 # When the Timer is constructed, a new Thread is created to manage timed
17 # delays and run Actions.
18 #
19 # XXX: there is no way to stop the timer currently. I'm keeping it this way
20 # to weed out old Timer implementation legacy in rbot code. -jsn.
21 class Timer
22
23   # class representing individual timed action
24   class Action
25
26     # Time when the Action should be called next
27     attr_accessor :next
28
29     # options are:
30     #   start::    Time when the Action should be run for the first time.
31     #               Repeatable Actions will be repeated after that, see
32     #               :period. One-time Actions will not (obviously)
33     #               Default: Time.now + :period
34     #   period::   How often repeatable Action should be run, in seconds.
35     #               Default: 1
36     #   blocked::  if true, Action starts as blocked (i.e. will stay dormant
37     #               until unblocked)
38     #   args::     Arguments to pass to the Action callback. Default: []
39     #   repeat::   Should the Action be called repeatedly? Default: false
40     #   code::     You can specify the Action body using &block, *or* using
41     #               this option.
42
43     def initialize(options = {}, &block)
44       opts = {
45         :period => 1,
46         :blocked => false,
47         :args => [],
48         :repeat => false
49       }.merge(options)
50
51       @block = nil
52       debug("adding timer #{self} :period => #{opts[:period]}, :repeat => #{opts[:repeat].inspect}")
53       self.configure(opts, &block)
54       debug("added #{self}")
55     end
56
57     # Provides for on-the-fly reconfiguration of the Actions
58     # Accept the same arguments as the constructor
59     def configure(opts = {}, &block)
60       @period = opts[:period] if opts.include? :period
61       @blocked = opts[:blocked] if opts.include? :blocked
62       @repeat = opts[:repeat] if opts.include? :repeat
63
64       if block_given?
65         @block = block 
66       elsif opts[:code]
67         @block = opts[:code]
68       end
69
70       raise 'huh?? blockless action?' unless @block
71       if opts.include? :args
72         @args = Array === opts[:args] ? opts[:args] : [opts[:args]]
73       end
74
75       if opts[:start] and (Time === opts[:start])
76         self.next = opts[:start]
77       else
78         self.next = Time.now + (opts[:start] || @period)
79       end
80     end
81
82     # modify the Action period
83     def reschedule(period, &block)
84       self.configure(:period => period, &block)
85     end
86
87     # blocks an Action, so it won't be run
88     def block
89       @blocked = true
90     end
91
92     # unblocks a blocked Action
93     def unblock
94       @blocked = false
95     end
96
97     def blocked?
98       @blocked
99     end
100
101     # calls the Action callback, resets .next to the Time of the next call,
102     # if the Action is repeatable.
103     def run(now = Time.now)
104       raise 'inappropriate time to run()' unless self.next && self.next <= now
105       self.next = nil
106       begin
107         @block.call(*@args)
108       rescue Exception => e
109         error "Timer action #{self.inspect}: block #{@block.inspect} failed!"
110         error e.pretty_inspect
111         debug e.backtrace.join("\n")
112       end
113
114       if @repeat && @period > 0
115         self.next = now + @period
116       end
117
118       return self.next
119     end
120   end
121
122   # creates a new Timer and starts it.
123   def initialize
124     self.extend(MonitorMixin)
125     @tick = self.new_cond
126     @thread = nil
127     @actions = Hash.new
128     @current = nil
129     self.start
130   end
131
132   # creates and installs a new Action, repeatable by default.
133   #    period:: Action period
134   #    opts::   options for Action#new, see there
135   #    block::  Action callback code
136   # returns the id of the created Action
137   def add(period, opts = {}, &block)
138     a = Action.new({:repeat => true, :period => period}.merge(opts), &block)
139     self.synchronize do
140       @actions[a.object_id] = a
141       @tick.signal
142     end
143     return a.object_id
144   end
145
146   # creates and installs a new Action, one-time by default.
147   #    period:: Action delay
148   #    opts::   options for Action#new, see there
149   #    block::  Action callback code
150   # returns the id of the created Action
151   def add_once(period, opts = {}, &block)
152     self.add(period, {:repeat => false}.merge(opts), &block)
153   end
154
155   # blocks an existing Action
156   #    aid:: Action id, obtained previously from add() or add_once()
157   def block(aid)
158     debug "blocking #{aid}"
159     self.synchronize { self[aid].block }
160   end
161
162   # unblocks an existing blocked Action
163   #    aid:: Action id, obtained previously from add() or add_once()
164   def unblock(aid)
165     debug "unblocking #{aid}"
166     self.synchronize do
167       self[aid].unblock
168       @tick.signal
169     end
170   end
171
172   # removes an existing blocked Action
173   #    aid:: Action id, obtained previously from add() or add_once()
174   def remove(aid)
175     self.synchronize do
176       @actions.delete(aid) # or raise "nonexistent action #{aid}"
177     end
178   end
179
180   alias :delete :remove
181
182   # Provides for on-the-fly reconfiguration of Actions
183   #    aid::   Action id, obtained previously from add() or add_once()
184   #    opts::  see Action#new
185   #   block:: (optional) new Action callback code
186   def configure(aid, opts = {}, &block)
187     self.synchronize do
188       self[aid].configure(opts, &block)
189       @tick.signal
190     end
191   end
192
193   # changes Action period
194   #   aid:: Action id
195   #   period:: new period
196   #   block:: (optional) new Action callback code
197   def reschedule(aid, period, &block)
198     self.configure(aid, :period => period, &block)
199   end
200
201   protected
202
203   def start
204     raise 'double-started timer' if @thread
205     @thread = Thread.new do
206       loop do
207         tmout = self.run_actions
208         self.synchronize { @tick.wait(tmout) }
209       end
210     end
211   end
212
213   def [](aid)
214     aid ||= @current
215     raise "no current action" unless aid
216     raise "nonexistent action #{aid}" unless @actions.include? aid
217     @actions[aid]
218   end
219
220   def run_actions(now = Time.now)
221     nxt = nil
222     @actions.keys.each do |k|
223       a = @actions[k]
224       next if (!a) or a.blocked?
225
226       if a.next <= now
227         begin
228           @current = k
229           v = a.run(now)
230         ensure
231           @current = nil
232         end
233           
234         unless v
235           @actions.delete k
236           next
237         end
238       else
239         v = a.next
240       end
241
242       nxt = v if v and ((!nxt) or (v < nxt))
243     end
244
245     if nxt
246       delta = nxt - now
247       delta = 0 if delta < 0
248       return delta
249     else
250       return nil
251     end
252   end
253
254 end