From c57d56e3792a2bc38d778c82a877c57055df4922 Mon Sep 17 00:00:00 2001 From: Nick Elser Date: Wed, 6 May 2015 00:32:29 -0700 Subject: [PATCH] saner interface & executable --- .gitignore | 1 + README.md | 12 ++++++------ bin/zhong | 15 +++++++++++++++ lib/zhong.rb | 19 +++++++++++++++---- lib/zhong/job.rb | 31 +++++++++++++++++++++---------- lib/zhong/scheduler.rb | 30 +++++++++++++++++------------- lib/zhong/util.rb | 11 +++++++++++ 7 files changed, 86 insertions(+), 33 deletions(-) create mode 100755 bin/zhong create mode 100644 lib/zhong/util.rb diff --git a/.gitignore b/.gitignore index c1ecbde..a199874 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ lib/bundler/man pkg rdoc spec/reports +zhong.rb test/tmp test/version_tmp tmp diff --git a/README.md b/README.md index c3310f4..2439bc4 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ gem 'zhong' ```ruby r = Redis.new -Zhong.schedule(redis: r) do - category "stuff" do - every(5.seconds, "foo") { puts "foo" } - every(1.week, "baz", at: "mon 22:45") { puts "baz" } +Zhong.schedule(redis: r) do |s| + s.category "stuff" do + s.every(5.seconds, "foo") { puts "foo" } + s.every(1.week, "baz", at: "mon 22:45") { puts "baz" } end - category "clutter" do - every(1.second, "compute", if: -> (t) { rand < 0.5 }) { puts "something happened" } + s.category "clutter" do + s.every(1.second, "compute", if: -> (t) { rand < 0.5 }) { puts "something happened" } end end ``` diff --git a/bin/zhong b/bin/zhong new file mode 100755 index 0000000..58ef84e --- /dev/null +++ b/bin/zhong @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +STDERR.sync = STDOUT.sync = true + +require "bundler/setup" +require "zhong" + +usage = "zhong " +file = ARGV.shift || abort(usage) + +file = "./#{file}" unless file.match(/^[\/.]/) + +require file + +Zhong.start diff --git a/lib/zhong.rb b/lib/zhong.rb index 8f64cc2..285c35b 100644 --- a/lib/zhong.rb +++ b/lib/zhong.rb @@ -5,6 +5,8 @@ require "zhong/version" +require "zhong/util" + require "zhong/at" require "zhong/every" @@ -13,10 +15,19 @@ module Zhong class << self - def schedule(**opts, &block) - @scheduler = Scheduler.new(opts) - @scheduler.instance_eval(&block) - @scheduler.start + def schedule(**opts) + @scheduler = Scheduler.new(opts).tap do |s| + yield(s) + end + end + + def start + fail "You must run `Zhong.schedule` first" unless scheduler + scheduler.start + end + + def scheduler + @scheduler end end end diff --git a/lib/zhong/job.rb b/lib/zhong/job.rb index 15bb78c..08e904b 100644 --- a/lib/zhong/job.rb +++ b/lib/zhong/job.rb @@ -2,23 +2,26 @@ module Zhong class Job attr_reader :name, :category - def initialize(scheduler:, name:, every: nil, at: nil, only_if: nil, category: nil, &block) + def initialize(name, config = {}, &block) @name = name - @category = category + @category = config[:category] - @at = At.parse(at, grace: scheduler.config[:grace]) - @every = Every.parse(every) + @at = At.parse(config[:at], grace: config.fetch(:grace, 15.minutes)) + @every = Every.parse(config[:every]) if @at && !@every @logger.error "warning: #{self} has `at` but no `every`; could run far more often than expected!" end + fail "must specific either `at` or `every` for a job" unless @at || @every + @block = block - @redis = scheduler.config[:redis] - @logger = scheduler.config[:logger] - @tz = scheduler.config[:tz] - @if = only_if - @lock = Suo::Client::Redis.new(lock_key, client: @redis, stale_lock_expiration: scheduler.config[:long_running_timeout]) + + @redis = config[:redis] + @logger = config[:logger] + @tz = config[:tz] + @if = config[:if] + @lock = Suo::Client::Redis.new(lock_key, client: @redis, stale_lock_expiration: config[:long_running_timeout]) @timeout = 5 refresh_last_ran @@ -36,6 +39,8 @@ def run(time = Time.now) return end + @thread = nil + ran_set = @lock.lock do refresh_last_ran @@ -85,7 +90,13 @@ def disabled? end def to_s - [@category, @name].compact.join(".") + [@category, @name].compact.join(".").freeze + end + + def next_at + every_time = @every.next_at(@last_ran) if @last_ran && @every + at_time = @at.next_at(time) if @at + [every_time, at_time, Time.now].compact.max || "now" end private diff --git a/lib/zhong/scheduler.rb b/lib/zhong/scheduler.rb index 11db394..6c06495 100644 --- a/lib/zhong/scheduler.rb +++ b/lib/zhong/scheduler.rb @@ -2,27 +2,37 @@ module Zhong class Scheduler attr_reader :config, :redis, :jobs + DEFAULT_CONFIG = { + timeout: 0.5, + grace: 15.minutes, + long_running_timeout: 5.minutes + }.freeze + + TRAPPED_SIGNALS = %w(QUIT INT TERM).freeze + def initialize(config = {}) @jobs = {} - @config = {timeout: 0.5, grace: 15.minutes, long_running_timeout: 5.minutes}.merge(config) - @logger = @config[:logger] ||= self.class.default_logger - @redis = @config[:redis] ||= Redis.new + @config = DEFAULT_CONFIG.merge(config) + @logger = @config[:logger] ||= Util.default_logger + @redis = @config[:redis] ||= Redis.new(ENV["REDIS_URL"]) end def category(name) - @category = name + fail "cannot nest categories: #{name} would be nested in #{@category}" if @category + + @category = name.to_s - yield + yield(self) @category = nil end def every(period, name, opts = {}, &block) - add(Job.new(scheduler: self, name: name, every: period, at: opts[:at], only_if: opts[:if], category: @category, &block)) + add(Job.new(name, opts.merge(@config).merge(every: period, category: @category), &block)) end def start - %w(QUIT INT TERM).each do |sig| + TRAPPED_SIGNALS.each do |sig| Signal.trap(sig) { stop } end @@ -66,11 +76,5 @@ def redis_time now = Time.at(s + ms / (10**6)) config[:tz] ? now.in_time_zone(config[:tz]) : now end - - def self.default_logger - Logger.new(STDOUT).tap do |logger| - logger.formatter = -> (_, datetime, _, msg) { "#{datetime}: #{msg}\n" } - end - end end end diff --git a/lib/zhong/util.rb b/lib/zhong/util.rb new file mode 100644 index 0000000..f4397f2 --- /dev/null +++ b/lib/zhong/util.rb @@ -0,0 +1,11 @@ +module Zhong + module Util + class << self + def default_logger + Logger.new(STDOUT).tap do |logger| + logger.formatter = -> (_, datetime, _, msg) { "#{datetime}: #{msg}\n" } + end + end + end + end +end