clockwork 0.7.7 → 1.0.0

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