class Rack::Timeout::Scheduler

Runs code at a later time

Basic usage:

Scheduler.run_in(5) { do_stuff }  # <- calls do_stuff 5 seconds from now

Scheduled events run in sequence in a separate thread, the main thread continues on. That means you may need to join the scheduler if the main thread is only waiting on scheduled events to run.

Scheduler.join

Basic usage is through a singleton instance, its methods are available as class methods, as shown above. One could also instantiate separate instances which would get you separate run threads, but generally there’s no point in it.

Constants

MAX_IDLE_SECS

Public Class Methods

new() click to toggle source
# File lib/rack/timeout/support/scheduler.rb, line 57
def initialize
  @runner    = nil
  @events    = []         # array of `RunEvent`s
  @mx_events = Mutex.new  # mutex to change said array
  @mx_runner = Mutex.new  # mutex for creating a runner thread
end
singleton() click to toggle source

accessor to the singleton instance

# File lib/rack/timeout/support/scheduler.rb, line 145
def self.singleton
  @singleton ||= new
end

Public Instance Methods

delay(event, secs) click to toggle source

reschedules an event by the given number of seconds. can be negative to run sooner. returns nil and does nothing if the event is not already in the queue (might’ve run already), otherwise updates the event time in-place; returns the updated event.

# File lib/rack/timeout/support/scheduler.rb, line 122
def delay(event, secs)
  @mx_events.synchronize {
    return unless @events.include? event
    event.monotime += secs
    runner.run
    return event
  }
end
join() click to toggle source

waits on the runner thread to finish

# File lib/rack/timeout/support/scheduler.rb, line 108
def join
  @joined = true
  runner.join
end
run_every(seconds, &block) click to toggle source

schedules a block to run every x seconds; returns the created event object

# File lib/rack/timeout/support/scheduler.rb, line 137
def run_every(seconds, &block)
  schedule RepeatEvent.new(fsecs, block, seconds)
end
run_in(secs, &block) click to toggle source

schedules a block to run in the given number of seconds; returns the created event object

# File lib/rack/timeout/support/scheduler.rb, line 132
def run_in(secs, &block)
  schedule RunEvent.new(fsecs + secs, block)
end
schedule(event) click to toggle source

adds a RunEvent struct to the run schedule

# File lib/rack/timeout/support/scheduler.rb, line 114
def schedule(event)
  @mx_events.synchronize { @events << event }
  runner.run  # wakes up the runner thread so it can recalculate sleep length taking this new event into consideration
  return event
end

Private Instance Methods

run_loop!() click to toggle source

the actual runner thread loop

# File lib/rack/timeout/support/scheduler.rb, line 77
def run_loop!
  Thread.current.abort_on_exception = true                       # always be aborting
  sleep_for, run, last_run = nil, nil, fsecs                     # sleep_for: how long to sleep before next run; last_run: time of last run; run: just initializing it outside of the synchronize scope, will contain events to run now
  loop do                                                        # begin event reader loop
    @mx_events.synchronize {                                     #
      @events.reject!(&:cancelled?)                              # get rid of cancelled events
      if @events.empty?                                          # if there are no further events …
        return if @joined                                        # exit the run loop if this runner thread has been joined (the thread will die and the join will return)
        return if fsecs - last_run > MAX_IDLE_SECS               # exit the run loop if done nothing for the past MAX_IDLE_SECS seconds
        sleep_for = MAX_IDLE_SECS                                # sleep for MAX_IDLE_SECS (mind it that we get awaken when new events are scheduled)
      else                                                       #
        sleep_for = [@events.map(&:monotime).min - fsecs, 0].max # if we have events, set to sleep until it's time for the next one to run. (the max bit ensure we don't have negative sleep times)
      end                                                        #
      @mx_events.sleep sleep_for                                 # do sleep
                                                                 #
      now = fsecs                                                #
      run, defer = @events.partition { |ev| ev.monotime <= now } # separate events to run now and events to run later
      defer += run.select { |ev| ev.is_a? RepeatEvent }          # repeat events both run and are deferred
      @events.replace(defer)                                     # keep only events to run later
    }                                                            #
                                                                 #
    next if run.empty?                                           # done here if there's nothing to run now
    run.sort_by(&:monotime).each { |ev| ev.run! }                # run the events scheduled to run now
    last_run = fsecs                                             # store that we did run things at this time, go immediately on to the next loop iteration as it may be time to run more things
  end
end
runner() click to toggle source

returns the runner thread, creating it if needed

# File lib/rack/timeout/support/scheduler.rb, line 68
def runner
  @mx_runner.synchronize {
    return @runner unless @runner.nil? || !@runner.alive?
    @joined = false
    @runner = Thread.new { run_loop! }
  }
end