zhong 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cdb0c3e763f67f1b0a6401a13cd9090fa3a7a45a
4
- data.tar.gz: 77c008fecedfabc3994d95a621a0c2dd7e0fa786
3
+ metadata.gz: 2cf0507becd43b3bd5e220ca807c539eccdc955d
4
+ data.tar.gz: 46ae372e6e8b04b970e19ce1d4768552b4dfe044
5
5
  SHA512:
6
- metadata.gz: fccf0555dc69c9ef3cfffa60050e0af92fada4d294047bcdac7d36e95e1a98528f4a1f7533da179c4203e5db39db623df72f5be9459c61fb9cb9f84288ec6d7a
7
- data.tar.gz: d9063ca1237fd65f9abbabe979788483174bda374670da24050b4bed064d90ab2cbbe6882ca89590c9e2b3690ccc69c50eb933ab34a239cdd326ae718f31172f
6
+ metadata.gz: 8c6c347eef8f5520a0119a676a1fb11f322e6a5e947cada7606d50f38030a1d9728bdedb609d95020be7fe3543008ab6986096691c3f8d364a5d080977a5e203
7
+ data.tar.gz: 428004dbde08236b4b4018e00b15dadd955e9463b54c3ce735a00b59d74c5b5789072da7e385f3767dc086d024f44fdcc891e5a42e963e412df737c1e68f32ca
data/.gitignore CHANGED
@@ -13,6 +13,7 @@ lib/bundler/man
13
13
  pkg
14
14
  rdoc
15
15
  spec/reports
16
+ zhong.rb
16
17
  test/tmp
17
18
  test/version_tmp
18
19
  tmp
@@ -1,3 +1,8 @@
1
+ ## 0.1.1
2
+
3
+ - Handle multiple ats (at: ["mon 8:00, tues 9:30"]).
4
+ - Job callbacks (:before_tick, :after_tick, etc).
5
+
1
6
  ## 0.1.0
2
7
 
3
8
  - First release.
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Zhong
2
2
 
3
- Useful, reliable distributed cron.
3
+ Useful, reliable distributed cron. Tired of your cron-like scheduler running key jobs twice? Would you like to be able to run your cron server on multiple machines and have it "just work"? Have we got the gem for you.
4
+
5
+ Zhong uses Redis to acquire exclusive locks on jobs, as well as recording when they last ran. This means that you can rest easy at night, knowing that your customers are getting their monthly Goat Fancy magazine subscriptions and you are rolling around in your piles of money without a care in the world.
4
6
 
5
7
  # Installation
6
8
 
@@ -15,16 +17,28 @@ gem 'zhong'
15
17
  ```ruby
16
18
  r = Redis.new
17
19
 
18
- Zhong.schedule(redis: r) do
19
- category "stuff" do
20
- every(5.seconds, "foo") { puts "foo" }
21
- every(1.week, "baz", at: "mon 22:45") { puts "baz" }
20
+ Zhong.schedule(redis: r) do |s|
21
+ s.category "stuff" do
22
+ s.every(5.seconds, "foo") { puts "foo" }
23
+ s.every(1.week, "baz", at: ["mon 22:45", "wed 23:13"]) { puts "baz" }
24
+ end
25
+
26
+ s.category "clutter" do
27
+ s.every(1.second, "compute", if: -> (t) { t.wday == 3 && rand < 0.5 }) { puts "something happened on wednesday" }
22
28
  end
23
29
 
24
- category "clutter" do
25
- every(1.second, "compute", if: -> (t) { rand < 0.5 }) { puts "something happened" }
30
+ # note: callbacks that return nil or false will cause event to not run
31
+ s.on(:before_tick) do
32
+ puts "ding"
33
+ true
34
+ end
35
+
36
+ s.on(:after_tick) do
37
+ puts "dong"
38
+ true
26
39
  end
27
40
  end
41
+
28
42
  ```
29
43
 
30
44
  ## TODO
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ STDERR.sync = STDOUT.sync = true
4
+
5
+ require "bundler/setup"
6
+ require "zhong"
7
+
8
+ usage = "zhong <zhong.rb>"
9
+ file = ARGV.shift || abort(usage)
10
+
11
+ file = "./#{file}" unless file.match(/^[\/.]/)
12
+
13
+ require file
14
+
15
+ Zhong.start
@@ -5,6 +5,8 @@ require "active_support/time"
5
5
 
6
6
  require "zhong/version"
7
7
 
8
+ require "zhong/util"
9
+
8
10
  require "zhong/at"
9
11
  require "zhong/every"
10
12
 
@@ -13,10 +15,19 @@ require "zhong/scheduler"
13
15
 
14
16
  module Zhong
15
17
  class << self
16
- def schedule(**opts, &block)
17
- @scheduler = Scheduler.new(opts)
18
- @scheduler.instance_eval(&block)
19
- @scheduler.start
18
+ def schedule(**opts)
19
+ @scheduler = Scheduler.new(opts).tap do |s|
20
+ yield(s)
21
+ end
22
+ end
23
+
24
+ def start
25
+ fail "You must run `Zhong.schedule` first" unless scheduler
26
+ scheduler.start
27
+ end
28
+
29
+ def scheduler
30
+ @scheduler
20
31
  end
21
32
  end
22
33
  end
@@ -43,6 +43,11 @@ module Zhong
43
43
  def self.parse(at, grace: 0)
44
44
  return unless at
45
45
 
46
+ # TODO: refactor this mess
47
+ if at.respond_to?(:each)
48
+ return MultiAt.new(at.map { |a| parse(a, grace: grace) })
49
+ end
50
+
46
51
  case at
47
52
  when /\A([[:alpha:]]+)\s+(.*)\z/
48
53
  wday = WDAYS[$1]
@@ -67,4 +72,16 @@ module Zhong
67
72
  throw FailedToParse, at
68
73
  end
69
74
  end
75
+
76
+ class MultiAt
77
+ attr_accessor :ats
78
+
79
+ def initialize(ats = [])
80
+ @ats = ats
81
+ end
82
+
83
+ def next_at(time = Time.now)
84
+ ats.map { |at| at.next_at(time) }.min
85
+ end
86
+ end
70
87
  end
@@ -2,23 +2,26 @@ module Zhong
2
2
  class Job
3
3
  attr_reader :name, :category
4
4
 
5
- def initialize(scheduler:, name:, every: nil, at: nil, only_if: nil, category: nil, &block)
5
+ def initialize(name, config = {}, &block)
6
6
  @name = name
7
- @category = category
7
+ @category = config[:category]
8
8
 
9
- @at = At.parse(at, grace: scheduler.config[:grace])
10
- @every = Every.parse(every)
9
+ @at = At.parse(config[:at], grace: config.fetch(:grace, 15.minutes))
10
+ @every = Every.parse(config[:every])
11
11
 
12
12
  if @at && !@every
13
13
  @logger.error "warning: #{self} has `at` but no `every`; could run far more often than expected!"
14
14
  end
15
15
 
16
+ fail "must specific either `at` or `every` for a job" unless @at || @every
17
+
16
18
  @block = block
17
- @redis = scheduler.config[:redis]
18
- @logger = scheduler.config[:logger]
19
- @tz = scheduler.config[:tz]
20
- @if = only_if
21
- @lock = Suo::Client::Redis.new(lock_key, client: @redis, stale_lock_expiration: scheduler.config[:long_running_timeout])
19
+
20
+ @redis = config[:redis]
21
+ @logger = config[:logger]
22
+ @tz = config[:tz]
23
+ @if = config[:if]
24
+ @lock = Suo::Client::Redis.new(lock_key, client: @redis, stale_lock_expiration: config[:long_running_timeout])
22
25
  @timeout = 5
23
26
 
24
27
  refresh_last_ran
@@ -36,6 +39,8 @@ module Zhong
36
39
  return
37
40
  end
38
41
 
42
+ @thread = nil
43
+
39
44
  ran_set = @lock.lock do
40
45
  refresh_last_ran
41
46
 
@@ -85,7 +90,13 @@ module Zhong
85
90
  end
86
91
 
87
92
  def to_s
88
- [@category, @name].compact.join(".")
93
+ [@category, @name].compact.join(".").freeze
94
+ end
95
+
96
+ def next_at
97
+ every_time = @every.next_at(@last_ran) if @last_ran && @every
98
+ at_time = @at.next_at(Time.now) if @at
99
+ [every_time, at_time, Time.now].compact.max || "now"
89
100
  end
90
101
 
91
102
  private
@@ -2,38 +2,69 @@ module Zhong
2
2
  class Scheduler
3
3
  attr_reader :config, :redis, :jobs
4
4
 
5
+ DEFAULT_CONFIG = {
6
+ timeout: 0.5,
7
+ grace: 15.minutes,
8
+ long_running_timeout: 5.minutes
9
+ }.freeze
10
+
11
+ TRAPPED_SIGNALS = %w(QUIT INT TERM).freeze
12
+
5
13
  def initialize(config = {})
6
14
  @jobs = {}
7
- @config = {timeout: 0.5, grace: 15.minutes, long_running_timeout: 5.minutes}.merge(config)
8
- @logger = @config[:logger] ||= self.class.default_logger
9
- @redis = @config[:redis] ||= Redis.new
15
+ @callbacks = {}
16
+ @config = DEFAULT_CONFIG.merge(config)
17
+ @logger = @config[:logger] ||= Util.default_logger
18
+ @redis = @config[:redis] ||= Redis.new(ENV["REDIS_URL"])
10
19
  end
11
20
 
12
21
  def category(name)
13
- @category = name
22
+ fail "cannot nest categories: #{name} would be nested in #{@category}" if @category
14
23
 
15
- yield
24
+ @category = name.to_s
25
+
26
+ yield(self)
16
27
 
17
28
  @category = nil
18
29
  end
19
30
 
20
31
  def every(period, name, opts = {}, &block)
21
- add(Job.new(scheduler: self, name: name, every: period, at: opts[:at], only_if: opts[:if], category: @category, &block))
32
+ job = Job.new(name, opts.merge(@config).merge(every: period, category: @category), &block)
33
+ add(job)
34
+ end
35
+
36
+ def error_handler(&block)
37
+ @error_handler = block if block_given?
38
+ @error_handler
39
+ end
40
+
41
+ def on(event, &block)
42
+ fail "Unsupported callback #{event}" unless [:before_tick, :after_tick, :before_run, :after_run].include?(event.to_sym)
43
+ (@callbacks[event.to_sym] ||= []) << block
22
44
  end
23
45
 
24
46
  def start
25
- %w(QUIT INT TERM).each do |sig|
47
+ TRAPPED_SIGNALS.each do |sig|
26
48
  Signal.trap(sig) { stop }
27
49
  end
28
50
 
29
51
  @logger.info "starting at #{redis_time}"
30
52
 
31
53
  loop do
32
- now = redis_time
54
+ if fire_callbacks(:before_tick)
55
+ now = redis_time
33
56
 
34
- jobs.each { |_, job| job.run(now) }
57
+ jobs.each do |_, job|
58
+ if fire_callbacks(:before_run, job, now)
59
+ job.run(now)
60
+ fire_callbacks(:after_run, job, now)
61
+ end
62
+ end
35
63
 
36
- sleep(interval)
64
+ fire_callbacks(:after_tick)
65
+
66
+ sleep(interval)
67
+ end
37
68
 
38
69
  break if @stop
39
70
  end
@@ -46,6 +77,10 @@ module Zhong
46
77
  Thread.new { @logger.info "stopped" }
47
78
  end
48
79
 
80
+ def fire_callbacks(event, *args)
81
+ @callbacks[event].to_a.all? { |h| h.call(*args) }
82
+ end
83
+
49
84
  private
50
85
 
51
86
  def add(job)
@@ -66,11 +101,5 @@ module Zhong
66
101
  now = Time.at(s + ms / (10**6))
67
102
  config[:tz] ? now.in_time_zone(config[:tz]) : now
68
103
  end
69
-
70
- def self.default_logger
71
- Logger.new(STDOUT).tap do |logger|
72
- logger.formatter = -> (_, datetime, _, msg) { "#{datetime}: #{msg}\n" }
73
- end
74
- end
75
104
  end
76
105
  end
@@ -0,0 +1,11 @@
1
+ module Zhong
2
+ module Util
3
+ class << self
4
+ def default_logger
5
+ Logger.new(STDOUT).tap do |logger|
6
+ logger.formatter = -> (_, datetime, _, msg) { "#{datetime}: #{msg}\n" }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Zhong
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zhong
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Elser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-05 00:00:00.000000000 Z
11
+ date: 2015-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: suo
@@ -128,6 +128,7 @@ email:
128
128
  executables:
129
129
  - console
130
130
  - setup
131
+ - zhong
131
132
  extensions: []
132
133
  extra_rdoc_files: []
133
134
  files:
@@ -141,11 +142,13 @@ files:
141
142
  - Rakefile
142
143
  - bin/console
143
144
  - bin/setup
145
+ - bin/zhong
144
146
  - lib/zhong.rb
145
147
  - lib/zhong/at.rb
146
148
  - lib/zhong/every.rb
147
149
  - lib/zhong/job.rb
148
150
  - lib/zhong/scheduler.rb
151
+ - lib/zhong/util.rb
149
152
  - lib/zhong/version.rb
150
153
  - test/minitest_helper.rb
151
154
  - test/zhong_test.rb