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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 12eec9dd8d0889bfe3253afef4fa850332fcd541
4
- data.tar.gz: 7419a5b65a4f96e48b7e4817b9281fc5b0e0e1aa
3
+ metadata.gz: f76b4a74e17281ea1007a2c313e146bcda43e1fb
4
+ data.tar.gz: 6ff1046d4cf009062327e79fb9be628032b252ce
5
5
  SHA512:
6
- metadata.gz: 8cc387df0177ea5374563b75079e2ae573b44ad041bda0175b09aa68a23cc31cacdd0741e5f725f83d95f553eca61ae73230c3207aa5fae935e34a653d3ed42d
7
- data.tar.gz: ae5e7d28eca45c3cbeeb5fee8323d5e8c4bc62dcdd192e7d7b570144dea5a43c239d9f086165163189ad15fb4df30bb862b5fc06e6d48ae8d3e4a4d6fcf7b699
6
+ metadata.gz: 319ea6bea73cdd606b4c3103beee120564aa29976255b2eb14992029b2f13768e33164b5134ad5e0b39a038086380294f02d7226a5c2e1f5d092ef32ef03a2e0
7
+ data.tar.gz: 44144ce58a99d38ced64a4e6259bafa66c7d6373ad125844efc27f45c990c764ad1e04e8e1e1b33f834e9b9be6ae3e812f2cfd3f9ce65303d19a070070b63832
@@ -0,0 +1,25 @@
1
+ ### 0.2.0 (2013-11-30)
2
+
3
+ * Officially support JRuby 1.7.5+. Earlier versions may work.
4
+
5
+ JRuby support requires the use of the `jruby-pg` gem, though that gem seems to currently be incompatible with ActiveRecord, so the ActiveRecord adapter specs don't pass (or even run). It works fine with Sequel and the other adapters, though.
6
+
7
+ * Officially support Rubinius 2.1.1+. Earlier versions may work.
8
+
9
+ * Use `multi_json` so we always use the fastest JSON parser available. (BukhariH)
10
+
11
+ * :sync mode now ignores scheduled jobs (jobs queued with a specific run_at).
12
+
13
+ ### 0.1.0 (2013-11-18)
14
+
15
+ * Initial public release, after a test-driven rewrite.
16
+
17
+ Officially support Ruby 2.0.0 and Postgres 9.2+.
18
+
19
+ Also support ActiveRecord and bare PG::Connections, in or out of a ConnectionPool.
20
+
21
+ Added a Railtie for easier setup with Rails, as well as a migration generator.
22
+
23
+ ### 0.0.1 (2013-11-07)
24
+
25
+ * Copy-pasted from an app of mine. Very Sequel-specific. Nobody look at it, let's pretend it never happened.
data/Gemfile CHANGED
@@ -1,4 +1,18 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in que.gemspec
3
+ group :development, :test do
4
+ gem 'rake'
5
+
6
+ gem 'activerecord', :require => nil
7
+ gem 'sequel', :require => nil
8
+ gem 'connection_pool', :require => nil
9
+ gem 'pg', :require => nil, :platform => :ruby
10
+ gem 'pg_jruby', :require => nil, :platform => :jruby
11
+ end
12
+
13
+ group :test do
14
+ gem 'rspec', '~> 2.14.1'
15
+ gem 'pry'
16
+ end
17
+
4
18
  gemspec
data/README.md CHANGED
@@ -18,7 +18,7 @@ Que's secondary goal is performance. It won't be able to match the speed or thro
18
18
 
19
19
  The rakefile includes a benchmark that tries to compare the performance and concurrency of Que's locking mechanism to that of DelayedJob and QueueClassic. On my i5 quad-core laptop, the results are along the lines of:
20
20
 
21
- ~/que $ rake benchmark
21
+ ~/que $ rake benchmark_queues
22
22
  Benchmarking 1000 jobs, 10 workers and synchronous_commit = on...
23
23
  Benchmarking delayed_job... 1000 jobs in 30.086127964 seconds = 33 jobs per second
24
24
  Benchmarking queue_classic... 1000 jobs in 19.642309724 seconds = 51 jobs per second
@@ -27,7 +27,7 @@ The rakefile includes a benchmark that tries to compare the performance and conc
27
27
 
28
28
  Or, minus the I/O limitations of my 5400 rpm hard drive:
29
29
 
30
- ~/que $ SYNCHRONOUS_COMMIT=off rake benchmark
30
+ ~/que $ SYNCHRONOUS_COMMIT=off rake benchmark_queues
31
31
  Benchmarking 1000 jobs, 10 workers and synchronous_commit = off...
32
32
  Benchmarking delayed_job... 1000 jobs in 4.906474583 seconds = 204 jobs per second
33
33
  Benchmarking queue_classic... 1000 jobs in 1.587542394 seconds = 630 jobs per second
data/Rakefile CHANGED
@@ -1,413 +1,3 @@
1
1
  require 'bundler/gem_tasks'
2
2
 
3
- require 'rspec/core/rake_task'
4
-
5
- RSpec::Core::RakeTask.new :default do |spec|
6
- spec.pattern = './spec/**/*_spec.rb'
7
- end
8
-
9
- # Shortcut to skip the adapter specs, and run only with the basic PG
10
- # connection. I use this occasionally to make sure ActiveRecord isn't loaded,
11
- # so any accidental Rails-isms are caught.
12
- RSpec::Core::RakeTask.new :pg do |spec|
13
- spec.pattern = './spec/*_spec.rb'
14
- end
15
-
16
- task :benchmark do
17
- # The following is a somewhat simplistic benchmark (aren't they all) meant
18
- # to compare the speed and concurrency of the locking mechanisms used by Que
19
- # (standard and lateral queries), DelayedJob and QueueClassic - it does this
20
- # by simply sending the raw queries that each system sends during operation.
21
-
22
- # It is NOT meant to benchmark the overall performance of each system (which
23
- # would include the time each spends working in Ruby), but to see which one
24
- # supports the highest concurrency under load, assuming that there will be
25
- # many workers and that Postgres will be the bottleneck. I'm unsure how
26
- # useful it is for this, but it's a start.
27
-
28
- JOB_COUNT = (ENV['JOB_COUNT'] || 1000).to_i
29
- WORKER_COUNT = (ENV['WORKER_COUNT'] || 10).to_i
30
- SYNCHRONOUS_COMMIT = ENV['SYNCHRONOUS_COMMIT'] || 'on'
31
-
32
- require 'uri'
33
- require 'pg'
34
- require 'connection_pool'
35
-
36
- uri = URI.parse ENV["DATABASE_URL"] || "postgres://postgres:@localhost/que-test"
37
-
38
- new_connection = proc do
39
- PG::Connection.open :host => uri.host,
40
- :user => uri.user,
41
- :password => uri.password,
42
- :port => uri.port || 5432,
43
- :dbname => uri.path[1..-1]
44
- end
45
-
46
- pg = new_connection.call
47
-
48
-
49
-
50
- # Necessary setup, mostly for QueueClassic. I apologize for this - I hope your editor supports code folding.
51
- pg.async_exec <<-SQL
52
- SET SESSION client_min_messages = 'WARNING';
53
-
54
- -- Que table.
55
- DROP TABLE IF EXISTS que_jobs;
56
- CREATE TABLE que_jobs
57
- (
58
- priority integer NOT NULL DEFAULT 1,
59
- run_at timestamptz NOT NULL DEFAULT now(),
60
- job_id bigserial NOT NULL,
61
- job_class text NOT NULL,
62
- args json NOT NULL DEFAULT '[]'::json,
63
- error_count integer NOT NULL DEFAULT 0,
64
- last_error text,
65
-
66
- CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
67
- );
68
-
69
- DROP TABLE IF EXISTS que_lateral_jobs;
70
- CREATE TABLE que_lateral_jobs
71
- (
72
- priority integer NOT NULL DEFAULT 1,
73
- run_at timestamptz NOT NULL DEFAULT now(),
74
- job_id bigserial NOT NULL,
75
- job_class text NOT NULL,
76
- args json NOT NULL DEFAULT '[]'::json,
77
- error_count integer NOT NULL DEFAULT 0,
78
- last_error text,
79
-
80
- CONSTRAINT que_lateral_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
81
- );
82
-
83
- DROP TABLE IF EXISTS delayed_jobs;
84
- -- DelayedJob table.
85
- CREATE TABLE delayed_jobs
86
- (
87
- id serial NOT NULL,
88
- priority integer NOT NULL DEFAULT 0,
89
- attempts integer NOT NULL DEFAULT 0,
90
- handler text NOT NULL,
91
- last_error text,
92
- run_at timestamp without time zone,
93
- locked_at timestamp without time zone,
94
- failed_at timestamp without time zone,
95
- locked_by character varying(255),
96
- queue character varying(255),
97
- created_at timestamp without time zone,
98
- updated_at timestamp without time zone,
99
- CONSTRAINT delayed_jobs_pkey PRIMARY KEY (id)
100
- );
101
- ALTER TABLE delayed_jobs
102
- OWNER TO postgres;
103
-
104
- CREATE INDEX delayed_jobs_priority
105
- ON delayed_jobs
106
- USING btree
107
- (priority, run_at);
108
-
109
-
110
-
111
- -- QueueClassic table and functions.
112
- DROP FUNCTION IF EXISTS lock_head(tname varchar);
113
- DROP FUNCTION IF EXISTS lock_head(q_name varchar, top_boundary integer);
114
- DROP FUNCTION IF EXISTS queue_classic_notify() cascade;
115
- DROP TABLE IF EXISTS queue_classic_jobs;
116
-
117
- CREATE TABLE queue_classic_jobs (
118
- id bigserial PRIMARY KEY,
119
- q_name text not null check (length(q_name) > 0),
120
- method text not null check (length(method) > 0),
121
- args text not null,
122
- locked_at timestamptz
123
- );
124
-
125
- alter table queue_classic_jobs alter column args type json using (args::json);
126
-
127
- create function queue_classic_notify() returns trigger as $$ begin
128
- perform pg_notify(new.q_name, '');
129
- return null;
130
- end $$ language plpgsql;
131
-
132
- create trigger queue_classic_notify
133
- after insert on queue_classic_jobs
134
- for each row
135
- execute procedure queue_classic_notify();
136
-
137
- CREATE INDEX idx_qc_on_name_only_unlocked ON queue_classic_jobs (q_name, id) WHERE locked_at IS NULL;
138
-
139
- CREATE OR REPLACE FUNCTION lock_head(q_name varchar, top_boundary integer)
140
- RETURNS SETOF queue_classic_jobs AS $$
141
- DECLARE
142
- unlocked bigint;
143
- relative_top integer;
144
- job_count integer;
145
- BEGIN
146
- -- The purpose is to release contention for the first spot in the table.
147
- -- The select count(*) is going to slow down dequeue performance but allow
148
- -- for more workers. Would love to see some optimization here...
149
-
150
- EXECUTE 'SELECT count(*) FROM '
151
- || '(SELECT * FROM queue_classic_jobs WHERE q_name = '
152
- || quote_literal(q_name)
153
- || ' LIMIT '
154
- || quote_literal(top_boundary)
155
- || ') limited'
156
- INTO job_count;
157
-
158
- SELECT TRUNC(random() * (top_boundary - 1))
159
- INTO relative_top;
160
-
161
- IF job_count < top_boundary THEN
162
- relative_top = 0;
163
- END IF;
164
-
165
- LOOP
166
- BEGIN
167
- EXECUTE 'SELECT id FROM queue_classic_jobs '
168
- || ' WHERE locked_at IS NULL'
169
- || ' AND q_name = '
170
- || quote_literal(q_name)
171
- || ' ORDER BY id ASC'
172
- || ' LIMIT 1'
173
- || ' OFFSET ' || quote_literal(relative_top)
174
- || ' FOR UPDATE NOWAIT'
175
- INTO unlocked;
176
- EXIT;
177
- EXCEPTION
178
- WHEN lock_not_available THEN
179
- -- do nothing. loop again and hope we get a lock
180
- END;
181
- END LOOP;
182
-
183
- RETURN QUERY EXECUTE 'UPDATE queue_classic_jobs '
184
- || ' SET locked_at = (CURRENT_TIMESTAMP)'
185
- || ' WHERE id = $1'
186
- || ' AND locked_at is NULL'
187
- || ' RETURNING *'
188
- USING unlocked;
189
-
190
- RETURN;
191
- END;
192
- $$ LANGUAGE plpgsql;
193
-
194
- CREATE OR REPLACE FUNCTION lock_head(tname varchar)
195
- RETURNS SETOF queue_classic_jobs AS $$
196
- BEGIN
197
- RETURN QUERY EXECUTE 'SELECT * FROM lock_head($1,10)' USING tname;
198
- END;
199
- $$ LANGUAGE plpgsql;
200
-
201
-
202
-
203
-
204
-
205
- INSERT INTO que_jobs (job_class, args, priority)
206
- SELECT 'Que::Job', ('[' || i || ',{}]')::json, 1
207
- FROM generate_Series(1,#{JOB_COUNT}) AS i;
208
-
209
- INSERT INTO que_lateral_jobs (job_class, args, priority)
210
- SELECT 'Que::Job', ('[' || i || ',{}]')::json, 1
211
- FROM generate_Series(1,#{JOB_COUNT}) AS i;
212
-
213
- INSERT INTO delayed_jobs (handler, run_at, created_at, updated_at)
214
- SELECT '--- !ruby/struct:NewsletterJob\ntext: lorem ipsum...\nemails: blah@blah.com\n', now(), now(), now()
215
- FROM generate_Series(1,#{JOB_COUNT}) AS i;
216
-
217
- INSERT INTO queue_classic_jobs (q_name, method, args)
218
- SELECT 'default', 'Kernel.puts', '["hello world"]'
219
- FROM generate_Series(1,#{JOB_COUNT}) AS i;
220
-
221
-
222
-
223
-
224
- -- Necessary tables and functions made, now stock them with jobs and analyze.
225
- ANALYZE;
226
- SQL
227
-
228
-
229
- queries = {
230
- :que => (
231
- <<-SQL
232
- WITH RECURSIVE cte AS (
233
- SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
234
- FROM (
235
- SELECT job
236
- FROM que_jobs AS job
237
- WHERE run_at <= now()
238
- ORDER BY priority, run_at, job_id
239
- LIMIT 1
240
- ) AS t1
241
- UNION ALL (
242
- SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
243
- FROM (
244
- SELECT (
245
- SELECT job
246
- FROM que_jobs AS job
247
- WHERE run_at <= now() AND (priority, run_at, job_id) > (cte.priority, cte.run_at, cte.job_id)
248
- ORDER BY priority, run_at, job_id
249
- LIMIT 1
250
- ) AS job
251
- FROM cte
252
- WHERE NOT cte.locked
253
- LIMIT 1
254
- ) AS t1
255
- )
256
- )
257
- SELECT job_id, priority, run_at, args, job_class, error_count
258
- FROM cte
259
- WHERE locked
260
- LIMIT 1
261
- SQL
262
- ),
263
- :que_lateral => (
264
- <<-SQL
265
- WITH RECURSIVE cte AS (
266
- SELECT *, pg_try_advisory_lock(s.job_id) AS locked
267
- FROM (
268
- SELECT *
269
- FROM que_lateral_jobs
270
- WHERE run_at <= now()
271
- ORDER BY priority, run_at, job_id
272
- LIMIT 1
273
- ) s
274
- UNION ALL (
275
- SELECT j.*, pg_try_advisory_lock(j.job_id) AS locked
276
- FROM (
277
- SELECT *
278
- FROM cte
279
- WHERE NOT locked
280
- ) t,
281
- LATERAL (
282
- SELECT *
283
- FROM que_lateral_jobs
284
- WHERE run_at <= now()
285
- AND (priority, run_at, job_id) > (t.priority, t.run_at, t.job_id)
286
- ORDER BY priority, run_at, job_id
287
- LIMIT 1
288
- ) j
289
- )
290
- )
291
- SELECT *
292
- FROM cte
293
- WHERE locked
294
- LIMIT 1
295
- SQL
296
- ),
297
- :delayed_job => (
298
- # From delayed_job_active_record
299
- <<-SQL
300
- UPDATE delayed_jobs
301
- SET locked_at = now(),
302
- locked_by = $1::text
303
- WHERE id IN (
304
- SELECT id
305
- FROM delayed_jobs
306
- WHERE (
307
- (run_at <= now() AND (locked_at IS NULL OR locked_at < now()) OR locked_by = $1) AND failed_at IS NULL
308
- )
309
- ORDER BY priority ASC, run_at ASC
310
- LIMIT 1
311
- FOR UPDATE
312
- )
313
- RETURNING *
314
- SQL
315
- )
316
- }
317
-
318
- connections = WORKER_COUNT.times.map do
319
- conn = new_connection.call
320
- conn.async_exec "SET SESSION synchronous_commit = #{SYNCHRONOUS_COMMIT}"
321
- queries.each do |name, sql|
322
- conn.prepare(name.to_s, sql)
323
- end
324
- conn
325
- end
326
-
327
-
328
-
329
- # Track the ids that are worked, to make sure they're all hit.
330
- $results = {
331
- :delayed_job => [],
332
- :queue_classic => [],
333
- :que => [],
334
- :que_lateral => []
335
- }
336
-
337
- def work_job(type, conn)
338
- case type
339
- when :delayed_job
340
- return unless r = conn.exec_prepared("delayed_job", [conn.object_id]).first
341
- $results[type] << r['id']
342
- conn.async_exec "DELETE FROM delayed_jobs WHERE id = $1", [r['id']]
343
-
344
- when :queue_classic
345
- return unless r = conn.async_exec("SELECT * FROM lock_head($1, $2)", ['default', 9]).first
346
- $results[type] << r['id']
347
- conn.async_exec "DELETE FROM queue_classic_jobs WHERE id = $1", [r['id']]
348
-
349
- when :que
350
- begin
351
- return unless r = conn.exec_prepared("que").first
352
- # Have to double-check that the job is valid, as explained at length in Que::Job.work.
353
- 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
354
- 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']]
355
- $results[type] << r['job_id']
356
- ensure
357
- conn.async_exec "SELECT pg_advisory_unlock_all()" if r
358
- end
359
-
360
- when :que_lateral
361
- begin
362
- return unless r = conn.exec_prepared("que_lateral").first
363
- 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
364
- 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']]
365
- $results[type] << r['job_id']
366
- ensure
367
- conn.async_exec "SELECT pg_advisory_unlock_all()" if r
368
- end
369
-
370
- end
371
- end
372
-
373
- puts "Benchmarking #{JOB_COUNT} jobs, #{WORKER_COUNT} workers and synchronous_commit = #{SYNCHRONOUS_COMMIT}..."
374
-
375
- {
376
- :delayed_job => :delayed_jobs,
377
- :queue_classic => :queue_classic_jobs,
378
- :que => :que_jobs,
379
- :que_lateral => :que_lateral_jobs
380
- }.each do |type, table|
381
- print "Benchmarking #{type}... "
382
- start = Time.now
383
-
384
- threads = connections.map do |conn|
385
- Thread.new do
386
- loop do
387
- begin
388
- break unless work_job(type, conn)
389
- rescue
390
- # DelayedJob deadlocks sometimes.
391
- end
392
- end
393
- end
394
- end
395
-
396
- threads.each &:join
397
- time = Time.now - start
398
- puts "#{JOB_COUNT} jobs in #{time} seconds = #{(JOB_COUNT / time).round} jobs per second"
399
-
400
-
401
- # These checks are commented out because I can't seem to get DelayedJob to
402
- # pass them (Que and QueueClassic don't have the same problem). It seems
403
- # to repeat some jobs multiple times on every run, and its run times are
404
- # also highly variable.
405
-
406
- # worked = $results[type].map(&:to_i).sort
407
- # puts "Jobs worked more than once! #{worked.inspect}" unless worked == worked.uniq
408
- # puts "Jobs worked less than once! #{worked.inspect}" unless worked.length == JOB_COUNT
409
-
410
- puts "Jobs left in DB" unless pg.async_exec("SELECT count(*) FROM #{table}").first['count'].to_i == 0
411
- puts "Advisory locks left over!" if pg.async_exec("SELECT * FROM pg_locks WHERE locktype = 'advisory'").first
412
- end
413
- end
3
+ Dir["./tasks/*.rb"].sort.each &method(:require)