tickwork 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|