]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/timer.rb
*** (timer) new timer implementation
[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 class Timer
16   class Action
17     attr_accessor :next
18
19     def initialize(options = {}, &block)
20       opts = {
21         :period => 1,
22         :blocked => false,
23         :args => [],
24         :repeat => false
25       }.merge(options)
26
27       @block = nil
28       debug("adding timer #{self} :period => #{opts[:period]}, :repeat => #{opts[:repeat].inspect}")
29       self.configure(opts, &block)
30       debug("added #{self}")
31     end
32
33     def configure(opts = {}, &block)
34       @period = opts[:period] if opts.include? :period
35       @blocked = opts[:blocked] if opts.include? :blocked
36       @repeat = opts[:repeat] if opts.include? :repeat
37
38       if block_given?
39         @block = block 
40       elsif opts[:code]
41         @block = opts[:code]
42       end
43
44       raise 'huh?? blockless action?' unless @block
45       if opts.include? :args
46         @args = Array === opts[:args] ? opts[:args] : [opts[:args]]
47       end
48
49       if opts[:start] and (Time === opts[:start])
50         self.next = opts[:start]
51       else
52         self.next = Time.now + (opts[:start] || @period)
53       end
54     end
55
56     def reschedule(period, &block)
57       self.configure(:period => period, &block)
58     end
59
60     def block
61       @blocked = true
62     end
63
64     def unblock
65       @blocked = false
66     end
67
68     def blocked?
69       @blocked
70     end
71
72     def run(now = Time.now)
73       raise 'inappropriate time to run()' unless self.next && self.next <= now
74       self.next = nil
75       begin
76         @block.call(*@args)
77       rescue Exception => e
78         error "Timer action #{self.inspect}: block #{@block.inspect} failed!"
79         error e.pretty_inspect
80       end
81
82       if @repeat && @period > 0
83         self.next = now + @period
84       end
85
86       return self.next
87     end
88   end
89
90   def initialize
91     self.extend(MonitorMixin)
92     @tick = self.new_cond
93     @thread = nil
94     @actions = Hash.new
95     @current = nil
96     self.start
97   end
98
99   def add(period, opts = {}, &block)
100     a = Action.new({:repeat => true, :period => period}.merge(opts), &block)
101     self.synchronize do
102       @actions[a.object_id] = a
103       @tick.signal
104     end
105     return a.object_id
106   end
107
108   def add_once(period, opts = {}, &block)
109     self.add(period, {:repeat => false}.merge(opts), &block)
110   end
111
112   def block(aid)
113     debug "blocking #{aid}"
114     self.synchronize { self[aid].block }
115   end
116
117   def unblock(aid)
118     debug "unblocking #{aid}"
119     self.synchronize do
120       self[aid].unblock
121       @tick.signal
122     end
123   end
124
125   def remove(aid)
126     self.synchronize do
127       @actions.delete(aid) # or raise "nonexistent action #{aid}"
128     end
129   end
130
131   alias :delete :remove
132
133   def configure(aid, opts = {}, &block)
134     self.synchronize do
135       self[aid].configure(opts, &block)
136       @tick.signal
137     end
138   end
139
140   def reschedule(aid, period, &block)
141     self.configure(aid, :period => period, &block)
142   end
143
144   protected
145
146   def start
147     raise 'double-started timer' if @thread
148     @thread = Thread.new do
149       loop do
150         tmout = self.run_actions
151         self.synchronize { @tick.wait(tmout) }
152       end
153     end
154   end
155
156   def [](aid)
157     aid ||= @current
158     raise "no current action" unless aid
159     raise "nonexistent action #{aid}" unless @actions.include? aid
160     @actions[aid]
161   end
162
163   def run_actions(now = Time.now)
164     nxt = nil
165     @actions.keys.each do |k|
166       a = @actions[k]
167       next if (!a) or a.blocked?
168
169       if a.next <= now
170         begin
171           @current = k
172           v = a.run(now)
173         ensure
174           @current = nil
175         end
176           
177         unless v
178           @actions.delete k
179           next
180         end
181       else
182         v = a.next
183       end
184
185       nxt = v if v and ((!nxt) or (v < nxt))
186     end
187
188     if nxt
189       delta = nxt - now
190       delta = 0 if delta < 0
191       return delta
192     else
193       return nil
194     end
195   end
196
197 end