require 'thread' require 'singleton' module Mauve # # This is a class to wrap our threads that have processing loops. # # The thread is kept in a wrapper to allow it to be frozen and thawed at # convenient times. # class MauveThread # # The sleep interval between runs of the main loop. Defaults to 5 seconds. # attr_reader :poll_every # # Set the thread up # def initialize @thread = nil end # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) end # Set the sleep interval between runs of the main loop. This can be # anything greater than or equal to zero. If a number less than zero gets # entered, it will be increased to zero. # # @param [Numeric] i The number of seconds to sleep # @raise [ArgumentError] If +i+ is not numeric # @return [Numeric] # def poll_every=(i) raise ArgumentError.new("poll_every must be numeric") unless i.is_a?(Numeric) # # Set the minimum poll frequency. # if i.to_f < 0 logger.debug "Increasing thread polling interval to 0s from #{i}" i = 0 end @poll_every = i end # This determines if a thread should stop # # @return [Boolean] def should_stop? [:freezing, :stopping].include?(self.state) end # This is the current state of the thread. It can be one of # [:stopped, :starting, :started, :freezing, :frozen, :stopping, :killing, :killed] # # If the thread is not alive it will be +:stopped+. # # @return [Symbol] One of [:stopped, :starting, :started, :freezing, # :frozen, :stopping, :killing, :killed] def state if self.alive? @thread.key?(:state) ? @thread[:state] : :unknown else :stopped end end # This sets the state of a thread. It also records the last time the # thread changed status. # # @param [Symbol] s One of [:stopped, :starting, :started, :freezing, # :frozen, :stopping, :killing, :killed] # @raise [ArgumentError] if +s+ is not a valid symbol or the thread is not # ready # @return [Symbol] the current thread state. # def state=(s) raise ArgumentError, "Bad state for mauve_thread #{s.inspect}" unless [:stopped, :starting, :started, :freezing, :frozen, :stopping, :killing, :killed].include?(s) raise ArgumentError, "Thread not ready yet." unless @thread.is_a?(Thread) unless @thread[:state] == s @thread[:state] = s @thread[:last_state_change] = Time.now logger.debug(s.to_s.capitalize) end @thread[:state] end # This returns the time of the last state change, or nil if the thread is dead. # # @return [Time or Nilclass] def last_state_change if self.alive? and @thread.key?(:last_state_change) return @thread[:last_state_change] else return nil end end # This asks the thread to freeze at the next opportunity. # def freeze self.state = :freezing 20.times { Kernel.sleep 0.2 ; break if @thread.stop? } logger.warn("Thread has not frozen!") unless @thread.stop? end # This returns true if the thread has frozen successfully. # # @return [Boolean] def frozen? self.stop? and self.state == :frozen end # This starts the thread. It wakes it up if it is alive, or starts it from # fresh if it is dead. # def run if self.alive? # Wake up if we're stopped. if self.stop? @thread.wakeup end else @logger = nil Thread.new do run_thread { main_loop } end end end alias start run alias thaw run # This checks to see if the thread is alive # # @return [Boolean] def alive? @thread.is_a?(Thread) and @thread.alive? end # This checks to see if the thread is stopped # # @return [Boolean] def stop? self.alive? and @thread.stop? end # This joins the thread # def join @thread.join if @thread.is_a?(Thread) end # def raise(ex) # @thread.raise(ex) # end # This returns the thread's backtrace # # @return [Array or Nilclass] def backtrace @thread.backtrace if @thread.is_a?(Thread) end # This restarts the thread # # def restart self.stop self.start end # This stops the thread # # def stop self.state = :stopping 10.times do break unless self.alive? Kernel.sleep 1 if self.alive? end # # OK I've had enough now. # self.kill if self.alive? self.join end alias exit stop # This kills the thread -- faster than #stop # def kill self.state = :killing @thread.kill self.state = :killed end # This returns the thread itself. # # @return [Thread] def thread @thread end private # This is the main run loop for the thread. In here are all the calls # allowing use to freeze and thaw the thread neatly. # # This thread will run untill the thread state is changed to :stopping. # def run_thread(interval = 5.0) # # Good to go. # @thread = Thread.current self.state = :starting @poll_every ||= interval # # Make sure we get a number. # @poll_every = 5 unless @poll_every.is_a?(Numeric) rate_limit = 0.1 while self.state != :stopping do self.state = :started if self.state == :starting # # Schtop! # if self.state == :freezing self.state = :frozen Thread.stop self.state = :started end yield_start = Time.now.to_f yield # # Ah-ha! Sleep with a break clause. Make sure we poll every @poll_every seconds. # ((@poll_every.to_f - Time.now.to_f + yield_start.to_f)/rate_limit). round.to_i.times do break if self.should_stop? # # This is a rate-limiting step # Kernel.sleep rate_limit end end self.state = :stopped end end end