tickwork 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.ruby-gemset +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +13 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +373 -0
- data/Rakefile +9 -0
- data/example.rb +28 -0
- data/gemfiles/activesupport3.gemfile +10 -0
- data/gemfiles/activesupport4.gemfile +11 -0
- data/lib/tickwork/at.rb +62 -0
- data/lib/tickwork/data_store.rb +28 -0
- data/lib/tickwork/event.rb +83 -0
- data/lib/tickwork/manager.rb +174 -0
- data/lib/tickwork.rb +56 -0
- data/test/at_test.rb +116 -0
- data/test/data_stores/fake_store.rb +21 -0
- data/test/event_test.rb +67 -0
- data/test/manager_test.rb +576 -0
- data/test/null_logger.rb +19 -0
- data/test/tickwork_test.rb +91 -0
- data/tickworkwork.gemspec +28 -0
- metadata +172 -0
@@ -0,0 +1,174 @@
|
|
1
|
+
module Tickwork
|
2
|
+
class Manager
|
3
|
+
class NoHandlerDefined < RuntimeError; end
|
4
|
+
class NoDataStoreDefined < RuntimeError; end
|
5
|
+
class DuplicateJobName < RuntimeError; end
|
6
|
+
|
7
|
+
MANAGER_KEY = '__manager'
|
8
|
+
|
9
|
+
attr_reader :config
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@events = []
|
13
|
+
@callbacks = {}
|
14
|
+
@config = default_configuration
|
15
|
+
@handler = nil
|
16
|
+
@error_handler = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def thread_available?
|
20
|
+
Thread.list.select { |t| t['creator'] == self }.count < config[:max_threads]
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
yield(config)
|
25
|
+
if config[:sleep_timeout]
|
26
|
+
config[:logger].warn 'INCORRECT USAGE: sleep_timeout is not used'
|
27
|
+
if config[:sleep_timeout] < 1
|
28
|
+
config[:logger].warn 'sleep_timeout must be >= 1 second'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
if config[:data_store].nil?
|
32
|
+
raise NoDataStoreDefined.new
|
33
|
+
end
|
34
|
+
if config[:tick_size] > 60
|
35
|
+
config[:logger].warn 'tick_size is greater than 60. Events scheduled for a specific time may be missed'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_configuration
|
40
|
+
{
|
41
|
+
logger: Logger.new(STDOUT),
|
42
|
+
thread: false,
|
43
|
+
max_threads: 10,
|
44
|
+
namespace: '_tickwork_',
|
45
|
+
tick_size: 60, # 1 minute
|
46
|
+
max_ticks: 10,
|
47
|
+
max_catchup: 3600 # 1 hour
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def handler(&block)
|
52
|
+
@handler = block if block_given?
|
53
|
+
raise NoHandlerDefined unless @handler
|
54
|
+
@handler
|
55
|
+
end
|
56
|
+
|
57
|
+
def error_handler(&block)
|
58
|
+
@error_handler = block if block_given?
|
59
|
+
@error_handler
|
60
|
+
end
|
61
|
+
|
62
|
+
def data_store
|
63
|
+
config[:data_store]
|
64
|
+
end
|
65
|
+
|
66
|
+
def on(event, options={}, &block)
|
67
|
+
raise "Unsupported callback #{event}" unless [:before_tick, :after_tick, :before_run, :after_run].include?(event.to_sym)
|
68
|
+
(@callbacks[event.to_sym]||=[]) << block
|
69
|
+
end
|
70
|
+
|
71
|
+
def every(period, job, options={}, &block)
|
72
|
+
if period < config[:tick_size]
|
73
|
+
config[:logger].warn 'period is smaller than tick size. will fail to schedule all events'
|
74
|
+
end
|
75
|
+
if options[:at].respond_to?(:each)
|
76
|
+
every_with_multiple_times(period, job, options, &block)
|
77
|
+
else
|
78
|
+
register(period, job, block, options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def fire_callbacks(event, *args)
|
83
|
+
@callbacks[event].nil? || @callbacks[event].all? { |h| h.call(*args) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def data_store_key
|
87
|
+
@data_store_key ||= config[:namespace] + MANAGER_KEY
|
88
|
+
end
|
89
|
+
|
90
|
+
# pretty straight forward if you think about it
|
91
|
+
# run the ticks from the last time we ran to our max
|
92
|
+
# but don't run ticks in the future
|
93
|
+
def run
|
94
|
+
raise NoDataStoreDefined.new if data_store.nil?
|
95
|
+
log "Starting clock for #{@events.size} events: [ #{@events.map(&:to_s).join(' ')} ]"
|
96
|
+
|
97
|
+
last = last_t = data_store.get(data_store_key)
|
98
|
+
last ||= Time.now.to_i - config[:tick_size]
|
99
|
+
if !config[:max_catchup].nil? && config[:max_catchup] > 0 && last < Time.now.to_i - config[:max_catchup]
|
100
|
+
last = Time.now.to_i - config[:max_catchup] - config[:tick_size]
|
101
|
+
end
|
102
|
+
|
103
|
+
ticks = 0
|
104
|
+
tick_time = last + config[:tick_size]
|
105
|
+
|
106
|
+
while ticks < config[:max_ticks] && tick_time <= Time.now.to_i do
|
107
|
+
tick(tick_time)
|
108
|
+
last = tick_time
|
109
|
+
tick_time += config[:tick_size]
|
110
|
+
ticks += 1
|
111
|
+
end
|
112
|
+
data_store.set(data_store_key, last)
|
113
|
+
last
|
114
|
+
end
|
115
|
+
|
116
|
+
def tick(t=Time.now.to_i)
|
117
|
+
t = Time.at(t) # TODO refactor below
|
118
|
+
if (fire_callbacks(:before_tick))
|
119
|
+
events = events_to_run(t)
|
120
|
+
events.each do |event|
|
121
|
+
if (fire_callbacks(:before_run, event, t))
|
122
|
+
event.run(t)
|
123
|
+
fire_callbacks(:after_run, event, t)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
fire_callbacks(:after_tick)
|
128
|
+
events
|
129
|
+
end
|
130
|
+
|
131
|
+
def clear!
|
132
|
+
data_store.set(data_store_key, nil)
|
133
|
+
end
|
134
|
+
|
135
|
+
def log_error(e)
|
136
|
+
config[:logger].error(e)
|
137
|
+
end
|
138
|
+
|
139
|
+
def handle_error(e)
|
140
|
+
error_handler.call(e) if error_handler
|
141
|
+
end
|
142
|
+
|
143
|
+
def log(msg)
|
144
|
+
config[:logger].info(msg)
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
def events_to_run(t)
|
149
|
+
@events.select{ |event| event.run_now?(t) }
|
150
|
+
end
|
151
|
+
|
152
|
+
def register(period, job, block, options)
|
153
|
+
options.merge({:namespace => config[:namespace]})
|
154
|
+
event = Event.new(self, period, job, block || handler, options)
|
155
|
+
guard_duplicate_events(event)
|
156
|
+
@events << event
|
157
|
+
event
|
158
|
+
end
|
159
|
+
|
160
|
+
def guard_duplicate_events(event)
|
161
|
+
if @events.map{|e| e.to_s }.include? event.to_s
|
162
|
+
raise DuplicateJobName
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def every_with_multiple_times(period, job, options={}, &block)
|
167
|
+
each_options = options.clone
|
168
|
+
options[:at].each do |at|
|
169
|
+
each_options[:at] = at
|
170
|
+
register(period, job + '_' + at, block, each_options)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/tickwork.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'active_support/time'
|
3
|
+
|
4
|
+
require 'tickwork/at'
|
5
|
+
require 'tickwork/event'
|
6
|
+
require 'tickwork/manager'
|
7
|
+
|
8
|
+
module Tickwork
|
9
|
+
class << self
|
10
|
+
def included(klass)
|
11
|
+
klass.send "include", Methods
|
12
|
+
klass.extend Methods
|
13
|
+
end
|
14
|
+
|
15
|
+
def manager
|
16
|
+
@manager ||= Manager.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def manager=(manager)
|
20
|
+
@manager = manager
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Methods
|
25
|
+
def configure(&block)
|
26
|
+
Tickwork.manager.configure(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def handler(&block)
|
30
|
+
Tickwork.manager.handler(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def error_handler(&block)
|
34
|
+
Tickwork.manager.error_handler(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def on(event, options={}, &block)
|
38
|
+
Tickwork.manager.on(event, options, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def every(period, job, options={}, &block)
|
42
|
+
Tickwork.manager.every(period, job, options, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
Tickwork.manager.run
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear!
|
50
|
+
Tickwork.manager.clear! unless Tickwork.manager.nil?
|
51
|
+
Tickwork.manager = Manager.new
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
extend Methods
|
56
|
+
end
|
data/test/at_test.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require File.expand_path('../../lib/tickwork', __FILE__)
|
2
|
+
require "minitest/autorun"
|
3
|
+
require 'mocha/mini_test'
|
4
|
+
require 'time'
|
5
|
+
require 'active_support/time'
|
6
|
+
|
7
|
+
describe 'Tickwork::At' do
|
8
|
+
def time_in_day(hour, minute)
|
9
|
+
Time.new(2013, 1, 1, hour, minute, 0)
|
10
|
+
end
|
11
|
+
|
12
|
+
it '16:20' do
|
13
|
+
at = Tickwork::At.parse('16:20')
|
14
|
+
assert !at.ready?(time_in_day(16, 19))
|
15
|
+
assert at.ready?(time_in_day(16, 20))
|
16
|
+
assert !at.ready?(time_in_day(16, 21))
|
17
|
+
end
|
18
|
+
|
19
|
+
it '8:20' do
|
20
|
+
at = Tickwork::At.parse('8:20')
|
21
|
+
assert !at.ready?(time_in_day(8, 19))
|
22
|
+
assert at.ready?(time_in_day(8, 20))
|
23
|
+
assert !at.ready?(time_in_day(8, 21))
|
24
|
+
end
|
25
|
+
|
26
|
+
it '**:20 with two stars' do
|
27
|
+
at = Tickwork::At.parse('**:20')
|
28
|
+
|
29
|
+
assert !at.ready?(time_in_day(15, 19))
|
30
|
+
assert at.ready?(time_in_day(15, 20))
|
31
|
+
assert !at.ready?(time_in_day(15, 21))
|
32
|
+
|
33
|
+
assert !at.ready?(time_in_day(16, 19))
|
34
|
+
assert at.ready?(time_in_day(16, 20))
|
35
|
+
assert !at.ready?(time_in_day(16, 21))
|
36
|
+
end
|
37
|
+
|
38
|
+
it '*:20 with one star' do
|
39
|
+
at = Tickwork::At.parse('*:20')
|
40
|
+
|
41
|
+
assert !at.ready?(time_in_day(15, 19))
|
42
|
+
assert at.ready?(time_in_day(15, 20))
|
43
|
+
assert !at.ready?(time_in_day(15, 21))
|
44
|
+
|
45
|
+
assert !at.ready?(time_in_day(16, 19))
|
46
|
+
assert at.ready?(time_in_day(16, 20))
|
47
|
+
assert !at.ready?(time_in_day(16, 21))
|
48
|
+
end
|
49
|
+
|
50
|
+
it '16:**' do
|
51
|
+
at = Tickwork::At.parse('16:**')
|
52
|
+
|
53
|
+
assert !at.ready?(time_in_day(15, 59))
|
54
|
+
assert at.ready?(time_in_day(16, 00))
|
55
|
+
assert at.ready?(time_in_day(16, 30))
|
56
|
+
assert at.ready?(time_in_day(16, 59))
|
57
|
+
assert !at.ready?(time_in_day(17, 00))
|
58
|
+
end
|
59
|
+
|
60
|
+
it '8:**' do
|
61
|
+
at = Tickwork::At.parse('8:**')
|
62
|
+
|
63
|
+
assert !at.ready?(time_in_day(7, 59))
|
64
|
+
assert at.ready?(time_in_day(8, 00))
|
65
|
+
assert at.ready?(time_in_day(8, 30))
|
66
|
+
assert at.ready?(time_in_day(8, 59))
|
67
|
+
assert !at.ready?(time_in_day(9, 00))
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'Saturday 12:00' do
|
71
|
+
at = Tickwork::At.parse('Saturday 12:00')
|
72
|
+
|
73
|
+
assert !at.ready?(Time.new(2010, 1, 1, 12, 00))
|
74
|
+
assert at.ready?(Time.new(2010, 1, 2, 12, 00)) # Saturday
|
75
|
+
assert !at.ready?(Time.new(2010, 1, 3, 12, 00))
|
76
|
+
assert at.ready?(Time.new(2010, 1, 9, 12, 00))
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'sat 12:00' do
|
80
|
+
at = Tickwork::At.parse('sat 12:00')
|
81
|
+
|
82
|
+
assert !at.ready?(Time.new(2010, 1, 1, 12, 00))
|
83
|
+
assert at.ready?(Time.new(2010, 1, 2, 12, 00))
|
84
|
+
assert !at.ready?(Time.new(2010, 1, 3, 12, 00))
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'invalid time 32:00' do
|
88
|
+
assert_raises Tickwork::At::FailedToParse do
|
89
|
+
Tickwork::At.parse('32:00')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'invalid multi-line with Sat 12:00' do
|
94
|
+
assert_raises Tickwork::At::FailedToParse do
|
95
|
+
Tickwork::At.parse("sat 12:00\nreally invalid time")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'invalid multi-line with 8:30' do
|
100
|
+
assert_raises Tickwork::At::FailedToParse do
|
101
|
+
Tickwork::At.parse("8:30\nreally invalid time")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'invalid multi-line with *:10' do
|
106
|
+
assert_raises Tickwork::At::FailedToParse do
|
107
|
+
Tickwork::At.parse("*:10\nreally invalid time")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'invalid multi-line with 12:**' do
|
112
|
+
assert_raises Tickwork::At::FailedToParse do
|
113
|
+
Tickwork::At.parse("12:**\nreally invalid time")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Tickwork
|
2
|
+
class FakeStore
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@data_store = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def get(key)
|
9
|
+
@data_store[key]
|
10
|
+
end
|
11
|
+
|
12
|
+
def set(key, value)
|
13
|
+
@data_store[key] = value
|
14
|
+
end
|
15
|
+
|
16
|
+
# not part of the interface but used for testing
|
17
|
+
def size
|
18
|
+
@data_store.size
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/test/event_test.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path('../../lib/tickwork', __FILE__)
|
2
|
+
require File.expand_path('../data_stores/fake_store.rb', __FILE__)
|
3
|
+
require 'mocha/mini_test'
|
4
|
+
require "minitest/autorun"
|
5
|
+
|
6
|
+
describe Tickwork::Event do
|
7
|
+
describe '#thread?' do
|
8
|
+
before do
|
9
|
+
@manager = Class.new
|
10
|
+
@data_store = Tickwork::FakeStore.new
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'manager config thread option set to true' do
|
14
|
+
before do
|
15
|
+
@manager.stubs(:config).returns({ :thread => true })
|
16
|
+
@manager.stubs(:data_store).returns(@data_store)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'is true' do
|
20
|
+
event = Tickwork::Event.new(@manager, nil, 'unnamed', nil)
|
21
|
+
assert_equal true, event.thread?
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'is false when event thread option set' do
|
25
|
+
event = Tickwork::Event.new(@manager, nil, 'unnamed', nil, :thread => false)
|
26
|
+
assert_equal false, event.thread?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'manager config thread option not set' do
|
31
|
+
before do
|
32
|
+
@manager.stubs(:config).returns({})
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'is true if event thread option is true' do
|
36
|
+
event = Tickwork::Event.new(@manager, nil, 'unnamed', nil, :thread => true)
|
37
|
+
assert_equal true, event.thread?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'job name' do
|
42
|
+
before do
|
43
|
+
@manager.stubs(:config).returns({})
|
44
|
+
end
|
45
|
+
it 'is required' do
|
46
|
+
assert_raises(Tickwork::Event::IllegalJobName) do
|
47
|
+
Tickwork::Event.new(@manager, nil, nil, nil)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
it 'must be a string' do
|
51
|
+
assert_raises(Tickwork::Event::IllegalJobName) do
|
52
|
+
Tickwork::Event.new(@manager, nil, Class.new, nil)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
it 'must not be empty' do
|
56
|
+
assert_raises(Tickwork::Event::IllegalJobName) do
|
57
|
+
Tickwork::Event.new(@manager, nil, '', nil)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
it 'raises exception on manager key name clash' do
|
61
|
+
assert_raises(Tickwork::Event::IllegalJobName) do
|
62
|
+
Tickwork::Event.new(@manager, nil, '__manager', nil)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|