]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/timer.rb
plugin(script): remove deprecated $SAFE
[user/henk/code/ruby/rbot.git] / lib / rbot / timer.rb
index 03a4c91e268505750b91fc0979079c987e2d6ebd..64b0ee431d4aeb261541e43bda38f31dc9953770 100644 (file)
-module Timer
+# changes:
+#  1. Timer::Timer ---> Timer
+#  2. timer id is now the object_id of the action
+#  3. Timer resolution removed, we're always arbitrary precision now
+#  4. I don't see any obvious races [not that i did see any in old impl, though]
+#  5. We're tickless now, so no need to jerk start/stop
+#  6. We should be pretty fast now, wrt old impl
+#  7. reschedule/remove/block now accept nil as an action id (meaning "current")
+#  8. repeatability is ignored for 0-period repeatable timers
+#  9. configure() method superceeds reschedule() [the latter stays as compat]
 
-  # timer event, something to do and when/how often to do it
+require 'thread'
+require 'monitor'
+
+# Timer handler, manage multiple Action objects, calling them when required.
+# When the Timer is constructed, a new Thread is created to manage timed
+# delays and run Actions.
+#
+# XXX: there is no way to stop the timer currently. I'm keeping it this way
+# to weed out old Timer implementation legacy in rbot code. -jsn.
+class Timer
+
+  # class representing individual timed action
   class Action
-    
-    # when this action is due next (updated by tick())
-    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
-      @period = period
-      @in = period
-      @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
+    # Time when the Action should be called next
+    attr_accessor :next
 
-    def inspect 
-      "#<#{self.class}:#{@period}s:#{@once ? 'once' : 'repeat'}>"
-    end
+    # Options are:
+    # start::    Time when the Action should be run for the first time.
+    #            Repeatable Actions will be repeated after that, see
+    #            :period. One-time Actions will not (obviously)
+    #            Default: Time.now + :period
+    # period::   How often repeatable Action should be run, in seconds.
+    #            Default: 1
+    # blocked::  if true, Action starts as blocked (i.e. will stay dormant
+    #            until unblocked)
+    # args::     Arguments to pass to the Action callback. Default: []
+    # repeat::   Should the Action be called repeatedly? Default: false
+    # code::     You can specify the Action body using &block, *or* using
+    #            this option.
 
-    def due?
-      @in <= 0
+    def initialize(options = {}, &block)
+      opts = {
+        :period => 1,
+        :blocked => false,
+        :args => [],
+        :repeat => false
+      }.merge(options)
+
+      @block = nil
+      debug("adding timer #{self} :period => #{opts[:period]}, :repeat => #{opts[:repeat].inspect}")
+      self.configure(opts, &block)
+      debug("added #{self}")
     end
 
-    # run the action by calling its proc
-    def run
-      @in += @period
-      # really short duration timers can overrun and leave @in negative,
-      # for these we set @in to @period
-      @in = @period if @in <= 0
-      if(@data)
-        @func.call(@data)
+    # Provides for on-the-fly reconfiguration of the Actions
+    # Accept the same arguments as the constructor
+    def configure(opts = {}, &block)
+      @period = opts[:period] if opts.include? :period
+      @blocked = opts[:blocked] if opts.include? :blocked
+      @repeat = opts[:repeat] if opts.include? :repeat
+
+      if block_given?
+        @block = block
+      elsif opts[:code]
+        @block = opts[:code]
+      end
+
+      raise 'huh?? blockless action?' unless @block
+      if opts.include? :args
+        @args = Array === opts[:args] ? opts[:args] : [opts[:args]]
+      end
+
+      if opts[:start] and (Time === opts[:start])
+        self.next = opts[:start]
       else
-        @func.call
+        self.next = Time.now + (opts[:start] || @period)
       end
-      return @once
-    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 spawn a thread and
-  # tick itself, intelligently shutting down the thread if there are no
-  # pending actions.
-  class Timer
-    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 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
+    # modify the Action period
+    def reschedule(period, &block)
+      self.configure(:period => period, &block)
     end
 
-    # remove action with handle +handle+ from the timer
-    def remove(handle)
-      @timers.delete(handle)
+    # blocks an Action, so it won't be run
+    def block
+      @blocked = true
     end
-    
-    # block action with handle +handle+
-    def block(handle)
-      @timers[handle].blocked = true
+
+    # unblocks a blocked Action
+    def unblock
+      @blocked = false
     end
 
-    # unblock action with handle +handle+
-    def unblock(handle)
-      @timers[handle].blocked = false
+    def blocked?
+      @blocked
     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)
-        # don't do anything on the first tick
-        @lasttime = Time.now
-        return
+    # calls the Action callback, resets .next to the Time of the next call,
+    # if the Action is repeatable.
+    def run(now = Time.now)
+      raise 'inappropriate time to run()' unless self.next && self.next <= now
+      self.next = nil
+      begin
+        @block.call(*@args)
+      rescue Exception => e
+        error "Timer action #{self.inspect}: block #{@block.inspect} failed!"
+        error e.pretty_inspect
+        debug e.backtrace.join("\n")
       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
 
-    # for backwards compat - this is a bit primitive
-    def run(granularity=0.1)
-      while(true)
-        sleep(granularity)
-        tick
+      if @repeat && @period > 0
+        self.next = now + @period
       end
+
+      return self.next
     end
+  end
 
-    def running?
-      @thread && @thread.alive?
+  # creates a new Timer and starts it.
+  def initialize
+    self.extend(MonitorMixin)
+    @tick = self.new_cond
+    @thread = nil
+    @actions = Hash.new
+    @current = nil
+    self.start
+  end
+
+  # Creates and installs a new Action, repeatable by default.
+  # _period_:: Action period
+  # _opts_::   options for Action#new, see there
+  # _block_::  Action callback code
+  #
+  # Returns the id of the created Action
+  def add(period, opts = {}, &block)
+    a = Action.new({:repeat => true, :period => period}.merge(opts), &block)
+    self.synchronize do
+      @actions[a.object_id] = a
+      @tick.signal
     end
+    return a.object_id
+  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
+  # Creates and installs a new Action, one-time by default.
+  # _period_:: Action delay
+  # _opts_::   options for Action#new, see there
+  # _block_::  Action callback code
+  #
+  # Returns the id of the created Action
+  def add_once(period, opts = {}, &block)
+    self.add(period, {:repeat => false}.merge(opts), &block)
+  end
+
+  # blocks an existing Action
+  # _aid_:: Action id, obtained previously from add() or add_once()
+  def block(aid)
+    debug "blocking #{aid}"
+    self.synchronize { self[aid].block }
+  end
+
+  # unblocks an existing blocked Action
+  # _aid_:: Action id, obtained previously from add() or add_once()
+  def unblock(aid)
+    debug "unblocking #{aid}"
+    self.synchronize do
+      self[aid].unblock
+      @tick.signal
     end
+  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?
+  # removes an existing blocked Action
+  # _aid_:: Action id, obtained previously from add() or add_once()
+  def remove(aid)
+    self.synchronize do
+      @actions.delete(aid) # or raise "nonexistent action #{aid}"
     end
+  end
 
-    # stop the timer from ticking
-    def stop
-      @should_be_running = false
-      stop_thread
+  alias :delete :remove
+
+  # Provides for on-the-fly reconfiguration of Actions
+  # _aid_::   Action id, obtained previously from add() or add_once()
+  # _opts_::  see Action#new
+  # _block_:: (optional) new Action callback code
+  def configure(aid, opts = {}, &block)
+    self.synchronize do
+      self[aid].configure(opts, &block)
+      @tick.signal
     end
-    
-    private
-    
-    def start_on_add
-      if running?
-        stop_thread
-        start_thread
-      elsif @should_be_running
-        start_thread
+  end
+
+  # changes Action period
+  # _aid_:: Action id
+  # _period_:: new period
+  # _block_:: (optional) new Action callback code
+  def reschedule(aid, period, &block)
+    self.configure(aid, :period => period, &block)
+  end
+
+  def start
+    raise 'already started' if @thread
+    @stopping = false
+    debug "starting timer #{self}"
+    @thread = Thread.new do
+      loop do
+        tmout = self.run_actions
+        break if tmout and tmout < 0
+        self.synchronize { @tick.wait(tmout) }
       end
     end
-    
-    def stop_thread
-      return unless running?
-      @thread.kill
+  end
+
+  def stop
+    unless @thread
+      warning 'trying to stop already stopped timer'
+      return
     end
-    
-    def start_thread
-      return if running?
-      @thread = Thread.new do
-        while(true)
-          tick
-          exit if @timers.empty?
-          sleep(@next_action_time)
-        end
+    debug "stopping timer #{self}..."
+    @stopping = true
+    self.synchronize { @tick.signal }
+    @thread.join(60) or @thread.kill
+    debug "timer #{self} stopped"
+    @thread = nil
+  end
+
+  protected
+
+  def [](aid)
+    aid ||= @current
+    raise "no current action" unless aid
+    raise "nonexistent action #{aid}" unless @actions.include? aid
+    @actions[aid]
+  end
+
+  def run_actions(now = Time.now)
+    @actions.keys.each do |k|
+      return -1 if @stopping
+      a = @actions[k] or next
+      next if a.blocked? || a.next > now
+
+      begin
+        @current = k
+        a.run(now)
+      ensure
+        @current = nil
       end
+
+      @actions.delete k unless a.next
     end
 
+    nxt = @actions.values.find_all { |v| !v.blocked? }.map{ |v| v.next }.min
+
+    if nxt
+      delta = nxt - now
+      delta = 0 if delta < 0
+      return delta
+    else
+      return nil
+    end
   end
+
 end