Skip to content

Commit a2b02e4

Browse files
committed
Improve the API significantly.
1 parent aafe19c commit a2b02e4

File tree

10 files changed

+121
-97
lines changed

10 files changed

+121
-97
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.1.5
2+
3+
- Improve the API.
4+
- Add scheduler test.
5+
16
## 0.1.4
27

38
- Fix several bugs related to time parsing.

README.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,37 @@ gem 'zhong'
1515
## Usage
1616

1717
```ruby
18-
r = Redis.new
19-
20-
Zhong.schedule(redis: r) do |s|
21-
s.category "stuff" do
22-
s.every(5.seconds, "foo") { puts "foo" }
23-
s.every(1.minute, "biz", at: ["**:26", "**:27"]) { puts "biz" }
24-
s.every(1.week, "baz", at: ["mon 22:45", "wed 23:13"]) { puts "baz" }
25-
s.every(10.seconds, "boom") { raise "fail" }
18+
Zhong.redis = Redis.new(ENV["ZHONG_REDIS_URL"])
19+
20+
Zhong.schedule do
21+
category "stuff" do
22+
every 5.seconds, "foo" do
23+
puts "foo"
24+
end
25+
26+
every(1.minute, "biz", at: ["**:26", "**:27"]) { puts "biz" }
27+
every(1.week, "baz", at: ["mon 22:45", "wed 23:13"]) { puts "baz" }
28+
every(10.seconds, "boom") { raise "fail" }
2629
end
2730

28-
s.category "clutter" do
29-
s.every(1.second, "compute", if: -> (t) { t.wday == 3 && rand < 0.5 }) do
31+
category "clutter" do
32+
every(1.second, "compute", if: -> (t) { t.wday == 3 && rand < 0.5 }) do
3033
puts "something happened on wednesday, maybe"
3134
end
3235
end
3336

3437
# note: callbacks that return nil or false will cause event to not run
35-
s.on(:before_tick) do
38+
on(:before_tick) do
3639
puts "ding"
3740
true
3841
end
3942

40-
s.on(:after_tick) do
43+
on(:after_tick) do
4144
puts "dong"
4245
true
4346
end
4447

45-
s.error_handler do |e, job|
48+
error_handler do |e, job|
4649
puts "damn, #{job} messed up: #{e}"
4750
end
4851
end

bin/zhong

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
STDERR.sync = STDOUT.sync = true
44

5-
require "bundler/setup"
65
require "zhong"
76

8-
usage = "zhong <zhong.rb>"
9-
file = ARGV.shift || abort(usage)
7+
file = ARGV.shift or abort "zhong <path to zhong.rb>"
108

119
file = "./#{file}" unless file.match(/^[\/.]/)
1210

1311
require file
1412

15-
Zhong.start
13+
begin
14+
Zhong.start
15+
rescue => boom
16+
STDERR.puts boom.message
17+
STDERR.puts boom.backtrace.join("\n")
18+
exit 1
19+
end

lib/zhong.rb

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
require "zhong/version"
77

8-
require "zhong/util"
9-
108
require "zhong/at"
119
require "zhong/every"
1210

@@ -15,19 +13,32 @@
1513

1614
module Zhong
1715
class << self
18-
def schedule(**opts)
19-
@scheduler = Scheduler.new(opts).tap do |s|
20-
yield(s)
21-
end
22-
end
16+
attr_writer :logger, :redis
17+
end
2318

24-
def start
25-
fail "You must run `Zhong.schedule` first" unless scheduler
26-
scheduler.start
27-
end
19+
def self.schedule(&block)
20+
scheduler.instance_eval(&block) if block_given?
21+
end
22+
23+
def self.start
24+
scheduler.start
25+
end
2826

29-
def scheduler
30-
@scheduler
27+
def self.stop
28+
scheduler.stop
29+
end
30+
31+
def self.scheduler
32+
@scheduler ||= Scheduler.new(logger: logger, redis: redis)
33+
end
34+
35+
def self.logger
36+
@logger ||= Logger.new(STDOUT).tap do |logger|
37+
logger.formatter = -> (_, datetime, _, msg) { "#{datetime}: #{msg}\n" }
3138
end
3239
end
40+
41+
def self.redis
42+
@redis ||= Redis.new(url: ENV["REDIS_URL"])
43+
end
3344
end

lib/zhong/every.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class FailedToParse < StandardError; end
1414
def initialize(period)
1515
@period = period
1616

17-
fail ArgumentError unless valid?
17+
fail "`every` must be >= 1 second" unless valid?
1818
end
1919

2020
private def valid?

lib/zhong/job.rb

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Zhong
22
class Job
3-
attr_reader :name, :category, :last_ran
3+
attr_reader :name, :category, :last_ran, :logger
44

55
def initialize(name, config = {}, &block)
66
@name = name
@@ -17,8 +17,8 @@ def initialize(name, config = {}, &block)
1717
@redis = config[:redis]
1818
@tz = config[:tz]
1919
@if = config[:if]
20-
@lock = Suo::Client::Redis.new(lock_key, client: @redis, stale_lock_expiration: config[:long_running_timeout])
21-
@timeout = 5
20+
@long_running_timeout = config[:long_running_timeout]
21+
@running = false
2222

2323
refresh_last_ran
2424
end
@@ -30,18 +30,13 @@ def run?(time = Time.now)
3030
def run(time = Time.now, error_handler = nil)
3131
return unless run?(time)
3232

33-
if running?
34-
@logger.info "already running: #{self}"
35-
return
36-
end
37-
38-
@thread = nil
3933
locked = false
4034
errored = false
4135

4236
begin
43-
@lock.lock do
37+
redis_lock.lock do
4438
locked = true
39+
@running = true
4540

4641
refresh_last_ran
4742

@@ -50,44 +45,35 @@ def run(time = Time.now, error_handler = nil)
5045
break unless run?(time)
5146

5247
if disabled?
53-
@logger.info "disabled: #{self}"
48+
logger.info "disabled: #{self}"
5449
break
5550
end
5651

57-
@logger.info "running: #{self}"
52+
logger.info "running: #{self}"
5853

5954
if @block
60-
@thread = Thread.new do
61-
begin
62-
@block.call
63-
rescue => boom
64-
@logger.error "#{self} failed: #{boom}"
65-
error_handler.call(boom, self) if error_handler
66-
end
67-
68-
nil # do not retain thread return value
55+
begin
56+
@block.call
57+
rescue => boom
58+
logger.error "#{self} failed: #{boom}"
59+
error_handler.call(boom, self) if error_handler
6960
end
7061
end
7162

7263
ran!(time)
7364
end
7465
rescue Suo::LockClientError => boom
75-
@logger.error "unable to run due to client error: #{boom}"
66+
logger.error "unable to run due to client error: #{boom}"
7667
errored = true
7768
end
7869

79-
@logger.info "unable to acquire exclusive run lock: #{self}" if !locked && !errored
80-
end
70+
@running = false
8171

82-
def stop
83-
return unless running?
84-
Thread.new { @logger.error "killing #{self} due to stop" } # thread necessary due to trap context
85-
@thread.join(@timeout)
86-
@thread.kill
72+
logger.info "unable to acquire exclusive run lock: #{self}" if !locked && !errored
8773
end
8874

8975
def running?
90-
@thread && @thread.alive?
76+
@running
9177
end
9278

9379
def refresh_last_ran
@@ -140,6 +126,10 @@ def ran!(time)
140126
@redis.set(last_ran_key, @last_ran.to_i)
141127
end
142128

129+
def redis_lock
130+
@lock ||= Suo::Client::Redis.new(lock_key, client: @redis, stale_lock_expiration: @long_running_timeout)
131+
end
132+
143133
def last_ran_key
144134
"zhong:last_ran:#{self}"
145135
end

lib/zhong/scheduler.rb

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ class Scheduler
55
DEFAULT_CONFIG = {
66
timeout: 0.5,
77
grace: 15.minutes,
8-
long_running_timeout: 5.minutes
8+
long_running_timeout: 5.minutes,
9+
tz: nil
910
}.freeze
1011

1112
def initialize(config = {})
1213
@jobs = {}
1314
@callbacks = {}
1415
@config = DEFAULT_CONFIG.merge(config)
15-
@logger = @config[:logger] ||= Util.default_logger
16-
@redis = @config[:redis] ||= Redis.new(ENV["REDIS_URL"])
16+
17+
@logger = @config[:logger]
18+
@redis = @config[:redis]
19+
@tz = @config[:tz]
1720
end
1821

1922
def category(name)
@@ -28,8 +31,15 @@ def category(name)
2831

2932
def every(period, name, opts = {}, &block)
3033
fail "must specify a period for #{name} (#{caller.first})" unless period
34+
3135
job = Job.new(name, opts.merge(@config).merge(every: period, category: @category), &block)
32-
add(job)
36+
37+
if jobs.key?(job.to_s)
38+
@logger.error "duplicate job #{job}, skipping"
39+
return
40+
end
41+
42+
@jobs[job.to_s] = job
3343
end
3444

3545
def error_handler(&block)
@@ -45,6 +55,8 @@ def on(event, &block)
4555
def start
4656
@logger.info "starting at #{redis_time}"
4757

58+
@stop = false
59+
4860
trap_signals
4961

5062
loop do
@@ -57,20 +69,25 @@ def start
5769

5870
fire_callbacks(:after_tick)
5971

60-
sleep_until_next_tick
72+
sleep_until_next_second
6173
end
6274

6375
break if @stop
6476
end
77+
78+
Thread.new { @logger.info "stopped" }
6579
end
6680

6781
def stop
6882
Thread.new { @logger.error "stopping" } # thread necessary due to trap context
6983
@stop = true
70-
jobs.values.each(&:stop)
71-
Thread.new { @logger.info "stopped" }
7284
end
7385

86+
private
87+
88+
TRAPPED_SIGNALS = %w(QUIT INT TERM).freeze
89+
private_constant :TRAPPED_SIGNALS
90+
7491
def fire_callbacks(event, *args)
7592
@callbacks[event].to_a.all? { |h| h.call(*args) }
7693
end
@@ -87,35 +104,21 @@ def run_job(job, time = redis_time)
87104
fire_callbacks(:after_run, job, time)
88105
end
89106

90-
private
91-
92-
TRAPPED_SIGNALS = %w(QUIT INT TERM).freeze
93-
private_constant :TRAPPED_SIGNALS
94-
95107
def trap_signals
96108
TRAPPED_SIGNALS.each do |sig|
97109
Signal.trap(sig) { stop }
98110
end
99111
end
100112

101-
def add(job)
102-
if @jobs.key?(job.to_s)
103-
@logger.error "duplicate job #{job}, skipping"
104-
return
105-
end
106-
107-
@jobs[job.to_s] = job
108-
end
109-
110-
def sleep_until_next_tick
113+
def sleep_until_next_second
111114
GC.start
112-
sleep(1.0 - Time.now.subsec + 0.001)
115+
sleep(1.0 - Time.now.subsec + 0.0001)
113116
end
114117

115118
def redis_time
116119
s, ms = @redis.time # returns [seconds since epoch, microseconds]
117120
now = Time.at(s + ms / (10**6))
118-
config[:tz] ? now.in_time_zone(config[:tz]) : now
121+
@tz ? now.in_time_zone(@tz) : now
119122
end
120123
end
121124
end

lib/zhong/util.rb

Lines changed: 0 additions & 11 deletions
This file was deleted.

lib/zhong/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module Zhong
2-
VERSION = "0.1.4"
2+
VERSION = "0.1.5"
33
end

0 commit comments

Comments
 (0)