queue_classic 0.2.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/queue_classic.rb +5 -5
- data/lib/queue_classic/database.rb +128 -0
- data/lib/queue_classic/durable_array.rb +21 -36
- data/lib/queue_classic/job.rb +8 -0
- data/lib/queue_classic/queue.rb +68 -13
- data/lib/queue_classic/tasks.rb +18 -2
- data/lib/queue_classic/worker.rb +28 -4
- data/readme.md +33 -0
- data/test/database_helpers.rb +6 -44
- data/test/durable_array_test.rb +57 -60
- data/test/helper.rb +0 -10
- data/test/job_test.rb +1 -1
- data/test/queue_test.rb +45 -18
- data/test/worker_test.rb +16 -21
- metadata +9 -25
- data/lib/queue_classic/api.rb +0 -50
- data/readme.markdown +0 -218
- data/test/api_test.rb +0 -40
data/lib/queue_classic.rb
CHANGED
@@ -2,16 +2,16 @@ require 'json'
|
|
2
2
|
require 'pg'
|
3
3
|
require 'uri'
|
4
4
|
|
5
|
-
$: << File.expand_path(
|
5
|
+
$: << File.expand_path(__FILE__, 'lib')
|
6
6
|
|
7
7
|
require 'queue_classic/durable_array'
|
8
|
+
require 'queue_classic/database'
|
8
9
|
require 'queue_classic/worker'
|
9
10
|
require 'queue_classic/queue'
|
10
|
-
require 'queue_classic/api'
|
11
11
|
require 'queue_classic/job'
|
12
12
|
|
13
13
|
module QC
|
14
|
-
|
14
|
+
def self.method_missing(sym, *args, &block)
|
15
|
+
Queue.send(sym, *args, &block)
|
16
|
+
end
|
15
17
|
end
|
16
|
-
|
17
|
-
QC::Queue.instance.setup :data_store => QC::DurableArray.new(:database => ENV["DATABASE_URL"])
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module QC
|
2
|
+
class Database
|
3
|
+
|
4
|
+
MAX_TOP_BOUND = 9
|
5
|
+
DEFAULT_QUEUE_NAME = "queue_classic_jobs"
|
6
|
+
|
7
|
+
def self.create_queue(name)
|
8
|
+
db = new(name)
|
9
|
+
db.create_table
|
10
|
+
db.disconnect
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :table_name
|
15
|
+
|
16
|
+
def initialize(queue_name=nil)
|
17
|
+
@top_boundry = ENV["TOP_BOUND"] || MAX_TOP_BOUND
|
18
|
+
@table_name = queue_name || DEFAULT_QUEUE_NAME
|
19
|
+
@db_params = URI.parse(ENV["DATABASE_URL"])
|
20
|
+
end
|
21
|
+
|
22
|
+
def init_db
|
23
|
+
drop_table
|
24
|
+
create_table
|
25
|
+
load_functions
|
26
|
+
end
|
27
|
+
|
28
|
+
def waiting_conns
|
29
|
+
execute("SELECT * FROM pg_stat_activity WHERE datname = '#{@name}' AND waiting = 't' AND application_name = 'queue_classic'")
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_conns
|
33
|
+
execute("SELECT * FROM pg_stat_activity WHERE datname = '#{@name}' AND application_name = 'queue_classic'")
|
34
|
+
end
|
35
|
+
|
36
|
+
def silence_warnings
|
37
|
+
execute("SET client_min_messages TO 'warning'")
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute(sql)
|
41
|
+
connection.exec(sql)
|
42
|
+
end
|
43
|
+
|
44
|
+
def disconnect
|
45
|
+
connection.finish
|
46
|
+
end
|
47
|
+
|
48
|
+
def connection
|
49
|
+
if defined? @connection
|
50
|
+
@connection
|
51
|
+
else
|
52
|
+
@name = @db_params.path.gsub("/","")
|
53
|
+
@connection = PGconn.connect(
|
54
|
+
:dbname => @db_params.path.gsub("/",""),
|
55
|
+
:user => @db_params.user,
|
56
|
+
:password => @db_params.password,
|
57
|
+
:host => @db_params.host
|
58
|
+
)
|
59
|
+
@connection.exec("LISTEN queue_classic_jobs")
|
60
|
+
@connection.exec("SET application_name = 'queue_classic'")
|
61
|
+
silence_warnings unless ENV["LOGGING_ENABLED"]
|
62
|
+
@connection
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def drop_table
|
67
|
+
execute("DROP TABLE IF EXISTS #{@table_name} CASCADE")
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_table
|
71
|
+
execute("CREATE TABLE #{@table_name} (id serial, details text, locked_at timestamp)")
|
72
|
+
execute("CREATE INDEX #{@table_name}_id_idx ON #{@table_name} (id)")
|
73
|
+
end
|
74
|
+
|
75
|
+
def load_functions
|
76
|
+
execute(<<-EOD)
|
77
|
+
-- We are declaring the return type to be queue_classic_jobs.
|
78
|
+
-- This is ok since I am assuming that all of the users added queues will
|
79
|
+
-- have identical columns to queue_classic_jobs.
|
80
|
+
-- When QC supports queues with columns other than the default, we will have to change this.
|
81
|
+
|
82
|
+
CREATE OR REPLACE FUNCTION lock_head(tname varchar) RETURNS SETOF queue_classic_jobs AS $$
|
83
|
+
DECLARE
|
84
|
+
unlocked integer;
|
85
|
+
relative_top integer;
|
86
|
+
job_count integer;
|
87
|
+
BEGIN
|
88
|
+
SELECT TRUNC(random() * #{@top_boundry} + 1) INTO relative_top;
|
89
|
+
EXECUTE 'SELECT count(*) FROM' || tname || '' INTO job_count;
|
90
|
+
|
91
|
+
IF job_count < 10 THEN
|
92
|
+
relative_top = 0;
|
93
|
+
END IF;
|
94
|
+
|
95
|
+
LOOP
|
96
|
+
BEGIN
|
97
|
+
EXECUTE 'SELECT id FROM '
|
98
|
+
|| tname::regclass
|
99
|
+
|| ' WHERE locked_at IS NULL'
|
100
|
+
|| ' ORDER BY id ASC'
|
101
|
+
|| ' LIMIT 1'
|
102
|
+
|| ' OFFSET ' || relative_top
|
103
|
+
|| ' FOR UPDATE NOWAIT'
|
104
|
+
INTO unlocked;
|
105
|
+
EXIT;
|
106
|
+
EXCEPTION
|
107
|
+
WHEN lock_not_available THEN
|
108
|
+
-- do nothing. loop again and hope we get a lock
|
109
|
+
END;
|
110
|
+
END LOOP;
|
111
|
+
|
112
|
+
|
113
|
+
RETURN QUERY EXECUTE 'UPDATE '
|
114
|
+
|| tname::regclass
|
115
|
+
|| ' SET locked_at = (CURRENT_TIMESTAMP)'
|
116
|
+
|| ' WHERE id = $1'
|
117
|
+
|| ' AND locked_at is NULL'
|
118
|
+
|| ' RETURNING *'
|
119
|
+
USING unlocked;
|
120
|
+
|
121
|
+
RETURN;
|
122
|
+
END;
|
123
|
+
$$ LANGUAGE plpgsql;
|
124
|
+
EOD
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
@@ -1,77 +1,62 @@
|
|
1
1
|
module QC
|
2
2
|
class DurableArray
|
3
3
|
|
4
|
-
def initialize(
|
5
|
-
@
|
4
|
+
def initialize(database)
|
5
|
+
@database = database
|
6
|
+
@table_name = @database.table_name
|
6
7
|
end
|
7
8
|
|
8
9
|
def <<(details)
|
9
|
-
execute("INSERT INTO
|
10
|
-
execute("NOTIFY
|
10
|
+
execute("INSERT INTO #{@table_name} (details) VALUES ('#{details.to_json}')")
|
11
|
+
execute("NOTIFY queue_classic_jobs, 'new-job'")
|
11
12
|
end
|
12
13
|
|
13
14
|
def count
|
14
|
-
execute("SELECT COUNT(*)
|
15
|
+
execute("SELECT COUNT(*) FROM #{@table_name}")[0]["count"].to_i
|
15
16
|
end
|
16
17
|
|
17
18
|
def delete(job)
|
18
|
-
execute("DELETE FROM
|
19
|
+
execute("DELETE FROM #{@table_name} WHERE id = #{job.id}")
|
19
20
|
job
|
20
21
|
end
|
21
22
|
|
22
23
|
def find(job)
|
23
|
-
find_one {"SELECT * FROM
|
24
|
+
find_one {"SELECT * FROM #{@table_name} WHERE id = #{job.id}"}
|
25
|
+
end
|
26
|
+
|
27
|
+
def search_details_column(q)
|
28
|
+
find_many { "SELECT * FROM #{@table_name} WHERE details LIKE '%#{q}%'" }
|
24
29
|
end
|
25
30
|
|
26
31
|
def lock_head
|
27
|
-
|
28
|
-
connection.transaction do
|
29
|
-
if job = find_one {"SELECT * FROM jobs WHERE locked_at IS NULL ORDER BY id ASC LIMIT 1 FOR UPDATE"}
|
30
|
-
execute("UPDATE jobs SET locked_at = (CURRENT_TIMESTAMP) WHERE id = #{job.id} AND locked_at IS NULL")
|
31
|
-
end
|
32
|
-
end
|
33
|
-
job
|
32
|
+
find_one { "SELECT * FROM lock_head('#{@table_name}')" }
|
34
33
|
end
|
35
34
|
|
36
35
|
def first
|
37
36
|
if job = lock_head
|
38
37
|
job
|
39
38
|
else
|
40
|
-
|
41
|
-
connection.wait_for_notify {|e,p,msg| job = lock_head if msg == "new-job" }
|
39
|
+
@database.connection.wait_for_notify(1) {|e,p,msg| job = lock_head if msg == "new-job" }
|
42
40
|
job
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
46
44
|
def each
|
47
|
-
execute("SELECT * FROM
|
45
|
+
execute("SELECT * FROM #{@table_name} ORDER BY id ASC").each do |r|
|
48
46
|
yield Job.new(r)
|
49
47
|
end
|
50
48
|
end
|
51
49
|
|
52
|
-
def
|
53
|
-
|
50
|
+
def find_one(&blk)
|
51
|
+
find_many(&blk).pop
|
54
52
|
end
|
55
53
|
|
56
|
-
def
|
57
|
-
|
58
|
-
if res.count > 0
|
59
|
-
res.map {|r| Job.new(r)}.pop
|
60
|
-
end
|
54
|
+
def find_many
|
55
|
+
execute(yield).map {|r| Job.new(r)}
|
61
56
|
end
|
62
57
|
|
63
|
-
def
|
64
|
-
|
65
|
-
@connection ||= if db_params.scheme == "postgres"
|
66
|
-
PGconn.connect(
|
67
|
-
:dbname => db_params.path.gsub("/",""),
|
68
|
-
:user => db_params.user,
|
69
|
-
:password => db_params.password,
|
70
|
-
:host => db_params.host
|
71
|
-
)
|
72
|
-
else
|
73
|
-
PGconn.connect(:dbname => @db_string)
|
74
|
-
end
|
58
|
+
def execute(sql)
|
59
|
+
@database.execute(sql)
|
75
60
|
end
|
76
61
|
|
77
62
|
end
|
data/lib/queue_classic/job.rb
CHANGED
data/lib/queue_classic/queue.rb
CHANGED
@@ -1,30 +1,85 @@
|
|
1
|
-
|
1
|
+
module QC
|
2
|
+
module AbstractQueue
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
def enqueue(job,*params)
|
5
|
+
if job.respond_to?(:details) and job.respond_to?(:params)
|
6
|
+
job = job.signature
|
7
|
+
params = *job.params
|
8
|
+
end
|
9
|
+
array << {"job" => job, "params" => params}
|
8
10
|
end
|
9
11
|
|
10
|
-
def
|
11
|
-
|
12
|
+
def dequeue
|
13
|
+
array.first
|
12
14
|
end
|
13
15
|
|
14
|
-
def
|
15
|
-
|
16
|
+
def query(signature)
|
17
|
+
array.search_details_column(signature)
|
16
18
|
end
|
17
19
|
|
18
20
|
def delete(job)
|
19
|
-
|
21
|
+
array.delete(job)
|
20
22
|
end
|
21
23
|
|
22
24
|
def delete_all
|
23
|
-
|
25
|
+
array.each {|j| delete(j) }
|
24
26
|
end
|
25
27
|
|
26
28
|
def length
|
27
|
-
|
29
|
+
array.count
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module QC
|
36
|
+
module ConnectionHelper
|
37
|
+
|
38
|
+
def connection_status
|
39
|
+
{:total => database.all_conns.count, :waiting => database.waiting_conns.count}
|
40
|
+
end
|
41
|
+
|
42
|
+
def disconnect
|
43
|
+
database.disconnect
|
28
44
|
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module QC
|
50
|
+
class Queue
|
51
|
+
|
52
|
+
include AbstractQueue
|
53
|
+
extend AbstractQueue
|
54
|
+
|
55
|
+
include ConnectionHelper
|
56
|
+
extend ConnectionHelper
|
57
|
+
|
58
|
+
def self.array
|
59
|
+
if defined? @@array
|
60
|
+
@@array
|
61
|
+
else
|
62
|
+
@@database = Database.new
|
63
|
+
@@array = DurableArray.new(@@database)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.database
|
68
|
+
@@database
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(queue_name)
|
72
|
+
@database = Database.new(queue_name)
|
73
|
+
@array = DurableArray.new(@database)
|
74
|
+
end
|
75
|
+
|
76
|
+
def array
|
77
|
+
@array
|
78
|
+
end
|
79
|
+
|
80
|
+
def database
|
81
|
+
@database
|
82
|
+
end
|
83
|
+
|
29
84
|
end
|
30
85
|
end
|
data/lib/queue_classic/tasks.rb
CHANGED
@@ -1,13 +1,29 @@
|
|
1
1
|
namespace :jobs do
|
2
|
+
|
2
3
|
task :work => :environment do
|
3
4
|
QC::Worker.new.start
|
4
5
|
end
|
6
|
+
|
5
7
|
end
|
8
|
+
|
6
9
|
namespace :qc do
|
7
|
-
|
10
|
+
|
11
|
+
task :work => :environment do
|
8
12
|
QC::Worker.new.start
|
9
13
|
end
|
10
|
-
|
14
|
+
|
15
|
+
task :jobs => :environment do
|
11
16
|
QC.queue_length
|
12
17
|
end
|
18
|
+
|
19
|
+
task :load_functions => :environment do
|
20
|
+
db = QC::Database.new
|
21
|
+
db.load_functions
|
22
|
+
db.disconnect
|
23
|
+
end
|
24
|
+
|
25
|
+
task :create_queue, :name, :needs => :environment do |t,args|
|
26
|
+
QC::Database.create_queue(args[:name])
|
27
|
+
end
|
28
|
+
|
13
29
|
end
|
data/lib/queue_classic/worker.rb
CHANGED
@@ -1,18 +1,42 @@
|
|
1
1
|
module QC
|
2
2
|
class Worker
|
3
3
|
|
4
|
+
def initialize
|
5
|
+
@running = true
|
6
|
+
@queue = QC::Queue.new(ENV["QUEUE"])
|
7
|
+
handle_signals
|
8
|
+
end
|
9
|
+
|
10
|
+
def running?
|
11
|
+
@running
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_signals
|
15
|
+
%W(INT TRAP).each do |sig|
|
16
|
+
trap(sig) do
|
17
|
+
if running?
|
18
|
+
@running = false
|
19
|
+
else
|
20
|
+
raise Interrupt
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
4
26
|
def start
|
5
|
-
|
27
|
+
while running? do
|
28
|
+
work
|
29
|
+
end
|
6
30
|
end
|
7
31
|
|
8
32
|
def work
|
9
|
-
if job =
|
33
|
+
if job = @queue.dequeue #blocks until we have a job
|
10
34
|
begin
|
11
|
-
|
35
|
+
job.work
|
12
36
|
rescue Object => e
|
13
37
|
handle_failure(job,e)
|
14
38
|
ensure
|
15
|
-
|
39
|
+
@queue.delete(job)
|
16
40
|
end
|
17
41
|
end
|
18
42
|
end
|
data/readme.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Queue Classic
|
2
|
+
__Beta 0.3.1__
|
3
|
+
|
4
|
+
__Queue Classic 0.3.1 is in Beta.__ I have been using this library with 30-150 Heroku workers and have had great results.
|
5
|
+
|
6
|
+
I am using this in production applications and plan to maintain and support this library for a long time.
|
7
|
+
|
8
|
+
Queue Classic is a queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features a blocking dequeue, database maintained locks and
|
9
|
+
no ridiculous dependencies. As a matter of fact, Queue Classic only requires the __pg__ and __json__.
|
10
|
+
|
11
|
+
[Discussion Group](http://groups.google.com/group/queue_classic)
|
12
|
+
|
13
|
+
[Wiki](https://github.com/ryandotsmith/queue_classic/wiki)
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
1. $ gem install queue_classic
|
18
|
+
2. psql=# CREATE TABLE queue_classic_jobs (id serial, details text, locked_at timestamp);
|
19
|
+
3. psql=# CREATE INDEX queue_classic_jobs_id_idx ON queue_classic_jobs (id);
|
20
|
+
4. $ rake qc:load_functions
|
21
|
+
5. irb: QC.enqueue "Class.method", "arg"
|
22
|
+
6. $ rake jobs:work
|
23
|
+
|
24
|
+
### Upgrade from < 0.2.3 to 0.3.0
|
25
|
+
|
26
|
+
$ psql your_database
|
27
|
+
your_database=# ALTER TABLE jobs RENAME TO queue_classic_jobs;
|
28
|
+
|
29
|
+
### Dependencies
|
30
|
+
|
31
|
+
Postgres version 9
|
32
|
+
|
33
|
+
Ruby (gems: pg, json)
|
data/test/database_helpers.rb
CHANGED
@@ -1,49 +1,11 @@
|
|
1
1
|
module DatabaseHelpers
|
2
2
|
|
3
|
-
def
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
def init_db(table_name=nil)
|
4
|
+
database = QC::Database.new(table_name)
|
5
|
+
database.silence_warnings
|
6
|
+
database.init_db
|
7
|
+
database.disconnect
|
8
|
+
true
|
7
9
|
end
|
8
10
|
|
9
|
-
def create_database
|
10
|
-
postgres.exec "CREATE DATABASE #{ENV['DATABASE_URL']}"
|
11
|
-
end
|
12
|
-
|
13
|
-
def drop_database
|
14
|
-
postgres.exec "DROP DATABASE IF EXISTS #{ENV['DATABASE_URL']}"
|
15
|
-
end
|
16
|
-
|
17
|
-
def create_table
|
18
|
-
jobs_db.exec(
|
19
|
-
"CREATE TABLE jobs" +
|
20
|
-
"(" +
|
21
|
-
"id SERIAL," +
|
22
|
-
"details text," +
|
23
|
-
"locked_at timestamp without time zone" +
|
24
|
-
");"
|
25
|
-
)
|
26
|
-
jobs_db.exec("CREATE INDEX jobs_id_idx ON jobs (id)")
|
27
|
-
end
|
28
|
-
|
29
|
-
def drop_table
|
30
|
-
jobs_db.exec("DROP TABLE IF EXISTS jobs")
|
31
|
-
end
|
32
|
-
|
33
|
-
def disconnect
|
34
|
-
jobs_db.finish
|
35
|
-
postgres.finish
|
36
|
-
end
|
37
|
-
|
38
|
-
def jobs_db
|
39
|
-
@jobs_db ||= PGconn.connect(:dbname => ENV["DATABASE_URL"])
|
40
|
-
@jobs_db.exec("SET client_min_messages TO 'warning'")
|
41
|
-
@jobs_db
|
42
|
-
end
|
43
|
-
|
44
|
-
def postgres
|
45
|
-
@postgres ||= PGconn.connect(:dbname => 'postgres')
|
46
|
-
@postgres.exec("SET client_min_messages TO 'warning'")
|
47
|
-
@postgres
|
48
|
-
end
|
49
11
|
end
|
data/test/durable_array_test.rb
CHANGED
@@ -1,95 +1,92 @@
|
|
1
1
|
require File.expand_path("../helper.rb", __FILE__)
|
2
2
|
|
3
|
-
|
4
|
-
include DatabaseHelpers
|
3
|
+
context "DurableArray" do
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
array
|
11
|
-
assert_equal({"test" => "ok"}, array.first.details)
|
5
|
+
setup do
|
6
|
+
@database = QC::Database.new
|
7
|
+
@database.drop_table
|
8
|
+
@database.init_db
|
9
|
+
@array = QC::DurableArray.new(@database)
|
12
10
|
end
|
13
11
|
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
teardown do
|
13
|
+
@database.disconnect
|
14
|
+
end
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
assert_equal 2, array.count
|
16
|
+
test "decode json into hash" do
|
17
|
+
@array << {"test" => "ok"}
|
18
|
+
assert_equal({"test" => "ok"}, @array.first.details)
|
22
19
|
end
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
test "count returns number of rows" do
|
22
|
+
@array << {"test" => "ok"}
|
23
|
+
assert_equal 1, @array.count
|
24
|
+
@array << {"test" => "ok"}
|
25
|
+
assert_equal 2, @array.count
|
26
|
+
end
|
27
27
|
|
28
|
+
test "first returns first job" do
|
28
29
|
job = {"job" => "one"}
|
29
|
-
array << job
|
30
|
-
assert_equal job, array.first.details
|
30
|
+
@array << job
|
31
|
+
assert_equal job, @array.first.details
|
31
32
|
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
array
|
36
|
-
|
37
|
-
array << {"job" => "one"}
|
38
|
-
array << {"job" => "two"}
|
39
|
-
assert_equal({"job" => "one"}, array.first.details)
|
34
|
+
test "first returns first job when many are in the array" do
|
35
|
+
@array << {"job" => "one"}
|
36
|
+
@array << {"job" => "two"}
|
37
|
+
assert_equal({"job" => "one"}, @array.first.details)
|
40
38
|
end
|
41
39
|
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
test "find_many returns empty array when nothing is found" do
|
41
|
+
assert_equal([], @array.find_many {"select * from queue_classic_jobs"})
|
42
|
+
end
|
45
43
|
|
46
|
-
|
47
|
-
job
|
44
|
+
test "delete removes job from the array" do
|
45
|
+
@array << {"job" => "one"}
|
46
|
+
job = @array.first
|
48
47
|
|
49
48
|
assert_equal( {"job" => "one"}, job.details)
|
50
49
|
|
51
|
-
assert_equal(1
|
52
|
-
array.delete(job)
|
53
|
-
assert_equal(0
|
50
|
+
assert_equal(1,@array.count)
|
51
|
+
@array.delete(job)
|
52
|
+
assert_equal(0,@array.count)
|
54
53
|
end
|
55
54
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
array << {"job" => "one"}
|
61
|
-
job = array.first
|
55
|
+
test "delete returns job after delete" do
|
56
|
+
@array << {"job" => "one"}
|
57
|
+
job = @array.first
|
62
58
|
|
63
59
|
assert_equal({"job" => "one"}, job.details)
|
64
60
|
|
65
|
-
res = array.delete(job)
|
61
|
+
res = @array.delete(job)
|
66
62
|
assert_equal({"job" => "one"}, res.details)
|
67
63
|
end
|
68
64
|
|
69
|
-
|
70
|
-
|
71
|
-
array
|
72
|
-
|
73
|
-
array << {"job" => "one"}
|
74
|
-
array << {"job" => "two"}
|
65
|
+
test "each yields the details for each job" do
|
66
|
+
@array << {"job" => "one"}
|
67
|
+
@array << {"job" => "two"}
|
75
68
|
results = []
|
76
|
-
array.each {|v| results << v.details}
|
69
|
+
@array.each {|v| results << v.details}
|
77
70
|
assert_equal([{"job" => "one"},{"job" => "two"}], results)
|
78
71
|
end
|
79
72
|
|
80
|
-
|
81
|
-
|
82
|
-
assert_equal "ryandotsmith",
|
83
|
-
assert_equal "localhost",
|
84
|
-
assert_equal "queue_classic_test",
|
73
|
+
test "connection build db connection from uri" do
|
74
|
+
a = QC::Database.new("postgres://ryandotsmith:@localhost/queue_classic_test")
|
75
|
+
assert_equal "ryandotsmith", a.connection.user
|
76
|
+
assert_equal "localhost", a.connection.host
|
77
|
+
assert_equal "queue_classic_test", a.connection.db
|
78
|
+
a.disconnect
|
85
79
|
end
|
86
80
|
|
87
|
-
|
88
|
-
array
|
89
|
-
|
90
|
-
|
91
|
-
assert_equal "ryandotsmith", array.connection.user
|
92
|
-
assert_equal "queue_classic_test", array.connection.db
|
81
|
+
test "seach" do
|
82
|
+
@array << {"job" => "A.signature"}
|
83
|
+
jobs = @array.search_details_column("A.signature")
|
84
|
+
assert_equal "A.signature", jobs.first.signature
|
93
85
|
end
|
94
86
|
|
87
|
+
test "seach when data will not match" do
|
88
|
+
@array << {"job" => "A.signature"}
|
89
|
+
jobs = @array.search_details_column("B.signature")
|
90
|
+
assert_equal [], jobs
|
91
|
+
end
|
95
92
|
end
|
data/test/helper.rb
CHANGED
@@ -1,22 +1,12 @@
|
|
1
1
|
$: << File.expand_path("lib")
|
2
2
|
$: << File.expand_path("test")
|
3
3
|
|
4
|
-
ENV["DATABASE_URL"] = 'queue_classic_test'
|
5
|
-
|
6
4
|
require 'queue_classic'
|
7
5
|
require 'database_helpers'
|
8
6
|
|
9
7
|
require 'minitest/unit'
|
10
8
|
MiniTest::Unit.autorun
|
11
9
|
|
12
|
-
def set_data_store(store=nil)
|
13
|
-
QC::Queue.instance.setup(
|
14
|
-
:data_store => (
|
15
|
-
store || QC::DurableArray.new(:database => ENV["DATABASE_URL"])
|
16
|
-
)
|
17
|
-
)
|
18
|
-
end
|
19
|
-
|
20
10
|
def context(*args, &block)
|
21
11
|
return super unless (name = args.first) && block
|
22
12
|
klass = Class.new(MiniTest::Unit::TestCase) do
|
data/test/job_test.rb
CHANGED
data/test/queue_test.rb
CHANGED
@@ -1,33 +1,60 @@
|
|
1
1
|
require File.expand_path("../helper.rb", __FILE__)
|
2
2
|
|
3
|
-
|
4
|
-
include DatabaseHelpers
|
3
|
+
context "Queue" do
|
5
4
|
|
6
|
-
|
7
|
-
|
5
|
+
setup { init_db }
|
6
|
+
|
7
|
+
test "Queue class responds to enqueue" do
|
8
|
+
QC::Queue.enqueue("Klass.method")
|
9
|
+
end
|
10
|
+
|
11
|
+
test "Queue class has a default table name" do
|
12
|
+
QC::Queue.enqueue("Klass.method")
|
13
|
+
end
|
14
|
+
|
15
|
+
test "Queue class responds to dequeue" do
|
16
|
+
QC::Queue.enqueue("Klass.method")
|
17
|
+
assert_equal "Klass.method", QC::Queue.dequeue.signature
|
8
18
|
end
|
9
19
|
|
10
|
-
|
11
|
-
QC::Queue.
|
12
|
-
|
20
|
+
test "Queue class responds to delete" do
|
21
|
+
QC::Queue.enqueue("Klass.method")
|
22
|
+
job = QC::Queue.dequeue
|
23
|
+
QC::Queue.delete(job)
|
13
24
|
end
|
14
25
|
|
15
|
-
|
16
|
-
QC::Queue.
|
17
|
-
QC::Queue.
|
26
|
+
test "Queue class responds to delete_all" do
|
27
|
+
2.times { QC::Queue.enqueue("Klass.method") }
|
28
|
+
job1,job2 = QC::Queue.dequeue, QC::Queue.dequeue
|
29
|
+
QC::Queue.delete_all
|
30
|
+
end
|
18
31
|
|
19
|
-
|
32
|
+
test "Queue class return the length of the queue" do
|
33
|
+
2.times { QC::Queue.enqueue("Klass.method") }
|
34
|
+
assert_equal 2, QC::Queue.length
|
20
35
|
end
|
21
36
|
|
22
|
-
|
23
|
-
QC::Queue.
|
37
|
+
test "Queue class finds jobs using query method" do
|
38
|
+
QC::Queue.enqueue("Something.hard_to_find")
|
39
|
+
jobs = QC::Queue.query("Something.hard_to_find")
|
40
|
+
assert_equal 1, jobs.length
|
41
|
+
assert_equal "Something.hard_to_find", jobs.first.signature
|
42
|
+
end
|
24
43
|
|
25
|
-
|
26
|
-
QC::Queue.
|
44
|
+
test "queue instance responds to enqueue" do
|
45
|
+
QC::Queue.enqueue("Something.hard_to_find")
|
46
|
+
init_db(:custom_queue_name)
|
47
|
+
@queue = QC::Queue.new(:custom_queue_name)
|
48
|
+
@queue.enqueue "Klass.method"
|
49
|
+
@queue.disconnect
|
50
|
+
end
|
27
51
|
|
28
|
-
|
29
|
-
QC::Queue.
|
30
|
-
|
52
|
+
test "queue only uses 1 connection per class" do
|
53
|
+
QC::Queue.length
|
54
|
+
QC::Queue.enqueue "Klass.method"
|
55
|
+
QC::Queue.delete QC::Queue.dequeue
|
56
|
+
QC::Queue.dequeue
|
57
|
+
assert_equal 1, QC.connection_status[:total]
|
31
58
|
end
|
32
59
|
|
33
60
|
end
|
data/test/worker_test.rb
CHANGED
@@ -6,7 +6,7 @@ class TestNotifier
|
|
6
6
|
end
|
7
7
|
|
8
8
|
# This not only allows me to test what happens
|
9
|
-
# when a failure
|
9
|
+
# when a failure occurs but it also demonstrates
|
10
10
|
# how to override the worker to handle failures the way
|
11
11
|
# you want.
|
12
12
|
class TestWorker < QC::Worker
|
@@ -20,31 +20,26 @@ class TestWorker < QC::Worker
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
include DatabaseHelpers
|
23
|
+
context "Worker" do
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
setup do
|
26
|
+
init_db
|
27
|
+
@worker = TestWorker.new
|
28
|
+
end
|
29
29
|
|
30
|
-
|
31
|
-
|
30
|
+
test "working a job" do
|
31
|
+
QC::Queue.enqueue "TestNotifier.deliver", {}
|
32
32
|
|
33
|
-
assert_equal(1, QC.
|
34
|
-
worker.work
|
35
|
-
assert_equal(0, QC.
|
36
|
-
assert_equal(0, worker.failed_count)
|
33
|
+
assert_equal(1, QC::Queue.length)
|
34
|
+
@worker.work
|
35
|
+
assert_equal(0, QC::Queue.length)
|
36
|
+
assert_equal(0, @worker.failed_count)
|
37
37
|
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
clean_database
|
42
|
-
|
43
|
-
QC.enqueue "TestNotifier.no_method", {}
|
44
|
-
worker = TestWorker.new
|
39
|
+
test "resuce failed job" do
|
40
|
+
QC::Queue.enqueue "TestNotifier.no_method", {}
|
45
41
|
|
46
|
-
worker.work
|
47
|
-
assert_equal 1, worker.failed_count
|
42
|
+
@worker.work
|
43
|
+
assert_equal 1, @worker.failed_count
|
48
44
|
end
|
49
|
-
|
50
45
|
end
|
metadata
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: queue_classic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 2
|
8
|
-
- 2
|
9
|
-
version: 0.2.2
|
4
|
+
prerelease:
|
5
|
+
version: 0.3.1
|
10
6
|
platform: ruby
|
11
7
|
authors:
|
12
8
|
- Ryan Smith
|
@@ -14,7 +10,7 @@ autorequire:
|
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
12
|
|
17
|
-
date: 2011-
|
13
|
+
date: 2011-04-26 00:00:00 -07:00
|
18
14
|
default_executable:
|
19
15
|
dependencies:
|
20
16
|
- !ruby/object:Gem::Dependency
|
@@ -25,10 +21,6 @@ dependencies:
|
|
25
21
|
requirements:
|
26
22
|
- - ">="
|
27
23
|
- !ruby/object:Gem::Version
|
28
|
-
segments:
|
29
|
-
- 0
|
30
|
-
- 10
|
31
|
-
- 1
|
32
24
|
version: 0.10.1
|
33
25
|
type: :runtime
|
34
26
|
version_requirements: *id001
|
@@ -40,12 +32,10 @@ dependencies:
|
|
40
32
|
requirements:
|
41
33
|
- - ">="
|
42
34
|
- !ruby/object:Gem::Version
|
43
|
-
segments:
|
44
|
-
- 0
|
45
35
|
version: "0"
|
46
36
|
type: :runtime
|
47
37
|
version_requirements: *id002
|
48
|
-
description: Queue Classic is
|
38
|
+
description: Queue Classic (beta) is a queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features asynchronous job polling, database maintained locks and no ridiculous dependencies. As a matter of fact, Queue Classic only requires the pg and json.
|
49
39
|
email: ryan@heroku.com
|
50
40
|
executables: []
|
51
41
|
|
@@ -54,15 +44,14 @@ extensions: []
|
|
54
44
|
extra_rdoc_files: []
|
55
45
|
|
56
46
|
files:
|
57
|
-
- readme.
|
58
|
-
- lib/queue_classic/
|
47
|
+
- readme.md
|
48
|
+
- lib/queue_classic/database.rb
|
59
49
|
- lib/queue_classic/durable_array.rb
|
60
50
|
- lib/queue_classic/job.rb
|
61
51
|
- lib/queue_classic/queue.rb
|
62
52
|
- lib/queue_classic/tasks.rb
|
63
53
|
- lib/queue_classic/worker.rb
|
64
54
|
- lib/queue_classic.rb
|
65
|
-
- test/api_test.rb
|
66
55
|
- test/database_helpers.rb
|
67
56
|
- test/durable_array_test.rb
|
68
57
|
- test/helper.rb
|
@@ -70,7 +59,7 @@ files:
|
|
70
59
|
- test/queue_test.rb
|
71
60
|
- test/worker_test.rb
|
72
61
|
has_rdoc: true
|
73
|
-
homepage: http://github.com/ryandotsmith/
|
62
|
+
homepage: http://github.com/ryandotsmith/queue_classic
|
74
63
|
licenses: []
|
75
64
|
|
76
65
|
post_install_message:
|
@@ -83,26 +72,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
72
|
requirements:
|
84
73
|
- - ">="
|
85
74
|
- !ruby/object:Gem::Version
|
86
|
-
segments:
|
87
|
-
- 0
|
88
75
|
version: "0"
|
89
76
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
77
|
none: false
|
91
78
|
requirements:
|
92
79
|
- - ">="
|
93
80
|
- !ruby/object:Gem::Version
|
94
|
-
segments:
|
95
|
-
- 0
|
96
81
|
version: "0"
|
97
82
|
requirements: []
|
98
83
|
|
99
84
|
rubyforge_project:
|
100
|
-
rubygems_version: 1.
|
85
|
+
rubygems_version: 1.6.2
|
101
86
|
signing_key:
|
102
87
|
specification_version: 3
|
103
|
-
summary: Queue Classic is
|
88
|
+
summary: Queue Classic (beta) is a queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features asynchronous job polling, database maintained locks and no ridiculous dependencies. As a matter of fact, Queue Classic only requires the pg and json.(simple)
|
104
89
|
test_files:
|
105
|
-
- test/api_test.rb
|
106
90
|
- test/durable_array_test.rb
|
107
91
|
- test/job_test.rb
|
108
92
|
- test/queue_test.rb
|
data/lib/queue_classic/api.rb
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
module QC
|
2
|
-
module Api
|
3
|
-
|
4
|
-
def queue
|
5
|
-
@queue ||= Queue.instance
|
6
|
-
end
|
7
|
-
|
8
|
-
def enqueue(job,*params)
|
9
|
-
if job.respond_to?(:details) and job.respond_to?(:params)
|
10
|
-
p = *job.params
|
11
|
-
queue.enqueue(job.signature, p)
|
12
|
-
else
|
13
|
-
queue.enqueue(job,params)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def dequeue
|
18
|
-
queue.dequeue
|
19
|
-
end
|
20
|
-
|
21
|
-
def delete(job)
|
22
|
-
queue.delete(job)
|
23
|
-
end
|
24
|
-
|
25
|
-
def delete_all
|
26
|
-
queue.delete_all
|
27
|
-
end
|
28
|
-
|
29
|
-
def queue_length
|
30
|
-
queue.length
|
31
|
-
end
|
32
|
-
|
33
|
-
def work(job)
|
34
|
-
klass = job.klass
|
35
|
-
method = job.method
|
36
|
-
params = job.params
|
37
|
-
|
38
|
-
if params.class == Array
|
39
|
-
klass.send(method,*params)
|
40
|
-
else
|
41
|
-
klass.send(method,params)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def logging_enabled?
|
46
|
-
ENV["LOGGING"]
|
47
|
-
end
|
48
|
-
|
49
|
-
end
|
50
|
-
end
|
data/readme.markdown
DELETED
@@ -1,218 +0,0 @@
|
|
1
|
-
# Queue Classic
|
2
|
-
__Beta 0.2.2__
|
3
|
-
|
4
|
-
__Queue Classic 0.2.2 is in Beta.__ I have been using this library with 30-50 Heroku workers and have had great results.
|
5
|
-
|
6
|
-
I am using this in production applications and plan to maintain and support this library for a long time.
|
7
|
-
|
8
|
-
Queue Classic is an alternative queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features a blocking dequeue, database maintained locks and
|
9
|
-
no ridiculous dependencies. As a matter of fact, Queue Classic only requires the __pg__ and __json__.
|
10
|
-
|
11
|
-
## Installation
|
12
|
-
|
13
|
-
### TL;DR
|
14
|
-
1. gem install queue_classic
|
15
|
-
2. add jobs table to your database
|
16
|
-
3. QC.enqueue "Class.method", :arg1 => val1
|
17
|
-
4. rake qc:work
|
18
|
-
|
19
|
-
### Dependencies
|
20
|
-
|
21
|
-
Postgres version 9
|
22
|
-
|
23
|
-
Ruby (gems: pg, json)
|
24
|
-
|
25
|
-
### Gem
|
26
|
-
|
27
|
-
gem install queue_classic
|
28
|
-
|
29
|
-
### Database
|
30
|
-
|
31
|
-
Queue Classic needs a database, so make sure that DATABASE_URL points to your database. If you are unsure about whether this var is set, run:
|
32
|
-
echo $DATABASE_URL
|
33
|
-
in your shell. If you are using Heroku, this var is set and pointing to your primary database.
|
34
|
-
|
35
|
-
Once your Database is set, you will need to add a jobs table. If you are using rails, add a migration with the following tables:
|
36
|
-
|
37
|
-
class CreateJobsTable < ActiveRecord::Migration
|
38
|
-
def self.up
|
39
|
-
create_table :jobs do |t|
|
40
|
-
t.text :details
|
41
|
-
t.timestamp :locked_at
|
42
|
-
t.index :id
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.down
|
47
|
-
drop_table :jobs
|
48
|
-
end
|
49
|
-
end
|
50
|
-
After running this migration, your database should be ready to go. As a sanity check, enqueue a job and then issue a SELECT in the postgres console.
|
51
|
-
|
52
|
-
Be sure and add the index to the id column. This will help out the worker if the queue should ever reach an obscene length. It made a huge difference
|
53
|
-
when running the benchmark.
|
54
|
-
|
55
|
-
__script/console__
|
56
|
-
QC.enqueue "Class.method"
|
57
|
-
__Terminal__
|
58
|
-
psql you_database_name
|
59
|
-
select * from jobs;
|
60
|
-
You should see the job "Class.method"
|
61
|
-
|
62
|
-
### Rakefile
|
63
|
-
|
64
|
-
As a convenience, I added a rake task that responds to `rake jobs:work` There are also rake tasks in the `qc` name space.
|
65
|
-
To get access to these tasks, Add `require 'queue_classic/tasks'` to your Rakefile.
|
66
|
-
|
67
|
-
## Fundamentals
|
68
|
-
|
69
|
-
### Enqueue
|
70
|
-
|
71
|
-
To place a job onto the queue, you should specify a class and a class method. There are a few ways to enqueue:
|
72
|
-
|
73
|
-
QC.enqueue('Class.method', :arg1 => 'value1', :arg2 => 'value2')
|
74
|
-
|
75
|
-
Requires:
|
76
|
-
|
77
|
-
class Class
|
78
|
-
def self.method(args)
|
79
|
-
puts args["arg1"]
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
QC.enqueue('Class.method', 'value1', 'value2')
|
84
|
-
|
85
|
-
Requires:
|
86
|
-
|
87
|
-
class Class
|
88
|
-
def self.method(arg1,arg2)
|
89
|
-
puts arg1
|
90
|
-
puts arg2
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
|
95
|
-
The job gets stored in the jobs table with a details field set to: `{ job: Class.method, params: {arg1: value1, arg2: value2}}` (as JSON)
|
96
|
-
Here is a more concrete example of a job implementation using a Rails ActiveRecord Model:
|
97
|
-
|
98
|
-
class Invoice < ActiveRecord::Base
|
99
|
-
def self.process(invoice_id)
|
100
|
-
invoice = find(invoice_id)
|
101
|
-
invoice.process!
|
102
|
-
end
|
103
|
-
|
104
|
-
def self.process_all
|
105
|
-
Invoice.all do |invoice|
|
106
|
-
QC.enqueue "Invoice.process", invoice.id
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
|
112
|
-
### Dequeue
|
113
|
-
|
114
|
-
Traditionally, a queue's dequeue operation will remove the item from the queue. However, Queue Classic will not delete the item from the queue right away; instead, the workers will lock
|
115
|
-
the job and then the worker will delete the job once it has finished working it. Queue Classic's greatest strength is it's ability to safely lock jobs. Unlike other
|
116
|
-
database backed queing libraries, Queue Classic uses the database time to lock. This allows you to be more relaxed about the time synchronization amongst your worker machines.
|
117
|
-
|
118
|
-
Queue Classic takes advantage of Postgres' PUB/SUB featuers to dequeue a job. Basically there is a channel in which the workers LISTEN. When a new job is added to the queue, the queue sends NOTIFY
|
119
|
-
messages on the channel. Once a NOTIFY is sent, each worker races to acquire a lock on a job. A job is awareded to the victor while the rest go back to wait for another job. This eliminates
|
120
|
-
the need to Sleep & Select.
|
121
|
-
|
122
|
-
### The Worker
|
123
|
-
|
124
|
-
The worker calls dequeue and then calls the enqueued method with the supplied arguments. Once the method terminates, the job is deleted from the queue. In the case that your method
|
125
|
-
does not terminate, or the worker unexpectingly dies, Queue Classic will do following:
|
126
|
-
|
127
|
-
* Rescue the Exception %
|
128
|
-
* Call handle_failure(job,exception)
|
129
|
-
* Delete the job
|
130
|
-
|
131
|
-
% - To my knowledge, the only thing that can usurp ensure is a segfault.
|
132
|
-
|
133
|
-
By default, handle_failure will puts the job and the exception. This is not very good and you should override this method. It is simple to do so.
|
134
|
-
If you are using Queue Classic with Rails, You should:
|
135
|
-
|
136
|
-
1. Remove require 'queue_classic/tasks' from Rakefile
|
137
|
-
2. Create new file in lib/tasks. Call it queue_classic.rb (name is arbitrary)
|
138
|
-
3. Insert something like the following:
|
139
|
-
|
140
|
-
require 'queue_classic'
|
141
|
-
|
142
|
-
class MyWorker < QC::Worker
|
143
|
-
def handle_failure(job,exception)
|
144
|
-
# You can do many things inside of this method. Here are a few examples:
|
145
|
-
|
146
|
-
# Log to Exceptional
|
147
|
-
Exceptional.handle(exception, "Background Job Failed" + job.inspect)
|
148
|
-
|
149
|
-
# Log to Hoptoad
|
150
|
-
HoptoadNotifier.notify(
|
151
|
-
:error_class => "Background Job",
|
152
|
-
:error_message => "Special Error: #{e.message}",
|
153
|
-
:parameters => job.details
|
154
|
-
)
|
155
|
-
|
156
|
-
# Log to STDOUT (Heroku Logplex listens to stdout)
|
157
|
-
puts job.inspect
|
158
|
-
puts exception.inspect
|
159
|
-
puts exception.backtrace
|
160
|
-
|
161
|
-
# Retry the job
|
162
|
-
QC.enqueue(job)
|
163
|
-
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
namespace :jobs do
|
168
|
-
task :work => :environment do
|
169
|
-
MyWorker.new.start
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
## Performance
|
174
|
-
I am pleased at the performance of Queue Classic. It ran 3x faster than the DJ. (I have yet to benchmark redis backed queues)
|
175
|
-
|
176
|
-
ruby benchmark.rb
|
177
|
-
user system total real
|
178
|
-
0.950000 0.620000 1.570000 ( 9.479941)
|
179
|
-
|
180
|
-
Hardware: Mac Book Pro 2.8 GHz Intel Core i7. SSD. 4 GB memory.
|
181
|
-
|
182
|
-
Software: Ruby 1.9.2-p0, PostgreSQL 9.0.2
|
183
|
-
|
184
|
-
It is fast because:
|
185
|
-
|
186
|
-
* I wrote my own SQL
|
187
|
-
* I do not create many Ruby Objects
|
188
|
-
* I do not call very many methods
|
189
|
-
|
190
|
-
## FAQ
|
191
|
-
|
192
|
-
How is this different than DJ?
|
193
|
-
> TL;DR = Store job as JSON (better introspection), Queue manages the time for locking jobs (workers can be out of sync.), No magic (less code), Small footprint (ORM Free).
|
194
|
-
|
195
|
-
> __Introspection__ I want the data in the queue to be as simple as possible. Since we only store the Class, Method and Args, introspection into the queue is
|
196
|
-
quite simple.
|
197
|
-
|
198
|
-
> __Locking__ You might have noticed that DJ's worker calls Time.now(). In a cloud environment, this could allow for workers to be confused about
|
199
|
-
the status of a job. Classic Queue locks a job using Postgres' TIMESTAMP function.
|
200
|
-
|
201
|
-
> __Magic__ I find disdain for methods on my objects that have nothing to do with the purpose of the object. Methods like "should" and "delay"
|
202
|
-
are quite distasteful and obscure what is actually going on. If you use TestUnit for this reason, you might like Queue Classic. Anyway, I think
|
203
|
-
the fundamental concept of a message queue is not that difficult to grasp; therefore, I have taken the time to make Queue Classic as transparent as possilbe.
|
204
|
-
|
205
|
-
> __Footprint__ You don't need ActiveRecord or any other ORM to find the head or add to the tail. Take a look at the DurableArray class to see the SQL Classic Queue employees.
|
206
|
-
|
207
|
-
Why doesn't your queue retry failed jobs?
|
208
|
-
> I believe the Class method should handle any sort of exception. Also, I think
|
209
|
-
that the model you are working on should know about it's state. For instance, if you are
|
210
|
-
creating jobs for the emailing of newsletters; put a emailed_at column on your newsletter model
|
211
|
-
and then right before the job quits, touch the emailed_at column. That being said, you can do whatever you
|
212
|
-
want in handle_failure. I will not decide what is best for your application.
|
213
|
-
|
214
|
-
Can I use this library with 50 Heroku Workers?
|
215
|
-
> Yes.
|
216
|
-
|
217
|
-
Is Queue Classic ready for production? Can I do it live?!?
|
218
|
-
> I started this project on 1/24/2011. I have been using this in production for some high-traffic apps at Heroku since 2/24/2011.
|
data/test/api_test.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
require File.expand_path("../helper.rb", __FILE__)
|
2
|
-
|
3
|
-
context "QC::Api" do
|
4
|
-
setup { clean_database }
|
5
|
-
|
6
|
-
test "enqueue takes a job without params" do
|
7
|
-
QC.enqueue "Notifier.send"
|
8
|
-
|
9
|
-
job = QC.dequeue
|
10
|
-
assert_equal({"job" => "Notifier.send", "params" => [] }, job.details)
|
11
|
-
end
|
12
|
-
|
13
|
-
test "enqueue takes a hash" do
|
14
|
-
QC.enqueue "Notifier.send", {:arg => 1}
|
15
|
-
|
16
|
-
job = QC.dequeue
|
17
|
-
assert_equal({"job" => "Notifier.send", "params" => [{"arg" => 1}] }, job.details)
|
18
|
-
end
|
19
|
-
|
20
|
-
test "enqueue takes a job" do
|
21
|
-
h = {"id" => 1, "details" => {"job" => 'Notifier.send'}.to_json, "locked_at" => nil}
|
22
|
-
job = QC::Job.new(h)
|
23
|
-
QC.enqueue(job)
|
24
|
-
|
25
|
-
job = QC.dequeue
|
26
|
-
assert_equal({"job" => "Notifier.send", "params" => []}, job.details)
|
27
|
-
end
|
28
|
-
|
29
|
-
test "enqueue takes a job and maintain params" do
|
30
|
-
h = {"id" => 1, "details" => {"job" => 'Notifier.send', "params" => ["1"]}.to_json, "locked_at" => nil}
|
31
|
-
job = QC::Job.new(h)
|
32
|
-
QC.enqueue(job)
|
33
|
-
|
34
|
-
job = QC.dequeue
|
35
|
-
assert_equal({"job" => "Notifier.send", "params" => ["1"]}, job.details)
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
end
|
40
|
-
|