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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ae7f3fa42909c77c861db2ef125fab2ba01205b
4
- data.tar.gz: a41224c39e1966276a5a66403b705426c5d3ff63
3
+ metadata.gz: 549f3341849e84bc2068f82448e39e98c0ef2051
4
+ data.tar.gz: 0746db47bbe386bd31e348b9d925985e8b185c04
5
5
  SHA512:
6
- metadata.gz: 4a0d7b6b9f80e240ce3ac76c3fd488870ca648439c66917eceec454c7595dbbac7193d17a91ea099b763f25e39f76038761521d799ae3eaf4b5f09ed5c64b995
7
- data.tar.gz: 92ff9563d1b98f03b096704524059a8d3f72d4ccd2626517af62b5067784641ff4eccb4f8864a9830fdc95ec06a3d8b0e5d6cc04c47656a247c2c0b40ad1d6df
6
+ metadata.gz: 5d9d9f8e660d294671ef3fd6d99f96fa2e092049920f72b754c62fa59857466978e6a7f10544fd35de9e52e6d72d99fb51dadb08cc2c0b8b002906f6628fe672
7
+ data.tar.gz: 3174b46493945c6b126323fa20dbaa1bf53bfd40a6ff07926a934ea4820e5e67276331d872d77e182fc57127e590f916a2042b48df9e9bee63965cd4d0f20f26
@@ -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....
@@ -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)
@@ -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
 
@@ -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
- # In the case no arguments are passed to the initializer,
10
- # the defaults are pulled from the environment variables.
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], args[:q_names], args[:top_bound])
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
- # Start a loop and work jobs indefinitely.
22
- # Call this method to start the worker.
23
- # This is the easiest way to start working jobs.
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
- # Call this method to stop the worker.
31
- # The worker may not stop immediately if the worker
32
- # is sleeping.
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
- # This method will tell the ruby process to FORK.
38
- # Define setup_child to hook into the forking process.
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
- # This method will lock a job & process the job.
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
- # Return a hash when a job is locked.
58
- # Caller responsible for deleting the job when finished.
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
- # Errors are delegated to the handle_failure method.
74
- # Also, this method will make the best attempt to delete the job
75
- # from the queue before returning.
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
- queue.delete(job[:id])
83
- log(:at => "delete_job", :job => job[:id])
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
- klass = eval(job[:method].split(".").first)
93
- message = job[:method].split(".").last
94
- klass.send(message, *args)
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, q_name, q_names, top_bound)
117
- name = q_name || QC::QUEUE
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
- * [Fuzzy-FIFO support](http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/Lock_Free.pdf).
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
- $ QUEUE="priority_queue, secondary_queue" bundle exec rake qc:work
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
- def handle_failure(job, e)
102
- FailedQueue.enqueue(job)
103
- end
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
- job = worker.lock_job
113
- Thread.new do
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", "2.2.3"
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
- Next you need to run the queue classic generator to create the database
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 expcted to happen.
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
 
@@ -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
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: 3.0.0beta
4
+ version: 3.0.0rc
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Smith (♠ ace hacker)