que 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/Gemfile +15 -1
- data/README.md +2 -2
- data/Rakefile +1 -411
- data/lib/que/job.rb +6 -5
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +7 -2
- data/que.gemspec +1 -7
- data/spec/spec_helper.rb +1 -0
- data/spec/{connection_spec.rb → unit/connection_spec.rb} +0 -0
- data/spec/{helper_spec.rb → unit/helper_spec.rb} +4 -0
- data/spec/{pool_spec.rb → unit/pool_spec.rb} +19 -9
- data/spec/{queue_spec.rb → unit/queue_spec.rb} +0 -0
- data/spec/{work_spec.rb → unit/work_spec.rb} +1 -1
- data/spec/{worker_spec.rb → unit/worker_spec.rb} +0 -0
- data/tasks/benchmark.rb +93 -0
- data/tasks/benchmark_queues.rb +398 -0
- data/tasks/rspec.rb +12 -0
- metadata +30 -110
data/lib/que/job.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'multi_json'
|
2
2
|
|
3
3
|
module Que
|
4
4
|
class Job
|
@@ -36,7 +36,7 @@ module Que
|
|
36
36
|
args << options if options.any?
|
37
37
|
end
|
38
38
|
|
39
|
-
attrs = {:job_class => to_s, :args =>
|
39
|
+
attrs = {:job_class => to_s, :args => MultiJson.dump(args)}
|
40
40
|
|
41
41
|
if t = run_at || @default_run_at && @default_run_at.call
|
42
42
|
attrs[:run_at] = t
|
@@ -46,7 +46,7 @@ module Que
|
|
46
46
|
attrs[:priority] = p
|
47
47
|
end
|
48
48
|
|
49
|
-
if Que.mode == :sync
|
49
|
+
if Que.mode == :sync && !attrs[:run_at]
|
50
50
|
run_job(attrs)
|
51
51
|
else
|
52
52
|
Que.execute *insert_sql(attrs)
|
@@ -134,8 +134,9 @@ module Que
|
|
134
134
|
|
135
135
|
def run_job(attrs)
|
136
136
|
attrs = indifferentiate(attrs)
|
137
|
-
attrs[:args] = indifferentiate(
|
138
|
-
|
137
|
+
attrs[:args] = indifferentiate(MultiJson.load(attrs[:args]))
|
138
|
+
klass = attrs[:job_class].split('::').inject(Object, &:const_get)
|
139
|
+
klass.new(attrs).tap(&:_run)
|
139
140
|
end
|
140
141
|
|
141
142
|
def indifferentiate(input)
|
data/lib/que/version.rb
CHANGED
data/lib/que/worker.rb
CHANGED
@@ -89,11 +89,16 @@ module Que
|
|
89
89
|
# This has to be called when trapping a SIGTERM, so it can't lock the monitor.
|
90
90
|
def stop!
|
91
91
|
@thread[:directive] = :stop
|
92
|
-
@thread.wakeup
|
93
92
|
end
|
94
93
|
|
95
94
|
def wait_until_stopped
|
96
|
-
|
95
|
+
loop do
|
96
|
+
case @thread.status
|
97
|
+
when false then break
|
98
|
+
when 'sleep' then @thread.wakeup
|
99
|
+
end
|
100
|
+
sleep 0.0001
|
101
|
+
end
|
97
102
|
end
|
98
103
|
|
99
104
|
private
|
data/que.gemspec
CHANGED
@@ -19,12 +19,6 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
21
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
|
-
spec.add_development_dependency 'rake'
|
23
|
-
spec.add_development_dependency 'rspec', '~> 2.14.1'
|
24
|
-
spec.add_development_dependency 'pry'
|
25
22
|
|
26
|
-
spec.
|
27
|
-
spec.add_development_dependency 'activerecord'
|
28
|
-
spec.add_development_dependency 'pg'
|
29
|
-
spec.add_development_dependency 'connection_pool'
|
23
|
+
spec.add_dependency 'multi_json', '~> 1.0'
|
30
24
|
end
|
data/spec/spec_helper.rb
CHANGED
File without changes
|
@@ -1,6 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Que, 'helpers' do
|
4
|
+
it "should be able to run arbitrary SQL" do
|
5
|
+
Que.execute("SELECT 1 AS one").to_a.should == [{'one' => '1'}]
|
6
|
+
end
|
7
|
+
|
4
8
|
it "should be able to drop and create the jobs table" do
|
5
9
|
DB.table_exists?(:que_jobs).should be true
|
6
10
|
Que.drop!
|
@@ -6,16 +6,25 @@ describe "Managing the Worker pool" do
|
|
6
6
|
$logger.messages.should == ["[Que] Set mode to :off"]
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
describe "Que.mode = :sync" do
|
10
|
+
it "should make jobs run in the same thread as they are queued" do
|
11
|
+
Que.mode = :sync
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
ArgsJob.queue(5, :testing => "synchronous").should be_an_instance_of ArgsJob
|
14
|
+
$passed_args.should == [5, {'testing' => "synchronous"}]
|
15
|
+
DB[:que_jobs].count.should be 0
|
16
|
+
|
17
|
+
$logger.messages.length.should be 2
|
18
|
+
$logger.messages[0].should == "[Que] Set mode to :sync"
|
19
|
+
$logger.messages[1].should =~ /\A\[Que\] Worked job in/
|
20
|
+
end
|
15
21
|
|
16
|
-
|
17
|
-
|
18
|
-
|
22
|
+
it "should not affect jobs that are queued with specific run_ats" do
|
23
|
+
Que.mode = :sync
|
24
|
+
|
25
|
+
ArgsJob.queue(5, :testing => "synchronous", :run_at => Time.now + 60)
|
26
|
+
DB[:que_jobs].select_map(:job_class).should == ["ArgsJob"]
|
27
|
+
end
|
19
28
|
end
|
20
29
|
|
21
30
|
describe "Que.mode = :async" do
|
@@ -103,9 +112,10 @@ describe "Managing the Worker pool" do
|
|
103
112
|
|
104
113
|
it "should poke a worker every Que.sleep_period seconds" do
|
105
114
|
begin
|
106
|
-
Que.sleep_period = 0.001 # 1 ms
|
107
115
|
Que.mode = :async
|
116
|
+
|
108
117
|
sleep_until { Que::Worker.workers.all? &:sleeping? }
|
118
|
+
Que.sleep_period = 0.01 # 10 ms
|
109
119
|
Que::Job.queue
|
110
120
|
sleep_until { DB[:que_jobs].count == 0 }
|
111
121
|
ensure
|
File without changes
|
@@ -240,7 +240,7 @@ describe Que::Job, '.work' do
|
|
240
240
|
DB[:que_jobs].count.should be 1
|
241
241
|
job = DB[:que_jobs].first
|
242
242
|
job[:error_count].should be 1
|
243
|
-
job[:last_error].should =~
|
243
|
+
job[:last_error].should =~ /uninitialized constant:? NonexistentClass/
|
244
244
|
job[:run_at].should be_within(3).of Time.now + 4
|
245
245
|
end
|
246
246
|
end
|
File without changes
|
data/tasks/benchmark.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
task :benchmark do
|
2
|
+
# The following benchmark is meant to test Que's scalability by having it
|
3
|
+
# bombard Postgres from many, many processes. Note that this benchmark tests
|
4
|
+
# the entire Que stack, not just the locking queries.
|
5
|
+
|
6
|
+
JOB_COUNT = (ENV['JOB_COUNT'] || 1000).to_i
|
7
|
+
PROCESS_COUNT = (ENV['PROCESS_COUNT'] || 1).to_i
|
8
|
+
WORKER_COUNT = (ENV['WORKER_COUNT'] || 4).to_i
|
9
|
+
SYNCHRONOUS_COMMIT = ENV['SYNCHRONOUS_COMMIT'] || 'on'
|
10
|
+
|
11
|
+
require 'que'
|
12
|
+
require 'uri'
|
13
|
+
require 'pg'
|
14
|
+
require 'connection_pool'
|
15
|
+
|
16
|
+
uri = URI.parse ENV["DATABASE_URL"] || "postgres://postgres:@localhost/que-test"
|
17
|
+
|
18
|
+
new_connection = proc do
|
19
|
+
conn = PG::Connection.open :host => uri.host,
|
20
|
+
:user => uri.user,
|
21
|
+
:password => uri.password,
|
22
|
+
:port => uri.port || 5432,
|
23
|
+
:dbname => uri.path[1..-1]
|
24
|
+
|
25
|
+
conn.async_exec "SET SESSION synchronous_commit = #{SYNCHRONOUS_COMMIT}"
|
26
|
+
conn
|
27
|
+
end
|
28
|
+
|
29
|
+
Que.connection = pg = new_connection.call
|
30
|
+
Que.drop! rescue nil
|
31
|
+
Que.create!
|
32
|
+
|
33
|
+
# Stock table with jobs and analyze.
|
34
|
+
pg.async_exec <<-SQL
|
35
|
+
INSERT INTO que_jobs (job_class, args, priority)
|
36
|
+
SELECT 'Que::Job', ('[' || i || ',{}]')::json, 1
|
37
|
+
FROM generate_Series(1,#{JOB_COUNT}) AS i;
|
38
|
+
ANALYZE;
|
39
|
+
SQL
|
40
|
+
|
41
|
+
# Fork!
|
42
|
+
$parent_pid = Process.pid
|
43
|
+
def parent?
|
44
|
+
Process.pid == $parent_pid
|
45
|
+
end
|
46
|
+
|
47
|
+
# Synchronize all workers to start at the same time using, what else?
|
48
|
+
# Advisory locks. I am such a one-trick pony.
|
49
|
+
pg.async_exec("SELECT pg_advisory_lock(0)")
|
50
|
+
|
51
|
+
PROCESS_COUNT.times { Process.fork if parent? }
|
52
|
+
|
53
|
+
if parent?
|
54
|
+
# This is the main process, get ready to start monitoring the queues.
|
55
|
+
|
56
|
+
# First hold until all the children are ready.
|
57
|
+
sleep 0.1 until pg.async_exec("select count(*) from pg_locks where locktype = 'advisory' and objid = 0").first['count'].to_i == PROCESS_COUNT + 1
|
58
|
+
|
59
|
+
puts "Benchmarking: #{JOB_COUNT} jobs, #{PROCESS_COUNT} processes with #{WORKER_COUNT} workers each, synchronous_commit = #{SYNCHRONOUS_COMMIT}..."
|
60
|
+
pg.async_exec("select pg_advisory_unlock_all()") # Go!
|
61
|
+
start = Time.now
|
62
|
+
|
63
|
+
loop do
|
64
|
+
sleep 0.01
|
65
|
+
break if pg.async_exec("SELECT 1 AS one FROM que_jobs LIMIT 1").none? # There must be a better way to do this?
|
66
|
+
end
|
67
|
+
time = Time.now - start
|
68
|
+
|
69
|
+
locks = pg.async_exec("SELECT * FROM pg_locks WHERE locktype = 'advisory'").to_a
|
70
|
+
puts "Advisory locks left over! #{locks.inspect}" if locks.any?
|
71
|
+
puts "#{JOB_COUNT} jobs in #{time} seconds = #{(JOB_COUNT / time).round} jobs per second"
|
72
|
+
else
|
73
|
+
# This is a child, get ready to start hitting the queues.
|
74
|
+
pool = ConnectionPool.new :size => WORKER_COUNT, &new_connection
|
75
|
+
|
76
|
+
Que.connection = pool
|
77
|
+
|
78
|
+
pool.with do |conn|
|
79
|
+
# Block here until the advisory lock is released, which is our start pistol.
|
80
|
+
conn.async_exec "SELECT pg_advisory_lock(0); SELECT pg_advisory_unlock_all();"
|
81
|
+
end
|
82
|
+
|
83
|
+
Que.mode = :async
|
84
|
+
Que.worker_count = WORKER_COUNT
|
85
|
+
|
86
|
+
loop do
|
87
|
+
sleep 1
|
88
|
+
break if pool.with { |pg| pg.async_exec("SELECT 1 AS one FROM que_jobs LIMIT 1").none? }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
Process.waitall
|
93
|
+
end
|
@@ -0,0 +1,398 @@
|
|
1
|
+
task :benchmark_queues do
|
2
|
+
# The following is a somewhat simplistic benchmark (aren't they all) meant
|
3
|
+
# to compare the speed and concurrency of the locking mechanisms used by Que
|
4
|
+
# (standard and lateral queries), DelayedJob and QueueClassic - it does this
|
5
|
+
# by simply sending the raw queries that each system sends during operation.
|
6
|
+
|
7
|
+
# It is NOT meant to benchmark the overall performance of each system (which
|
8
|
+
# would include the time each spends working in Ruby), but to see which one
|
9
|
+
# supports the highest concurrency under load, assuming that there will be
|
10
|
+
# many workers and that Postgres will be the bottleneck. I'm unsure how
|
11
|
+
# useful it is for this, but it's a start.
|
12
|
+
|
13
|
+
JOB_COUNT = (ENV['JOB_COUNT'] || 1000).to_i
|
14
|
+
WORKER_COUNT = (ENV['WORKER_COUNT'] || 10).to_i
|
15
|
+
SYNCHRONOUS_COMMIT = ENV['SYNCHRONOUS_COMMIT'] || 'on'
|
16
|
+
|
17
|
+
require 'uri'
|
18
|
+
require 'pg'
|
19
|
+
require 'connection_pool'
|
20
|
+
|
21
|
+
uri = URI.parse ENV["DATABASE_URL"] || "postgres://postgres:@localhost/que-test"
|
22
|
+
|
23
|
+
new_connection = proc do
|
24
|
+
PG::Connection.open :host => uri.host,
|
25
|
+
:user => uri.user,
|
26
|
+
:password => uri.password,
|
27
|
+
:port => uri.port || 5432,
|
28
|
+
:dbname => uri.path[1..-1]
|
29
|
+
end
|
30
|
+
|
31
|
+
pg = new_connection.call
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
# Necessary setup, mostly for QueueClassic. I apologize for this - I hope your editor supports code folding.
|
36
|
+
pg.async_exec <<-SQL
|
37
|
+
SET SESSION client_min_messages = 'WARNING';
|
38
|
+
|
39
|
+
-- Que table.
|
40
|
+
DROP TABLE IF EXISTS que_jobs;
|
41
|
+
CREATE TABLE que_jobs
|
42
|
+
(
|
43
|
+
priority integer NOT NULL DEFAULT 1,
|
44
|
+
run_at timestamptz NOT NULL DEFAULT now(),
|
45
|
+
job_id bigserial NOT NULL,
|
46
|
+
job_class text NOT NULL,
|
47
|
+
args json NOT NULL DEFAULT '[]'::json,
|
48
|
+
error_count integer NOT NULL DEFAULT 0,
|
49
|
+
last_error text,
|
50
|
+
|
51
|
+
CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
|
52
|
+
);
|
53
|
+
|
54
|
+
DROP TABLE IF EXISTS que_lateral_jobs;
|
55
|
+
CREATE TABLE que_lateral_jobs
|
56
|
+
(
|
57
|
+
priority integer NOT NULL DEFAULT 1,
|
58
|
+
run_at timestamptz NOT NULL DEFAULT now(),
|
59
|
+
job_id bigserial NOT NULL,
|
60
|
+
job_class text NOT NULL,
|
61
|
+
args json NOT NULL DEFAULT '[]'::json,
|
62
|
+
error_count integer NOT NULL DEFAULT 0,
|
63
|
+
last_error text,
|
64
|
+
|
65
|
+
CONSTRAINT que_lateral_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
|
66
|
+
);
|
67
|
+
|
68
|
+
DROP TABLE IF EXISTS delayed_jobs;
|
69
|
+
-- DelayedJob table.
|
70
|
+
CREATE TABLE delayed_jobs
|
71
|
+
(
|
72
|
+
id serial NOT NULL,
|
73
|
+
priority integer NOT NULL DEFAULT 0,
|
74
|
+
attempts integer NOT NULL DEFAULT 0,
|
75
|
+
handler text NOT NULL,
|
76
|
+
last_error text,
|
77
|
+
run_at timestamp without time zone,
|
78
|
+
locked_at timestamp without time zone,
|
79
|
+
failed_at timestamp without time zone,
|
80
|
+
locked_by character varying(255),
|
81
|
+
queue character varying(255),
|
82
|
+
created_at timestamp without time zone,
|
83
|
+
updated_at timestamp without time zone,
|
84
|
+
CONSTRAINT delayed_jobs_pkey PRIMARY KEY (id)
|
85
|
+
);
|
86
|
+
ALTER TABLE delayed_jobs
|
87
|
+
OWNER TO postgres;
|
88
|
+
|
89
|
+
CREATE INDEX delayed_jobs_priority
|
90
|
+
ON delayed_jobs
|
91
|
+
USING btree
|
92
|
+
(priority, run_at);
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
-- QueueClassic table and functions.
|
97
|
+
DROP FUNCTION IF EXISTS lock_head(tname varchar);
|
98
|
+
DROP FUNCTION IF EXISTS lock_head(q_name varchar, top_boundary integer);
|
99
|
+
DROP FUNCTION IF EXISTS queue_classic_notify() cascade;
|
100
|
+
DROP TABLE IF EXISTS queue_classic_jobs;
|
101
|
+
|
102
|
+
CREATE TABLE queue_classic_jobs (
|
103
|
+
id bigserial PRIMARY KEY,
|
104
|
+
q_name text not null check (length(q_name) > 0),
|
105
|
+
method text not null check (length(method) > 0),
|
106
|
+
args text not null,
|
107
|
+
locked_at timestamptz
|
108
|
+
);
|
109
|
+
|
110
|
+
alter table queue_classic_jobs alter column args type json using (args::json);
|
111
|
+
|
112
|
+
create function queue_classic_notify() returns trigger as $$ begin
|
113
|
+
perform pg_notify(new.q_name, '');
|
114
|
+
return null;
|
115
|
+
end $$ language plpgsql;
|
116
|
+
|
117
|
+
create trigger queue_classic_notify
|
118
|
+
after insert on queue_classic_jobs
|
119
|
+
for each row
|
120
|
+
execute procedure queue_classic_notify();
|
121
|
+
|
122
|
+
CREATE INDEX idx_qc_on_name_only_unlocked ON queue_classic_jobs (q_name, id) WHERE locked_at IS NULL;
|
123
|
+
|
124
|
+
CREATE OR REPLACE FUNCTION lock_head(q_name varchar, top_boundary integer)
|
125
|
+
RETURNS SETOF queue_classic_jobs AS $$
|
126
|
+
DECLARE
|
127
|
+
unlocked bigint;
|
128
|
+
relative_top integer;
|
129
|
+
job_count integer;
|
130
|
+
BEGIN
|
131
|
+
-- The purpose is to release contention for the first spot in the table.
|
132
|
+
-- The select count(*) is going to slow down dequeue performance but allow
|
133
|
+
-- for more workers. Would love to see some optimization here...
|
134
|
+
|
135
|
+
EXECUTE 'SELECT count(*) FROM '
|
136
|
+
|| '(SELECT * FROM queue_classic_jobs WHERE q_name = '
|
137
|
+
|| quote_literal(q_name)
|
138
|
+
|| ' LIMIT '
|
139
|
+
|| quote_literal(top_boundary)
|
140
|
+
|| ') limited'
|
141
|
+
INTO job_count;
|
142
|
+
|
143
|
+
SELECT TRUNC(random() * (top_boundary - 1))
|
144
|
+
INTO relative_top;
|
145
|
+
|
146
|
+
IF job_count < top_boundary THEN
|
147
|
+
relative_top = 0;
|
148
|
+
END IF;
|
149
|
+
|
150
|
+
LOOP
|
151
|
+
BEGIN
|
152
|
+
EXECUTE 'SELECT id FROM queue_classic_jobs '
|
153
|
+
|| ' WHERE locked_at IS NULL'
|
154
|
+
|| ' AND q_name = '
|
155
|
+
|| quote_literal(q_name)
|
156
|
+
|| ' ORDER BY id ASC'
|
157
|
+
|| ' LIMIT 1'
|
158
|
+
|| ' OFFSET ' || quote_literal(relative_top)
|
159
|
+
|| ' FOR UPDATE NOWAIT'
|
160
|
+
INTO unlocked;
|
161
|
+
EXIT;
|
162
|
+
EXCEPTION
|
163
|
+
WHEN lock_not_available THEN
|
164
|
+
-- do nothing. loop again and hope we get a lock
|
165
|
+
END;
|
166
|
+
END LOOP;
|
167
|
+
|
168
|
+
RETURN QUERY EXECUTE 'UPDATE queue_classic_jobs '
|
169
|
+
|| ' SET locked_at = (CURRENT_TIMESTAMP)'
|
170
|
+
|| ' WHERE id = $1'
|
171
|
+
|| ' AND locked_at is NULL'
|
172
|
+
|| ' RETURNING *'
|
173
|
+
USING unlocked;
|
174
|
+
|
175
|
+
RETURN;
|
176
|
+
END;
|
177
|
+
$$ LANGUAGE plpgsql;
|
178
|
+
|
179
|
+
CREATE OR REPLACE FUNCTION lock_head(tname varchar)
|
180
|
+
RETURNS SETOF queue_classic_jobs AS $$
|
181
|
+
BEGIN
|
182
|
+
RETURN QUERY EXECUTE 'SELECT * FROM lock_head($1,10)' USING tname;
|
183
|
+
END;
|
184
|
+
$$ LANGUAGE plpgsql;
|
185
|
+
|
186
|
+
|
187
|
+
|
188
|
+
|
189
|
+
|
190
|
+
INSERT INTO que_jobs (job_class, args, priority)
|
191
|
+
SELECT 'Que::Job', ('[' || i || ',{}]')::json, 1
|
192
|
+
FROM generate_Series(1,#{JOB_COUNT}) AS i;
|
193
|
+
|
194
|
+
INSERT INTO que_lateral_jobs (job_class, args, priority)
|
195
|
+
SELECT 'Que::Job', ('[' || i || ',{}]')::json, 1
|
196
|
+
FROM generate_Series(1,#{JOB_COUNT}) AS i;
|
197
|
+
|
198
|
+
INSERT INTO delayed_jobs (handler, run_at, created_at, updated_at)
|
199
|
+
SELECT '--- !ruby/struct:NewsletterJob\ntext: lorem ipsum...\nemails: blah@blah.com\n', now(), now(), now()
|
200
|
+
FROM generate_Series(1,#{JOB_COUNT}) AS i;
|
201
|
+
|
202
|
+
INSERT INTO queue_classic_jobs (q_name, method, args)
|
203
|
+
SELECT 'default', 'Kernel.puts', '["hello world"]'
|
204
|
+
FROM generate_Series(1,#{JOB_COUNT}) AS i;
|
205
|
+
|
206
|
+
|
207
|
+
|
208
|
+
|
209
|
+
-- Necessary tables and functions made, now stock them with jobs and analyze.
|
210
|
+
ANALYZE;
|
211
|
+
SQL
|
212
|
+
|
213
|
+
|
214
|
+
queries = {
|
215
|
+
:que => (
|
216
|
+
<<-SQL
|
217
|
+
WITH RECURSIVE cte AS (
|
218
|
+
SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
|
219
|
+
FROM (
|
220
|
+
SELECT job
|
221
|
+
FROM que_jobs AS job
|
222
|
+
WHERE run_at <= now()
|
223
|
+
ORDER BY priority, run_at, job_id
|
224
|
+
LIMIT 1
|
225
|
+
) AS t1
|
226
|
+
UNION ALL (
|
227
|
+
SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
|
228
|
+
FROM (
|
229
|
+
SELECT (
|
230
|
+
SELECT job
|
231
|
+
FROM que_jobs AS job
|
232
|
+
WHERE run_at <= now() AND (priority, run_at, job_id) > (cte.priority, cte.run_at, cte.job_id)
|
233
|
+
ORDER BY priority, run_at, job_id
|
234
|
+
LIMIT 1
|
235
|
+
) AS job
|
236
|
+
FROM cte
|
237
|
+
WHERE NOT cte.locked
|
238
|
+
LIMIT 1
|
239
|
+
) AS t1
|
240
|
+
)
|
241
|
+
)
|
242
|
+
SELECT job_id, priority, run_at, args, job_class, error_count
|
243
|
+
FROM cte
|
244
|
+
WHERE locked
|
245
|
+
LIMIT 1
|
246
|
+
SQL
|
247
|
+
),
|
248
|
+
:que_lateral => (
|
249
|
+
<<-SQL
|
250
|
+
WITH RECURSIVE cte AS (
|
251
|
+
SELECT *, pg_try_advisory_lock(s.job_id) AS locked
|
252
|
+
FROM (
|
253
|
+
SELECT *
|
254
|
+
FROM que_lateral_jobs
|
255
|
+
WHERE run_at <= now()
|
256
|
+
ORDER BY priority, run_at, job_id
|
257
|
+
LIMIT 1
|
258
|
+
) s
|
259
|
+
UNION ALL (
|
260
|
+
SELECT j.*, pg_try_advisory_lock(j.job_id) AS locked
|
261
|
+
FROM (
|
262
|
+
SELECT *
|
263
|
+
FROM cte
|
264
|
+
WHERE NOT locked
|
265
|
+
) t,
|
266
|
+
LATERAL (
|
267
|
+
SELECT *
|
268
|
+
FROM que_lateral_jobs
|
269
|
+
WHERE run_at <= now()
|
270
|
+
AND (priority, run_at, job_id) > (t.priority, t.run_at, t.job_id)
|
271
|
+
ORDER BY priority, run_at, job_id
|
272
|
+
LIMIT 1
|
273
|
+
) j
|
274
|
+
)
|
275
|
+
)
|
276
|
+
SELECT *
|
277
|
+
FROM cte
|
278
|
+
WHERE locked
|
279
|
+
LIMIT 1
|
280
|
+
SQL
|
281
|
+
),
|
282
|
+
:delayed_job => (
|
283
|
+
# From delayed_job_active_record
|
284
|
+
<<-SQL
|
285
|
+
UPDATE delayed_jobs
|
286
|
+
SET locked_at = now(),
|
287
|
+
locked_by = $1::text
|
288
|
+
WHERE id IN (
|
289
|
+
SELECT id
|
290
|
+
FROM delayed_jobs
|
291
|
+
WHERE (
|
292
|
+
(run_at <= now() AND (locked_at IS NULL OR locked_at < now()) OR locked_by = $1) AND failed_at IS NULL
|
293
|
+
)
|
294
|
+
ORDER BY priority ASC, run_at ASC
|
295
|
+
LIMIT 1
|
296
|
+
FOR UPDATE
|
297
|
+
)
|
298
|
+
RETURNING *
|
299
|
+
SQL
|
300
|
+
)
|
301
|
+
}
|
302
|
+
|
303
|
+
connections = WORKER_COUNT.times.map do
|
304
|
+
conn = new_connection.call
|
305
|
+
conn.async_exec "SET SESSION synchronous_commit = #{SYNCHRONOUS_COMMIT}"
|
306
|
+
queries.each do |name, sql|
|
307
|
+
conn.prepare(name.to_s, sql)
|
308
|
+
end
|
309
|
+
conn
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
|
314
|
+
# Track the ids that are worked, to make sure they're all hit.
|
315
|
+
$results = {
|
316
|
+
:delayed_job => [],
|
317
|
+
:queue_classic => [],
|
318
|
+
:que => [],
|
319
|
+
:que_lateral => []
|
320
|
+
}
|
321
|
+
|
322
|
+
def work_job(type, conn)
|
323
|
+
case type
|
324
|
+
when :delayed_job
|
325
|
+
return unless r = conn.exec_prepared("delayed_job", [conn.object_id]).first
|
326
|
+
$results[type] << r['id']
|
327
|
+
conn.async_exec "DELETE FROM delayed_jobs WHERE id = $1", [r['id']]
|
328
|
+
|
329
|
+
when :queue_classic
|
330
|
+
return unless r = conn.async_exec("SELECT * FROM lock_head($1, $2)", ['default', 9]).first
|
331
|
+
$results[type] << r['id']
|
332
|
+
conn.async_exec "DELETE FROM queue_classic_jobs WHERE id = $1", [r['id']]
|
333
|
+
|
334
|
+
when :que
|
335
|
+
begin
|
336
|
+
return unless r = conn.exec_prepared("que").first
|
337
|
+
# Have to double-check that the job is valid, as explained at length in Que::Job.work.
|
338
|
+
return true unless conn.async_exec("SELECT * FROM que_jobs WHERE priority = $1 AND run_at = $2 AND job_id = $3", [r['priority'], r['run_at'], r['job_id']]).first
|
339
|
+
conn.async_exec "DELETE FROM que_jobs WHERE priority = $1 AND run_at = $2 AND job_id = $3", [r['priority'], r['run_at'], r['job_id']]
|
340
|
+
$results[type] << r['job_id']
|
341
|
+
ensure
|
342
|
+
conn.async_exec "SELECT pg_advisory_unlock_all()" if r
|
343
|
+
end
|
344
|
+
|
345
|
+
when :que_lateral
|
346
|
+
begin
|
347
|
+
return unless r = conn.exec_prepared("que_lateral").first
|
348
|
+
return true unless conn.async_exec("SELECT * FROM que_lateral_jobs WHERE priority = $1 AND run_at = $2 AND job_id = $3", [r['priority'], r['run_at'], r['job_id']]).first
|
349
|
+
conn.async_exec "DELETE FROM que_lateral_jobs WHERE priority = $1 AND run_at = $2 AND job_id = $3", [r['priority'], r['run_at'], r['job_id']]
|
350
|
+
$results[type] << r['job_id']
|
351
|
+
ensure
|
352
|
+
conn.async_exec "SELECT pg_advisory_unlock_all()" if r
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
puts "Benchmarking #{JOB_COUNT} jobs, #{WORKER_COUNT} workers and synchronous_commit = #{SYNCHRONOUS_COMMIT}..."
|
359
|
+
|
360
|
+
{
|
361
|
+
:delayed_job => :delayed_jobs,
|
362
|
+
:queue_classic => :queue_classic_jobs,
|
363
|
+
:que => :que_jobs,
|
364
|
+
:que_lateral => :que_lateral_jobs
|
365
|
+
}.each do |type, table|
|
366
|
+
print "Benchmarking #{type}... "
|
367
|
+
start = Time.now
|
368
|
+
|
369
|
+
threads = connections.map do |conn|
|
370
|
+
Thread.new do
|
371
|
+
loop do
|
372
|
+
begin
|
373
|
+
break unless work_job(type, conn)
|
374
|
+
rescue
|
375
|
+
# DelayedJob deadlocks sometimes.
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
threads.each &:join
|
382
|
+
time = Time.now - start
|
383
|
+
puts "#{JOB_COUNT} jobs in #{time} seconds = #{(JOB_COUNT / time).round} jobs per second"
|
384
|
+
|
385
|
+
|
386
|
+
# These checks are commented out because I can't seem to get DelayedJob to
|
387
|
+
# pass them (Que and QueueClassic don't have the same problem). It seems
|
388
|
+
# to repeat some jobs multiple times on every run, and its run times are
|
389
|
+
# also highly variable.
|
390
|
+
|
391
|
+
# worked = $results[type].map(&:to_i).sort
|
392
|
+
# puts "Jobs worked more than once! #{worked.inspect}" unless worked == worked.uniq
|
393
|
+
# puts "Jobs worked less than once! #{worked.inspect}" unless worked.length == JOB_COUNT
|
394
|
+
|
395
|
+
puts "Jobs left in DB" unless pg.async_exec("SELECT count(*) FROM #{table}").first['count'].to_i == 0
|
396
|
+
puts "Advisory locks left over!" if pg.async_exec("SELECT * FROM pg_locks WHERE locktype = 'advisory'").first
|
397
|
+
end
|
398
|
+
end
|