]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/timer.rb
timer: tick when rescheduling
[user/henk/code/ruby/rbot.git] / lib / rbot / timer.rb
index 64b060bae65814a4cfe01e5f6f645fa15e6b5e72..bebe41f2fd589ba2fd9ccd41db8d354eb7dde0f2 100644 (file)
@@ -2,18 +2,18 @@ module Timer
 
   # timer event, something to do and when/how often to do it
   class Action
-    
+
     # when this action is due next (updated by tick())
-    attr_accessor :in
-    
+    attr_reader :in
+
     # is this action blocked? if so it won't be run
     attr_accessor :blocked
-    
+
     # period:: how often (seconds) to run the action
     # data::   optional data to pass to the proc
     # once::   optional, if true, this action will be run once then removed
     # func::   associate a block to be called to perform the action
-    # 
+    #
     # create a new action
     def initialize(period, data=nil, once=false, &func)
       @blocked = false
@@ -22,102 +22,205 @@ module Timer
       @func = func
       @data = data
       @once = once
+      @last_tick = Time.new
+    end
+
+    def tick
+      diff = Time.new - @last_tick
+      @in -= diff
+      @last_tick = Time.new
+    end
+
+    def inspect
+      "#<#{self.class}:#{@period}s:#{@once ? 'once' : 'repeat'}>"
+    end
+
+    def due?
+      @in <= 0
     end
 
     # run the action by calling its proc
     def run
       @in += @period
-      if(@data)
-        @func.call(@data)
-      else
-        @func.call
+      # really short duration timers can overrun and leave @in negative,
+      # for these we set @in to @period
+      @in = @period if @in <= 0
+      begin
+        if(@data)
+          @func.call(@data)
+        else
+          @func.call
+        end
+      rescue Exception => e
+        error "Timer action #{self.inspect} with function #{@func.inspect} failed!"
+        error e.pretty_inspect
+        # TODO maybe we want to block this Action?
       end
       return @once
     end
+
+    # reschedule the Action to change its period
+    def reschedule(new_period)
+      @period = new_period
+      @in = new_period
+    end
   end
-  
+
   # timer handler, manage multiple Action objects, calling them when required.
   # The timer must be ticked by whatever controls it, i.e. regular calls to
   # tick() at whatever granularity suits your application's needs.
-  # Alternatively you can call run(), and the timer will tick itself, but this
-  # blocks so you gotta do it in a thread (remember ruby's threads block on
-  # syscalls so that can suck).
+  #
+  # Alternatively you can call run(), and the timer will spawn a thread and
+  # tick itself, intelligently shutting down the thread if there are no
+  # pending actions.
   class Timer
-    def initialize
-      @timers = Array.new
+    def initialize(granularity = 0.1)
+      @granularity = granularity
+      @timers = Hash.new
       @handle = 0
       @lasttime = 0
+      @should_be_running = false
+      @thread = false
+      @next_action_time = 0
     end
-    
+
     # period:: how often (seconds) to run the action
     # data::   optional data to pass to the action's proc
     # func::   associate a block with add() to perform the action
-    # 
+    #
     # add an action to the timer
     def add(period, data=nil, &func)
+      debug "adding timer, period #{period}"
       @handle += 1
       @timers[@handle] = Action.new(period, data, &func)
+      start_on_add
       return @handle
     end
 
-    # period:: how often (seconds) to run the action
+    # period:: how long (seconds) until the action is run
     # data::   optional data to pass to the action's proc
     # func::   associate a block with add() to perform the action
-    # 
+    #
     # add an action to the timer which will be run just once, after +period+
     def add_once(period, data=nil, &func)
+      debug "adding one-off timer, period #{period}"
       @handle += 1
       @timers[@handle] = Action.new(period, data, true, &func)
+      start_on_add
       return @handle
     end
 
     # remove action with handle +handle+ from the timer
     def remove(handle)
-      @timers.delete_at(handle)
+      @timers.delete(handle)
     end
-    
+
     # block action with handle +handle+
     def block(handle)
+      raise "no such timer #{handle}" unless @timers[handle]
       @timers[handle].blocked = true
     end
 
     # unblock action with handle +handle+
     def unblock(handle)
+      raise "no such timer #{handle}" unless @timers[handle]
       @timers[handle].blocked = false
     end
-    
+
+    # reschedule action with handle +handle+ to change its period
+    def reschedule(handle, period)
+      raise "no such timer #{handle}" unless @timers[handle]
+      @timers[handle].reschedule(period)
+      tick
+    end
+
     # you can call this when you know you're idle, or you can split off a
     # thread and call the run() method to do it for you.
-    def tick 
-      if(@lasttime != 0)
-        diff = (Time.now - @lasttime).to_f
-        @lasttime = Time.now
-        @timers.compact.each { |timer|
-          timer.in = timer.in - diff
-        }
-        @timers.compact.each { |timer|
-          if (!timer.blocked)
-            if(timer.in <= 0)
-              if(timer.run)
-                # run once
-                @timers.delete(timer)
-              end
-            end
-          end
-        }
-      else
+    def tick
+      if(@lasttime == 0)
         # don't do anything on the first tick
         @lasttime = Time.now
+        return
       end
+      @next_action_time = 0
+      diff = (Time.now - @lasttime).to_f
+      @lasttime = Time.now
+      @timers.each { |key,timer|
+        timer.tick
+        next if timer.blocked
+        if(timer.due?)
+          if(timer.run)
+            # run once
+            @timers.delete(key)
+          end
+        end
+        if @next_action_time == 0 || timer.in < @next_action_time
+          @next_action_time = timer.in
+        end
+      }
+      #debug "ticked. now #{@timers.length} timers remain"
+      #debug "next timer due at #{@next_action_time}"
     end
 
-    # the timer will tick() itself. this blocks, so run it in a thread, and
-    # watch out for blocking syscalls
+    # for backwards compat - this is a bit primitive
     def run(granularity=0.1)
       while(true)
         sleep(granularity)
         tick
       end
     end
+
+    def running?
+      @thread && @thread.alive?
+    end
+
+    # return the number of seconds until the next action is due, or 0 if
+    # none are outstanding - will only be accurate immediately after a
+    # tick()
+    def next_action_time
+      @next_action_time
+    end
+
+    # start the timer, it spawns a thread to tick the timer, intelligently
+    # shutting down if no events remain and starting again when needed.
+    def start
+      return if running?
+      @should_be_running = true
+      start_thread unless @timers.empty?
+    end
+
+    # stop the timer from ticking
+    def stop
+      @should_be_running = false
+      stop_thread
+    end
+
+    private
+
+    def start_on_add
+      if running?
+        stop_thread
+        start_thread
+      elsif @should_be_running
+        start_thread
+      end
+    end
+
+    def stop_thread
+      return unless running?
+      @thread.kill
+    end
+
+    def start_thread
+      return if running?
+      @thread = Thread.new do
+        while(true)
+          tick
+          exit if @timers.empty?
+          sleep(@next_action_time)
+        end
+      end
+    end
+
   end
 end