queue_classic 2.0.5 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,46 +1,51 @@
1
+ require 'thread'
2
+
1
3
  module QC
2
4
  module Conn
3
5
  extend self
6
+ @exec_mutex = Mutex.new
4
7
 
5
8
  def execute(stmt, *params)
6
- log(:level => :debug, :action => "exec_sql", :sql => stmt.inspect)
7
- begin
8
- params = nil if params.empty?
9
- r = connection.exec(stmt, params)
10
- result = []
11
- r.each {|t| result << t}
12
- result.length > 1 ? result : result.pop
13
- rescue PGError => e
14
- log(:error => e.inspect)
15
- disconnect
16
- raise
9
+ @exec_mutex.synchronize do
10
+ log(:at => "exec_sql", :sql => stmt.inspect)
11
+ begin
12
+ params = nil if params.empty?
13
+ r = connection.exec(stmt, params)
14
+ result = []
15
+ r.each {|t| result << t}
16
+ result.length > 1 ? result : result.pop
17
+ rescue PGError => e
18
+ log(:error => e.inspect)
19
+ disconnect
20
+ raise
21
+ end
17
22
  end
18
23
  end
19
24
 
20
25
  def notify(chan)
21
- log(:level => :debug, :action => "NOTIFY")
26
+ log(:at => "NOTIFY")
22
27
  execute('NOTIFY "' + chan + '"') #quotes matter
23
28
  end
24
29
 
25
30
  def listen(chan)
26
- log(:level => :debug, :action => "LISTEN")
31
+ log(:at => "LISTEN")
27
32
  execute('LISTEN "' + chan + '"') #quotes matter
28
33
  end
29
34
 
30
35
  def unlisten(chan)
31
- log(:level => :debug, :action => "UNLISTEN")
36
+ log(:at => "UNLISTEN")
32
37
  execute('UNLISTEN "' + chan + '"') #quotes matter
33
38
  end
34
39
 
35
40
  def drain_notify
36
41
  until connection.notifies.nil?
37
- log(:level => :debug, :action => "drain_notifications")
42
+ log(:at => "drain_notifications")
38
43
  end
39
44
  end
40
45
 
41
46
  def wait_for_notify(t)
42
47
  connection.wait_for_notify(t) do |event, pid, msg|
43
- log(:level => :debug, :action => "received_notification")
48
+ log(:at => "received_notification")
44
49
  end
45
50
  end
46
51
 
@@ -65,22 +70,21 @@ module QC
65
70
 
66
71
  def connection=(connection)
67
72
  unless connection.instance_of? PG::Connection
68
- raise(
69
- ArgumentError,
70
- "connection must be an instance of PG::Connection, but was #{connection.class}"
71
- )
73
+ c = connection.class
74
+ err = "connection must be an instance of PG::Connection, but was #{c}"
75
+ raise(ArgumentError, err)
72
76
  end
73
77
  @connection = connection
74
78
  end
75
79
 
76
80
  def disconnect
77
- connection.finish
78
- ensure
79
- @connection = nil
81
+ begin connection.finish
82
+ ensure @connection = nil
83
+ end
80
84
  end
81
85
 
82
86
  def connect
83
- log(:level => :debug, :action => "establish_conn")
87
+ log(:at => "establish_conn")
84
88
  conn = PGconn.connect(
85
89
  db_url.host,
86
90
  db_url.port || 5432,
@@ -90,7 +94,7 @@ module QC
90
94
  db_url.password
91
95
  )
92
96
  if conn.status != PGconn::CONNECTION_OK
93
- log(:level => :error, :message => conn.error)
97
+ log(:error => conn.error)
94
98
  end
95
99
  conn
96
100
  end
@@ -4,7 +4,7 @@ module QC
4
4
 
5
5
  def insert(q_name, method, args, chan=nil)
6
6
  QC.log_yield(:action => "insert_job") do
7
- s = "INSERT INTO #{TABLE_NAME} (q_name, method, args) VALUES ($1, $2, $3)"
7
+ s="INSERT INTO #{TABLE_NAME} (q_name, method, args) VALUES ($1, $2, $3)"
8
8
  res = Conn.execute(s, q_name, method, OkJson.encode(args))
9
9
  Conn.notify(chan) if chan
10
10
  end
@@ -2,7 +2,6 @@ module QC
2
2
  class Queue
3
3
 
4
4
  attr_reader :name, :chan
5
-
6
5
  def initialize(name, notify=QC::LISTENING_WORKER)
7
6
  @name = name
8
7
  @chan = @name if notify
@@ -8,7 +8,10 @@ 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
- QC::Worker.new.start
11
+ trap('INT') {exit}
12
+ trap('TERM') {@worker.stop}
13
+ @worker = QC::Worker.new
14
+ @worker.start
12
15
  end
13
16
 
14
17
  desc "Returns the number of jobs in the (default or QUEUE) queue"
@@ -2,126 +2,97 @@ module QC
2
2
  class Worker
3
3
 
4
4
  attr_reader :queue
5
-
6
- def initialize(*args)
7
- if args.length == 5
8
- q_name, top_bound, fork_worker, listening_worker, max_attempts = *args
9
- elsif args.length <= 1
10
- opts = args.first || {}
11
- q_name = opts[:q_name] || QC::QUEUE
12
- top_bound = opts[:top_bound] || QC::TOP_BOUND
13
- fork_worker = opts[:fork_worker] || QC::FORK_WORKER
14
- listening_worker = opts[:listening_worker] || QC::LISTENING_WORKER
15
- max_attempts = opts[:max_attempts] || QC::MAX_LOCK_ATTEMPTS
16
- else
17
- raise ArgumentError, 'wrong number of arguments (expected no args, an options hash, or 5 separate args)'
18
- end
5
+ # In the case no arguments are passed to the initializer,
6
+ # the defaults are pulled from the environment variables.
7
+ def initialize(args={})
8
+ @q_name = args[:q_name] ||= QC::QUEUE
9
+ @top_bound = args[:top_bound] ||= QC::TOP_BOUND
10
+ @fork_worker = args[:fork_worker] ||= QC::FORK_WORKER
11
+ @listening_worker = args[:listening_worker] ||= QC::LISTENING_WORKER
12
+ @max_attempts = args[:max_attempts] ||= QC::MAX_LOCK_ATTEMPTS
19
13
 
20
14
  @running = true
21
- @queue = Queue.new(q_name, listening_worker)
22
- @top_bound = top_bound
23
- @fork_worker = fork_worker
24
- @listening_worker = listening_worker
25
- @max_attempts = max_attempts
26
- handle_signals
27
-
28
- log(
29
- :level => :debug,
30
- :action => "worker_initialized",
31
- :queue => q_name,
32
- :top_bound => top_bound,
33
- :fork_worker => fork_worker,
34
- :listening_worker => listening_worker,
35
- :max_attempts => max_attempts
36
- )
37
- end
38
-
39
- def running?
40
- @running
41
- end
42
-
43
- def fork_worker?
44
- @fork_worker
45
- end
46
-
47
- def can_listen?
48
- @listening_worker
15
+ @queue = Queue.new(@q_name, @listening_worker)
16
+ log(args.merge(:at => "worker_initialized"))
49
17
  end
50
18
 
51
- def handle_signals
52
- %W(INT TERM).each do |sig|
53
- trap(sig) do
54
- if running?
55
- @running = false
56
- log(:level => :debug, :action => "handle_signal", :running => @running)
57
- else
58
- raise Interrupt
59
- end
60
- end
19
+ # Start a loop and work jobs indefinitely.
20
+ # Call this method to start the worker.
21
+ # This is the easiest way to start working jobs.
22
+ def start
23
+ while @running
24
+ @fork_worker ? fork_and_work : work
61
25
  end
62
26
  end
63
27
 
64
- # This method should be overriden if
65
- # your worker is forking and you need to
66
- # re-establish database connectoins
67
- def setup_child
68
- end
69
-
70
- def start
71
- while running?
72
- if fork_worker?
73
- fork_and_work
74
- else
75
- work
76
- end
77
- end
28
+ # Call this method to stop the worker.
29
+ # The worker may not stop immediately if the worker
30
+ # is sleeping.
31
+ def stop
32
+ @running = false
78
33
  end
79
34
 
35
+ # This method will tell the ruby process to FORK.
36
+ # Define setup_child to hook into the forking process.
37
+ # Using setup_child is good for re-establishing database connections.
80
38
  def fork_and_work
81
- @cpid = fork { setup_child; work }
82
- log(:level => :debug, :action => :fork, :pid => @cpid)
39
+ @cpid = fork {setup_child; work}
40
+ log(:at => :fork, :pid => @cpid)
83
41
  Process.wait(@cpid)
84
42
  end
85
43
 
44
+ # This method will lock a job & evaluate the code defined by the job.
45
+ # Also, this method will make the best attempt to delete the job
46
+ # from the queue before returning.
86
47
  def work
87
48
  if job = lock_job
88
- QC.log_yield(:level => :info, :action => "work_job", :job => job[:id]) do
49
+ QC.log_yield(:at => "work", :job => job[:id]) do
89
50
  begin
90
51
  call(job)
91
- rescue Object => e
92
- log(:level => :debug, :action => "failed_work", :job => job[:id], :error => e.inspect)
52
+ rescue => e
93
53
  handle_failure(job, e)
94
54
  ensure
95
55
  @queue.delete(job[:id])
96
- log(:level => :debug, :action => "delete_job", :job => job[:id])
56
+ log(:at => "delete_job", :job => job[:id])
97
57
  end
98
58
  end
99
59
  end
100
60
  end
101
61
 
62
+ # lock_job will attempt to lock a job in the queue's table. It uses an
63
+ # exponential backoff in the event that a job was not locked. This method
64
+ # will return a hash when a job is obtained.
65
+ #
66
+ # This method will terminate early if the stop method is called or
67
+ # @max_attempts has been reached.
68
+ #
69
+ # It is important that callers delete the job when finished.
70
+ # *@queue.delete(job[:id])*
102
71
  def lock_job
103
- log(:level => :debug, :action => "lock_job")
72
+ log(:at => "lock_job")
104
73
  attempts = 0
105
74
  job = nil
106
- until !running? || job
75
+ until !@running || job
107
76
  job = @queue.lock(@top_bound)
108
77
  if job.nil?
109
- log(:level => :debug, :action => "failed_lock", :attempts => attempts)
78
+ log(:at => "failed_lock", :attempts => attempts)
110
79
  if attempts < @max_attempts
111
- seconds = 2**attempts
112
- wait(seconds)
80
+ wait(2**attempts)
113
81
  attempts += 1
114
82
  next
115
83
  else
116
84
  break
117
85
  end
118
86
  else
119
- log(:level => :debug, :action => "finished_lock", :job => job[:id])
87
+ log(:at => "finished_lock", :job => job[:id])
120
88
  end
121
89
  end
122
90
  job
123
91
  end
124
92
 
93
+ # Each job includes a method column. We will use ruby's eval
94
+ # to grab the ruby object from memory. We send the method to
95
+ # the object and pass the args.
125
96
  def call(job)
126
97
  args = job[:args]
127
98
  klass = eval(job[:method].split(".").first)
@@ -129,27 +100,35 @@ module QC
129
100
  klass.send(message, *args)
130
101
  end
131
102
 
103
+ # If @listening_worker is set, the worker will use the database
104
+ # to sleep. The database approach preferred over a syscall since
105
+ # the database will break the sleep when new jobs are inserted into
106
+ # the queue.
132
107
  def wait(t)
133
- if can_listen?
134
- log(:level => :debug, :action => "listen_wait", :wait => t)
108
+ if @listening_worker
109
+ log(:at => "listen_wait", :wait => t)
135
110
  Conn.listen(@queue.chan)
136
111
  Conn.wait_for_notify(t)
137
112
  Conn.unlisten(@queue.chan)
138
113
  Conn.drain_notify
139
- log(:level => :debug, :action => "finished_listening")
114
+ log(:at => "finished_listening")
140
115
  else
141
- log(:level => :debug, :action => "sleep_wait", :wait => t)
116
+ log(:at => "sleep_wait", :wait => t)
142
117
  Kernel.sleep(t)
143
118
  end
144
119
  end
145
120
 
146
- #override this method to do whatever you want
121
+ # This method will be called when an exception
122
+ # is raised during the execution of the job.
147
123
  def handle_failure(job,e)
148
- puts "!"
149
- puts "! \t FAIL"
150
- puts "! \t \t #{job.inspect}"
151
- puts "! \t \t #{e.inspect}"
152
- puts "!"
124
+ log(:at => "handle_failure")
125
+ end
126
+
127
+ # This method should be overriden if
128
+ # your worker is forking and you need to
129
+ # re-establish database connections
130
+ def setup_child
131
+ log(:at => "setup_child")
153
132
  end
154
133
 
155
134
  def log(data)
data/readme.md CHANGED
@@ -1,175 +1,34 @@
1
1
  # queue_classic
2
2
 
3
- v2.0.4
3
+ v2.1.0
4
4
 
5
- queue_classic provides PostgreSQL-backed queueing focused on concurrent job
6
- locking and minimizing database load while providing a simple, intuitive user
7
- experience.
5
+ queue_classic provides a simple interface to a PostgreSQL-backed message queue. queue_classic specializes in concurrent locking and minimizing database load while providing a simple, intuitive developer experience. queue_classic assumes that you are already using PostgreSQL in your production environment and that adding another dependency (e.g. redis, beanstalkd, 0mq) is undesirable.
8
6
 
9
- queue_classic features:
7
+ Features:
10
8
 
11
- * Support for multiple queues with heterogeneous workers
12
- * Utilization of Postgres' PUB/SUB
13
- * JSON encoding
14
- * Forking workers
15
- * Postgres' rock-solid locking mechanism
16
- * Fuzzy-FIFO support [academic paper](http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/Lock_Free.pdf)
17
- * Instrumentation via log output
18
- * Long term support
9
+ * Leverage of PostgreSQL's listen/notify & row locking.
10
+ * Support for multiple queues with heterogeneous workers.
11
+ * JSON data format.
12
+ * Forking workers.
13
+ * [Fuzzy-FIFO support](http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/Lock_Free.pdf)
19
14
 
20
- ## Proven
15
+ Contents:
21
16
 
22
- queue_classic was designed out of the necessity for a fast, reliable, low
23
- maintenance message queue. It was built upon PostgreSQL to avoid the necessity
24
- of adding redis or 0MQ services to my applications. It was designed to be
25
- simple, with a small API and very few features. For a simple mechanism to
26
- distribute jobs to worker processes, especially if you are already running
27
- PostgreSQL, queue_classic is exactly what you should be using. If you need
28
- more advanced queueing features, you should investigate 0MQ, rabbitmq, or redis.
29
-
30
- ### Heroku Postgres
31
-
32
- The Heroku Postgres team uses queue_classic to monitor the health of
33
- customer databases, processing 200 jobs per second using a [fugu](https://postgres.heroku.com/pricing)
34
- database. They chose queue_classic because of its simplicity and reliability.
35
-
36
- ### Cloudapp
37
-
38
- Larry uses queue_classic to deliver cloudapp's push notifications and collect
39
- file meta-data from S3, processing nearly 14 jobs per second.
40
-
41
- ```
42
- I haven't even touched QC since setting it up.
43
- The best queue is the one you don't have to hand hold.
44
-
45
- -- Larry Marburger
46
- ```
47
-
48
- ## Setup
49
-
50
- In addition to installing the rubygem, you will need to prepare your database.
51
- Database preparation includes creating a table and loading PL/pgSQL functions.
52
- You can issue the database preparation commands using **PSQL(1)** or place them in a
53
- database migration.
54
-
55
- ### Quick Start
56
-
57
- ```bash
58
- $ gem install queue_classic
59
- $ createdb queue_classic_test
60
- $ export QC_DATABASE_URL="postgres://username:password@localhost/queue_classic_test"
61
- $ ruby -r queue_classic -e "QC::Setup.create"
62
- $ ruby -r queue_classic -e "QC.enqueue('Kernel.puts', 'hello world')"
63
- $ ruby -r queue_classic -e "QC::Worker.new.work"
64
- ```
65
-
66
- ### Ruby on Rails Setup
67
-
68
- **Gemfile**
69
-
70
- ```ruby
71
- source :rubygems
72
- gem "queue_classic", "2.0.1"
73
- ```
74
-
75
- **Rakefile**
76
-
77
- ```ruby
78
- require "queue_classic"
79
- require "queue_classic/tasks"
80
- ```
81
-
82
- **config/initializers/queue_classic.rb**
83
-
84
- ```ruby
85
- # queue_classic will by default look for an environment variable DATABASE_URL
86
- # or QC_DATABASE_URL for a connection string in the format
87
- # "postgres://username:password@localhost/database_name". If you use Heroku,
88
- # this will already be set.
89
- #
90
- # If you don't want to set this variable, you can set the connection in an
91
- # initializer.
92
- QC::Conn.connection = ActiveRecord::Base.connection.raw_connection
93
- ```
94
-
95
- queue_classic requires a database table and a PL/pgSQL function to be loaded
96
- into your database. You can load the table and the function by running a migration
97
- or using a rake task.
98
-
99
- **db/migrations/add_queue_classic.rb**
100
-
101
- If you use the migration, and you wish to use commands that reset the database
102
- from the stored schema (e.g. `rake db:reset`), your application must be
103
- configured with `config.active_record.schema_format = :sql` in
104
- `config/application.rb`. If you don't do this, the PL/pgSQL function that
105
- queue_classic creates will be lost when you reset the database.
106
-
107
- ```ruby
108
- require 'queue_classic'
109
-
110
- class AddQueueClassic < ActiveRecord::Migration
111
-
112
- def self.up
113
- QC::Setup.create
114
- end
115
-
116
- def self.down
117
- QC::Setup.drop
118
- end
119
-
120
- end
121
- ```
122
-
123
- **Rake Task**
124
-
125
- ```bash
126
- # Creating the table and functions
127
- $ bundle exec rake qc:create
128
-
129
- # Dropping the table and functions
130
- $ bundle exec rake qc:drop
131
- ```
132
-
133
-
134
- ### Sequel Setup
135
-
136
- **db/migrations/1_add_queue_classic.rb**
137
-
138
- ```ruby
139
- require 'queue_classic'
140
-
141
- Sequel.migration do
142
- up {QC::Setup.create}
143
- down {QC::Setup.drop}
144
- end
145
- ```
146
-
147
- ## Configure
148
-
149
- All configuration takes place in the form of environment vars.
150
- See [queue_classic.rb](https://github.com/ryandotsmith/queue_classic/blob/master/lib/queue_classic.rb#L23-62)
151
- for a list of options.
17
+ * [Documentation](http://rubydoc.info/gems/queue_classic/2.1.0/frames)
18
+ * [Usage](#usage)
19
+ * [Setup](#setup)
20
+ * [Configuration](#configuration)
21
+ * [Hacking](#hacking-on-queue_classic)
22
+ * [License](#license)
152
23
 
153
24
  ## Usage
154
25
 
155
- Users of queue_classic will be producing jobs (enqueue) or
156
- consuming jobs (lock then delete).
26
+ There are 2 ways to use queue_classic.
157
27
 
158
- ### Producer
28
+ * Producing Jobs
29
+ * Working Jobs
159
30
 
160
- You certainly don't need the queue_classic rubygem to put a job in the queue.
161
-
162
- ```bash
163
- $ psql queue_classic_test -c "INSERT INTO queue_classic_jobs (q_name, method, args) VALUES ('default', 'Kernel.puts', '[\"hello world\"]');"
164
- ```
165
-
166
- However, the rubygem will take care of converting your args to JSON and it will also dispatch
167
- PUB/SUB notifications if the feature is enabled. It will also manage a connection to the database
168
- that is independent of any other connection you may have in your application. Note: If your
169
- queue table is in your application's database then your application's process will have 2 connections
170
- to the database; one for your application and another for queue_classic.
171
-
172
- The Ruby API for producing jobs is pretty simple:
31
+ ### Producing Jobs
173
32
 
174
33
  ```ruby
175
34
  # This method has no arguments.
@@ -186,380 +45,151 @@ QC.enqueue("Kernel.puts", {"hello" => "world"})
186
45
 
187
46
  # This method has an array argument.
188
47
  QC.enqueue("Kernel.puts", ["hello", "world"])
48
+
49
+ # This method uses a non-default queue.
50
+ p_queue = QC::Queue.new(q_name: "priority_queue")
51
+ p_queue.enqueue("Kernel.puts", ["hello", "world"])
189
52
  ```
190
53
 
191
- The basic idea is that all arguments should be easily encoded to json. OkJson
192
- is used to encode the arguments, so the arguments can be anything that OkJson can encode.
54
+ QueueClassic uses [OkJson](https://github.com/kr/okjson) to encode the job's payload.
193
55
 
194
56
  ```ruby
195
- # Won't work!
196
- OkJson.encode({:test => "test"})
197
-
198
57
  # OK
199
58
  OkJson.encode({"test" => "test"})
200
- ```
201
-
202
- To see more information on usage, take a look at the test files in the source code. Also,
203
- read up on [OkJson](https://github.com/kr/okjson)
204
-
205
- #### Multiple Queues
206
-
207
- The table containing the jobs has a column named *q_name*. This column
208
- is the abstraction queue_classic uses to represent multiple queues. This allows
209
- the programmer to place triggers and indexes on distinct queues.
210
-
211
- ```ruby
212
- # attach to the priority_queue. this will insert
213
- # jobs with the column q_name = 'priority_queue'
214
- p_queue = QC::Queue.new("priority_queue")
215
-
216
- # This method has no arguments.
217
- p_queue.enqueue("Time.now")
218
-
219
- # This method has 1 argument.
220
- p_queue.enqueue("Kernel.puts", "hello world")
221
-
222
- # This method has 2 arguments.
223
- p_queue.enqueue("Kernel.printf", "hello %s", "world")
224
59
 
225
- # This method has a hash argument.
226
- p_queue.enqueue("Kernel.puts", {"hello" => "world"})
227
-
228
- # This method has an array argument.
229
- p_queue.enqueue("Kernel.puts", ["hello", "world"])
60
+ # NOT OK
61
+ OkJson.encode({:test => "test"})
230
62
  ```
231
63
 
232
- This code example shows how to produce jobs into a custom queue,
233
- to consume jobs from the custom queue be sure and set the `$QUEUE`
234
- var to the q_name in the worker's UNIX environment.
235
-
236
- ### Consumer
64
+ ### Working Jobs
237
65
 
238
- There are several approaches to working jobs. The first is to include
239
- a task file provided by queue_classic and the other approach is to
240
- write a custom bin file.
66
+ There are two ways to work jobs. The first approach is to use the Rake task. The second approach is to use a custom executable.
241
67
 
242
68
  #### Rake Task
243
69
 
244
- Be sure to include `queue_classic` and `queue_classic/tasks`
245
- in your primary Rakefile.
70
+ Require queue_classic in your Rakefile.
71
+
72
+ ```ruby
73
+ require 'queue_classic'
74
+ require 'queue_classic/tasks'
75
+ ```
246
76
 
247
- To work jobs from the default queue:
77
+ Start the worker via the Rakefile.
248
78
 
249
79
  ```bash
250
80
  $ bundle exec rake qc:work
251
81
  ```
252
- To work jobs from a custom queue:
82
+
83
+ Setup a worker to work a non-default queue.
253
84
 
254
85
  ```bash
255
86
  $ QUEUE="p_queue" bundle exec rake qc:work
256
87
  ```
257
88
 
258
- #### Bin File
89
+ #### Custom Worker
259
90
 
260
- Start by making a bin directory in your project's root directory.
261
- Then add an executable file called worker.
262
-
263
- **bin/worker**
91
+ This example is probably not production ready; however, it serves as an example of how to leverage the code in the Worker class to fit your non-default requirements.
264
92
 
265
93
  ```ruby
266
- #!/usr/bin/env ruby
267
- # encoding: utf-8
268
-
269
- trap('INT') {exit}
270
- trap('TERM') {exit}
271
-
272
- require "your_app"
273
- require "queue_classic"
274
- QC::Worker.new.start
275
- ```
276
-
277
- #### Subclass QC::Worker
94
+ require 'timeout'
95
+ require 'queue_classic'
278
96
 
279
- Now that we have seen how to run a worker process, let's take a look at how to customize a worker.
280
- The class `QC::Worker` will probably suit most of your needs; however, there are some mechanisms
281
- that you will want to override. For instance, if you are using a forking worker, you will need to
282
- open a new database connection in the child process that is doing your work. Also, you may want to
283
- define how a failed job should behave. The default failed handler will simply print the job to stdout.
284
- You can define a failure method that will enqueue the job again, or move it to another table, etc....
97
+ trap('INT') {exit}
98
+ trap('TERM') {worker.stop}
285
99
 
286
- ```ruby
287
- require "queue_classic"
100
+ FailedQueue = QC::Queue.new("failed_jobs")
288
101
 
289
102
  class MyWorker < QC::Worker
290
-
291
- # retry the job
292
- def handle_failure(job, exception)
293
- @queue.enqueue(job[:method], *job[:args])
294
- end
295
-
296
- # the forked proc needs a new db connection
297
- def setup_child
298
- ActiveRecord::Base.establish_connection
299
- end
300
-
103
+ def handle_failure(job, e)
104
+ FailedQueue.enqueue(job)
105
+ end
301
106
  end
302
- ```
303
107
 
304
- Notice that we have access to the `@queue` instance variable. Read the tests
305
- and the worker class for more information on what you can do inside of the worker.
306
-
307
- **bin/worker**
308
-
309
- ```ruby
310
- #!/usr/bin/env ruby
311
- # encoding: utf-8
312
-
313
- trap('INT') {exit}
314
- trap('TERM') {exit}
315
-
316
- require "your_app"
317
- require "queue_classic"
318
- require "my_worker"
319
-
320
- MyWorker.new.start
108
+ worker = MyWorker.new(max_attempts: 10, listening_worker: true)
109
+
110
+ loop do
111
+ job = worker.lock_job
112
+ Thread.new do
113
+ begin
114
+ Timeout::timeout(5) {worker.call(job)}
115
+ rescue => e
116
+ handle_failure(job, e)
117
+ ensure
118
+ worker.delete(job[:id])
119
+ end
120
+ end
121
+ end
321
122
  ```
322
123
 
323
- #### QC::Worker Details
324
-
325
- ##### General Idea
326
-
327
- The worker class (QC::Worker) is designed to be extended via inheritance. Any of
328
- its methods should be considered for extension. There are a few in particular
329
- that act as stubs in hopes that the user will override them. Such methods
330
- include: `handle_failure() and setup_child()`. See the section near the bottom
331
- for a detailed descriptor of how to subclass the worker.
332
-
333
- ##### Algorithm
334
-
335
- When we ask the worker to start, it will enter a loop with a stop condition
336
- dependent upon a method named `running?` . While in the method, the worker will
337
- attempt to select and lock a job. If it can not on its first attempt, it will
338
- use an exponential back-off technique to try again.
339
-
340
- ##### Signals
341
-
342
- *INT, TERM* Both of these signals will ensure that the running? method returns
343
- false. If the worker is waiting -- as it does per the exponential backoff
344
- technique; then a second signal must be sent.
345
-
346
- ##### Forking
124
+ ## Setup
347
125
 
348
- There are many reasons why you would and would not want your worker to fork.
349
- An argument against forking may be that you want low latency in your job
350
- execution. An argument in favor of forking is that your jobs leak memory and do
351
- all sorts of crazy things, thus warranting the cleanup that fork allows.
352
- Nevertheless, forking is not enabled by default. To instruct your worker to
353
- fork, ensure the following shell variable is set:
126
+ In addition to installing the rubygem, you will need to prepare your database. Database preparation includes creating a table and loading PL/pgSQL functions. You can issue the database preparation commands using `PSQL(1)` or use a database migration script.
354
127
 
355
- ```bash
356
- $ export QC_FORK_WORKER='true'
357
- ```
358
-
359
- One last note on forking. It is often the case that after Ruby forks a process,
360
- some sort of setup needs to be done. For instance, you may want to re-establish
361
- a database connection, or get a new file descriptor. queue_classic's worker
362
- provides a hook that is called immediately after `Kernel.fork`. To use this hook
363
- subclass the worker and override `setup_child()`.
364
-
365
- ##### LISTEN/NOTIFY
366
-
367
- The exponential back-off algorithm will require our worker to wait if it does
368
- not succeed in locking a job. How we wait is something that can vary. PostgreSQL
369
- has a wonderful feature that we can use to wait intelligently. Processes can LISTEN on a channel and be
370
- alerted to notifications. queue_classic uses this feature to block until a
371
- notification is received. If this feature is disabled, the worker will call
372
- `Kernel.sleep(t)` where t is set by our exponential back-off algorithm. However,
373
- if we are using LISTEN/NOTIFY then we can enter a type of sleep that can be
374
- interrupted by a NOTIFY. For example, say we just started to wait for 2 seconds.
375
- After the first millisecond of waiting, a job was enqueued. With LISTEN/NOTIFY
376
- enabled, our worker would immediately preempt the wait and attempt to lock the job. This
377
- allows our worker to be much more responsive. In the case there is no
378
- notification, the worker will quit waiting after the timeout has expired.
379
-
380
- LISTEN/NOTIFY is disabled by default but can be enabled by setting the following shell variable:
128
+ ### Quick Start
381
129
 
382
130
  ```bash
383
- $ export QC_LISTENING_WORKER='true'
131
+ $ gem install queue_classic
132
+ $ createdb queue_classic_test
133
+ $ export QC_DATABASE_URL="postgres://username:password@localhost/queue_classic_test"
134
+ $ ruby -r queue_classic -e "QC::Setup.create"
135
+ $ ruby -r queue_classic -e "QC.enqueue('Kernel.puts', 'hello world')"
136
+ $ ruby -r queue_classic -e "QC::Worker.new.work"
384
137
  ```
385
138
 
386
- ##### Failure
387
-
388
- I bet your worker will encounter a job that raises an exception. queue_classic
389
- thinks that you should know about this exception by means of you established
390
- exception tracker. (i.e. Hoptoad, Exceptional) To that end, queue_classic offers
391
- a method that you can override. This method will be passed 2 arguments: the
392
- exception instance and the job. Here are a few examples of things you might want
393
- to do inside `handle_failure()`.
394
-
395
- ## Instrumentation
139
+ ### Ruby on Rails Setup
396
140
 
397
- QC will log elapsed time, errors and general usage in the form of data.
398
- To customize the output of the log data, override `QC.log(data)` and `QC.log_yield(data)`.
399
- By default, QC uses a simple wrapper around $stdout to put the log data in k=v
400
- format. For instance:
141
+ Declare dependencies in Gemfile.
401
142
 
143
+ ```ruby
144
+ source :rubygems
145
+ gem "queue_classic", "2.1.0"
402
146
  ```
403
- lib=queue_classic level=info action=insert_job elapsed=16
404
- ```
405
-
406
- ## Tips and Tricks
407
-
408
- ### Running Synchronously for tests
409
-
410
- Author: [@em_csquared](https://twitter.com/#!/em_csquared)
411
147
 
412
- I was testing some code that started out handling some work in a web request and
413
- wanted to move that work over to a queue. After completing a red-green-refactor
414
- I did not want my tests to have to worry about workers or even hit the database.
415
-
416
- Turns out its easy to get queue_classic to just work in a synchronous way with:
148
+ Require these files in your Rakefile so that you can run `rake qc:work`.
417
149
 
418
150
  ```ruby
419
- def QC.enqueue(function_call, *args)
420
- eval("#{function_call} *#{args.inspect}")
421
- end
151
+ require "queue_classic"
152
+ require "queue_classic/tasks"
422
153
  ```
423
154
 
424
- Now you can test queue_classic as if it was calling your method directly!
425
-
426
-
427
- ### Dispatching new jobs to workers without new code
428
-
429
- Author: [@ryandotsmith (ace hacker)](https://twitter.com/#!/ryandotsmith)
430
-
431
- The other day I found myself in a position in which I needed to delete a few
432
- thousand records. The tough part of this situation is that I needed to ensure
433
- the ActiveRecord callbacks were made on these objects thus making a simple SQL
434
- statement unfeasible. Also, I didn't want to wait all day to select and destroy
435
- these objects. queue_classic to the rescue! (no pun intended)
436
-
437
- The API of queue_classic enables you to quickly dispatch jobs to workers. In my
438
- case I wanted to call `Invoice.destroy(id)` a few thousand times. I fired up a
439
- Heroku console session and executed this line:
155
+ 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
440
156
 
441
157
  ```ruby
442
- Invoice.find(:all, :select => "id", :conditions => "some condition").map {|i| QC.enqueue("Invoice.destroy", i.id) }
158
+ require 'queue_classic'
159
+ QC::Conn.connection = ActiveRecord::Base.connection.raw_connection
443
160
  ```
444
161
 
445
- With the help of 20 workers I was able to destroy all of these records
446
- (preserving their callbacks) in a few minutes.
447
-
448
- ### Enqueueing batches of jobs
449
-
450
- Author: [@ryandotsmith (ace hacker)](https://twitter.com/#!/ryandotsmith)
451
-
452
- I have seen several cases where the application will enqueue jobs in batches. For instance, you may be sending
453
- 1,000 emails out. In this case, it would be foolish to do 1,000 individual transaction. Instead, you want to open
454
- a new transaction, enqueue all of your jobs and then commit the transaction. This will save tons of time in the
455
- database.
162
+ **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.
456
163
 
457
- To achieve this we will create a helper method:
164
+ ```ruby
165
+ require 'queue_classic'
458
166
 
459
- Now in your application code you can do something like:
167
+ class AddQueueClassic < ActiveRecord::Migration
168
+ def self.up
169
+ QC::Setup.create
170
+ end
460
171
 
461
- ```ruby
462
- QC::Conn.transaction do
463
- Account.all.each do |act|
464
- QC.enqueue("Emailer.send_notice", act.id)
172
+ def self.down
173
+ QC::Setup.drop
465
174
  end
466
175
  end
467
176
  ```
468
177
 
469
- ### Scheduling Jobs
470
-
471
- Author: [@ryandotsmith (ace hacker)](https://twitter.com/#!/ryandotsmith)
178
+ ### Rake Task Setup
472
179
 
473
- Many popular queueing solution provide support for scheduling. Features like
474
- Redis-Scheduler and the run_at column in DJ are very important to the web
475
- application developer. While queue_classic does not offer any sort of scheduling
476
- features, I do not discount the importance of the concept. However, it is my
477
- belief that a scheduler has no place in a queueing library, to that end I will
478
- show you how to schedule jobs using queue_classic and the clockwork gem.
479
-
480
- #### Example
481
-
482
- In this example, we are working with a system that needs to compute a sales
483
- summary at the end of each day. Lets say that we need to compute a summary for
484
- each sales employee in the system.
485
-
486
- Instead of enqueueing jobs with run_at set to 24hour intervals,
487
- we will define a clock process to enqueue the jobs at a specified
488
- time on each day. Let us create a file and call it clock.rb:
489
-
490
- ```ruby
491
- handler {|job| QC.enqueue(job)}
492
- every(1.day, "SalesSummaryGenerator.build_daily_report", :at => "01:00")
493
- ```
494
-
495
- To start our scheduler, we will use the clockwork bin:
180
+ Alternatively, you can use the Rake task to prepare your database.
496
181
 
497
182
  ```bash
498
- $ clockwork clock.rb
499
- ```
500
-
501
- Now each day at 01:00 we will be sending the build_daily_report message to our
502
- SalesSummaryGenerator class.
503
-
504
- I found this abstraction quite powerful and easy to understand. Like
505
- queue_classic, the clockwork gem is simple to understand and has 0 dependencies.
506
- In production, I create a Heroku process type called clock. This is typically
507
- what my Procfile looks like:
508
-
509
- ```
510
- worker: rake jobs:work
511
- clock: clockwork clock.rb
512
- ```
513
-
514
- ## Upgrading From Older Versions
515
-
516
- ### 1.X to 2.X
517
-
518
- #### Database Schema Changes
519
-
520
- * all queues are in 1 table with a q_name column
521
- * table includes a method column and an args column
522
-
523
- #### Producer Changes
524
-
525
- * initializing a Queue instance takes a column name instead of a table name
526
-
527
- #### Consumer Changes
528
-
529
- * all of the worker configuratoin is passed in through the initializer
530
- * rake task uses data from env vars to initialize a worker
531
-
532
- ### 0.2.X to 0.3.X
533
-
534
- * Deprecated QC.queue_length in favor of QC.length
535
- * Locking functions need to be loaded into database via `$ rake qc:load_functions`
536
-
537
- Also, the default queue is no longer named jobs,
538
- it is named queue_classic_jobs. Renaming the table is the only change that needs to be made.
183
+ # Creating the table and functions
184
+ $ bundle exec rake qc:create
539
185
 
540
- ```bash
541
- $ psql your_database -c "ALTER TABLE jobs RENAME TO queue_classic_jobs;"
186
+ # Dropping the table and functions
187
+ $ bundle exec rake qc:drop
542
188
  ```
543
189
 
544
- Or if you are using Rails' Migrations:
545
-
546
- ```ruby
547
- class RenameJobsTable < ActiveRecord::Migration
548
-
549
- def self.up
550
- rename_table :jobs, :queue_classic_jobs
551
- remove_index :jobs, :id
552
- add_index :queue_classic_jobs, :id
553
- end
554
-
555
- def self.down
556
- rename_table :queue_classic_jobs, :jobs
557
- remove_index :queue_classic_jobs, :id
558
- add_index :jobs, :id
559
- end
190
+ ## Configuration
560
191
 
561
- end
562
- ```
192
+ All configuration takes place in the form of environment vars. See [queue_classic.rb](https://github.com/ryandotsmith/queue_classic/blob/master/lib/queue_classic.rb#L23-62) for a list of options.
563
193
 
564
194
  ## Hacking on queue_classic
565
195
 
@@ -579,6 +209,12 @@ $ export QC_DATABASE_URL="postgres://username:pass@localhost/queue_classic_test"
579
209
  $ rake
580
210
  ```
581
211
 
582
- ### License
212
+ ## License
213
+
214
+ Copyright (C) 2010 Ryan Smith
215
+
216
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
217
+
218
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
583
219
 
584
- queue_classic is released under the MIT License (http://www.opensource.org/licenses/mit-license.php).
220
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/test/worker_test.rb CHANGED
@@ -28,7 +28,7 @@ class WorkerTest < QCTest
28
28
 
29
29
  def test_work
30
30
  QC.enqueue("TestObject.no_args")
31
- worker = TestWorker.new("default", 1, false, false, 1)
31
+ worker = TestWorker.new
32
32
  assert_equal(1, QC.count)
33
33
  worker.work
34
34
  assert_equal(0, QC.count)
@@ -37,14 +37,14 @@ class WorkerTest < QCTest
37
37
 
38
38
  def test_failed_job
39
39
  QC.enqueue("TestObject.not_a_method")
40
- worker = TestWorker.new("default", 1, false, false, 1)
40
+ worker = TestWorker.new
41
41
  worker.work
42
42
  assert_equal(1, worker.failed_count)
43
43
  end
44
44
 
45
45
  def test_work_with_no_args
46
46
  QC.enqueue("TestObject.no_args")
47
- worker = TestWorker.new("default", 1, false, false, 1)
47
+ worker = TestWorker.new
48
48
  r = worker.work
49
49
  assert_nil(r)
50
50
  assert_equal(0, worker.failed_count)
@@ -52,7 +52,7 @@ class WorkerTest < QCTest
52
52
 
53
53
  def test_work_with_one_arg
54
54
  QC.enqueue("TestObject.one_arg", "1")
55
- worker = TestWorker.new("default", 1, false, false, 1)
55
+ worker = TestWorker.new
56
56
  r = worker.work
57
57
  assert_equal("1", r)
58
58
  assert_equal(0, worker.failed_count)
@@ -60,7 +60,7 @@ class WorkerTest < QCTest
60
60
 
61
61
  def test_work_with_two_args
62
62
  QC.enqueue("TestObject.two_args", "1", 2)
63
- worker = TestWorker.new("default", 1, false, false, 1)
63
+ worker = TestWorker.new
64
64
  r = worker.work
65
65
  assert_equal(["1", 2], r)
66
66
  assert_equal(0, worker.failed_count)
@@ -69,7 +69,7 @@ class WorkerTest < QCTest
69
69
  def test_work_custom_queue
70
70
  p_queue = QC::Queue.new("priority_queue")
71
71
  p_queue.enqueue("TestObject.two_args", "1", 2)
72
- worker = TestWorker.new("priority_queue", 1, false, false, 1)
72
+ worker = TestWorker.new(q_name: "priority_queue")
73
73
  r = worker.work
74
74
  assert_equal(["1", 2], r)
75
75
  assert_equal(0, worker.failed_count)
@@ -78,7 +78,7 @@ class WorkerTest < QCTest
78
78
  def test_worker_listens_on_chan
79
79
  p_queue = QC::Queue.new("priority_queue")
80
80
  p_queue.enqueue("TestObject.two_args", "1", 2)
81
- worker = TestWorker.new("priority_queue", 1, false, true, 1)
81
+ worker = TestWorker.new(q_name: "priority_queue", listening_worker: true)
82
82
  r = worker.work
83
83
  assert_equal(["1", 2], r)
84
84
  assert_equal(0, worker.failed_count)
@@ -86,7 +86,7 @@ class WorkerTest < QCTest
86
86
 
87
87
  def test_worker_ueses_one_conn
88
88
  QC.enqueue("TestObject.no_args")
89
- worker = TestWorker.new("default", 1, false, false, 1)
89
+ worker = TestWorker.new
90
90
  worker.work
91
91
  assert_equal(
92
92
  1,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: queue_classic
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.5
4
+ version: 2.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: