clockwork 0.7.7 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +55 -48
- data/Rakefile +1 -1
- data/clockwork.gemspec +1 -1
- data/clockworkd.1 +62 -0
- data/lib/clockwork/at.rb +5 -1
- data/lib/clockwork/database_events.rb +26 -0
- data/lib/clockwork/database_events/event.rb +38 -0
- data/lib/clockwork/database_events/manager.rb +20 -0
- data/lib/clockwork/database_events/registry.rb +39 -0
- data/lib/clockwork/database_events/sync_performer.rb +94 -0
- data/test/database_events/sync_performer_test.rb +283 -0
- data/test/database_events/test_helpers.rb +101 -0
- metadata +36 -52
- data/lib/clockwork/manager_with_database_tasks.rb +0 -150
- data/test/manager_with_database_tasks_test.rb +0 -292
@@ -1,150 +0,0 @@
|
|
1
|
-
module Clockwork
|
2
|
-
|
3
|
-
# add equality testing to At
|
4
|
-
class At
|
5
|
-
attr_reader :min, :hour, :wday
|
6
|
-
|
7
|
-
def == other
|
8
|
-
@min == other.min && @hour == other.hour && @wday == other.wday
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
module Methods
|
13
|
-
def sync_database_tasks(options={}, &block)
|
14
|
-
Clockwork.manager.sync_database_tasks(options, &block)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
extend Methods
|
19
|
-
|
20
|
-
class DatabaseEventSyncPerformer
|
21
|
-
|
22
|
-
def initialize(manager, model, proc)
|
23
|
-
@manager = manager
|
24
|
-
@model = model
|
25
|
-
@block = proc
|
26
|
-
@events = {}
|
27
|
-
end
|
28
|
-
|
29
|
-
# Ensure clockwork events reflect database tasks
|
30
|
-
# Adds any new tasks, modifies updated ones, and delets removed ones
|
31
|
-
def sync
|
32
|
-
model_ids_that_exist = []
|
33
|
-
|
34
|
-
@model.all.each do |db_task|
|
35
|
-
model_ids_that_exist << db_task.id
|
36
|
-
|
37
|
-
if !event_exists_for_task(db_task) || task_has_changed(db_task)
|
38
|
-
recreate_event_for_database_task(db_task)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
remove_deleted_database_tasks(model_ids_that_exist)
|
43
|
-
end
|
44
|
-
|
45
|
-
def clockwork_events
|
46
|
-
@events.values.flatten
|
47
|
-
end
|
48
|
-
|
49
|
-
# store events by task_id in array (array is needed as there is 1 event per At)
|
50
|
-
def add_event(e, task_id)
|
51
|
-
@events[task_id] ||= []
|
52
|
-
@events[task_id] << e
|
53
|
-
end
|
54
|
-
|
55
|
-
protected
|
56
|
-
|
57
|
-
def recreate_event_for_database_task(db_task)
|
58
|
-
@events[db_task.id] = nil
|
59
|
-
|
60
|
-
options = {
|
61
|
-
:from_database => true,
|
62
|
-
:db_task_id => db_task.id,
|
63
|
-
:performer => self,
|
64
|
-
:at => array_of_ats_for(db_task, :nil_if_empty => true)
|
65
|
-
}
|
66
|
-
|
67
|
-
@manager.every db_task.frequency, db_task.name, options, &@block
|
68
|
-
end
|
69
|
-
|
70
|
-
def event_exists_for_task(db_task)
|
71
|
-
@events[db_task.id]
|
72
|
-
end
|
73
|
-
|
74
|
-
def remove_deleted_database_tasks(model_ids_that_exist)
|
75
|
-
@events.reject!{|db_task_id, _| !model_ids_that_exist.include?(db_task_id) }
|
76
|
-
end
|
77
|
-
|
78
|
-
def task_has_changed(task)
|
79
|
-
events = @events[task.id]
|
80
|
-
event = @events[task.id].first # all events will have same frequency/name, just different ats
|
81
|
-
ats_for_task = array_of_ats_for(task)
|
82
|
-
ats_from_event = array_of_ats_from_event(task.id)
|
83
|
-
|
84
|
-
name_has_changed = task.name != event.job
|
85
|
-
frequency_has_changed = task.frequency != event.instance_variable_get(:@period)
|
86
|
-
|
87
|
-
at_has_changed = ats_for_task.length != ats_from_event.length
|
88
|
-
at_has_changed ||= ats_for_task.inject(false) do |memo, at|
|
89
|
-
memo ||= !ats_from_event.include?(At.parse(at))
|
90
|
-
end
|
91
|
-
|
92
|
-
name_has_changed || frequency_has_changed || at_has_changed
|
93
|
-
end
|
94
|
-
|
95
|
-
def array_of_ats_from_event(task_id)
|
96
|
-
@events[task_id].collect{|clockwork_event| clockwork_event.instance_variable_get(:@at) }.compact
|
97
|
-
end
|
98
|
-
|
99
|
-
def array_of_ats_for(task, opts={})
|
100
|
-
if task.at.to_s.empty?
|
101
|
-
opts[:nil_if_empty] ? nil : []
|
102
|
-
else
|
103
|
-
task.at.split(',').map(&:strip)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
class ManagerWithDatabaseTasks < Manager
|
109
|
-
|
110
|
-
def initialize
|
111
|
-
super
|
112
|
-
@database_event_sync_performers = []
|
113
|
-
end
|
114
|
-
|
115
|
-
def sync_database_tasks(options={}, &block)
|
116
|
-
[:model, :every].each do |option|
|
117
|
-
raise ArgumentError.new("requires :#{option} option") unless options.include?(option)
|
118
|
-
end
|
119
|
-
raise ArgumentError.new(":every must be greater or equal to 1.minute") if options[:every] < 1.minute
|
120
|
-
|
121
|
-
sync_performer = DatabaseEventSyncPerformer.new(self, options[:model], block)
|
122
|
-
@database_event_sync_performers << sync_performer
|
123
|
-
|
124
|
-
# create event that syncs clockwork events with database events
|
125
|
-
every options[:every], "sync_database_tasks_for_model_#{options[:model]}" do
|
126
|
-
sync_performer.sync
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
private
|
131
|
-
|
132
|
-
def events_from_database_as_array
|
133
|
-
@database_event_sync_performers.collect{|performer| performer.clockwork_events}.flatten
|
134
|
-
end
|
135
|
-
|
136
|
-
def events_to_run(t)
|
137
|
-
(@events + events_from_database_as_array).select{|event| event.run_now?(t) }
|
138
|
-
end
|
139
|
-
|
140
|
-
def register(period, job, block, options)
|
141
|
-
Event.new(self, period, job, block || handler, options).tap do |e|
|
142
|
-
if options[:from_database]
|
143
|
-
options[:performer].add_event(e, options[:db_task_id])
|
144
|
-
else
|
145
|
-
@events << e
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
@@ -1,292 +0,0 @@
|
|
1
|
-
require File.expand_path('../../lib/clockwork', __FILE__)
|
2
|
-
require 'clockwork/manager_with_database_tasks'
|
3
|
-
require 'rubygems'
|
4
|
-
require 'contest'
|
5
|
-
require 'mocha/setup'
|
6
|
-
require 'time'
|
7
|
-
require 'active_support/time'
|
8
|
-
|
9
|
-
class ManagerWithDatabaseTasksTest < Test::Unit::TestCase
|
10
|
-
|
11
|
-
class ScheduledTask; end
|
12
|
-
class ScheduledTaskType2; end
|
13
|
-
|
14
|
-
setup do
|
15
|
-
@manager = Clockwork::ManagerWithDatabaseTasks.new
|
16
|
-
class << @manager
|
17
|
-
def log(msg); end
|
18
|
-
end
|
19
|
-
@manager.handler { }
|
20
|
-
end
|
21
|
-
|
22
|
-
def assert_will_run(t)
|
23
|
-
if t.is_a? String
|
24
|
-
t = Time.parse(t)
|
25
|
-
end
|
26
|
-
assert_equal 1, @manager.tick(t).size
|
27
|
-
end
|
28
|
-
|
29
|
-
def assert_wont_run(t)
|
30
|
-
if t.is_a? String
|
31
|
-
t = Time.parse(t)
|
32
|
-
end
|
33
|
-
assert_equal 0, @manager.tick(t).size
|
34
|
-
end
|
35
|
-
|
36
|
-
def tick_at(now = Time.now, options = {})
|
37
|
-
seconds_to_tick_for = options[:and_every_second_for] || 0
|
38
|
-
number_of_ticks = seconds_to_tick_for + 1 # add one for right now
|
39
|
-
number_of_ticks.times{|i| @manager.tick(now + i) }
|
40
|
-
end
|
41
|
-
|
42
|
-
def next_minute(now = Time.now)
|
43
|
-
Time.new(now.year, now.month, now.day, now.hour, now.min + 1, 0)
|
44
|
-
end
|
45
|
-
|
46
|
-
describe "sync_database_tasks" do
|
47
|
-
|
48
|
-
describe "arguments" do
|
49
|
-
|
50
|
-
def test_does_not_raise_error_with_valid_arguments
|
51
|
-
@manager.sync_database_tasks(model: ScheduledTask, every: 1.minute) {}
|
52
|
-
end
|
53
|
-
|
54
|
-
def test_raises_argument_error_if_param_called_model_is_not_set
|
55
|
-
assert_raises ArgumentError do
|
56
|
-
@manager.sync_database_tasks(model: ScheduledTask) {}
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def test_raises_argument_error_if_param_called_every_is_not_set
|
61
|
-
assert_raises ArgumentError do
|
62
|
-
@manager.sync_database_tasks(every: 1.minute) {}
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def test_raises_argument_error_if_param_called_every_is_less_than_1_minute
|
67
|
-
assert_raises ArgumentError do
|
68
|
-
@manager.sync_database_tasks(model: ScheduledTask, every: 59.seconds) {}
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
context "when database reload frequency is greater than task frequency period" do
|
74
|
-
setup do
|
75
|
-
@tasks_run = []
|
76
|
-
@scheduled_task1 = stub(:frequency => 10, :name => 'ScheduledTask:1', :at => nil, :id => 1)
|
77
|
-
@scheduled_task2 = stub(:frequency => 10, :name => 'ScheduledTask:2', :at => nil, :id => 2)
|
78
|
-
@scheduled_task1_modified = stub(:frequency => 5, :name => 'ScheduledTaskModified:1', :at => nil, :id => 3)
|
79
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task1])
|
80
|
-
|
81
|
-
@database_reload_frequency = 1.minute
|
82
|
-
|
83
|
-
@now = Time.now
|
84
|
-
|
85
|
-
# setup the database sync
|
86
|
-
@manager.sync_database_tasks model: ScheduledTask, every: @database_reload_frequency do |job_name|
|
87
|
-
@tasks_run << job_name
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def test_fetches_and_registers_database_task
|
92
|
-
tick_at(@now, :and_every_second_for => 1.second)
|
93
|
-
assert_equal ["ScheduledTask:1"], @tasks_run
|
94
|
-
end
|
95
|
-
|
96
|
-
def test_multiple_database_tasks_can_be_registered
|
97
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task1, @scheduled_task2])
|
98
|
-
tick_at(@now, :and_every_second_for => 1.second)
|
99
|
-
assert_equal ["ScheduledTask:1", "ScheduledTask:2"], @tasks_run
|
100
|
-
end
|
101
|
-
|
102
|
-
def test_database_task_does_not_run_again_before_frequency_specified_in_database
|
103
|
-
tick_at(@now, :and_every_second_for => @scheduled_task1.frequency - 1.second) # runs at 1
|
104
|
-
assert_equal 1, @tasks_run.length
|
105
|
-
end
|
106
|
-
|
107
|
-
def test_database_task_runs_repeatedly_with_frequency_specified_in_database
|
108
|
-
tick_at(@now, :and_every_second_for => (2 * @scheduled_task1.frequency) + 1.second) # runs at 1, 11, and 21
|
109
|
-
assert_equal 3, @tasks_run.length
|
110
|
-
end
|
111
|
-
|
112
|
-
def test_reloads_tasks_from_database
|
113
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task1], [@scheduled_task2])
|
114
|
-
tick_at(@now, :and_every_second_for => @database_reload_frequency.seconds)
|
115
|
-
@manager.tick # @scheduled_task2 should run immediately on next tick (then every 10 seconds)
|
116
|
-
|
117
|
-
assert_equal [
|
118
|
-
"ScheduledTask:1",
|
119
|
-
"ScheduledTask:1",
|
120
|
-
"ScheduledTask:1",
|
121
|
-
"ScheduledTask:1",
|
122
|
-
"ScheduledTask:1",
|
123
|
-
"ScheduledTask:1",
|
124
|
-
"ScheduledTask:2"], @tasks_run
|
125
|
-
end
|
126
|
-
|
127
|
-
def test_reloaded_tasks_run_repeatedly
|
128
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task1], [@scheduled_task2])
|
129
|
-
tick_at(@now, :and_every_second_for => @database_reload_frequency.seconds + 11.seconds)
|
130
|
-
assert_equal ["ScheduledTask:2", "ScheduledTask:2"], @tasks_run[-2..-1]
|
131
|
-
end
|
132
|
-
|
133
|
-
def test_reloading_task_with_modified_frequency_will_run_with_new_frequency
|
134
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task1], [@scheduled_task1_modified])
|
135
|
-
|
136
|
-
tick_at(@now, :and_every_second_for => 66.seconds)
|
137
|
-
|
138
|
-
# task1 runs at: 1, 11, 21, 31, 41, 51 (6 runs)
|
139
|
-
# database tasks are reloaded at: 60
|
140
|
-
# task1_modified runs at: 61 (next tick after reload) and then 66 (2 runs)
|
141
|
-
assert_equal 8, @tasks_run.length
|
142
|
-
end
|
143
|
-
|
144
|
-
def test_stops_running_deleted_database_task
|
145
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task1], [])
|
146
|
-
tick_at(@now, :and_every_second_for => @database_reload_frequency.seconds)
|
147
|
-
before = @tasks_run.dup
|
148
|
-
|
149
|
-
# tick through reload, and run for enough ticks that previous task would have run
|
150
|
-
tick_at(@now + @database_reload_frequency.seconds + 20.seconds)
|
151
|
-
after = @tasks_run
|
152
|
-
|
153
|
-
assert_equal before, after
|
154
|
-
end
|
155
|
-
|
156
|
-
def test_task_with_edited_name_switches_to_new_name
|
157
|
-
tick_at @now, :and_every_second_for => @database_reload_frequency.seconds - 1.second
|
158
|
-
@tasks_run = [] # clear tasks run before change
|
159
|
-
|
160
|
-
modified_task_1 = stub(:frequency => 30, :name => 'ScheduledTask:1_modified', :at => nil, :id => 1)
|
161
|
-
ScheduledTask.stubs(:all).returns([modified_task_1])
|
162
|
-
tick_at @now + @database_reload_frequency.seconds, :and_every_second_for => @database_reload_frequency.seconds - 1.seconds
|
163
|
-
|
164
|
-
assert_equal ["ScheduledTask:1_modified", "ScheduledTask:1_modified"], @tasks_run
|
165
|
-
end
|
166
|
-
|
167
|
-
def test_task_with_edited_frequency_switches_to_new_frequency
|
168
|
-
tick_at @now, :and_every_second_for => @database_reload_frequency.seconds - 1.second
|
169
|
-
@tasks_run = [] # clear tasks run before change
|
170
|
-
|
171
|
-
modified_task_1 = stub(:frequency => 30, :name => 'ScheduledTask:1', :at => nil, :id => 1)
|
172
|
-
ScheduledTask.stubs(:all).returns([modified_task_1])
|
173
|
-
tick_at @now + @database_reload_frequency.seconds, :and_every_second_for => @database_reload_frequency.seconds - 1.seconds
|
174
|
-
|
175
|
-
assert_equal 2, @tasks_run.length
|
176
|
-
end
|
177
|
-
|
178
|
-
def test_task_with_edited_at_runs_at_new_at
|
179
|
-
task_1 = stub(:frequency => 1.day, :name => 'ScheduledTask:1', :at => '10:30', :id => 1)
|
180
|
-
ScheduledTask.stubs(:all).returns([task_1])
|
181
|
-
|
182
|
-
assert_will_run 'jan 1 2010 10:30:00'
|
183
|
-
assert_wont_run 'jan 1 2010 09:30:00'
|
184
|
-
tick_at @now, :and_every_second_for => @database_reload_frequency.seconds - 1.second
|
185
|
-
|
186
|
-
modified_task_1 = stub(:frequency => 1.day, :name => 'ScheduledTask:1', :at => '09:30', :id => 1)
|
187
|
-
ScheduledTask.stubs(:all).returns([modified_task_1])
|
188
|
-
tick_at @now + @database_reload_frequency.seconds, :and_every_second_for => @database_reload_frequency.seconds - 1.seconds
|
189
|
-
|
190
|
-
assert_will_run 'jan 1 2010 09:30:00'
|
191
|
-
assert_wont_run 'jan 1 2010 10:30:00'
|
192
|
-
end
|
193
|
-
|
194
|
-
def test_daily_task_with_at_should_only_run_once
|
195
|
-
next_minute = next_minute(@now)
|
196
|
-
at = next_minute.strftime('%H:%M')
|
197
|
-
@scheduled_task_with_at = stub(:frequency => 1.day, :name => 'ScheduledTaskWithAt:1', :at => at, :id => 5)
|
198
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task_with_at])
|
199
|
-
|
200
|
-
# tick from now, though specified :at time
|
201
|
-
tick_at(@now, :and_every_second_for => (2 * @database_reload_frequency.seconds) + 1.second)
|
202
|
-
|
203
|
-
assert_equal 1, @tasks_run.length
|
204
|
-
end
|
205
|
-
|
206
|
-
def test_comma_separated_at_from_task_leads_to_multiple_event_ats
|
207
|
-
task = stub(:frequency => 1.day, :name => 'ScheduledTask:1', :at => '16:20, 18:10', :id => 1)
|
208
|
-
ScheduledTask.stubs(:all).returns([task])
|
209
|
-
|
210
|
-
tick_at @now, :and_every_second_for => @database_reload_frequency.seconds
|
211
|
-
|
212
|
-
assert_wont_run 'jan 1 2010 16:19:59'
|
213
|
-
assert_will_run 'jan 1 2010 16:20:00'
|
214
|
-
assert_wont_run 'jan 1 2010 16:20:01'
|
215
|
-
|
216
|
-
assert_wont_run 'jan 1 2010 18:09:59'
|
217
|
-
assert_will_run 'jan 1 2010 18:10:00'
|
218
|
-
assert_wont_run 'jan 1 2010 18:10:01'
|
219
|
-
end
|
220
|
-
|
221
|
-
def test_having_multiple_sync_database_tasks_will_work
|
222
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task1])
|
223
|
-
|
224
|
-
# setup 2nd database sync
|
225
|
-
@scheduled_task_type2 = stub(:frequency => 10, :name => 'ScheduledTaskType2:1', :at => nil, :id => 6)
|
226
|
-
|
227
|
-
ScheduledTaskType2.stubs(:all).returns([@scheduled_task_type2])
|
228
|
-
@manager.sync_database_tasks model: ScheduledTaskType2, every: @database_reload_frequency do |job_name|
|
229
|
-
@tasks_run << job_name
|
230
|
-
end
|
231
|
-
|
232
|
-
tick_at(@now, :and_every_second_for => 1.second)
|
233
|
-
|
234
|
-
assert_equal ["ScheduledTask:1", "ScheduledTaskType2:1"], @tasks_run
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
context "when database reload frequency is less than task frequency period" do
|
239
|
-
setup do
|
240
|
-
@tasks_run = []
|
241
|
-
@scheduled_task1 = stub(:frequency => 5.minutes, :name => 'ScheduledTask:1', :at => nil, :id => 1)
|
242
|
-
@scheduled_task2 = stub(:frequency => 10, :name => 'ScheduledTask:2', :at => nil, :id => 2)
|
243
|
-
@scheduled_task1_modified = stub(:frequency => 5, :name => 'ScheduledTaskModified:1', :at => nil)
|
244
|
-
ScheduledTask.stubs(:all).returns([@scheduled_task1])
|
245
|
-
|
246
|
-
@database_reload_frequency = 1.minute
|
247
|
-
|
248
|
-
@now = Time.now
|
249
|
-
@next_minute = next_minute(@now) # database sync task only happens on minute boundary
|
250
|
-
|
251
|
-
# setup the database sync
|
252
|
-
@manager.sync_database_tasks model: ScheduledTask, every: @database_reload_frequency do |job_name|
|
253
|
-
@tasks_run << job_name
|
254
|
-
end
|
255
|
-
end
|
256
|
-
|
257
|
-
def test_it_only_runs_the_task_once_within_the_task_frequency_period
|
258
|
-
tick_at(@now, :and_every_second_for => 5.minutes)
|
259
|
-
assert_equal 1, @tasks_run.length
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
context "with task with :at as empty string" do
|
264
|
-
setup do
|
265
|
-
@task_with_empty_string_at = stub(:frequency => 10, :name => 'ScheduledTask:1', :at => '', :id => 1)
|
266
|
-
ScheduledTask.stubs(:all).returns([@task_with_empty_string_at])
|
267
|
-
|
268
|
-
@tasks_run = []
|
269
|
-
|
270
|
-
@manager.sync_database_tasks(model: ScheduledTask, every: 1.minute) do |job_name|
|
271
|
-
@tasks_run << job_name
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
def test_it_does_not_raise_an_error
|
276
|
-
begin
|
277
|
-
tick_at(Time.now, :and_every_second_for => 10.seconds)
|
278
|
-
rescue => e
|
279
|
-
assert false, "Raised an error: #{e.message}"
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
def test_it_runs_the_task
|
284
|
-
begin
|
285
|
-
tick_at(Time.now, :and_every_second_for => 10.seconds)
|
286
|
-
rescue => e
|
287
|
-
end
|
288
|
-
assert_equal 1, @tasks_run.length
|
289
|
-
end
|
290
|
-
end
|
291
|
-
end
|
292
|
-
end
|