queue_classic 1.0.2 → 2.0.0rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|