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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +5 -0
- data/README.md +21 -7
- data/bin/zhong +15 -0
- data/lib/zhong.rb +15 -4
- data/lib/zhong/at.rb +17 -0
- data/lib/zhong/job.rb +21 -10
- data/lib/zhong/scheduler.rb +45 -16
- data/lib/zhong/util.rb +11 -0
- data/lib/zhong/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2cf0507becd43b3bd5e220ca807c539eccdc955d
|
4
|
+
data.tar.gz: 46ae372e6e8b04b970e19ce1d4768552b4dfe044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c6c347eef8f5520a0119a676a1fb11f322e6a5e947cada7606d50f38030a1d9728bdedb609d95020be7fe3543008ab6986096691c3f8d364a5d080977a5e203
|
7
|
+
data.tar.gz: 428004dbde08236b4b4018e00b15dadd955e9463b54c3ce735a00b59d74c5b5789072da7e385f3767dc086d024f44fdcc891e5a42e963e412df737c1e68f32ca
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
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
|
-
|
25
|
-
|
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
|
data/bin/zhong
ADDED
@@ -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
|
data/lib/zhong.rb
CHANGED
@@ -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
|
17
|
-
@scheduler = Scheduler.new(opts)
|
18
|
-
|
19
|
-
|
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
|
data/lib/zhong/at.rb
CHANGED
@@ -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
|
data/lib/zhong/job.rb
CHANGED
@@ -2,23 +2,26 @@ module Zhong
|
|
2
2
|
class Job
|
3
3
|
attr_reader :name, :category
|
4
4
|
|
5
|
-
def initialize(
|
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:
|
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
|
-
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
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
|
data/lib/zhong/scheduler.rb
CHANGED
@@ -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
|
-
@
|
8
|
-
@
|
9
|
-
@
|
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
|
22
|
+
fail "cannot nest categories: #{name} would be nested in #{@category}" if @category
|
14
23
|
|
15
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
54
|
+
if fire_callbacks(:before_tick)
|
55
|
+
now = redis_time
|
33
56
|
|
34
|
-
|
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
|
-
|
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
|
data/lib/zhong/util.rb
ADDED
data/lib/zhong/version.rb
CHANGED
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.
|
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-
|
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
|