queue_classic 1.0.2 → 2.0.0rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/queue_classic.rb +67 -8
- data/lib/queue_classic/conn.rb +97 -0
- data/lib/queue_classic/okjson.rb +15 -40
- data/lib/queue_classic/queries.rb +56 -0
- data/lib/queue_classic/queue.rb +14 -49
- data/lib/queue_classic/tasks.rb +25 -14
- data/lib/queue_classic/worker.rb +37 -23
- data/readme.md +520 -31
- data/test/helper.rb +21 -17
- data/test/queue_test.rb +29 -60
- data/test/worker_test.rb +57 -25
- metadata +10 -19
- data/lib/queue_classic/database.rb +0 -188
- data/lib/queue_classic/durable_array.rb +0 -51
- data/lib/queue_classic/job.rb +0 -42
- data/lib/queue_classic/logger.rb +0 -17
- data/test/database_helpers.rb +0 -13
- data/test/database_test.rb +0 -73
- data/test/durable_array_test.rb +0 -94
- data/test/job_test.rb +0 -82
data/test/helper.rb
CHANGED
@@ -1,26 +1,30 @@
|
|
1
1
|
$: << File.expand_path("lib")
|
2
2
|
$: << File.expand_path("test")
|
3
3
|
|
4
|
-
ENV[
|
4
|
+
ENV["DATABASE_URL"] ||= "postgres:///queue_classic_test"
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
8
|
-
|
9
|
-
require 'minitest/unit'
|
6
|
+
require "queue_classic"
|
7
|
+
require "minitest/unit"
|
10
8
|
MiniTest::Unit.autorun
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
10
|
+
QC::Log.level = Logger::ERROR
|
11
|
+
|
12
|
+
class QCTest < MiniTest::Unit::TestCase
|
13
|
+
|
14
|
+
def setup
|
15
|
+
init_db
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
QC.delete_all
|
20
|
+
end
|
21
|
+
|
22
|
+
def init_db(table_name="queue_classic_jobs")
|
23
|
+
QC::Conn.execute("SET client_min_messages TO 'warning'")
|
24
|
+
QC::Conn.execute("DROP TABLE IF EXISTS #{table_name} CASCADE")
|
25
|
+
QC::Conn.execute("CREATE TABLE #{table_name} (id serial, q_name varchar(255), method varchar(255), args text, locked_at timestamptz)")
|
26
|
+
QC::Queries.load_functions
|
27
|
+
QC::Conn.disconnect
|
21
28
|
end
|
22
|
-
(class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') }
|
23
29
|
|
24
|
-
klass.send :include, DatabaseHelpers
|
25
|
-
klass.class_eval &block
|
26
30
|
end
|
data/test/queue_test.rb
CHANGED
@@ -1,78 +1,47 @@
|
|
1
1
|
require File.expand_path("../helper.rb", __FILE__)
|
2
|
-
require 'ostruct'
|
3
2
|
|
4
|
-
|
3
|
+
class QueueTest < QCTest
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
test "Queue class responds to enqueue" do
|
9
|
-
QC::Queue.enqueue("Klass.method")
|
10
|
-
end
|
11
|
-
|
12
|
-
test "Queue class has a default table name" do
|
13
|
-
default_table_name = QC::Database.new.table_name
|
14
|
-
assert_equal default_table_name, QC::Queue.database.table_name
|
15
|
-
end
|
16
|
-
|
17
|
-
test "Queue class responds to dequeue" do
|
18
|
-
QC::Queue.enqueue("Klass.method")
|
19
|
-
assert_equal "Klass.method", QC::Queue.dequeue.signature
|
5
|
+
def test_enqueue
|
6
|
+
QC.enqueue("Klass.method")
|
20
7
|
end
|
21
8
|
|
22
|
-
|
23
|
-
QC
|
24
|
-
|
25
|
-
QC
|
9
|
+
def test_lock
|
10
|
+
QC.enqueue("Klass.method")
|
11
|
+
expected = {:id=>"1", :method=>"Klass.method", :args=>[]}
|
12
|
+
assert_equal(expected, QC.lock)
|
26
13
|
end
|
27
14
|
|
28
|
-
|
29
|
-
|
30
|
-
job1,job2 = QC::Queue.dequeue, QC::Queue.dequeue
|
31
|
-
QC::Queue.delete_all
|
15
|
+
def test_lock_when_empty
|
16
|
+
assert_nil(QC.lock)
|
32
17
|
end
|
33
18
|
|
34
|
-
|
35
|
-
|
36
|
-
assert_equal
|
19
|
+
def test_count
|
20
|
+
QC.enqueue("Klass.method")
|
21
|
+
assert_equal(1, QC.count)
|
37
22
|
end
|
38
23
|
|
39
|
-
|
40
|
-
QC
|
41
|
-
|
42
|
-
|
43
|
-
assert_equal
|
24
|
+
def test_delete
|
25
|
+
QC.enqueue("Klass.method")
|
26
|
+
assert_equal(1, QC.count)
|
27
|
+
QC.delete(QC.lock[:id])
|
28
|
+
assert_equal(0, QC.count)
|
44
29
|
end
|
45
30
|
|
46
|
-
|
47
|
-
QC
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
31
|
+
def test_delete_all
|
32
|
+
QC.enqueue("Klass.method")
|
33
|
+
QC.enqueue("Klass.method")
|
34
|
+
assert_equal(2, QC.count)
|
35
|
+
QC.delete_all
|
36
|
+
assert_equal(0, QC.count)
|
52
37
|
end
|
53
38
|
|
54
|
-
|
55
|
-
QC::Queue.
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
assert_equal 1, @database.execute("SELECT count(*) from pg_stat_activity")[0]["count"].to_i
|
39
|
+
def test_queue_instance
|
40
|
+
queue = QC::Queue.new("queue_classic_jobs", false)
|
41
|
+
queue.enqueue("Klass.method")
|
42
|
+
assert_equal(1, queue.count)
|
43
|
+
queue.delete(queue.lock[:id])
|
44
|
+
assert_equal(0, queue.count)
|
61
45
|
end
|
62
46
|
|
63
|
-
test "Queue class enqueues a job" do
|
64
|
-
job = OpenStruct.new :signature => 'Klass.method', :params => ['param']
|
65
|
-
QC::Queue.enqueue(job)
|
66
|
-
dequeued_job = QC::Queue.dequeue
|
67
|
-
assert_equal "Klass.method", dequeued_job.signature
|
68
|
-
assert_equal 'param', dequeued_job.params
|
69
|
-
end
|
70
|
-
|
71
|
-
test "Queues have their own array" do
|
72
|
-
refute_equal(Class.new(QC::Queue).array, Class.new(QC::Queue).array)
|
73
|
-
end
|
74
|
-
|
75
|
-
test "Queues have their own database" do
|
76
|
-
refute_equal(Class.new(QC::Queue).database, Class.new(QC::Queue).database)
|
77
|
-
end
|
78
47
|
end
|
data/test/worker_test.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
require File.expand_path("../helper.rb", __FILE__)
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
end
|
3
|
+
module TestObject
|
4
|
+
extend self
|
5
|
+
def no_args; return nil; end
|
6
|
+
def one_arg(a); return a; end
|
7
|
+
def two_args(a,b); return [a,b]; end
|
6
8
|
end
|
7
9
|
|
8
10
|
# This not only allows me to test what happens
|
@@ -11,47 +13,77 @@ end
|
|
11
13
|
# you want.
|
12
14
|
class TestWorker < QC::Worker
|
13
15
|
attr_accessor :failed_count
|
14
|
-
|
16
|
+
|
17
|
+
def initialize(*args)
|
18
|
+
super(*args)
|
15
19
|
@failed_count = 0
|
16
|
-
super
|
17
20
|
end
|
21
|
+
|
18
22
|
def handle_failure(job,e)
|
19
23
|
@failed_count += 1
|
20
24
|
end
|
21
25
|
end
|
22
26
|
|
23
|
-
|
27
|
+
class WorkerTest < QCTest
|
24
28
|
|
25
|
-
|
26
|
-
|
27
|
-
|
29
|
+
def test_work
|
30
|
+
QC.enqueue("TestObject.no_args")
|
31
|
+
worker = TestWorker.new("default", 1, false, false, 1)
|
32
|
+
assert_equal(1, QC.count)
|
33
|
+
worker.work
|
34
|
+
assert_equal(0, QC.count)
|
35
|
+
assert_equal(0, worker.failed_count)
|
28
36
|
end
|
29
37
|
|
30
|
-
|
31
|
-
|
38
|
+
def test_failed_job
|
39
|
+
QC.enqueue("TestObject.not_a_method")
|
40
|
+
worker = TestWorker.new("default", 1, false, false, 1)
|
41
|
+
worker.work
|
42
|
+
assert_equal(1, worker.failed_count)
|
32
43
|
end
|
33
44
|
|
34
|
-
|
35
|
-
QC
|
45
|
+
def test_work_with_no_args
|
46
|
+
QC.enqueue("TestObject.no_args")
|
47
|
+
worker = TestWorker.new("default", 1, false, false, 1)
|
48
|
+
r = worker.work
|
49
|
+
assert_nil(r)
|
50
|
+
assert_equal(0, worker.failed_count)
|
51
|
+
end
|
36
52
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
53
|
+
def test_work_with_one_arg
|
54
|
+
QC.enqueue("TestObject.one_arg", "1")
|
55
|
+
worker = TestWorker.new("default", 1, false, false, 1)
|
56
|
+
r = worker.work
|
57
|
+
assert_equal("1", r)
|
58
|
+
assert_equal(0, worker.failed_count)
|
41
59
|
end
|
42
60
|
|
43
|
-
|
44
|
-
QC
|
61
|
+
def test_work_with_two_args
|
62
|
+
QC.enqueue("TestObject.two_args", "1", 2)
|
63
|
+
worker = TestWorker.new("default", 1, false, false, 1)
|
64
|
+
r = worker.work
|
65
|
+
assert_equal(["1", 2], r)
|
66
|
+
assert_equal(0, worker.failed_count)
|
67
|
+
end
|
45
68
|
|
46
|
-
|
47
|
-
|
69
|
+
def test_work_custom_queue
|
70
|
+
p_queue = QC::Queue.new("priority_queue")
|
71
|
+
p_queue.enqueue("TestObject.two_args", "1", 2)
|
72
|
+
worker = TestWorker.new("priority_queue", 1, false, false, 1)
|
73
|
+
r = worker.work
|
74
|
+
assert_equal(["1", 2], r)
|
75
|
+
assert_equal(0, worker.failed_count)
|
48
76
|
end
|
49
77
|
|
50
|
-
|
51
|
-
QC.enqueue
|
52
|
-
|
53
|
-
|
78
|
+
def test_worker_ueses_one_conn
|
79
|
+
QC.enqueue("TestObject.no_args")
|
80
|
+
worker = TestWorker.new("default", 1, false, false, 1)
|
81
|
+
worker.work
|
82
|
+
assert_equal(
|
83
|
+
1,
|
84
|
+
QC::Conn.execute("SELECT count(*) from pg_stat_activity")["count"].to_i,
|
54
85
|
"Multiple connections -- Are there other connections in other terminals?"
|
86
|
+
)
|
55
87
|
end
|
56
88
|
|
57
89
|
end
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: queue_classic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
5
|
-
prerelease:
|
4
|
+
version: 2.0.0rc1
|
5
|
+
prerelease: 5
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Ryan Smith
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-02-29 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: pg
|
16
|
-
requirement: &
|
16
|
+
requirement: &18736220 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.
|
21
|
+
version: 0.13.2
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *18736220
|
25
25
|
description: queue_classic is a queueing library for Ruby apps. (Rails, Sinatra, Etc...)
|
26
26
|
queue_classic features asynchronous job polling, database maintained locks and no
|
27
27
|
ridiculous dependencies. As a matter of fact, queue_classic only requires pg.
|
@@ -31,21 +31,15 @@ extensions: []
|
|
31
31
|
extra_rdoc_files: []
|
32
32
|
files:
|
33
33
|
- readme.md
|
34
|
+
- lib/queue_classic/conn.rb
|
34
35
|
- lib/queue_classic/okjson.rb
|
35
|
-
- lib/queue_classic/logger.rb
|
36
36
|
- lib/queue_classic/worker.rb
|
37
|
-
- lib/queue_classic/database.rb
|
38
|
-
- lib/queue_classic/job.rb
|
39
37
|
- lib/queue_classic/queue.rb
|
40
38
|
- lib/queue_classic/tasks.rb
|
41
|
-
- lib/queue_classic/
|
39
|
+
- lib/queue_classic/queries.rb
|
42
40
|
- lib/queue_classic.rb
|
43
|
-
- test/database_test.rb
|
44
|
-
- test/job_test.rb
|
45
|
-
- test/database_helpers.rb
|
46
41
|
- test/worker_test.rb
|
47
42
|
- test/queue_test.rb
|
48
|
-
- test/durable_array_test.rb
|
49
43
|
- test/helper.rb
|
50
44
|
homepage: http://github.com/ryandotsmith/queue_classic
|
51
45
|
licenses: []
|
@@ -62,9 +56,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
56
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
57
|
none: false
|
64
58
|
requirements:
|
65
|
-
- - ! '
|
59
|
+
- - ! '>'
|
66
60
|
- !ruby/object:Gem::Version
|
67
|
-
version:
|
61
|
+
version: 1.3.1
|
68
62
|
requirements: []
|
69
63
|
rubyforge_project:
|
70
64
|
rubygems_version: 1.8.10
|
@@ -72,8 +66,5 @@ signing_key:
|
|
72
66
|
specification_version: 3
|
73
67
|
summary: postgres backed queue
|
74
68
|
test_files:
|
75
|
-
- test/database_test.rb
|
76
|
-
- test/job_test.rb
|
77
69
|
- test/worker_test.rb
|
78
70
|
- test/queue_test.rb
|
79
|
-
- test/durable_array_test.rb
|
@@ -1,188 +0,0 @@
|
|
1
|
-
module QC
|
2
|
-
class Database
|
3
|
-
|
4
|
-
@@connection = nil
|
5
|
-
|
6
|
-
attr_reader :table_name
|
7
|
-
attr_reader :top_boundary
|
8
|
-
|
9
|
-
def initialize(queue_name=nil)
|
10
|
-
log("initialized")
|
11
|
-
|
12
|
-
@top_boundary = (ENV["QC_TOP_BOUND"] || 9).to_i
|
13
|
-
log("top_boundary=#{@top_boundary}")
|
14
|
-
|
15
|
-
@table_name = queue_name || "queue_classic_jobs"
|
16
|
-
log("table_name=#{@table_name}")
|
17
|
-
|
18
|
-
@channel_name = @table_name
|
19
|
-
log("channel_name=#{@channel_name}")
|
20
|
-
|
21
|
-
db_url = (ENV["QC_DATABASE_URL"] || ENV["DATABASE_URL"])
|
22
|
-
@db_params = URI.parse(db_url)
|
23
|
-
log("uri=#{db_url}")
|
24
|
-
end
|
25
|
-
|
26
|
-
def set_application_name
|
27
|
-
execute("SET application_name = 'queue_classic'")
|
28
|
-
end
|
29
|
-
|
30
|
-
def escape(string)
|
31
|
-
connection.escape(string)
|
32
|
-
end
|
33
|
-
|
34
|
-
def notify
|
35
|
-
log("NOTIFY")
|
36
|
-
execute("NOTIFY #{@channel_name}")
|
37
|
-
end
|
38
|
-
|
39
|
-
def listen
|
40
|
-
log("LISTEN")
|
41
|
-
execute("LISTEN #{@channel_name}")
|
42
|
-
end
|
43
|
-
|
44
|
-
def unlisten
|
45
|
-
log("UNLISTEN")
|
46
|
-
execute("UNLISTEN #{@channel_name}")
|
47
|
-
end
|
48
|
-
|
49
|
-
def drain_notify
|
50
|
-
until connection.notifies.nil?
|
51
|
-
log("draining notifications")
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def wait_for_notify(t)
|
56
|
-
log("waiting for notify timeout=#{t}")
|
57
|
-
connection.wait_for_notify(t) {|event, pid, msg| log("received notification #{event}")}
|
58
|
-
log("done waiting for notify")
|
59
|
-
end
|
60
|
-
|
61
|
-
def transaction
|
62
|
-
begin
|
63
|
-
execute 'BEGIN'
|
64
|
-
yield
|
65
|
-
execute 'COMMIT'
|
66
|
-
rescue Exception
|
67
|
-
execute 'ROLLBACK'
|
68
|
-
raise
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def transaction_idle?
|
73
|
-
connection.transaction_status == PGconn::PQTRANS_IDLE
|
74
|
-
end
|
75
|
-
|
76
|
-
def execute(sql, *params)
|
77
|
-
log("executing #{sql.inspect}, #{params.inspect}")
|
78
|
-
begin
|
79
|
-
params = nil if params.empty?
|
80
|
-
connection.exec(sql, params)
|
81
|
-
rescue PGError => e
|
82
|
-
log("execute exception=#{e.inspect}")
|
83
|
-
raise
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def connection
|
88
|
-
@@connection ||= connect
|
89
|
-
end
|
90
|
-
|
91
|
-
def disconnect
|
92
|
-
connection.finish
|
93
|
-
@@connection = nil
|
94
|
-
end
|
95
|
-
|
96
|
-
def connect
|
97
|
-
log("establishing connection")
|
98
|
-
conn = PGconn.connect(
|
99
|
-
@db_params.host,
|
100
|
-
@db_params.port || 5432,
|
101
|
-
nil, '', #opts, tty
|
102
|
-
@db_params.path.gsub("/",""), # database name
|
103
|
-
@db_params.user,
|
104
|
-
@db_params.password
|
105
|
-
)
|
106
|
-
if conn.status != PGconn::CONNECTION_OK
|
107
|
-
log("connection error=#{conn.error}")
|
108
|
-
end
|
109
|
-
conn
|
110
|
-
end
|
111
|
-
|
112
|
-
def load_functions
|
113
|
-
execute(<<-EOD)
|
114
|
-
-- We are declaring the return type to be queue_classic_jobs.
|
115
|
-
-- This is ok since I am assuming that all of the users added queues will
|
116
|
-
-- have identical columns to queue_classic_jobs.
|
117
|
-
-- When QC supports queues with columns other than the default, we will have to change this.
|
118
|
-
|
119
|
-
CREATE OR REPLACE FUNCTION lock_head(tname name, top_boundary integer) RETURNS SETOF queue_classic_jobs AS $$
|
120
|
-
DECLARE
|
121
|
-
unlocked integer;
|
122
|
-
relative_top integer;
|
123
|
-
job_count integer;
|
124
|
-
BEGIN
|
125
|
-
-- The purpose is to release contention for the first spot in the table.
|
126
|
-
-- The select count(*) is going to slow down dequeue performance but allow
|
127
|
-
-- for more workers. Would love to see some optimization here...
|
128
|
-
|
129
|
-
EXECUTE 'SELECT count(*) FROM ' ||
|
130
|
-
'(SELECT * FROM ' || quote_ident(tname) ||
|
131
|
-
' LIMIT ' || quote_literal(top_boundary) || ') limited'
|
132
|
-
INTO job_count;
|
133
|
-
|
134
|
-
SELECT TRUNC(random() * top_boundary + 1) INTO relative_top;
|
135
|
-
IF job_count < top_boundary THEN
|
136
|
-
relative_top = 0;
|
137
|
-
END IF;
|
138
|
-
|
139
|
-
LOOP
|
140
|
-
BEGIN
|
141
|
-
EXECUTE 'SELECT id FROM '
|
142
|
-
|| quote_ident(tname)
|
143
|
-
|| ' WHERE locked_at IS NULL'
|
144
|
-
|| ' ORDER BY id ASC'
|
145
|
-
|| ' LIMIT 1'
|
146
|
-
|| ' OFFSET ' || quote_literal(relative_top)
|
147
|
-
|| ' FOR UPDATE NOWAIT'
|
148
|
-
INTO unlocked;
|
149
|
-
EXIT;
|
150
|
-
EXCEPTION
|
151
|
-
WHEN lock_not_available THEN
|
152
|
-
-- do nothing. loop again and hope we get a lock
|
153
|
-
END;
|
154
|
-
END LOOP;
|
155
|
-
|
156
|
-
RETURN QUERY EXECUTE 'UPDATE '
|
157
|
-
|| quote_ident(tname)
|
158
|
-
|| ' SET locked_at = (CURRENT_TIMESTAMP)'
|
159
|
-
|| ' WHERE id = $1'
|
160
|
-
|| ' AND locked_at is NULL'
|
161
|
-
|| ' RETURNING *'
|
162
|
-
USING unlocked;
|
163
|
-
|
164
|
-
RETURN;
|
165
|
-
END;
|
166
|
-
$$ LANGUAGE plpgsql;
|
167
|
-
|
168
|
-
CREATE OR REPLACE FUNCTION lock_head(tname varchar) RETURNS SETOF queue_classic_jobs AS $$
|
169
|
-
BEGIN
|
170
|
-
RETURN QUERY EXECUTE 'SELECT * FROM lock_head($1,10)' USING tname;
|
171
|
-
END;
|
172
|
-
$$ LANGUAGE plpgsql;
|
173
|
-
EOD
|
174
|
-
end
|
175
|
-
|
176
|
-
def unload_functions
|
177
|
-
execute(<<-EOD)
|
178
|
-
DROP FUNCTION IF EXISTS lock_head(tname varchar);
|
179
|
-
DROP FUNCTION IF EXISTS lock_head(tname name, top_boundary integer);
|
180
|
-
EOD
|
181
|
-
end
|
182
|
-
|
183
|
-
def log(msg)
|
184
|
-
Logger.puts(["database", msg].join(" "))
|
185
|
-
end
|
186
|
-
|
187
|
-
end
|
188
|
-
end
|