search: some ddg reworking
[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   #
137   # Returns the id of the created Action
138   def add(period, opts = {}, &block)
139     a = Action.new({:repeat => true, :period => period}.merge(opts), &block)
140     self.synchronize do
141       @actions[a.object_id] = a
142       @tick.signal
143     end
144     return a.object_id
145   end
146
147   # Creates and installs a new Action, one-time by default.
148   # _period_:: Action delay
149   # _opts_::   options for Action#new, see there
150   # _block_::  Action callback code
151   #
152   # Returns the id of the created Action
153   def add_once(period, opts = {}, &block)
154     self.add(period, {:repeat => false}.merge(opts), &block)
155   end
156
157   # blocks an existing Action
158   # _aid_:: Action id, obtained previously from add() or add_once()
159   def block(aid)
160     debug "blocking #{aid}"
161     self.synchronize { self[aid].block }
162   end
163
164   # unblocks an existing blocked Action
165   # _aid_:: Action id, obtained previously from add() or add_once()
166   def unblock(aid)
167     debug "unblocking #{aid}"
168     self.synchronize do
169       self[aid].unblock
170       @tick.signal
171     end
172   end
173
174   # removes an existing blocked Action
175   # _aid_:: Action id, obtained previously from add() or add_once()
176   def remove(aid)
177     self.synchronize do
178       @actions.delete(aid) # or raise "nonexistent action #{aid}"
179     end
180   end
181
182   alias :delete :remove
183
184   # Provides for on-the-fly reconfiguration of Actions
185   # _aid_::   Action id, obtained previously from add() or add_once()
186   # _opts_::  see Action#new
187   # _block_:: (optional) new Action callback code
188   def configure(aid, opts = {}, &block)
189     self.synchronize do
190       self[aid].configure(opts, &block)
191       @tick.signal
192     end
193   end
194
195   # changes Action period
196   # _aid_:: Action id
197   # _period_:: new period
198   # _block_:: (optional) new Action callback code
199   def reschedule(aid, period, &block)
200     self.configure(aid, :period => period, &block)
201   end
202
203   def start
204     raise 'already started' if @thread
205     @stopping = false
206     debug "starting timer #{self}"
207     @thread = Thread.new do
208       loop do
209         tmout = self.run_actions
210         break if tmout and tmout < 0
211         self.synchronize { @tick.wait(tmout) }
212       end
213     end
214   end
215
216   def stop
217     unless @thread
218       warning 'trying to stop already stopped timer'
219       return
220     end
221     debug "stopping timer #{self}..."
222     @stopping = true
223     self.synchronize { @tick.signal }
224     @thread.join(60) or @thread.kill
225     debug "timer #{self} stopped"
226     @thread = nil
227   end
228
229   protected
230
231   def [](aid)
232     aid ||= @current
233     raise "no current action" unless aid
234     raise "nonexistent action #{aid}" unless @actions.include? aid
235     @actions[aid]
236   end
237
238   def run_actions(now = Time.now)
239     @actions.keys.each do |k|
240       return -1 if @stopping
241       a = @actions[k] or next
242       next if a.blocked? || a.next > now
243
244       begin
245         @current = k
246         a.run(now)
247       ensure
248         @current = nil
249       end
250
251       @actions.delete k unless a.next
252     end
253
254     nxt = @actions.values.find_all { |v| !v.blocked? }.map{ |v| v.next }.min
255
256     if nxt
257       delta = nxt - now
258       delta = 0 if delta < 0
259       return delta
260     else
261       return nil
262     end
263   end
264
265 end