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.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/README.md +85 -44
- data/Rakefile +412 -0
- data/lib/generators/que/install_generator.rb +22 -0
- data/lib/generators/que/templates/add_que.rb +9 -0
- data/lib/que.rb +55 -5
- data/lib/que/adapters/active_record.rb +9 -0
- data/lib/que/adapters/base.rb +49 -0
- data/lib/que/adapters/connection_pool.rb +14 -0
- data/lib/que/adapters/pg.rb +17 -0
- data/lib/que/adapters/sequel.rb +14 -0
- data/lib/que/job.rb +128 -149
- data/lib/que/railtie.rb +20 -0
- data/lib/que/rake_tasks.rb +35 -0
- data/lib/que/sql.rb +121 -0
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +93 -156
- data/que.gemspec +8 -6
- data/spec/adapters/active_record_spec.rb +39 -0
- data/spec/adapters/connection_pool_spec.rb +12 -0
- data/spec/adapters/pg_spec.rb +5 -0
- data/spec/adapters/sequel_spec.rb +25 -0
- data/spec/connection_spec.rb +12 -0
- data/spec/helper_spec.rb +19 -0
- data/spec/pool_spec.rb +116 -0
- data/spec/queue_spec.rb +134 -0
- data/spec/spec_helper.rb +48 -25
- data/spec/support/helpers.rb +9 -0
- data/spec/support/jobs.rb +33 -0
- data/spec/support/shared_examples/adapter.rb +16 -0
- data/spec/support/shared_examples/multithreaded_adapter.rb +42 -0
- data/spec/work_spec.rb +247 -0
- data/spec/worker_spec.rb +117 -0
- metadata +73 -15
- data/spec/unit/error_spec.rb +0 -45
- data/spec/unit/queue_spec.rb +0 -67
- data/spec/unit/work_spec.rb +0 -168
- data/spec/unit/worker_spec.rb +0 -31
@@ -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
|
data/spec/worker_spec.rb
ADDED
@@ -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
|