]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/timer.rb
+ (timer) comments + documentation
[user/henk/code/ruby/rbot.git] / 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       end
112
113       if @repeat && @period > 0
114         self.next = now + @period
115       end
116
117       return self.next
118     end
119   end
120
121   # creates a new Timer and starts it.
122   def initialize
123     self.extend(MonitorMixin)
124     @tick = self.new_cond
125     @thread = nil
126     @actions = Hash.new
127     @current = nil
128     self.start
129   end
130
131   # creates and installs a new Action, repeatable by default.
132   #    period:: Action period
133   #    opts::   options for Action#new, see there
134   #    block::  Action callback code
135   # returns the id of the created Action
136   def add(period, opts = {}, &block)
137     a = Action.new({:repeat => true, :period => period}.merge(opts), &block)
138     self.synchronize do
139       @actions[a.object_id] = a
140       @tick.signal
141     end
142     return a.object_id
143   end
144
145   # creates and installs a new Action, one-time by default.
146   #    period:: Action delay
147   #    opts::   options for Action#new, see there
148   #    block::  Action callback code
149   # returns the id of the created Action
150   def add_once(period, opts = {}, &block)
151     self.add(period, {:repeat => false}.merge(opts), &block)
152   end
153
154   # blocks an existing Action
155   #    aid:: Action id, obtained previously from add() or add_once()
156   def block(aid)
157     debug "blocking #{aid}"
158     self.synchronize { self[aid].block }
159   end
160
161   # unblocks an existing blocked Action
162   #    aid:: Action id, obtained previously from add() or add_once()
163   def unblock(aid)
164     debug "unblocking #{aid}"
165     self.synchronize do
166       self[aid].unblock
167       @tick.signal
168     end
169   end
170
171   # removes an existing blocked Action
172   #    aid:: Action id, obtained previously from add() or add_once()
173   def remove(aid)
174     self.synchronize do
175       @actions.delete(aid) # or raise "nonexistent action #{aid}"
176     end
177   end
178
179   alias :delete :remove
180
181   # Provides for on-the-fly reconfiguration of Actions
182   #    aid::   Action id, obtained previously from add() or add_once()
183   #    opts::  see Action#new
184   #   block:: (optional) new Action callback code
185   def configure(aid, opts = {}, &block)
186     self.synchronize do
187       self[aid].configure(opts, &block)
188       @tick.signal
189     end
190   end
191
192   # changes Action period
193   #   aid:: Action id
194   #   period:: new period
195   #   block:: (optional) new Action callback code
196   def reschedule(aid, period, &block)
197     self.configure(aid, :period => period, &block)
198   end
199
200   protected
201
202   def start
203     raise 'double-started timer' if @thread
204     @thread = Thread.new do
205       loop do
206         tmout = self.run_actions
207         self.synchronize { @tick.wait(tmout) }
208       end
209     end
210   end
211
212   def [](aid)
213     aid ||= @current
214     raise "no current action" unless aid
215     raise "nonexistent action #{aid}" unless @actions.include? aid
216     @actions[aid]
217   end
218
219   def run_actions(now = Time.now)
220     nxt = nil
221     @actions.keys.each do |k|
222       a = @actions[k]
223       next if (!a) or a.blocked?
224
225       if a.next <= now
226         begin
227           @current = k
228           v = a.run(now)
229         ensure
230           @current = nil
231         end
232           
233         unless v
234           @actions.delete k
235           next
236         end
237       else
238         v = a.next
239       end
240
241       nxt = v if v and ((!nxt) or (v < nxt))
242     end
243
244     if nxt
245       delta = nxt - now
246       delta = 0 if delta < 0
247       return delta
248     else
249       return nil
250     end
251   end
252
253 end