que 0.1.0 → 0.2.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,4 +1,4 @@
1
- require 'json'
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 => JSON.dump(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(JSON.load(attrs[:args]))
138
- const_get("::#{attrs[:job_class]}").new(attrs).tap(&:_run)
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)
@@ -1,3 +1,3 @@
1
1
  module Que
2
- Version = '0.1.0'
2
+ Version = '0.2.0'
3
3
  end
@@ -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
- @thread.join
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
@@ -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.add_development_dependency 'sequel'
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
@@ -1,4 +1,5 @@
1
1
  require 'que'
2
+ require 'json'
2
3
 
3
4
  Dir["./spec/support/**/*.rb"].sort.each &method(:require)
4
5
 
@@ -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
- it "Que.mode = :sync should make jobs run in the same thread as they are queued" do
10
- Que.mode = :sync
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
- ArgsJob.queue(5, :testing => "synchronous").should be_an_instance_of ArgsJob
13
- $passed_args.should == [5, {'testing' => "synchronous"}]
14
- DB[:que_jobs].count.should be 0
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
- $logger.messages.length.should be 2
17
- $logger.messages[0].should == "[Que] Set mode to :sync"
18
- $logger.messages[1].should =~ /\A\[Que\] Worked job in/
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
@@ -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 =~ /\Auninitialized constant NonexistentClass/
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
@@ -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