que 0.0.1 → 0.1.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.
@@ -0,0 +1,9 @@
1
+ # Helper for testing threaded code.
2
+ def sleep_until(timeout = 2)
3
+ deadline = Time.now + timeout
4
+ loop do
5
+ break if yield
6
+ raise "Thing never happened!" if Time.now > deadline
7
+ sleep 0.01
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ # Common Job classes for use in specs.
2
+
3
+ # Handy for blocking in the middle of processing a job.
4
+ class BlockJob < Que::Job
5
+ def run
6
+ $q1.push nil
7
+ $q2.pop
8
+ end
9
+ end
10
+
11
+ RSpec.configure do |config|
12
+ config.before { $q1, $q2 = Queue.new, Queue.new }
13
+ end
14
+
15
+
16
+
17
+ class ErrorJob < Que::Job
18
+ def run
19
+ raise "ErrorJob!"
20
+ end
21
+ end
22
+
23
+
24
+
25
+ class ArgsJob < Que::Job
26
+ def run(*args)
27
+ $passed_args = args
28
+ end
29
+ end
30
+
31
+ RSpec.configure do |config|
32
+ config.before { $passed_args = nil }
33
+ end
@@ -0,0 +1,16 @@
1
+ shared_examples "a Que adapter" do
2
+ it "should allow a Postgres connection to be checked out" do
3
+ Que.adapter.checkout do |conn|
4
+ conn.async_exec("SELECT 1 AS one").to_a.should == [{'one' => '1'}]
5
+ conn.server_version.should > 0
6
+ end
7
+ end
8
+
9
+ it "should allow nested checkouts" do
10
+ Que.adapter.checkout do |a|
11
+ Que.adapter.checkout do |b|
12
+ a.object_id.should == b.object_id
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ shared_examples "a multithreaded Que adapter" do
2
+ it "should allow multiple threads to check out their own connections" do
3
+ one = nil
4
+ two = nil
5
+
6
+ q1, q2 = Queue.new, Queue.new
7
+
8
+ thread = Thread.new do
9
+ Que.adapter.checkout do |conn|
10
+ q1.push nil
11
+ q2.pop
12
+ one = conn.object_id
13
+ end
14
+ end
15
+
16
+ Que.adapter.checkout do |conn|
17
+ q1.pop
18
+ q2.push nil
19
+ two = conn.object_id
20
+ end
21
+
22
+ thread.join
23
+ one.should_not == two
24
+ end
25
+
26
+ it "should allow multiple workers to complete jobs simultaneously" do
27
+ BlockJob.queue
28
+ worker_1 = Que::Worker.new
29
+ $q1.pop
30
+
31
+ Que::Job.queue
32
+ DB[:que_jobs].count.should be 2
33
+
34
+ worker_2 = Que::Worker.new
35
+ sleep_until { worker_2.sleeping? }
36
+ DB[:que_jobs].count.should be 1
37
+
38
+ $q2.push nil
39
+ sleep_until { worker_1.sleeping? }
40
+ DB[:que_jobs].count.should be 0
41
+ end
42
+ end
data/spec/work_spec.rb ADDED
@@ -0,0 +1,247 @@
1
+ require 'spec_helper'
2
+
3
+ describe Que::Job, '.work' do
4
+ it "should pass a job's arguments to the run method and delete it from the database" do
5
+ ArgsJob.queue 1, 'two', {'three' => 3}
6
+ DB[:que_jobs].count.should be 1
7
+ Que::Job.work.should be_an_instance_of ArgsJob
8
+ DB[:que_jobs].count.should be 0
9
+ $passed_args.should == [1, 'two', {'three' => 3}]
10
+
11
+ # Should clear advisory lock.
12
+ DB[:pg_locks].where(:locktype => 'advisory').should be_empty
13
+ end
14
+
15
+ it "should make a job's argument hashes indifferently accessible" do
16
+ DB[:que_jobs].count.should be 0
17
+ ArgsJob.queue 1, 'two', {'array' => [{'number' => 3}]}
18
+ DB[:que_jobs].count.should be 1
19
+ Que::Job.work.should be_an_instance_of ArgsJob
20
+ DB[:que_jobs].count.should be 0
21
+
22
+ $passed_args.last[:array].first[:number].should == 3
23
+
24
+ # Should clear advisory lock.
25
+ DB[:pg_locks].where(:locktype => 'advisory').should be_empty
26
+ end
27
+
28
+ it "should not fail if there are no jobs to work" do
29
+ Que::Job.work.should be nil
30
+ DB[:pg_locks].where(:locktype => 'advisory').should be_empty
31
+ end
32
+
33
+ it "should write messages to the logger" do
34
+ Que::Job.queue
35
+ Que::Job.work
36
+
37
+ $logger.messages.length.should == 1
38
+ $logger.messages[0].should =~ /\A\[Que\] Worked job in/
39
+ end
40
+
41
+ it "should not fail if there's no logger assigned" do
42
+ begin
43
+ Que.logger = nil
44
+
45
+ Que::Job.queue
46
+ Que::Job.work
47
+ ensure
48
+ Que.logger = $logger
49
+ end
50
+ end
51
+
52
+ it "should prefer a job with a higher priority" do
53
+ Que::Job.queue :priority => 5
54
+ Que::Job.queue :priority => 1
55
+ Que::Job.queue :priority => 5
56
+ DB[:que_jobs].order(:job_id).select_map(:priority).should == [5, 1, 5]
57
+
58
+ Que::Job.work.should be_an_instance_of Que::Job
59
+ DB[:que_jobs].select_map(:priority).should == [5, 5]
60
+ end
61
+
62
+ it "should prefer a job that was scheduled to run longer ago" do
63
+ Que::Job.queue :run_at => Time.now - 30
64
+ Que::Job.queue :run_at => Time.now - 60
65
+ Que::Job.queue :run_at => Time.now - 30
66
+
67
+ recent1, old, recent2 = DB[:que_jobs].order(:job_id).select_map(:run_at)
68
+
69
+ Que::Job.work.should be_an_instance_of Que::Job
70
+ DB[:que_jobs].order_by(:job_id).select_map(:run_at).should == [recent1, recent2]
71
+ end
72
+
73
+ it "should prefer a job that was queued earlier, judging by the job_id" do
74
+ run_at = Time.now - 30
75
+ Que::Job.queue :run_at => run_at
76
+ Que::Job.queue :run_at => run_at
77
+ Que::Job.queue :run_at => run_at
78
+
79
+ first, second, third = DB[:que_jobs].select_order_map(:job_id)
80
+
81
+ Que::Job.work.should be_an_instance_of Que::Job
82
+ DB[:que_jobs].select_order_map(:job_id).should == [second, third]
83
+ end
84
+
85
+ it "should only work a job whose scheduled time to run has passed" do
86
+ Que::Job.queue :run_at => Time.now + 30
87
+ Que::Job.queue :run_at => Time.now - 30
88
+ Que::Job.queue :run_at => Time.now + 30
89
+
90
+ future1, past, future2 = DB[:que_jobs].order(:job_id).select_map(:run_at)
91
+
92
+ Que::Job.work.should be_an_instance_of Que::Job
93
+ Que::Job.work.should be nil
94
+ DB[:que_jobs].order_by(:job_id).select_map(:run_at).should == [future1, future2]
95
+ end
96
+
97
+ it "should lock the job it selects" do
98
+ BlockJob.queue
99
+ id = DB[:que_jobs].get(:job_id)
100
+ thread = Thread.new { Que::Job.work }
101
+
102
+ $q1.pop
103
+ DB[:pg_locks].where(:locktype => 'advisory', :objid => id).count.should be 1
104
+ $q2.push nil
105
+
106
+ thread.join
107
+ end
108
+
109
+ it "should not work jobs that are advisory-locked" do
110
+ Que::Job.queue
111
+ id = DB[:que_jobs].get(:job_id)
112
+
113
+ begin
114
+ DB.select{pg_advisory_lock(id)}.single_value
115
+ Que::Job.work.should be nil
116
+ ensure
117
+ DB.select{pg_advisory_unlock(id)}.single_value
118
+ end
119
+ end
120
+
121
+ it "should handle subclasses of other jobs" do
122
+ class SubClassJob < Que::Job
123
+ @default_priority = 2
124
+
125
+ def run
126
+ $job_spec_result << :sub
127
+ end
128
+ end
129
+
130
+ class SubSubClassJob < SubClassJob
131
+ @default_priority = 4
132
+
133
+ def run
134
+ super
135
+ $job_spec_result << :subsub
136
+ end
137
+ end
138
+
139
+ $job_spec_result = []
140
+ SubClassJob.queue
141
+ DB[:que_jobs].select_map(:priority).should == [2]
142
+ Que::Job.work.should be_an_instance_of SubClassJob
143
+ $job_spec_result.should == [:sub]
144
+
145
+ $job_spec_result = []
146
+ SubSubClassJob.queue
147
+ DB[:que_jobs].select_map(:priority).should == [4]
148
+ Que::Job.work.should be_an_instance_of SubSubClassJob
149
+ $job_spec_result.should == [:sub, :subsub]
150
+ end
151
+
152
+ it "should handle namespaced subclasses" do
153
+ module ModuleJobModule
154
+ class ModuleJob < Que::Job
155
+ end
156
+ end
157
+
158
+ ModuleJobModule::ModuleJob.queue
159
+ DB[:que_jobs].get(:job_class).should == "ModuleJobModule::ModuleJob"
160
+ Que::Job.work.should be_an_instance_of ModuleJobModule::ModuleJob
161
+ end
162
+
163
+ it "should make it easy to destroy the job within the same transaction as other changes" do
164
+ class DestroyJob < Que::Job
165
+ def run
166
+ destroy
167
+ end
168
+ end
169
+
170
+ DestroyJob.queue
171
+ DB[:que_jobs].count.should be 1
172
+ Que::Job.work
173
+ DB[:que_jobs].count.should be 0
174
+ end
175
+
176
+ describe "when encountering an error" do
177
+ it "should exponentially back off the job" do
178
+ ErrorJob.queue
179
+ Que::Job.work.should be true
180
+
181
+ DB[:que_jobs].count.should be 1
182
+ job = DB[:que_jobs].first
183
+ job[:error_count].should be 1
184
+ job[:last_error].should =~ /\AErrorJob!\n/
185
+ job[:run_at].should be_within(3).of Time.now + 4
186
+
187
+ DB[:que_jobs].update :error_count => 5,
188
+ :run_at => Time.now - 60
189
+
190
+ Que::Job.work.should be true
191
+
192
+ DB[:que_jobs].count.should be 1
193
+ job = DB[:que_jobs].first
194
+ job[:error_count].should be 6
195
+ job[:last_error].should =~ /\AErrorJob!\n/
196
+ job[:run_at].should be_within(3).of Time.now + 1299
197
+ end
198
+
199
+ it "should pass it to an error handler, if one is defined" do
200
+ begin
201
+ errors = []
202
+ Que.error_handler = proc { |error| errors << error }
203
+
204
+ ErrorJob.queue
205
+ Que::Job.work.should be true
206
+
207
+ errors.count.should be 1
208
+ error = errors[0]
209
+ error.should be_an_instance_of RuntimeError
210
+ error.message.should == "ErrorJob!"
211
+ ensure
212
+ Que.error_handler = nil
213
+ end
214
+ end
215
+
216
+ it "should not do anything if the error handler itelf throws an error" do
217
+ begin
218
+ Que.error_handler = proc { |error| raise "Another error!" }
219
+ ErrorJob.queue
220
+ Que::Job.work.should be true
221
+ ensure
222
+ Que.error_handler = nil
223
+ end
224
+ end
225
+
226
+ it "should return false if the job throws a postgres error" do
227
+ class PGErrorJob < Que::Job
228
+ def run
229
+ Que.execute "bad SQL syntax"
230
+ end
231
+ end
232
+
233
+ PGErrorJob.queue
234
+ Que::Job.work.should be false
235
+ end
236
+
237
+ it "should behave sensibly if there's no corresponding job class" do
238
+ DB[:que_jobs].insert :job_class => "NonexistentClass"
239
+ Que::Job.work.should be true
240
+ DB[:que_jobs].count.should be 1
241
+ job = DB[:que_jobs].first
242
+ job[:error_count].should be 1
243
+ job[:last_error].should =~ /\Auninitialized constant NonexistentClass/
244
+ job[:run_at].should be_within(3).of Time.now + 4
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe Que::Worker do
4
+ it "should work jobs when started until there are none available" do
5
+ begin
6
+ Que::Job.queue
7
+ Que::Job.queue
8
+ DB[:que_jobs].count.should be 2
9
+
10
+ @worker = Que::Worker.new
11
+ sleep_until { @worker.sleeping? }
12
+ DB[:que_jobs].count.should be 0
13
+ ensure
14
+ if @worker
15
+ @worker.thread.kill
16
+ @worker.thread.join
17
+ end
18
+ end
19
+ end
20
+
21
+ it "#wake! should return truthy if the worker was asleep and is woken up, at which point it should work until no jobs are available" do
22
+ begin
23
+ @worker = Que::Worker.new
24
+ sleep_until { @worker.sleeping? }
25
+
26
+ Que::Job.queue
27
+ Que::Job.queue
28
+ DB[:que_jobs].count.should be 2
29
+
30
+ @worker.wake!.should be true
31
+ sleep_until { @worker.sleeping? }
32
+ DB[:que_jobs].count.should be 0
33
+ ensure
34
+ if @worker
35
+ @worker.thread.kill
36
+ @worker.thread.join
37
+ end
38
+ end
39
+ end
40
+
41
+ it "#wake! should return falsy if the worker was already working" do
42
+ begin
43
+ BlockJob.queue
44
+ @worker = Que::Worker.new
45
+
46
+ $q1.pop
47
+ DB[:que_jobs].count.should be 1
48
+ @worker.wake!.should be nil
49
+ ensure
50
+ if @worker
51
+ @worker.thread.kill
52
+ @worker.thread.join
53
+ end
54
+ end
55
+ end
56
+
57
+ it "should not be deterred by a job that raises an error" do
58
+ begin
59
+ ErrorJob.queue :priority => 1
60
+ Que::Job.queue :priority => 5
61
+
62
+ @worker = Que::Worker.new
63
+
64
+ sleep_until { @worker.sleeping? }
65
+
66
+ DB[:que_jobs].count.should be 1
67
+ job = DB[:que_jobs].first
68
+ job[:job_class].should == 'ErrorJob'
69
+ job[:run_at].should be_within(3).of Time.now + 4
70
+ ensure
71
+ if @worker
72
+ @worker.thread.kill
73
+ @worker.thread.join
74
+ end
75
+ end
76
+ end
77
+
78
+ it "should receive and respect a notification to shut down when it is working, after its current job completes" do
79
+ begin
80
+ BlockJob.queue :priority => 1
81
+ Que::Job.queue :priority => 5
82
+ DB[:que_jobs].count.should be 2
83
+
84
+ @worker = Que::Worker.new
85
+
86
+ $q1.pop
87
+ @worker.stop!
88
+ $q2.push nil
89
+
90
+ @worker.wait_until_stopped
91
+
92
+ DB[:que_jobs].count.should be 1
93
+ job = DB[:que_jobs].first
94
+ job[:job_class].should == 'Que::Job'
95
+ ensure
96
+ if @worker
97
+ @worker.thread.kill
98
+ @worker.thread.join
99
+ end
100
+ end
101
+ end
102
+
103
+ it "should receive and respect a notification to shut down when it is asleep" do
104
+ begin
105
+ @worker = Que::Worker.new
106
+ sleep_until { @worker.sleeping? }
107
+
108
+ @worker.stop!
109
+ @worker.wait_until_stopped
110
+ ensure
111
+ if @worker
112
+ @worker.thread.kill
113
+ @worker.thread.join
114
+ end
115
+ end
116
+ end
117
+ end