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.
@@ -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