clockwork 0.7.7 → 1.0.0
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/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
|