queue_classic 3.0.0beta → 3.0.0rc
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/queue_classic.rb +1 -1
- data/lib/queue_classic/queue.rb +22 -0
- data/lib/queue_classic/tasks.rb +15 -2
- data/lib/queue_classic/worker.rb +55 -28
- data/readme.md +21 -25
- data/test/worker_test.rb +9 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 549f3341849e84bc2068f82448e39e98c0ef2051
|
4
|
+
data.tar.gz: 0746db47bbe386bd31e348b9d925985e8b185c04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d9d9f8e660d294671ef3fd6d99f96fa2e092049920f72b754c62fa59857466978e6a7f10544fd35de9e52e6d72d99fb51dadb08cc2c0b8b002906f6628fe672
|
7
|
+
data.tar.gz: 3174b46493945c6b126323fa20dbaa1bf53bfd40a6ff07926a934ea4820e5e67276331d872d77e182fc57127e590f916a2042b48df9e9bee63965cd4d0f20f26
|
data/lib/queue_classic.rb
CHANGED
@@ -18,7 +18,7 @@ module QC
|
|
18
18
|
# notes the queue. You can point your workers
|
19
19
|
# at different queues but only one at a time.
|
20
20
|
QUEUE = ENV["QUEUE"] || "default"
|
21
|
-
QUEUES = ENV["QUEUES"] || []
|
21
|
+
QUEUES = (ENV["QUEUES"] && ENV["QUEUES"].split(",")) || []
|
22
22
|
|
23
23
|
# Set this to 1 for strict FIFO.
|
24
24
|
# There is nothing special about 9....
|
data/lib/queue_classic/queue.rb
CHANGED
@@ -3,6 +3,7 @@ require 'queue_classic/conn_adapter'
|
|
3
3
|
require 'json'
|
4
4
|
|
5
5
|
module QC
|
6
|
+
# The queue class maps a queue abstraction onto a database table.
|
6
7
|
class Queue
|
7
8
|
|
8
9
|
attr_reader :name, :top_bound
|
@@ -19,6 +20,20 @@ module QC
|
|
19
20
|
@adapter ||= QC.default_conn_adapter
|
20
21
|
end
|
21
22
|
|
23
|
+
# enqueue(m,a) inserts a row into the jobs table and trigger a notification.
|
24
|
+
# The job's queue is represented by a name column in the row.
|
25
|
+
# There is a trigger on the table which will send a NOTIFY event
|
26
|
+
# on a channel which corresponds to the name of the queue.
|
27
|
+
# The method argument is a string encoded ruby expression. The expression
|
28
|
+
# will be separated by a `.` character and then `eval`d.
|
29
|
+
# Examples of the method argument include: `puts`, `Kernel.puts`,
|
30
|
+
# `MyObject.new.puts`.
|
31
|
+
# The args argument will be encoded as JSON and stored as a JSON datatype
|
32
|
+
# in the row. (If the version of PG does not support JSON,
|
33
|
+
# then the args will be stored as text.
|
34
|
+
# The args are stored as a collection and then splatted inside the worker.
|
35
|
+
# Examples of args include: `'hello world'`, `['hello world']`,
|
36
|
+
# `'hello', 'world'`.
|
22
37
|
def enqueue(method, *args)
|
23
38
|
QC.log_yield(:measure => 'queue.enqueue') do
|
24
39
|
s="INSERT INTO #{TABLE_NAME} (q_name, method, args) VALUES ($1, $2, $3)"
|
@@ -37,6 +52,13 @@ module QC
|
|
37
52
|
end
|
38
53
|
end
|
39
54
|
|
55
|
+
def unlock(id)
|
56
|
+
QC.log_yield(:measure => 'queue.unlock') do
|
57
|
+
s = "UPDATE #{TABLE_NAME} set locked_at = null where id = $1"
|
58
|
+
conn_adapter.execute(s, id)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
40
62
|
def delete(id)
|
41
63
|
QC.log_yield(:measure => 'queue.delete') do
|
42
64
|
conn_adapter.execute("DELETE FROM #{TABLE_NAME} where id = $1", id)
|
data/lib/queue_classic/tasks.rb
CHANGED
@@ -8,9 +8,22 @@ end
|
|
8
8
|
namespace :qc do
|
9
9
|
desc "Start a new worker for the (default or $QUEUE) queue"
|
10
10
|
task :work => :environment do
|
11
|
-
trap('INT') {exit}
|
12
|
-
trap('TERM') {@worker.stop}
|
13
11
|
@worker = QC::Worker.new
|
12
|
+
|
13
|
+
trap('INT') do
|
14
|
+
$stderr.puts("Received INT. Shutting down.")
|
15
|
+
if !@worker.running
|
16
|
+
$stderr.puts("Worker has stopped running. Exit.")
|
17
|
+
exit(1)
|
18
|
+
end
|
19
|
+
@worker.stop
|
20
|
+
end
|
21
|
+
|
22
|
+
trap('TERM') do
|
23
|
+
$stderr.puts("Received Term. Shutting down.")
|
24
|
+
@worker.stop
|
25
|
+
end
|
26
|
+
|
14
27
|
@worker.start
|
15
28
|
end
|
16
29
|
|
data/lib/queue_classic/worker.rb
CHANGED
@@ -3,47 +3,63 @@ require 'queue_classic/queue'
|
|
3
3
|
require 'queue_classic/conn_adapter'
|
4
4
|
|
5
5
|
module QC
|
6
|
+
# A Worker object can process jobs from one or many queues.
|
6
7
|
class Worker
|
7
8
|
|
8
9
|
attr_accessor :queues, :running
|
9
|
-
|
10
|
-
#
|
10
|
+
|
11
|
+
# Creates a new worker but does not start the worker. See Worker#start.
|
12
|
+
# This method takes a single hash argument. The following keys are read:
|
13
|
+
# fork_worker:: Worker forks each job execution.
|
14
|
+
# wait_interval:: Time to wait between failed lock attempts
|
15
|
+
# connection:: PGConn object.
|
16
|
+
# q_name:: Name of a single queue to process.
|
17
|
+
# q_names:: Names of queues to process. Will process left to right.
|
18
|
+
# top_bound:: Offset to the head of the queue. 1 == strict FIFO.
|
11
19
|
def initialize(args={})
|
12
20
|
@fork_worker = args[:fork_worker] || QC::FORK_WORKER
|
13
21
|
@wait_interval = args[:wait_interval] || QC::WAIT_TIME
|
14
22
|
@conn_adapter = ConnAdapter.new(args[:connection])
|
15
23
|
@queues = setup_queues(@conn_adapter,
|
16
|
-
args[:q_name]
|
24
|
+
(args[:q_name] || QC::QUEUE),
|
25
|
+
(args[:q_names] || QC::QUEUES),
|
26
|
+
(args[:top_bound] || QC::TOP_BOUND))
|
17
27
|
log(args.merge(:at => "worker_initialized"))
|
18
28
|
@running = true
|
19
29
|
end
|
20
30
|
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# This is the
|
31
|
+
# Commences the working of jobs.
|
32
|
+
# start() spins on @running –which is initialized as true.
|
33
|
+
# This method is the primary entry point to starting the worker.
|
34
|
+
# The canonical example of starting a worker is as follows:
|
35
|
+
# QC::Worker.new.start
|
24
36
|
def start
|
25
37
|
while @running
|
26
38
|
@fork_worker ? fork_and_work : work
|
27
39
|
end
|
28
40
|
end
|
29
41
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
42
|
+
# Signals the worker to stop taking new work.
|
43
|
+
# This method has no immediate effect. However, there are
|
44
|
+
# two loops in the worker (one in #start and another in #lock_job)
|
45
|
+
# which check the @running variable to determine if further progress
|
46
|
+
# is desirable. In the case that @running is false, the aforementioned
|
47
|
+
# methods will short circuit and cause the blocking call to #start
|
48
|
+
# to unblock.
|
33
49
|
def stop
|
34
50
|
@running = false
|
35
51
|
end
|
36
52
|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
# Using setup_child is good for re-establishing database connections.
|
53
|
+
# Calls Worker#work but after the current process is forked.
|
54
|
+
# The parent process will wait on the child process to exit.
|
40
55
|
def fork_and_work
|
41
56
|
cpid = fork {setup_child; work}
|
42
57
|
log(:at => :fork, :pid => cpid)
|
43
58
|
Process.wait(cpid)
|
44
59
|
end
|
45
60
|
|
46
|
-
#
|
61
|
+
# Blocks on locking a job, and once a job is locked,
|
62
|
+
# it will process the job.
|
47
63
|
def work
|
48
64
|
queue, job = lock_job
|
49
65
|
if queue && job
|
@@ -54,8 +70,12 @@ module QC
|
|
54
70
|
end
|
55
71
|
|
56
72
|
# Attempt to lock a job in the queue's table.
|
57
|
-
#
|
58
|
-
#
|
73
|
+
# If a job can be locked, this method returns an array with
|
74
|
+
# 2 elements. The first element is the queue from which the job was locked
|
75
|
+
# and the second is a hash representation of the job.
|
76
|
+
# If a job is returned, its locked_at column has been set in the
|
77
|
+
# job's row. It is the caller's responsibility to delete the job row
|
78
|
+
# from the table when the job is complete.
|
59
79
|
def lock_job
|
60
80
|
log(:at => "lock_job")
|
61
81
|
job = nil
|
@@ -70,17 +90,26 @@ module QC
|
|
70
90
|
end
|
71
91
|
|
72
92
|
# A job is processed by evaluating the target code.
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
93
|
+
# if the job is evaluated with no exceptions
|
94
|
+
# then it is deleted from the queue.
|
95
|
+
# If the job has raised an exception the responsibility of what
|
96
|
+
# to do with the job is delegated to Worker#handle_failure.
|
97
|
+
# If the job is not finished and an INT signal is traped,
|
98
|
+
# this method will unlock the job in the queue.
|
76
99
|
def process(queue, job)
|
100
|
+
finished = false
|
77
101
|
begin
|
78
|
-
call(job)
|
102
|
+
call(job).tap do
|
103
|
+
queue.delete(job[:id])
|
104
|
+
finished = true
|
105
|
+
end
|
79
106
|
rescue => e
|
80
107
|
handle_failure(job, e)
|
108
|
+
finished = true
|
81
109
|
ensure
|
82
|
-
|
83
|
-
|
110
|
+
if !finished
|
111
|
+
queue.unlock(job[:id])
|
112
|
+
end
|
84
113
|
end
|
85
114
|
end
|
86
115
|
|
@@ -89,9 +118,9 @@ module QC
|
|
89
118
|
# the object and pass the args.
|
90
119
|
def call(job)
|
91
120
|
args = job[:args]
|
92
|
-
|
93
|
-
|
94
|
-
|
121
|
+
receiver_str, _, message = job[:method].rpartition('.')
|
122
|
+
receiver = eval(receiver_str)
|
123
|
+
receiver.send(message, *args)
|
95
124
|
end
|
96
125
|
|
97
126
|
# This method will be called when an exception
|
@@ -113,10 +142,8 @@ module QC
|
|
113
142
|
|
114
143
|
private
|
115
144
|
|
116
|
-
def setup_queues(adapter,
|
117
|
-
|
118
|
-
names = q_names || QC::QUEUES
|
119
|
-
names << name unless names.include?(name)
|
145
|
+
def setup_queues(adapter, queue, queues, top_bound)
|
146
|
+
names = queues.length > 0 ? queues : [queue]
|
120
147
|
names.map do |name|
|
121
148
|
QC::Queue.new(name, top_bound).tap do |q|
|
122
149
|
q.conn_adapter = adapter
|
data/readme.md
CHANGED
@@ -12,7 +12,7 @@ Features:
|
|
12
12
|
* JSON data format.
|
13
13
|
* Forking workers.
|
14
14
|
* Workers can work multiple queues.
|
15
|
-
* [
|
15
|
+
* Reduced row contention using a [relaxed FIFO](http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/Lock_Free.pdf) technique.
|
16
16
|
|
17
17
|
Contents:
|
18
18
|
|
@@ -70,22 +70,20 @@ require 'queue_classic/tasks'
|
|
70
70
|
```
|
71
71
|
|
72
72
|
Start the worker via the Rakefile.
|
73
|
-
|
74
73
|
```bash
|
75
74
|
$ bundle exec rake qc:work
|
76
75
|
```
|
77
76
|
|
78
77
|
Setup a worker to work a non-default queue.
|
79
|
-
|
80
78
|
```bash
|
81
79
|
$ QUEUE="priority_queue" bundle exec rake qc:work
|
82
80
|
```
|
83
81
|
|
84
82
|
Setup a worker to work multiple queues.
|
85
|
-
|
86
83
|
```bash
|
87
|
-
$
|
84
|
+
$ QUEUES="priority_queue, secondary_queue" bundle exec rake qc:work
|
88
85
|
```
|
86
|
+
In this scenario, on each iteration of the worker's loop, it will look for jobs in the first queue prior to looking at the second queue. This means that the first queue must be empty before the worker will look at the second queue.
|
89
87
|
|
90
88
|
#### Custom Worker
|
91
89
|
|
@@ -98,9 +96,9 @@ require 'queue_classic'
|
|
98
96
|
FailedQueue = QC::Queue.new("failed_jobs")
|
99
97
|
|
100
98
|
class MyWorker < QC::Worker
|
101
|
-
|
102
|
-
|
103
|
-
|
99
|
+
def handle_failure(job, e)
|
100
|
+
FailedQueue.enqueue(job[:method], *job[:args])
|
101
|
+
end
|
104
102
|
end
|
105
103
|
|
106
104
|
worker = MyWorker.new
|
@@ -109,10 +107,8 @@ trap('INT') {exit}
|
|
109
107
|
trap('TERM') {worker.stop}
|
110
108
|
|
111
109
|
loop do
|
112
|
-
|
113
|
-
|
114
|
-
Timeout::timeout(5) { worker.process(job) }
|
115
|
-
end
|
110
|
+
job = worker.lock_job
|
111
|
+
Timeout::timeout(5) { worker.process(job) }
|
116
112
|
end
|
117
113
|
```
|
118
114
|
|
@@ -134,27 +130,27 @@ $ ruby -r queue_classic -e "QC::Worker.new.work"
|
|
134
130
|
### Ruby on Rails Setup
|
135
131
|
|
136
132
|
Declare dependencies in Gemfile.
|
137
|
-
|
138
133
|
```ruby
|
139
134
|
source "http://rubygems.org"
|
140
|
-
gem "queue_classic", "
|
141
|
-
```
|
142
|
-
|
143
|
-
By default, queue_classic will use the QC_DATABASE_URL falling back on DATABASE_URL. The URL must be in the following format: `postgres://username:password@localhost/database_name`. If you use Heroku's PostgreSQL service, this will already be set. If you don't want to set this variable, you can set the connection in an initializer. **QueueClassic will maintain its own connection to the database.** This may double the number of connections to your database. Set QC::Conn.connection to share the connection between Rails & QueueClassic
|
144
|
-
|
145
|
-
```ruby
|
146
|
-
require 'queue_classic'
|
147
|
-
QC::Conn.connection = ActiveRecord::Base.connection.raw_connection
|
135
|
+
gem "queue_classic", "3.0.0beta"
|
148
136
|
```
|
149
137
|
|
150
|
-
|
151
|
-
migration. This will setup the necessary table to use queue classic.
|
152
|
-
|
138
|
+
Add the database tables and stored procedures.
|
153
139
|
```
|
154
140
|
rails generate queue_classic:install
|
155
141
|
rake db:migrate
|
156
142
|
```
|
157
143
|
|
144
|
+
By default, queue_classic will use the QC_DATABASE_URL falling back on DATABASE_URL. The URL must be in the following format: `postgres://username:password@localhost/database_name`. If you use Heroku's PostgreSQL service, this will already be set. If you don't want to set this variable, you can set the connection in an initializer. **QueueClassic will maintain its own connection to the database.** This may double the number of connections to your database.
|
145
|
+
|
146
|
+
You can share your active record connection with queue_classic –**however this is not thread safe.**
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
require 'queue_classic'
|
150
|
+
QC.default_conn_adapter = QC::ConnAdapter.new(
|
151
|
+
ActiveRecord::Base.connection.raw_connection)
|
152
|
+
```
|
153
|
+
|
158
154
|
**Note on using ActiveRecord migrations:** If you use the migration, and you wish to use commands that reset the database from the stored schema (e.g. `rake db:reset`), your application must be configured with `config.active_record.schema_format = :sql` in `config/application.rb`. If you don't do this, the PL/pgSQL function that queue_classic creates will be lost when you reset the database.
|
159
155
|
|
160
156
|
### Rake Task Setup
|
@@ -195,7 +191,7 @@ export DEBUG="true"
|
|
195
191
|
If you think you have found a bug, feel free to open an issue. Use the following template for the new issue:
|
196
192
|
|
197
193
|
1. List Versions: Ruby, PostgreSQL, queue_classic.
|
198
|
-
2. Define what you would have
|
194
|
+
2. Define what you would have expected to happen.
|
199
195
|
3. List what actually happened.
|
200
196
|
4. Provide sample codes & commands which will reproduce the problem.
|
201
197
|
|
data/test/worker_test.rb
CHANGED
@@ -5,6 +5,7 @@ module TestObject
|
|
5
5
|
def no_args; return nil; end
|
6
6
|
def one_arg(a); return a; end
|
7
7
|
def two_args(a,b); return [a,b]; end
|
8
|
+
def forty_two; OpenStruct.new(number: 42); end
|
8
9
|
end
|
9
10
|
|
10
11
|
# This not only allows me to test what happens
|
@@ -167,4 +168,12 @@ class WorkerTest < QCTest
|
|
167
168
|
assert_equal(3, r_queue.count)
|
168
169
|
end
|
169
170
|
|
171
|
+
def test_work_with_more_complex_construct
|
172
|
+
QC.enqueue("TestObject.forty_two.number")
|
173
|
+
worker = TestWorker.new
|
174
|
+
r = worker.work
|
175
|
+
assert_equal(42, r)
|
176
|
+
assert_equal(0, worker.failed_count)
|
177
|
+
end
|
178
|
+
|
170
179
|
end
|