que 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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