tom_queue 0.0.1.dev

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.
@@ -0,0 +1,134 @@
1
+ require 'tom_queue/helper'
2
+
3
+ describe TomQueue::DeferredWorkSet do
4
+ let(:set) { TomQueue::DeferredWorkSet.new }
5
+
6
+ it "should be creatable" do
7
+ set.should be_a(TomQueue::DeferredWorkSet)
8
+ end
9
+
10
+ it "should allow work to be scheduled" do
11
+ set.schedule(Time.now + 0.2, "something")
12
+ set.schedule(Time.now + 0.3, "else")
13
+ set.size.should == 2
14
+ end
15
+
16
+ describe "earliest" do
17
+
18
+ it "should return nil if there is no work in the set" do
19
+ set.earliest.should be_nil
20
+ end
21
+
22
+ it "should return the only item if there is one item in the set" do
23
+ work = double("Work")
24
+ set.schedule( Time.now + 0.3, work)
25
+ set.earliest.should == work
26
+ end
27
+
28
+ it "should return the item in the set with the lowest run_at value" do
29
+ set.schedule( Time.now + 0.2, work1 = double("Work") )
30
+ set.schedule( Time.now + 0.1, work2 = double("Work") )
31
+ set.schedule( Time.now + 0.3, work3 = double("Work") )
32
+ set.earliest.should == work2
33
+ end
34
+
35
+ end
36
+
37
+ describe "pop" do
38
+
39
+ it "should return nil when the timeout expires" do
40
+ set.pop(0.1).should be_nil
41
+ end
42
+
43
+ it "should block for the timeout value if there is no work in the queue" do
44
+ start_time = Time.now
45
+ set.pop(0.1)
46
+ Time.now.should > start_time + 0.1
47
+ end
48
+
49
+ it "should block until the earliest work in the set" do
50
+ start_time = Time.now
51
+ set.schedule(start_time + 1.5, "work")
52
+ set.schedule(start_time + 0.1, "work")
53
+ set.pop(10)
54
+ Time.now.should > start_time + 0.1
55
+ Time.now.should < start_time + 0.2
56
+ end
57
+
58
+ it "should return immediately if tehre is work scheduled in the past" do
59
+ set.schedule(Time.now - 0.1, "work")
60
+ set.pop(10).should == "work"
61
+ end
62
+
63
+ it "should have removed the returned work from the set" do
64
+ set.schedule(Time.now - 0.1, "work")
65
+ set.pop(10)
66
+ set.size.should == 0
67
+ end
68
+
69
+ it "should return old work in temporal order" do
70
+ set.schedule(Time.now - 0.1, "work2")
71
+ set.schedule(Time.now - 0.2, "work1")
72
+ set.pop(10).should == "work1"
73
+ set.pop(10).should == "work2"
74
+ end
75
+
76
+ it "should return the earliest work" do
77
+ start_time = Time.now
78
+ set.schedule(start_time + 0.1, "work")
79
+ set.pop(10).should == "work"
80
+ end
81
+
82
+ it "should return immediately if it is interrupted by an external thread" do
83
+ Thread.new { sleep 0.1; set.interrupt }
84
+ start_time = Time.now
85
+ set.schedule(start_time + 1.5, "work")
86
+ set.schedule(start_time + 5, "work")
87
+ set.pop(10)
88
+ Time.now.should > start_time + 0.1
89
+ Time.now.should < start_time + 0.2
90
+ end
91
+
92
+ it "should block until the earliest work, even if earlier work is added after the block" do
93
+ start_time = Time.now
94
+ Thread.new do
95
+ sleep 0.1
96
+ set.schedule(start_time + 0.2, "early")
97
+ end
98
+ set.schedule(start_time + 1.5, "late")
99
+ set.pop(10)
100
+ Time.now.should > start_time + 0.2
101
+ Time.now.should < start_time + 0.3
102
+ end
103
+
104
+ it "should raise an exception if two threads try to block on the same work set" do
105
+ Thread.new do
106
+ set.pop(1)
107
+ end
108
+ sleep 0.1
109
+ lambda {
110
+ set.pop(1)
111
+ }.should raise_exception(/another thread is already blocked/)
112
+ end
113
+
114
+ it "should not get deferred items caught outside the cache" do
115
+ start_time = Time.now
116
+ 50.times { |i| set.schedule(start_time+0.1+i*0.001, "bulk") }
117
+ set.schedule(start_time+0.2, "missing")
118
+
119
+ 50.times { set.pop(1).should == "bulk" }
120
+
121
+ set.schedule(start_time+0.3, "final")
122
+ set.pop(1).should == "missing"
123
+ set.pop(1).should == "final"
124
+ end
125
+
126
+ it "should not delete all elements with the same run_at" do
127
+ the_time = Time.now + 0.1
128
+ set.schedule(the_time, "work-1")
129
+ set.schedule(the_time, "work-2")
130
+ 2.times.collect { set.pop(1) }.sort.should == ["work-1", "work-2"]
131
+ end
132
+ end
133
+
134
+ end
@@ -0,0 +1,155 @@
1
+ require 'tom_queue/helper'
2
+ require 'tom_queue/delayed_job'
3
+
4
+ describe Delayed::Job, "integration spec", :timeout => 10 do
5
+
6
+ class TestJobClass
7
+ cattr_accessor :perform_hook
8
+
9
+ @@flunk_count = 0
10
+ cattr_accessor :flunk_count
11
+
12
+ @@asplode_count = 0
13
+ cattr_accessor :asplode_count
14
+
15
+ def initialize(name)
16
+ @name = name
17
+ end
18
+
19
+ def perform
20
+ @@perform_hook && @@perform_hook.call(@name)
21
+
22
+ if @@asplode_count > 0
23
+ @@asplode_count -= 1
24
+ Thread.exit
25
+ end
26
+
27
+ if @@flunk_count > 0
28
+ @@flunk_count -= 1
29
+ raise RuntimeError, "Failed to run job"
30
+ end
31
+ end
32
+
33
+ def reschedule_at(time, attempts)
34
+ time + 0.5
35
+ end
36
+
37
+ end
38
+
39
+ let(:job_name) { "Job-#{Time.now.to_f}" }
40
+
41
+ before do
42
+ # Clean-slate ...
43
+ TomQueue.default_prefix = "test-#{Time.now.to_f}"
44
+ TomQueue::DelayedJob.apply_hook!
45
+ Delayed::Job.delete_all
46
+ Delayed::Job.class_variable_set(:@@tomqueue_manager, nil)
47
+
48
+ # Keep track of how many times the job is run
49
+ @called = []
50
+ TestJobClass.perform_hook = lambda { |name| @called << name }
51
+
52
+ # Reset the flunk count
53
+ TestJobClass.flunk_count = 0
54
+ end
55
+
56
+ it "should actually be using the queue" do
57
+ Delayed::Job.enqueue(TestJobClass.new(job_name))
58
+
59
+ Delayed::Job.tomqueue_manager.queues[TomQueue::NORMAL_PRIORITY].status[:message_count].should == 1
60
+ end
61
+
62
+ it "should integrate with Delayed::Worker" do
63
+ Delayed::Job.enqueue(TestJobClass.new(job_name))
64
+
65
+ Delayed::Worker.new.work_off(1).should == [1, 0] # 1 success, 0 failed
66
+ @called.first.should == job_name
67
+ end
68
+
69
+ it "should still back-off jobs" do
70
+ Delayed::Job.enqueue(TestJobClass.new(job_name))
71
+ TestJobClass.flunk_count = 1
72
+
73
+ Benchmark.realtime {
74
+ Delayed::Worker.new.work_off(1).should == [0, 1]
75
+ Delayed::Worker.new.work_off(1).should == [1, 0]
76
+ }.should > 0.5
77
+ end
78
+
79
+ it "should support run_at" do
80
+ Benchmark.realtime {
81
+ Delayed::Job.enqueue(TestJobClass.new("job1"), :run_at => Time.now + 0.5)
82
+ Delayed::Job.enqueue(TestJobClass.new("job2"), :run_at => Time.now + 0.1)
83
+ Delayed::Worker.new.work_off(2).should == [2, 0]
84
+ }.should > 0.1
85
+ @called.should == ["job2", "job1"]
86
+ end
87
+
88
+ it "should support job priorities" do
89
+ TomQueue::DelayedJob.priority_map[0] = TomQueue::NORMAL_PRIORITY
90
+ TomQueue::DelayedJob.priority_map[1] = TomQueue::HIGH_PRIORITY
91
+ Delayed::Job.enqueue(TestJobClass.new("low1"), :priority => 0)
92
+ Delayed::Job.enqueue(TestJobClass.new("low2"), :priority => 0)
93
+ Delayed::Job.enqueue(TestJobClass.new("high"), :priority => 1)
94
+ Delayed::Job.enqueue(TestJobClass.new("low3"), :priority => 0)
95
+ Delayed::Job.enqueue(TestJobClass.new("low4"), :priority => 0)
96
+ Delayed::Worker.new.work_off(5)
97
+ @called.should == ["high", "low1", "low2", "low3", "low4"]
98
+ end
99
+
100
+ it "should not run a failed job" do
101
+ logfile = Tempfile.new('logfile')
102
+ TomQueue.logger = Logger.new(logfile.path)
103
+ Delayed::Job.delete_all
104
+ # this will send the notification
105
+ job = "Hello".delay.to_s
106
+
107
+ # now make the job look like it has failed
108
+ job.attempts = 0
109
+ job.failed_at = Time.now
110
+ job.last_error = "Some error"
111
+ job.save
112
+
113
+ job.should be_failed
114
+
115
+ Delayed::Job.tomqueue_republish
116
+
117
+ # The job should get ignored for both runs
118
+ Delayed::Worker.new.work_off(1)
119
+ Delayed::Worker.new.work_off(1)
120
+
121
+ # And, since it never got run, it should still exist!
122
+ Delayed::Job.find_by_id(job.id).should_not be_nil
123
+ # And it should have been noisy, too.
124
+ File.read(logfile.path).should =~ /Received notification for failed job #{job.id}/
125
+ end
126
+
127
+ # it "should re-run the job once max_run_time is reached if, say, a worker crashes" do
128
+ # Delayed::Worker.max_run_time = 2
129
+
130
+ # job = Delayed::Job.enqueue(TestJobClass.new("work"))
131
+
132
+ # # This thread will be abruptly terminated mid-job
133
+ # TestJobClass.asplode_count = 1
134
+ # lock_stale_time = Time.now.to_f + Delayed::Worker.max_run_time
135
+ # Thread.new { Delayed::Worker.new.work_off(1) }.join
136
+
137
+ # # This will shutdown the various channels, which should result in the message being
138
+ # # returned to the broker.
139
+ # Delayed::Job.tomqueue_manager.setup_amqp!
140
+
141
+ # # Make sure the job is still locked
142
+ # job.reload
143
+ # job.locked_at.should_not be_nil
144
+ # job.locked_by.should_not be_nil
145
+
146
+ # # Now wait for the max_run_time, which is artificially low
147
+ # while Delayed::Job.find_by_id(job.id)
148
+ # Delayed::Worker.new.work_off(1)
149
+ # end
150
+
151
+ # # Ensure the worker blocked until the job's original lock was actually stale.
152
+ # Time.now.to_f.should > lock_stale_time.to_f
153
+ # end
154
+
155
+ end
@@ -0,0 +1,818 @@
1
+ require 'tom_queue/helper'
2
+
3
+ describe TomQueue, "once hooked" do
4
+
5
+ let(:job) { Delayed::Job.create! }
6
+ let(:new_job) { Delayed::Job.new }
7
+
8
+
9
+ it "should set the Delayed::Worker sleep delay to 0" do
10
+ # This makes sure the Delayed::Worker loop spins around on
11
+ # an empty queue to block on TomQueue::QueueManager#pop, so
12
+ # the job will start as soon as we receive a push from RMQ
13
+ Delayed::Worker.sleep_delay.should == 0
14
+ end
15
+
16
+ describe "TomQueue::DelayedJob::Job" do
17
+ it "should use the TomQueue job as the Delayed::Job" do
18
+ Delayed::Job.should == TomQueue::DelayedJob::Job
19
+ end
20
+
21
+ it "should be a subclass of ::Delayed::Backend::ActiveRecord::Job" do
22
+ TomQueue::DelayedJob::Job.superclass.should == ::Delayed::Backend::ActiveRecord::Job
23
+ end
24
+ end
25
+
26
+ describe "Delayed::Job.tomqueue_manager" do
27
+ it "should return a TomQueue::QueueManager instance" do
28
+ Delayed::Job.tomqueue_manager.should be_a(TomQueue::QueueManager)
29
+ end
30
+
31
+ it "should have used the default prefix configured" do
32
+ Delayed::Job.tomqueue_manager.prefix.should == TomQueue.default_prefix
33
+ end
34
+
35
+ it "should return the same object on subsequent calls" do
36
+ Delayed::Job.tomqueue_manager.should == Delayed::Job.tomqueue_manager
37
+ end
38
+
39
+ it "should be reset by rspec (1)" do
40
+ TomQueue.default_prefix = "foo"
41
+ Delayed::Job.tomqueue_manager.prefix.should == "foo"
42
+ end
43
+
44
+ it "should be reset by rspec (2)" do
45
+ TomQueue.default_prefix = "bar"
46
+ Delayed::Job.tomqueue_manager.prefix.should == "bar"
47
+ end
48
+ end
49
+
50
+ describe "Delayed::Job#tomqueue_digest" do
51
+
52
+ it "should return a different value when the object is saved" do
53
+ first_digest = job.tomqueue_digest
54
+ job.update_attributes(:run_at => Time.now + 10.seconds)
55
+ job.tomqueue_digest.should_not == first_digest
56
+ end
57
+
58
+ it "should return the same value, regardless of the time zone (regression)" do
59
+ ActiveRecord::Base.time_zone_aware_attributes = true
60
+ old_zone, Time.zone = Time.zone, "Hawaii"
61
+
62
+ job = Delayed::Job.create!
63
+ first_digest = job.tomqueue_digest
64
+
65
+ Time.zone = "Auckland"
66
+
67
+ job = Delayed::Job.find(job.id)
68
+ job.tomqueue_digest.should == first_digest
69
+
70
+ Time.zone = old_zone
71
+ end
72
+ end
73
+
74
+ describe "Delayed::Job#tomqueue_payload" do
75
+
76
+ let(:payload) { JSON.load(job.tomqueue_payload)}
77
+
78
+ it "should return a hash" do
79
+ payload.should be_a(Hash)
80
+ end
81
+
82
+ it "should contain the job id" do
83
+ payload['delayed_job_id'].should == job.id
84
+ end
85
+
86
+ it "should contain the current updated_at timestamp (with second-level precision)" do
87
+ payload['delayed_job_updated_at'].should == job.updated_at.iso8601(0)
88
+ end
89
+
90
+ it "should contain the digest after saving" do
91
+ payload['delayed_job_digest'].should == job.tomqueue_digest
92
+ end
93
+ end
94
+
95
+ describe "Delayed::Job#tomqueue_publish" do
96
+
97
+ it "should return nil" do
98
+ job.tomqueue_publish.should be_nil
99
+ end
100
+
101
+ it "should raise an exception if it is called on an unsaved job" do
102
+ TomQueue.exception_reporter = double("SilentExceptionReporter", :notify => nil)
103
+ lambda {
104
+ Delayed::Job.new.tomqueue_publish
105
+ }.should raise_exception(ArgumentError, /cannot publish an unsaved Delayed::Job/)
106
+ end
107
+
108
+ describe "when it is called on a persisted job" do
109
+
110
+ before do
111
+ job # create the job first so we don't trigger the expectation twice
112
+
113
+ @called = false
114
+ Delayed::Job.tomqueue_manager.should_receive(:publish) do |payload, opts|
115
+ @called = true
116
+ @payload = payload
117
+ @opts = opts
118
+ end
119
+ end
120
+
121
+ it "should call publish on the queue manager" do
122
+ job.tomqueue_publish
123
+ @called.should be_true
124
+ end
125
+
126
+ describe "job priority" do
127
+ before do
128
+ TomQueue::DelayedJob.priority_map[-10] = TomQueue::BULK_PRIORITY
129
+ TomQueue::DelayedJob.priority_map[10] = TomQueue::HIGH_PRIORITY
130
+ end
131
+
132
+ it "should map the priority of the job to the TomQueue priority" do
133
+ new_job.priority = -10
134
+ new_job.save
135
+ @opts[:priority].should == TomQueue::BULK_PRIORITY
136
+ end
137
+
138
+ describe "if an unknown priority value is used" do
139
+ before do
140
+ new_job.priority = 99
141
+ end
142
+
143
+ it "should default the priority to TomQueue::NORMAL_PRIORITY" do
144
+ new_job.save
145
+ @opts[:priority].should == TomQueue::NORMAL_PRIORITY
146
+ end
147
+
148
+ it "should log a warning" do
149
+ TomQueue.logger.should_receive(:warn)
150
+ new_job.save
151
+ end
152
+ end
153
+ end
154
+
155
+ describe "run_at value" do
156
+
157
+ it "should use the job's :run_at value by default" do
158
+ job.tomqueue_publish
159
+ @opts[:run_at].should == job.run_at
160
+ end
161
+
162
+ it "should use the run_at value provided if provided by the caller" do
163
+ the_time = Time.now + 10.seconds
164
+ job.tomqueue_publish(the_time)
165
+ @opts[:run_at].should == the_time
166
+ end
167
+
168
+ end
169
+
170
+ describe "the payload" do
171
+
172
+ before { job.stub(:tomqueue_payload => "PAYLOAD") }
173
+
174
+ it "should be the return value from #tomqueue_payload" do
175
+ job.tomqueue_publish
176
+ @payload.should == "PAYLOAD"
177
+ end
178
+ end
179
+ end
180
+
181
+ describe "if an exception is raised during the publish" do
182
+ let(:exception) { RuntimeError.new("Bugger. Dropped the ball, sorry.") }
183
+
184
+ before do
185
+ TomQueue.exception_reporter = double("SilentExceptionReporter", :notify => nil)
186
+ Delayed::Job.tomqueue_manager.should_receive(:publish).and_raise(exception)
187
+ end
188
+
189
+ it "should not be raised out to the caller" do
190
+ lambda { new_job.save }.should_not raise_exception
191
+ end
192
+
193
+ it "should notify the exception reporter" do
194
+ TomQueue.exception_reporter.should_receive(:notify).with(exception)
195
+ new_job.save
196
+ end
197
+
198
+ it "should do nothing if the exception reporter is nil" do
199
+ TomQueue.exception_reporter = nil
200
+ lambda { new_job.save }.should_not raise_exception
201
+ end
202
+
203
+ it "should log an error message to the log" do
204
+ TomQueue.logger.should_receive(:error)
205
+ new_job.save
206
+ end
207
+ end
208
+ end
209
+
210
+ describe "publish callbacks in Job lifecycle" do
211
+
212
+ it "should allow Mock::ExpectationFailed exceptions to escape the callback" do
213
+ TomQueue.logger = Logger.new("/dev/null")
214
+ TomQueue.exception_reporter = nil
215
+ Delayed::Job.tomqueue_manager.should_receive(:publish).with("spurious arguments").once
216
+ lambda {
217
+ job.update_attributes(:run_at => Time.now + 5.seconds)
218
+ }.should raise_exception(RSpec::Mocks::MockExpectationError)
219
+
220
+ Delayed::Job.tomqueue_manager.publish("spurious arguments") # do this, otherwise it will fail
221
+ end
222
+
223
+ it "should not publish a message if the job has a non-nil failed_at" do
224
+ job.should_not_receive(:tomqueue_publish)
225
+ job.update_attributes(:failed_at => Time.now)
226
+ end
227
+
228
+ it "should be called after create when there is no explicit transaction" do
229
+ new_job.should_receive(:tomqueue_publish).with(no_args)
230
+ new_job.save!
231
+ end
232
+
233
+ it "should be called after update when there is no explicit transaction" do
234
+ job.should_receive(:tomqueue_publish).with(no_args)
235
+ job.run_at = Time.now + 10.seconds
236
+ job.save!
237
+ end
238
+
239
+ it "should be called after commit, when a record is saved" do
240
+ new_job.stub(:tomqueue_publish) { @called = true }
241
+ Delayed::Job.transaction do
242
+ new_job.save!
243
+
244
+ @called.should be_nil
245
+ end
246
+ @called.should be_true
247
+ end
248
+
249
+ it "should be called after commit, when a record is updated" do
250
+ job.stub(:tomqueue_publish) { @called = true }
251
+ Delayed::Job.transaction do
252
+ job.run_at = Time.now + 10.seconds
253
+ job.save!
254
+ @called.should be_nil
255
+ end
256
+
257
+ @called.should be_true
258
+ end
259
+
260
+ it "should not be called when a record is destroyed" do
261
+ job.should_not_receive(:tomqueue_publish)
262
+ job.destroy
263
+ end
264
+
265
+ it "should not be called by a destroy in a transaction" do
266
+ job.should_not_receive(:tomqueue_publish)
267
+ Delayed::Job.transaction { job.destroy }
268
+ end
269
+
270
+ it "should not be called if the update transaction is rolled back" do
271
+ job.stub(:tomqueue_publish) { @called = true }
272
+
273
+ Delayed::Job.transaction do
274
+ job.run_at = Time.now + 10.seconds
275
+ job.save!
276
+ raise ActiveRecord::Rollback
277
+ end
278
+ @called.should be_nil
279
+ end
280
+
281
+ it "should not be called if the create transaction is rolled back" do
282
+ job.should_not_receive(:tomqueue_publish)
283
+
284
+ Delayed::Job.transaction do
285
+ new_job.save!
286
+ raise ActiveRecord::Rollback
287
+ end
288
+ @called.should be_nil
289
+ end
290
+ end
291
+
292
+ describe "Delayed::Job.tomqueue_republish method" do
293
+ before { Delayed::Job.delete_all }
294
+
295
+ it "should exist" do
296
+ Delayed::Job.respond_to?(:tomqueue_republish).should be_true
297
+ end
298
+
299
+ it "should return nil" do
300
+ Delayed::Job.tomqueue_republish.should be_nil
301
+ end
302
+
303
+ it "should call #tomqueue_publish on all DB records" do
304
+ 10.times { Delayed::Job.create! }
305
+
306
+ Delayed::Job.tomqueue_manager.queues[TomQueue::NORMAL_PRIORITY].purge
307
+ queue = Delayed::Job.tomqueue_manager.queues[TomQueue::NORMAL_PRIORITY]
308
+ queue.message_count.should == 0
309
+
310
+ Delayed::Job.tomqueue_republish
311
+ queue.message_count.should == 10
312
+ end
313
+
314
+ it "should work with ActiveRecord scopes" do
315
+ first_ids = 10.times.collect { Delayed::Job.create!.id }
316
+ second_ids = 7.times.collect { Delayed::Job.create!.id }
317
+
318
+ Delayed::Job.tomqueue_manager.queues[TomQueue::NORMAL_PRIORITY].purge
319
+ queue = Delayed::Job.tomqueue_manager.queues[TomQueue::NORMAL_PRIORITY]
320
+ queue.message_count.should == 0
321
+
322
+ Delayed::Job.where('id IN (?)', second_ids).tomqueue_republish
323
+ queue.message_count.should == 7
324
+ end
325
+
326
+ end
327
+
328
+ describe "Delayed::Job.acquire_locked_job" do
329
+ let(:time) { Delayed::Job.db_time_now }
330
+ before { Delayed::Job.stub(:db_time_now => time) }
331
+
332
+ let(:job) { Delayed::Job.create! }
333
+ let(:worker) { Delayed::Worker.new }
334
+
335
+ # make sure the job exists!
336
+ before { job }
337
+
338
+ subject { Delayed::Job.acquire_locked_job(job.id, worker, &@block) }
339
+
340
+ describe "when the job doesn't exist" do
341
+ before { job.destroy }
342
+
343
+ it "should return nil" do
344
+ subject.should be_nil
345
+ end
346
+
347
+ it "should not yield if a block is provided" do
348
+ @block = lambda { |value| @called = true}
349
+ subject
350
+ @called.should be_nil
351
+ end
352
+ end
353
+
354
+ describe "when the job exists" do
355
+
356
+ it "should hold an explicit DB lock whilst performing the lock" do
357
+ pending("Only possible when using mysql2 adapter (ADAPTER=mysql environment)") unless ActiveRecord::Base.connection.class.to_s == "ActiveRecord::ConnectionAdapters::Mysql2Adapter"
358
+
359
+ # Ok, fudge a second parallel connection to MySQL
360
+ second_connection = ActiveRecord::Base.connection.dup
361
+ second_connection.reconnect!
362
+ ActiveRecord::Base.connection.reset!
363
+
364
+ # Assert we have separate connections
365
+ second_connection.select("SELECT connection_id() as id;").first["id"].should_not ==
366
+ ActiveRecord::Base.connection.select("SELECT connection_id() as id;").first["id"]
367
+
368
+ # This is called in a thread when the transaction is open to query the job, store the response
369
+ # and the time when the response comes back
370
+ parallel_query = lambda do |job_id|
371
+ begin
372
+ # Simulate another worker performing a SELECT ... FOR UPDATE request
373
+ @query_result = second_connection.select("SELECT * FROM delayed_jobs WHERE id=#{job_id} LIMIT 1 FOR UPDATE").first
374
+ @query_returned_at = Time.now.to_f
375
+ rescue
376
+ puts "Failed #{$!.inspect}"
377
+ end
378
+ end
379
+
380
+ # When we have the transaction open, we emit a query on a parallel thread and check the timing
381
+ @block = lambda do |job|
382
+ @thread = Thread.new(job.id, &parallel_query)
383
+ sleep 0.25
384
+ @leaving_transaction_at = Time.now.to_f
385
+ true
386
+ end
387
+
388
+ # Kick it all off !
389
+ subject
390
+
391
+ @thread.join
392
+
393
+ # now make sure the parallel thread blocked until the transaction returned
394
+ @query_returned_at.should > @leaving_transaction_at
395
+
396
+ # make sure the returned record showed the lock
397
+ @query_result["locked_at"].should_not be_nil
398
+ @query_result["locked_by"].should_not be_nil
399
+ end
400
+
401
+ describe "when the job is marked as failed" do
402
+ let(:failed_time) { Time.now - 10 }
403
+
404
+ before do
405
+ job.update_attribute(:failed_at, failed_time)
406
+ end
407
+
408
+ it "should return nil" do
409
+ subject.should be_nil
410
+ end
411
+
412
+ it "should not modify the failed_at value" do
413
+ subject
414
+ job.reload
415
+ job.failed_at.to_i.should == failed_time.to_i
416
+ end
417
+
418
+ it "should not lock the job" do
419
+ subject
420
+ job.reload
421
+ job.locked_by.should be_nil
422
+ job.locked_at.should be_nil
423
+ end
424
+ end
425
+
426
+ describe "when the notification is delivered too soon" do
427
+
428
+ before do
429
+ actual_time = Delayed::Job.db_time_now
430
+ Delayed::Job.stub(:db_time_now => actual_time - 10)
431
+ end
432
+
433
+ it "should return nil" do
434
+ subject.should be_nil
435
+ end
436
+
437
+ it "should re-post a notification" do
438
+ Delayed::Job.tomqueue_manager.should_receive(:publish) do |payload, args|
439
+ args[:run_at].to_i.should == job.run_at.to_i
440
+ end
441
+ subject
442
+ end
443
+
444
+ it "should not lock the job" do
445
+ subject
446
+ job.reload
447
+ job.locked_by.should be_nil
448
+ job.locked_at.should be_nil
449
+ end
450
+
451
+ end
452
+
453
+ describe "when the job is not locked" do
454
+
455
+ it "should acquire the lock fields on the job" do
456
+ subject
457
+ job.reload
458
+ job.locked_at.to_i.should == time.to_i
459
+ job.locked_by.should == worker.name
460
+ end
461
+
462
+ it "should return the job object" do
463
+ subject.should be_a(Delayed::Job)
464
+ subject.id.should == job.id
465
+ end
466
+
467
+ it "should yield the job to the block if present" do
468
+ @block = lambda { |value| @called = value}
469
+ subject
470
+ @called.should be_a(Delayed::Job)
471
+ @called.id.should == job.id
472
+ end
473
+
474
+ it "should not have locked the job when the block is called" do
475
+ @block = lambda { |job| @called = [job.id, job.locked_at, job.locked_by]; true }
476
+ subject
477
+ @called.should == [job.id, nil, nil]
478
+ end
479
+
480
+ describe "if the supplied block returns true" do
481
+ before { @block = lambda { |_| true } }
482
+
483
+ it "should lock the job" do
484
+ subject
485
+ job.reload
486
+ job.locked_at.to_i.should == time.to_i
487
+ job.locked_by.should == worker.name
488
+ end
489
+
490
+ it "should return the job" do
491
+ subject.should be_a(Delayed::Job)
492
+ subject.id.should == job.id
493
+ end
494
+ end
495
+
496
+ describe "if the supplied block returns false" do
497
+ before { @block = lambda { |_| false } }
498
+
499
+ it "should not lock the job" do
500
+ subject
501
+ job.reload
502
+ job.locked_at.should be_nil
503
+ job.locked_by.should be_nil
504
+ end
505
+
506
+ it "should return nil" do
507
+ subject.should be_nil
508
+ end
509
+ end
510
+ end
511
+
512
+ describe "when the job is locked with a valid lock" do
513
+
514
+ before do
515
+ @old_locked_by = job.locked_by = "some worker"
516
+ @old_locked_at = job.locked_at = Time.now
517
+ job.save!
518
+ end
519
+
520
+ it "should not yield to a block if provided" do
521
+ @called = false
522
+ @block = lambda { |_| @called = true}
523
+ subject
524
+ @called.should be_false
525
+ end
526
+
527
+ it "should return false" do
528
+ subject.should be_false
529
+ end
530
+
531
+ it "should not change the lock" do
532
+ subject
533
+ job.reload
534
+ job.locked_by.should == @old_locked_by
535
+ job.locked_at.to_i.should == @old_locked_at.to_i
536
+ end
537
+
538
+ end
539
+
540
+ describe "when the job is locked with a stale lock" do
541
+ before do
542
+ @old_locked_by = job.locked_by = "some worker"
543
+ @old_locked_at = job.locked_at = (Time.now - Delayed::Worker.max_run_time - 1)
544
+ job.save!
545
+ end
546
+
547
+ it "should return the job" do
548
+ subject.should be_a(Delayed::Job)
549
+ subject.id.should == job.id
550
+ end
551
+
552
+ it "should update the lock" do
553
+ subject
554
+ job.reload
555
+ job.locked_at.should_not == @old_locked_at
556
+ job.locked_by.should_not == @old_locked_by
557
+ end
558
+
559
+ # This is tricky - if we have a stale lock, the job object
560
+ # will have been updated by the first worker, so the digest will
561
+ # now be invalid (since updated_at will have changed)
562
+ #
563
+ # So, we don't yield, we just presume we're carrying on from where
564
+ # a previous worker left off and don't try and validate the job any
565
+ # further.
566
+ it "should not yield the block if supplied" do
567
+ @called = false
568
+ @block = lambda { |_| @called = true}
569
+ subject
570
+ @called.should be_false
571
+ end
572
+ end
573
+
574
+ end
575
+ end
576
+
577
+ describe "Delayed::Job.reserve - return the next job" do
578
+ let(:job) { Delayed::Job.create! }
579
+ let(:worker) { double("Worker", :name => "Worker-Name-#{Time.now.to_f}") }
580
+ let(:payload) { job.tomqueue_payload }
581
+ let(:work) { double("Work", :payload => payload, :ack! => nil) }
582
+
583
+ subject { Delayed::Job.reserve(worker) }
584
+
585
+ before do
586
+ Delayed::Job.tomqueue_manager.stub(:pop => work)
587
+ end
588
+
589
+ it "should call pop on the queue manager" do
590
+ Delayed::Job.tomqueue_manager.should_receive(:pop)
591
+
592
+ subject
593
+ end
594
+
595
+ describe "signal handling" do
596
+ it "should allow signal handlers during the pop" do
597
+ Delayed::Worker.raise_signal_exceptions = false
598
+ Delayed::Job.tomqueue_manager.should_receive(:pop) do
599
+ Delayed::Worker.raise_signal_exceptions.should be_true
600
+ work
601
+ end
602
+ Delayed::Job.reserve(worker)
603
+ end
604
+
605
+ it "should reset the signal handler var after the pop" do
606
+ Delayed::Worker.raise_signal_exceptions = false
607
+ subject
608
+ Delayed::Worker.raise_signal_exceptions.should == false
609
+ end
610
+
611
+ it "should reset the signal handler var even if it's already true" do
612
+ Delayed::Worker.raise_signal_exceptions = true
613
+ subject
614
+ Delayed::Worker.raise_signal_exceptions.should == true
615
+ end
616
+
617
+ it "should allow exceptions to escape the function" do
618
+ Delayed::Job.tomqueue_manager.should_receive(:pop) do
619
+ raise SignalException, "INT"
620
+ end
621
+ lambda { subject }.should raise_exception(SignalException)
622
+ end
623
+ end
624
+
625
+ describe "if a nil message is popped" do
626
+ before { Delayed::Job.tomqueue_manager.stub(:pop=>nil) }
627
+
628
+ it "should return nil" do
629
+ subject.should be_nil
630
+ end
631
+
632
+ it "should sleep for a second to avoid potentially tight loops" do
633
+ start_time = Time.now
634
+ subject
635
+ (Time.now - start_time).should > 1.0
636
+ end
637
+ end
638
+
639
+ describe "if the work payload doesn't cleanly JSON decode" do
640
+ before do
641
+ TomQueue.logger = Logger.new("/dev/null")
642
+ TomQueue.exception_reporter = nil
643
+ end
644
+
645
+ let(:payload) { "NOT JSON!!1" }
646
+
647
+ it "should report an exception" do
648
+ TomQueue.exception_reporter = mock("Reporter", :notify => nil)
649
+ TomQueue.exception_reporter.should_receive(:notify).with(instance_of(JSON::ParserError))
650
+ subject
651
+ end
652
+
653
+ it "should be happy if no exception reporter is set" do
654
+ TomQueue.exception_reporter = nil
655
+ subject
656
+ end
657
+
658
+ it "should ack the message" do
659
+ work.should_receive(:ack!)
660
+ subject
661
+ end
662
+
663
+ it "should log an error" do
664
+ TomQueue.logger.should_receive(:error)
665
+ subject
666
+ end
667
+
668
+ it "should return nil" do
669
+ subject.should be_nil
670
+ end
671
+ end
672
+
673
+ it "should call acquire_locked_job with the job_id and the worker" do
674
+ Delayed::Job.should_receive(:acquire_locked_job).with(job.id, worker)
675
+ subject
676
+ end
677
+
678
+ it "should attach a block to the call to acquire_locked_job" do
679
+ def stub_implementation(job_id, worker, &block)
680
+ block.should_not be_nil
681
+ end
682
+ Delayed::Job.should_receive(:acquire_locked_job, &method(:stub_implementation)).and_return(job)
683
+ subject
684
+ end
685
+
686
+ describe "the block provided to acquire_locked_job" do
687
+ before do |test|
688
+ def stub_implementation(job_id, worker, &block)
689
+ @block = block
690
+ end
691
+ Delayed::Job.should_receive(:acquire_locked_job, &method(:stub_implementation)).and_return(job)
692
+ end
693
+
694
+ it "should return true if the digest in the message payload matches the job" do
695
+ subject
696
+ @block.call(job).should be_true
697
+ end
698
+
699
+ it "should return false if the digest in the message payload doesn't match the job" do
700
+ subject
701
+ job.touch(:updated_at)
702
+ @block.call(job).should be_true
703
+ end
704
+
705
+ it "should return true if there is no digest in the payload object" do
706
+ work.stub(:payload => JSON.dump(JSON.load(payload).merge("delayed_job_digest" => nil)))
707
+ subject
708
+ @block.call(job).should be_true
709
+ end
710
+
711
+ end
712
+
713
+ describe "when acquire_locked_job returns the job object" do
714
+ # A.K.A We have a locked job!
715
+
716
+ let(:returned_job) { Delayed::Job.find(job.id) }
717
+ before { Delayed::Job.stub(:acquire_locked_job => returned_job) }
718
+
719
+ it "should not ack the message" do
720
+ work.should_not_receive(:ack!)
721
+ subject
722
+ end
723
+
724
+ it "should return the Job object" do
725
+ subject.should == returned_job
726
+ end
727
+
728
+ it "should associate the message object with the job" do
729
+ subject.tomqueue_work.should == work
730
+ end
731
+
732
+ end
733
+
734
+ describe "when acquire_locked_job returns false" do
735
+ # A.K.A The lock is held by another worker.
736
+ # - we post a notification to re-try after the max_run_time
737
+
738
+ before do
739
+ job.locked_at = Delayed::Job.db_time_now - 10
740
+ job.locked_by = "foobar"
741
+ job.save!
742
+ Delayed::Job.stub(:acquire_locked_job => false)
743
+ end
744
+
745
+ it "should ack the message" do
746
+ work.should_receive(:ack!)
747
+ subject
748
+ end
749
+
750
+ it "should publish a notification for after the max-run-time of the job" do
751
+ Delayed::Job.tomqueue_manager.should_receive(:publish) do |payload, opts|
752
+ opts[:run_at].to_i.should == job.locked_at.to_i + Delayed::Worker.max_run_time + 1
753
+ end
754
+ subject
755
+ end
756
+
757
+ it "should return nil" do
758
+ subject.should be_nil
759
+ end
760
+
761
+ end
762
+
763
+ describe "when acquire_locked_job returns nil" do
764
+ # A.K.A The job doesn't exist anymore!
765
+ # - we're done!
766
+
767
+ before { Delayed::Job.stub(:acquire_locked_job => nil) }
768
+
769
+ it "should ack the message" do
770
+ work.should_receive(:ack!)
771
+ subject
772
+ end
773
+
774
+ it "should return nil" do
775
+ subject.should be_nil
776
+ end
777
+
778
+ end
779
+
780
+
781
+ end
782
+
783
+ describe "Job#invoke_job" do
784
+ let(:payload) { double("DelayedJobPayload", :perform => nil)}
785
+ let(:job) { Delayed::Job.create!(:payload_object=>payload) }
786
+
787
+ it "should perform the job" do
788
+ payload.should_receive(:perform)
789
+ job.invoke_job
790
+ end
791
+
792
+ it "should not have a problem if tomqueue_work is nil" do
793
+ job.tomqueue_work = nil
794
+ job.invoke_job
795
+ end
796
+
797
+ describe "if there is a tomqueue work object set on the object" do
798
+ let(:work_object) { double("WorkObject", :ack! => nil)}
799
+ before { job.tomqueue_work = work_object}
800
+
801
+ it "should call ack! on the work object after the job has been invoked" do
802
+ payload.should_receive(:perform).ordered
803
+ work_object.should_receive(:ack!).ordered
804
+ job.invoke_job
805
+ end
806
+
807
+ it "should call ack! on the work object if an exception is raised" do
808
+ payload.should_receive(:perform).ordered.and_raise(RuntimeError, "OMG!!!11")
809
+ work_object.should_receive(:ack!).ordered
810
+ lambda {
811
+ job.invoke_job
812
+ }.should raise_exception(RuntimeError, "OMG!!!11")
813
+ end
814
+ end
815
+ end
816
+
817
+
818
+ end