class UV::Scheduler

Constants

DURATIONS
DURATIONS2
DURATIONS2M
DURATION_LETTERS
DU_KEYS
TZ_REGEX

Attributes

next[R]
reactor[R]
time_diff[R]

Public Class Methods

h_to_s(t=Time.now) click to toggle source

Produces a hour/min/sec/milli string representation of Time instance

# File lib/uv-rays/scheduler/time.rb, line 303
def self.h_to_s(t=Time.now)
    "#{t.strftime('%H:%M:%S')}.#{sprintf('%06d', t.usec)}"
end
new(reactor) click to toggle source
# File lib/uv-rays/scheduler.rb, line 179
def initialize(reactor)
    @reactor = reactor
    @schedules = Set.new
    @scheduled = []
    @next = nil     # Next schedule time
    @timer = nil    # Reference to the timer

    # Not really required when used correctly
    @critical = Mutex.new

    # Every hour we should re-calibrate this (just in case)
    calibrate_time

    @calibrate = @reactor.timer do
        calibrate_time
        @calibrate.start(3600000)
    end
    @calibrate.start(3600000)
    @calibrate.unref
end
parse_at(o, quiet = false) click to toggle source
# File lib/uv-rays/scheduler/time.rb, line 64
def self.parse_at(o, quiet = false)
    return (o.to_f * 1000).to_i if o.is_a?(Time)

    tz = nil
    s = o.to_s.gsub(TZ_REGEX) { |m|
        t = TZInfo::Timezone.get(m) rescue nil
        tz ||= t
        t ? '' : m
    }

    begin
        DateTime.parse(o)
    rescue
        raise ArgumentError, "no time information in #{o.inspect}"
    end if RUBY_VERSION < '1.9.0'

    t = Time.parse(s)
    t = tz.local_to_utc(t) if tz
    (t.to_f * 1000).to_i    # Convert to milliseconds

rescue StandardError => se
    return nil if quiet
    raise se
end
parse_cron(o, quiet = false, timezone: nil) click to toggle source
# File lib/uv-rays/scheduler/time.rb, line 89
def self.parse_cron(o, quiet = false, timezone: nil)
    if timezone
        tz = TimeInZone.new(timezone)
        CronParser.new(o, tz)
    else
        CronParser.new(o)
    end

rescue ArgumentError => ae
    return nil if quiet
    raise ae
end
parse_duration(string, quiet = false) click to toggle source

Turns a string like '1m10s' into a float like '70.0', more formally, turns a time duration expressed as a string into a Float instance (millisecond count).

w -> week d -> day h -> hour m -> minute s -> second M -> month y -> year 'nada' -> millisecond

Some examples:

Rufus::Scheduler.parse_duration "0.5"    # => 0.5
Rufus::Scheduler.parse_duration "500"    # => 0.5
Rufus::Scheduler.parse_duration "1000"   # => 1.0
Rufus::Scheduler.parse_duration "1h"     # => 3600.0
Rufus::Scheduler.parse_duration "1h10s"  # => 3610.0
Rufus::Scheduler.parse_duration "1w2d"   # => 777600.0

Negative time strings are OK (Thanks Danny Fullerton):

Rufus::Scheduler.parse_duration "-0.5"   # => -0.5
Rufus::Scheduler.parse_duration "-1h"    # => -3600.0
# File lib/uv-rays/scheduler/time.rb, line 158
def self.parse_duration(string, quiet = false)
    string = string.to_s

    return 0 if string == ''

    m = string.match(/^(-?)([\d\.#{DURATION_LETTERS}]+)$/)

    return nil if m.nil? && quiet
    raise ArgumentError.new("cannot parse '#{string}'") if m.nil?

    mod = m[1] == '-' ? -1.0 : 1.0
    val = 0.0

    s = m[2]

    while s.length > 0
        m = nil
        if m = s.match(/^(\d+|\d+\.\d*|\d*\.\d+)([#{DURATION_LETTERS}])(.*)$/)
            val += m[1].to_f * DURATIONS[m[2]]
        elsif s.match(/^\d+$/)
            val += s.to_i
        elsif s.match(/^\d*\.\d*$/)
            val += s.to_f
        elsif quiet
            return nil
        else
            raise ArgumentError.new(
                "cannot parse '#{string}' (unexpected '#{s}')"
            )
        end
        break unless m && m[3]
        s = m[3]
    end

    res = mod * val
    res.to_i
end
parse_in(o, quiet = false) click to toggle source
# File lib/uv-rays/scheduler/time.rb, line 57
def self.parse_in(o, quiet = false)
    # if o is an integer we are looking at ms
    o.is_a?(String) ? parse_duration(o, quiet) : o
end
parse_to_time(o) click to toggle source
# File lib/uv-rays/scheduler/time.rb, line 102
def self.parse_to_time(o)
    t = o
    t = parse(t) if t.is_a?(String)
    t = Time.now + t if t.is_a?(Numeric)

    raise ArgumentError.new(
        "cannot turn #{o.inspect} to a point in time, doesn't make sense"
    ) unless t.is_a?(Time)

    t
end
to_duration(seconds, options = {}) click to toggle source

Turns a number of seconds into a a time string

Rufus.to_duration 0                    # => '0s'
Rufus.to_duration 60                   # => '1m'
Rufus.to_duration 3661                 # => '1h1m1s'
Rufus.to_duration 7 * 24 * 3600        # => '1w'
Rufus.to_duration 30 * 24 * 3600 + 1   # => "4w2d1s"

It goes from seconds to the year. Months are not counted (as they are of variable length). Weeks are counted.

For 30 days months to be counted, the second parameter of this method can be set to true.

Rufus.to_duration 30 * 24 * 3600 + 1, true   # => "1M1s"

If a Float value is passed, milliseconds will be displayed without 'marker'

Rufus.to_duration 0.051                       # => "51"
Rufus.to_duration 7.051                       # => "7s51"
Rufus.to_duration 0.120 + 30 * 24 * 3600 + 1  # => "4w2d1s120"

(this behaviour mirrors the one found for parse_time_string()).

Options are :

  • :months, if set to true, months (M) of 30 days will be taken into account when building up the result

  • :drop_seconds, if set to true, seconds and milliseconds will be trimmed from the result

# File lib/uv-rays/scheduler/time.rb, line 229
def self.to_duration(seconds, options = {})
    h = to_duration_hash(seconds, options)

    return (options[:drop_seconds] ? '0m' : '0s') if h.empty?

    s = DU_KEYS.inject(String.new) { |r, key|
        count = h[key]
        count = nil if count == 0
        r << "#{count}#{key}" if count
        r
    }

    ms = h[:ms]
    s << ms.to_s if ms
    s
end
to_duration_hash(seconds, options = {}) click to toggle source

Turns a number of seconds (integer or Float) into a hash like in :

Rufus.to_duration_hash 0.051
  # => { :ms => "51" }
Rufus.to_duration_hash 7.051
  # => { :s => 7, :ms => "51" }
Rufus.to_duration_hash 0.120 + 30 * 24 * 3600 + 1
  # => { :w => 4, :d => 2, :s => 1, :ms => "120" }

This method is used by to_duration behind the scenes.

Options are :

  • :months, if set to true, months (M) of 30 days will be taken into account when building up the result

  • :drop_seconds, if set to true, seconds and milliseconds will be trimmed from the result

# File lib/uv-rays/scheduler/time.rb, line 264
def self.to_duration_hash(seconds, options = {})
    h = {}

    if (seconds % 1000) > 0
        h[:ms] = (seconds % 1000).to_i
        seconds = (seconds / 1000).to_i * 1000
    end

    if options[:drop_seconds]
        h.delete(:ms)
        seconds = (seconds - seconds % 60000)
    end

    durations = options[:months] ? DURATIONS2M : DURATIONS2

    durations.each do |key, duration|
        count = seconds / duration
        seconds = seconds % duration

        h[key.to_sym] = count if count > 0
    end

    h
end
utc_to_s(t=Time.now) click to toggle source

Produces the UTC string representation of a Time instance

like “2009/11/23 11:11:50.947109 UTC”

# File lib/uv-rays/scheduler/time.rb, line 297
def self.utc_to_s(t=Time.now)
    "#{t.utc.strftime('%Y-%m-%d %H:%M:%S')}.#{sprintf('%06d', t.usec)} UTC"
end

Public Instance Methods

at(time) click to toggle source

Create a one off event that occurs at a particular date and time

@param time [String, Time] a representation of a date and time that can be parsed @param callback [Proc] a block or method to execute when the event triggers @return [::UV::OneShot]

# File lib/uv-rays/scheduler.rb, line 239
def at(time)
    ms = Scheduler.parse_at(time) - @time_diff
    event = OneShot.new(self, ms)
    event.progress &Proc.new if block_given?
    schedule(event)
    event
end
calibrate_time() click to toggle source

As the libuv time is taken from an arbitrary point in time we

need to roughly synchronize between it and ruby's Time.now
# File lib/uv-rays/scheduler.rb, line 203
def calibrate_time
    @reactor.update_time
    @time_diff = (Time.now.to_f * 1000).to_i - @reactor.now
end
cron(schedule, timezone: nil) click to toggle source

Create a repeating event that uses a CRON line to determine the trigger time

@param schedule [String] a standard CRON job line. @param callback [Proc] a block or method to execute when the event triggers @return [::UV::Repeat]

# File lib/uv-rays/scheduler.rb, line 252
def cron(schedule, timezone: nil)
    ms = Scheduler.parse_cron(schedule, timezone: timezone)
    event = Repeat.new(self, ms)
    event.progress &Proc.new if block_given?
    schedule(event)
    event
end
every(time) click to toggle source

Create a repeating event that occurs each time period

@param time [String] a human readable string representing the time period. 3w2d4h1m2s for example. @param callback [Proc] a block or method to execute when the event triggers @return [::UV::Repeat]

# File lib/uv-rays/scheduler.rb, line 213
def every(time)
    ms = Scheduler.parse_in(time)
    event = Repeat.new(self, ms)
    event.progress &Proc.new if block_given?
    schedule(event)
    event
end
in(time) click to toggle source

Create a one off event that occurs after the time period

@param time [String] a human readable string representing the time period. 3w2d4h1m2s for example. @param callback [Proc] a block or method to execute when the event triggers @return [::UV::OneShot]

# File lib/uv-rays/scheduler.rb, line 226
def in(time)
    ms = @reactor.now + Scheduler.parse_in(time)
    event = OneShot.new(self, ms)
    event.progress &Proc.new if block_given?
    schedule(event)
    event
end
reschedule(event) click to toggle source

Schedules an event for execution

@param event [ScheduledEvent]

# File lib/uv-rays/scheduler.rb, line 263
def reschedule(event)
    # Check promise is not resolved
    return if event.resolved?

    @critical.synchronize {
        # Remove the event from the scheduled list and ensure it is in the schedules set
        if @schedules.include?(event)
            remove(event)
        else
            @schedules << event
        end

        # optimal algorithm for inserting into an already sorted list
        Bisect.insort(@scheduled, event)

        # Update the timer
        check_timer
    }
end
unschedule(event) click to toggle source

Removes an event from the schedule

@param event [ScheduledEvent]

# File lib/uv-rays/scheduler.rb, line 286
def unschedule(event)
    @critical.synchronize {
        # Only call delete and update the timer when required
        if @schedules.include?(event)
            @schedules.delete(event)
            remove(event)
            check_timer
        end
    }
end

Private Instance Methods

check_timer() click to toggle source

Ensures the current timer, if any, is still accurate by checking the head of the schedule

# File lib/uv-rays/scheduler.rb, line 327
def check_timer
    @reactor.update_time

    existing = @next
    schedule = @scheduled.first
    @next = schedule.nil? ? nil : schedule.next_scheduled

    if existing != @next
        # lazy load the timer
        if @timer.nil?
            new_timer
        else
            @timer.stop
        end

        if not @next.nil?
            in_time = @next - @reactor.now

            # Ensure there are never negative start times
            if in_time > 3
                @timer.start(in_time)
            else
                # Effectively next tick
                @timer.start(0)
            end
        end
    end
end
new_timer() click to toggle source

Provide some assurances on timer failure

# File lib/uv-rays/scheduler.rb, line 376
def new_timer
    @timer = @reactor.timer { on_timer }
    @timer.finally do
        new_timer
        unless @next.nil?
            @timer.start(@next)
        end
    end
end
on_timer() click to toggle source

Is called when the libuv timer fires

# File lib/uv-rays/scheduler.rb, line 357
def on_timer
    @critical.synchronize {
        schedule = @scheduled.shift
        @schedules.delete(schedule)
        schedule.trigger

        # execute schedules that are within 3ms of this event
        # Basic timer coalescing..
        now = @reactor.now + 3
        while @scheduled.first && @scheduled.first.next_scheduled <= now
            schedule = @scheduled.shift
            @schedules.delete(schedule)
            schedule.trigger
        end
        check_timer
    }
end
remove(obj) click to toggle source

Remove an element from the array

# File lib/uv-rays/scheduler.rb, line 302
def remove(obj)
    position = nil

    @scheduled.each_index do |i|
        # object level comparison
        if obj.equal? @scheduled[i]
            position = i
            break
        end
    end

    @scheduled.slice!(position) unless position.nil?
end
schedule(event) click to toggle source

First time schedule we want to bind to the promise

# File lib/uv-rays/scheduler.rb, line 317
def schedule(event)
    reschedule(event)

    event.finally do
        unschedule event
    end
end