queue_classic 2.3.0beta → 3.0.0beta

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c38026847651ea48626d07f07fffd717a56bd13f
4
- data.tar.gz: 49543473a45f7b5ca199f5675fa82c8d97dafbfd
3
+ metadata.gz: 9ae7f3fa42909c77c861db2ef125fab2ba01205b
4
+ data.tar.gz: a41224c39e1966276a5a66403b705426c5d3ff63
5
5
  SHA512:
6
- metadata.gz: 67bac654f7508dd9104cacd47fc9721c5c4e28550b68c08e9bbd3d5ae870b80415172141a9766b5e341e9fb3b2cc72cd7a28bdc0719a8fcbbc7bba25353e5780
7
- data.tar.gz: 95335e3b6990954cb10f99d157fe7a8b175da9e27d09b25c57a1cd84eee15f62dc9ed607bba286eb3fb75a4785add817c2f3c9c5408895606a8f34dc6a35b022
6
+ metadata.gz: 4a0d7b6b9f80e240ce3ac76c3fd488870ca648439c66917eceec454c7595dbbac7193d17a91ea099b763f25e39f76038761521d799ae3eaf4b5f09ed5c64b995
7
+ data.tar.gz: 92ff9563d1b98f03b096704524059a8d3f72d4ccd2626517af62b5067784641ff4eccb4f8864a9830fdc95ec06a3d8b0e5d6cc04c47656a247c2c0b40ad1d6df
@@ -8,8 +8,7 @@ module QC
8
8
 
9
9
  namespace "queue_classic:install"
10
10
  self.source_paths << File.join(File.dirname(__FILE__), 'templates')
11
- desc 'Generates (but does not run) a migration to add ' +
12
- 'a queue_classic table.'
11
+ desc 'Generates (but does not run) a migration to add a queue_classic table.'
13
12
 
14
13
  def self.next_migration_number(dirname)
15
14
  next_migration_number = current_migration_number(dirname) + 1
@@ -17,8 +16,7 @@ module QC
17
16
  end
18
17
 
19
18
  def create_migration_file
20
- migration_template 'add_queue_classic.rb',
21
- 'db/migrate/add_queue_classic.rb'
19
+ migration_template 'add_queue_classic.rb', 'db/migrate/add_queue_classic.rb'
22
20
  end
23
21
  end
24
22
  end
@@ -0,0 +1,106 @@
1
+ require 'thread'
2
+ require 'uri'
3
+ require 'pg'
4
+
5
+ module QC
6
+ class ConnAdapter
7
+
8
+ attr_accessor :connection
9
+ def initialize(c=nil)
10
+ @connection = c.nil? ? establish_new : validate!(c)
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def execute(stmt, *params)
15
+ @mutex.synchronize do
16
+ QC.log(:at => "exec_sql", :sql => stmt.inspect)
17
+ begin
18
+ params = nil if params.empty?
19
+ r = @connection.exec(stmt, params)
20
+ result = []
21
+ r.each {|t| result << t}
22
+ result.length > 1 ? result : result.pop
23
+ rescue PGError => e
24
+ QC.log(:error => e.inspect)
25
+ @connection.reset
26
+ raise
27
+ end
28
+ end
29
+ end
30
+
31
+ def wait(time, *channels)
32
+ @mutex.synchronize do
33
+ listen_cmds = channels.map {|c| 'LISTEN "' + c + '"'}
34
+ @connection.exec(listen_cmds.join(';'))
35
+ wait_for_notify(time)
36
+ unlisten_cmds = channels.map {|c| 'UNLISTEN "' + c +'"'}
37
+ @connection.exec(unlisten_cmds.join(';'))
38
+ drain_notify
39
+ end
40
+ end
41
+
42
+ def disconnect
43
+ @mutex.synchronize do
44
+ begin
45
+ @connection.close
46
+ rescue => e
47
+ QC.log(:at => 'disconnect', :error => e.message)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def wait_for_notify(t)
55
+ Array.new.tap do |msgs|
56
+ @connection.wait_for_notify(t) {|event, pid, msg| msgs << msg}
57
+ end
58
+ end
59
+
60
+ def drain_notify
61
+ until @connection.notifies.nil?
62
+ QC.log(:at => "drain_notifications")
63
+ end
64
+ end
65
+
66
+ def validate!(c)
67
+ return c if c.is_a?(PG::Connection)
68
+ klass = c.class
69
+ err = "connection must be an instance of PG::Connection, but was #{c}"
70
+ raise(ArgumentError, err)
71
+ end
72
+
73
+ def establish_new
74
+ QC.log(:at => "establish_conn")
75
+ conn = PGconn.connect(*normalize_db_url(db_url))
76
+ if conn.status != PGconn::CONNECTION_OK
77
+ QC.log(:error => conn.error)
78
+ end
79
+ conn.exec("SET application_name = '#{QC::APP_NAME}'")
80
+ conn
81
+ end
82
+
83
+ def normalize_db_url(url)
84
+ host = url.host
85
+ host = host.gsub(/%2F/i, '/') if host
86
+
87
+ [
88
+ host, # host or percent-encoded socket path
89
+ url.port || 5432,
90
+ nil, '', #opts, tty
91
+ url.path.gsub("/",""), # database name
92
+ url.user,
93
+ url.password
94
+ ]
95
+ end
96
+
97
+ def db_url
98
+ return @db_url if @db_url
99
+ url = ENV["QC_DATABASE_URL"] ||
100
+ ENV["DATABASE_URL"] ||
101
+ raise(ArgumentError, "missing QC_DATABASE_URL or DATABASE_URL")
102
+ @db_url = URI.parse(url)
103
+ end
104
+
105
+ end
106
+ end
@@ -1,35 +1,35 @@
1
1
  require 'queue_classic'
2
- require 'queue_classic/conn'
2
+ require 'queue_classic/conn_adapter'
3
3
  require 'json'
4
4
 
5
5
  module QC
6
6
  class Queue
7
- TABLE_NAME = "queue_classic_jobs"
8
- # Each row in the table will have a column that
9
- # notes the queue.
10
- QUEUE_NAME = ENV["QUEUE"] || "default"
11
- # Set this to 1 for strict FIFO.
12
- TOP_BOUND = (ENV["QC_TOP_BOUND"] || 9).to_i
13
7
 
8
+ attr_reader :name, :top_bound
9
+ def initialize(name, top_bound=nil)
10
+ @name = name
11
+ @top_bound = top_bound || QC::TOP_BOUND
12
+ end
13
+
14
+ def conn_adapter=(a)
15
+ @adapter = a
16
+ end
14
17
 
15
- attr_reader :conn, :name, :top_bound
16
- def initialize(opts={})
17
- @conn = opts[:conn] || Conn.new
18
- @name = opts[:name] || QUEUE_NAME
19
- @top_bound = opts[:top_bound] || TOP_BOUND
18
+ def conn_adapter
19
+ @adapter ||= QC.default_conn_adapter
20
20
  end
21
21
 
22
22
  def enqueue(method, *args)
23
23
  QC.log_yield(:measure => 'queue.enqueue') do
24
24
  s="INSERT INTO #{TABLE_NAME} (q_name, method, args) VALUES ($1, $2, $3)"
25
- res = conn.execute(s, name, method, JSON.dump(args))
25
+ res = conn_adapter.execute(s, name, method, JSON.dump(args))
26
26
  end
27
27
  end
28
28
 
29
29
  def lock
30
30
  QC.log_yield(:measure => 'queue.lock') do
31
31
  s = "SELECT * FROM lock_head($1, $2)"
32
- if r = conn.execute(s, name, top_bound)
32
+ if r = conn_adapter.execute(s, name, top_bound)
33
33
  {:id => r["id"],
34
34
  :method => r["method"],
35
35
  :args => JSON.parse(r["args"])}
@@ -37,30 +37,23 @@ module QC
37
37
  end
38
38
  end
39
39
 
40
- def wait
41
- QC.log_yield(:measure => 'queue.wait') do
42
- conn.wait(name)
43
- end
44
- end
45
-
46
40
  def delete(id)
47
41
  QC.log_yield(:measure => 'queue.delete') do
48
- s = "DELETE FROM #{TABLE_NAME} where id = $1"
49
- conn.execute(s, id)
42
+ conn_adapter.execute("DELETE FROM #{TABLE_NAME} where id = $1", id)
50
43
  end
51
44
  end
52
45
 
53
46
  def delete_all
54
47
  QC.log_yield(:measure => 'queue.delete_all') do
55
48
  s = "DELETE FROM #{TABLE_NAME} WHERE q_name = $1"
56
- conn.execute(s, name)
49
+ conn_adapter.execute(s, name)
57
50
  end
58
51
  end
59
52
 
60
53
  def count
61
54
  QC.log_yield(:measure => 'queue.count') do
62
55
  s = "SELECT COUNT(*) FROM #{TABLE_NAME} WHERE q_name = $1"
63
- r = conn.execute(s, name)
56
+ r = conn_adapter.execute(s, name)
64
57
  r["count"].to_i
65
58
  end
66
59
  end
@@ -1,5 +1,3 @@
1
- require 'queue_classic/conn'
2
-
3
1
  module QC
4
2
  module Setup
5
3
  Root = File.expand_path("../..", File.dirname(__FILE__))
@@ -7,16 +5,18 @@ module QC
7
5
  CreateTable = File.join(Root, "/sql/create_table.sql")
8
6
  DropSqlFunctions = File.join(Root, "/sql/drop_ddl.sql")
9
7
 
10
- def self.create(conn=nil)
11
- conn ||= Conn.new
8
+ def self.create(c=nil)
9
+ conn = QC::ConnAdapter.new(c)
12
10
  conn.execute(File.read(CreateTable))
13
11
  conn.execute(File.read(SqlFunctions))
12
+ conn.disconnect if c.nil? #Don't close a conn we didn't create.
14
13
  end
15
14
 
16
- def self.drop(conn=nil)
17
- conn ||= Conn.new
15
+ def self.drop(c=nil)
16
+ conn = QC::ConnAdapter.new(c)
18
17
  conn.execute("DROP TABLE IF EXISTS queue_classic_jobs CASCADE")
19
18
  conn.execute(File.read(DropSqlFunctions))
19
+ conn.disconnect if c.nil? #Don't close a conn we didn't create.
20
20
  end
21
21
  end
22
22
  end
@@ -1,42 +1,31 @@
1
- if Rake::Task.task_defined? "qc:count"
2
- ActiveSupport::Deprecation.warn <<-MSG
3
- queue_classic Rake tasks are now loaded automatically for Rails applications.
4
- Loading the tasks yourself is deprecated. Please update your Rakefile and remove:
1
+ task :environment
5
2
 
6
- require 'queue_classic'
7
- require 'queue_classic/tasks'
8
-
9
- MSG
10
- else
11
- task :environment
3
+ namespace :jobs do
4
+ desc "Alias for qc:work"
5
+ task :work => "qc:work"
6
+ end
12
7
 
13
- namespace :jobs do
14
- desc "Alias for qc:work"
15
- task :work => "qc:work"
8
+ namespace :qc do
9
+ desc "Start a new worker for the (default or $QUEUE) queue"
10
+ task :work => :environment do
11
+ trap('INT') {exit}
12
+ trap('TERM') {@worker.stop}
13
+ @worker = QC::Worker.new
14
+ @worker.start
16
15
  end
17
16
 
18
- namespace :qc do
19
- desc "Start a new worker for the (default or $QUEUE) queue"
20
- task :work => :environment do
21
- trap('INT') {exit}
22
- trap('TERM') {@worker.stop}
23
- @worker = QC::Worker.new
24
- @worker.start
25
- end
26
-
27
- desc "Returns the number of jobs in the (default or QUEUE) queue"
28
- task :count => :environment do
29
- puts QC::Worker.new.queue.count
30
- end
17
+ desc "Returns the number of jobs in the (default or QUEUE) queue"
18
+ task :count => :environment do
19
+ puts QC::Worker.new.queue.count
20
+ end
31
21
 
32
- desc "Setup queue_classic tables and functions in database"
33
- task :create => :environment do
34
- QC::Setup.create
35
- end
22
+ desc "Setup queue_classic tables and functions in database"
23
+ task :create => :environment do
24
+ QC::Setup.create
25
+ end
36
26
 
37
- desc "Remove queue_classic tables and functions from database."
38
- task :drop => :environment do
39
- QC::Setup.drop
40
- end
27
+ desc "Remove queue_classic tables and functions from database."
28
+ task :drop => :environment do
29
+ QC::Setup.drop
41
30
  end
42
31
  end
@@ -1,28 +1,19 @@
1
- require 'thread'
2
1
  require 'queue_classic'
3
2
  require 'queue_classic/queue'
3
+ require 'queue_classic/conn_adapter'
4
4
 
5
5
  module QC
6
6
  class Worker
7
- # Set this variable if you wish for
8
- # the worker to fork a UNIX process for
9
- # each locked job. Remember to re-establish
10
- # any database connections. See the worker
11
- # for more details.
12
- FORK_WORKER = !ENV["QC_FORK_WORKER"].nil?
13
- # The worker is capable of processing many jobs at a time.
14
- # It uses FORK(2) to accomplish parallel processing. CONCURRENCY
15
- # is used to set an uppoer bound on how many worker processes can
16
- # run concurrently.
17
- CONCURRENCY = Integer(ENV["QC_CONCURRENCY"] || 1)
18
7
 
19
- attr_accessor :queue, :running
8
+ attr_accessor :queues, :running
20
9
  # In the case no arguments are passed to the initializer,
21
10
  # the defaults are pulled from the environment variables.
22
11
  def initialize(args={})
23
- @fork_worker = args[:fork_worker] || FORK_WORKER || (CONCURRENCY > 1)
24
- @limiter = SizedQueue.new(args[:concurrency] || CONCURRENCY)
25
- @queue = args[:queue] || QC.default_queue
12
+ @fork_worker = args[:fork_worker] || QC::FORK_WORKER
13
+ @wait_interval = args[:wait_interval] || QC::WAIT_TIME
14
+ @conn_adapter = ConnAdapter.new(args[:connection])
15
+ @queues = setup_queues(@conn_adapter,
16
+ args[:q_name], args[:q_names], args[:top_bound])
26
17
  log(args.merge(:at => "worker_initialized"))
27
18
  @running = true
28
19
  end
@@ -47,26 +38,17 @@ module QC
47
38
  # Define setup_child to hook into the forking process.
48
39
  # Using setup_child is good for re-establishing database connections.
49
40
  def fork_and_work
50
- # If the limiter is full, then we will block until space permits.
51
- @limiter.enq(1)
52
- Thread.new do
53
- begin
54
- cpid = fork {setup_child; work}
55
- log(:at => :fork, :pid => cpid)
56
- Process.wait(cpid)
57
- ensure
58
- # Once we are done with our work and our process has exited,
59
- # we can allow another process to run.
60
- @limiter.deq
61
- end
62
- end
41
+ cpid = fork {setup_child; work}
42
+ log(:at => :fork, :pid => cpid)
43
+ Process.wait(cpid)
63
44
  end
64
45
 
65
46
  # This method will lock a job & process the job.
66
47
  def work
67
- if job = lock_job
48
+ queue, job = lock_job
49
+ if queue && job
68
50
  QC.log_yield(:at => "work", :job => job[:id]) do
69
- process(job)
51
+ process(queue, job)
70
52
  end
71
53
  end
72
54
  end
@@ -78,23 +60,26 @@ module QC
78
60
  log(:at => "lock_job")
79
61
  job = nil
80
62
  while @running
81
- break if job = @queue.lock
82
- @queue.wait
63
+ @queues.each do |queue|
64
+ if job = queue.lock
65
+ return [queue, job]
66
+ end
67
+ end
68
+ @conn_adapter.wait(@wait_interval, *@queues.map {|q| q.name})
83
69
  end
84
- job
85
70
  end
86
71
 
87
72
  # A job is processed by evaluating the target code.
88
73
  # Errors are delegated to the handle_failure method.
89
74
  # Also, this method will make the best attempt to delete the job
90
75
  # from the queue before returning.
91
- def process(job)
76
+ def process(queue, job)
92
77
  begin
93
78
  call(job)
94
79
  rescue => e
95
80
  handle_failure(job, e)
96
81
  ensure
97
- @queue.delete(job[:id])
82
+ queue.delete(job[:id])
98
83
  log(:at => "delete_job", :job => job[:id])
99
84
  end
100
85
  end
@@ -126,5 +111,18 @@ module QC
126
111
  QC.log(data)
127
112
  end
128
113
 
114
+ private
115
+
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)
120
+ names.map do |name|
121
+ QC::Queue.new(name, top_bound).tap do |q|
122
+ q.conn_adapter = adapter
123
+ end
124
+ end
125
+ end
126
+
129
127
  end
130
128
  end
data/lib/queue_classic.rb CHANGED
@@ -1,4 +1,36 @@
1
1
  module QC
2
+ # You can use the APP_NAME to query for
3
+ # postgres related process information in the
4
+ # pg_stat_activity table.
5
+ APP_NAME = ENV["QC_APP_NAME"] || "queue_classic"
6
+
7
+ # Number of seconds to block on the listen chanel for new jobs.
8
+ WAIT_TIME = (ENV["QC_LISTEN_TIME"] || 5).to_i
9
+
10
+ # Why do you want to change the table name?
11
+ # Just deal with the default OK?
12
+ # If you do want to change this, you will
13
+ # need to update the PL/pgSQL lock_head() function.
14
+ # Come on. Don't do it.... Just stick with the default.
15
+ TABLE_NAME = "queue_classic_jobs"
16
+
17
+ # Each row in the table will have a column that
18
+ # notes the queue. You can point your workers
19
+ # at different queues but only one at a time.
20
+ QUEUE = ENV["QUEUE"] || "default"
21
+ QUEUES = ENV["QUEUES"] || []
22
+
23
+ # Set this to 1 for strict FIFO.
24
+ # There is nothing special about 9....
25
+ TOP_BOUND = (ENV["QC_TOP_BOUND"] || 9).to_i
26
+
27
+ # Set this variable if you wish for
28
+ # the worker to fork a UNIX process for
29
+ # each locked job. Remember to re-establish
30
+ # any database connections. See the worker
31
+ # for more details.
32
+ FORK_WORKER = !ENV["QC_FORK_WORKER"].nil?
33
+
2
34
  # Defer method calls on the QC module to the
3
35
  # default queue. This facilitates QC.enqueue()
4
36
  def self.method_missing(sym, *args, &block)
@@ -15,7 +47,13 @@ module QC
15
47
  end
16
48
 
17
49
  def self.default_queue
18
- @default_queue ||= Queue.new
50
+ @default_queue ||= begin
51
+ Queue.new(QUEUE)
52
+ end
53
+ end
54
+
55
+ def self.default_conn_adapter
56
+ @conn_adapter ||= ConnAdapter.new
19
57
  end
20
58
 
21
59
  def self.log_yield(data)
@@ -37,7 +75,7 @@ module QC
37
75
  if block_given?
38
76
  start = Time.now
39
77
  result = yield
40
- data.merge(:elapsed => Integer((Time.now - start)*1000))
78
+ data.merge(:elapsed => Integer((Time.now - t0)*1000))
41
79
  end
42
80
  data.reduce(out=String.new) do |s, tup|
43
81
  s << [tup.first, tup.last].join("=") << " "
data/readme.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # queue_classic
2
2
 
3
- v2.2.1
3
+ Stable: [v2.2.3](https://github.com/ryandotsmith/queue_classic/tree/v2.2.3)
4
+ Latest: v3.0.0beta
4
5
 
5
6
  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.
6
7
 
@@ -9,12 +10,13 @@ Features:
9
10
  * Leverage of PostgreSQL's listen/notify & row locking.
10
11
  * Support for multiple queues with heterogeneous workers.
11
12
  * JSON data format.
12
- * Concurrent job processing using forking workers.
13
- * [Reduced contention FIFO design](http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/Lock_Free.pdf).
13
+ * Forking workers.
14
+ * Workers can work multiple queues.
15
+ * [Fuzzy-FIFO support](http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/Lock_Free.pdf).
14
16
 
15
17
  Contents:
16
18
 
17
- * [Documentation](http://rubydoc.info/gems/queue_classic/2.2.1/frames)
19
+ * [Documentation](http://rubydoc.info/gems/queue_classic/2.2.3/frames)
18
20
  * [Usage](#usage)
19
21
  * [Setup](#setup)
20
22
  * [Configuration](#configuration)
@@ -56,17 +58,11 @@ p_queue.enqueue("Kernel.puts", ["hello", "world"])
56
58
 
57
59
  ### Working Jobs
58
60
 
59
- 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. Each approach provides a set of configuration options accessable through the processes' environment:
60
-
61
- * `$CONCURRENCY=1` - The number of child processes to run concurrently.
62
- * `$FORK_WORKER=false` - Fork on each job execution. Enabled if `$CONCURRENCY` > 1
63
- * `$QUEUE=default` - The name of the queue to process.
64
- * `$TOP_BOUND=9` - The section of the queue that is elgible for dequeue operations. Setting this value to 1 will ensure a strict FIFO ordering.
61
+ 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.
65
62
 
66
63
  #### Rake Task
67
64
 
68
- Require queue_classic in your Rakefile. If you are using Rails, the tasks will
69
- be loaded automatically.
65
+ Require queue_classic in your Rakefile.
70
66
 
71
67
  ```ruby
72
68
  require 'queue_classic'
@@ -79,10 +75,16 @@ Start the worker via the Rakefile.
79
75
  $ bundle exec rake qc:work
80
76
  ```
81
77
 
82
- Setup a worker to work a non-default queue while processing 4 jobs at a time.
78
+ Setup a worker to work a non-default queue.
79
+
80
+ ```bash
81
+ $ QUEUE="priority_queue" bundle exec rake qc:work
82
+ ```
83
+
84
+ Setup a worker to work multiple queues.
83
85
 
84
86
  ```bash
85
- $ CONCURRENCY=4 QUEUE="priority_queue" bundle exec rake qc:work
87
+ $ QUEUE="priority_queue, secondary_queue" bundle exec rake qc:work
86
88
  ```
87
89
 
88
90
  #### Custom Worker
@@ -135,7 +137,7 @@ Declare dependencies in Gemfile.
135
137
 
136
138
  ```ruby
137
139
  source "http://rubygems.org"
138
- gem "queue_classic", "2.2.1"
140
+ gem "queue_classic", "2.2.3"
139
141
  ```
140
142
 
141
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
@@ -1,5 +1,4 @@
1
1
  require File.expand_path("../helper.rb", __FILE__)
2
- Thread.abort_on_exception = true
3
2
 
4
3
  if ENV["QC_BENCHMARK"]
5
4
  class BenchmarkTest < QCTest
@@ -7,26 +6,31 @@ if ENV["QC_BENCHMARK"]
7
6
  def test_enqueue
8
7
  n = 10_000
9
8
  start = Time.now
10
- n.times {QC.enqueue("1.odd?")}
9
+ n.times do
10
+ QC.enqueue("1.odd?", [])
11
+ end
11
12
  assert_equal(n, QC.count)
13
+
12
14
  elapsed = Time.now - start
13
15
  assert_in_delta(4, elapsed, 1)
14
16
  end
15
17
 
16
18
  def test_dequeue
17
- queue = QC::Queue.new
18
- worker = QC::Worker.new(:concurrency => 4, :queue => queue)
19
- queue.delete_all
20
- n = 20
21
-
22
- n.times {queue.enqueue("puts", "hello")}
23
- assert_equal(n, queue.count)
19
+ worker = QC::Worker.new
20
+ worker.running = true
21
+ n = 10_000
22
+ n.times do
23
+ QC.enqueue("1.odd?", [])
24
+ end
25
+ assert_equal(n, QC.count)
24
26
 
25
27
  start = Time.now
26
- n.times.map {worker.fork_and_work}.map(&:join)
27
-
28
+ n.times do
29
+ worker.work
30
+ end
28
31
  elapsed = Time.now - start
29
- assert_equal(0, queue.count)
32
+
33
+ assert_equal(0, QC.count)
30
34
  assert_in_delta(10, elapsed, 3)
31
35
  end
32
36
 
data/test/helper.rb CHANGED
@@ -9,10 +9,19 @@ require "minitest/autorun"
9
9
 
10
10
  class QCTest < Minitest::Test
11
11
 
12
+ def setup
13
+ init_db
14
+ end
15
+
16
+ def teardown
17
+ QC.delete_all
18
+ end
19
+
12
20
  def init_db
13
- c = QC::Conn.new
14
- QC::Setup.drop(c)
15
- QC::Setup.create(c)
21
+ c = QC::ConnAdapter.new
22
+ c.execute("SET client_min_messages TO 'warning'")
23
+ QC::Setup.drop(c.connection)
24
+ QC::Setup.create(c.connection)
16
25
  c.execute(File.read('./test/helper.sql'))
17
26
  c.disconnect
18
27
  end
data/test/queue_test.rb CHANGED
@@ -2,13 +2,7 @@ require File.expand_path("../helper.rb", __FILE__)
2
2
 
3
3
  class QueueTest < QCTest
4
4
 
5
- def setup
6
- init_db
7
- end
8
-
9
- def teardown
10
- QC.conn.disconnect
11
- end
5
+ ResetError = Class.new(PGError)
12
6
 
13
7
  def test_enqueue
14
8
  QC.enqueue("Klass.method")
@@ -52,8 +46,8 @@ class QueueTest < QCTest
52
46
  end
53
47
 
54
48
  def test_delete_all_by_queue_name
55
- p_queue = QC::Queue.new(:name => "priority_queue")
56
- s_queue = QC::Queue.new(:name => "secondary_queue")
49
+ p_queue = QC::Queue.new("priority_queue")
50
+ s_queue = QC::Queue.new("secondary_queue")
57
51
  p_queue.enqueue("Klass.method")
58
52
  s_queue.enqueue("Klass.method")
59
53
  assert_equal(1, p_queue.count)
@@ -61,19 +55,27 @@ class QueueTest < QCTest
61
55
  p_queue.delete_all
62
56
  assert_equal(0, p_queue.count)
63
57
  assert_equal(1, s_queue.count)
64
- ensure
65
- p_queue.conn.disconnect
66
- s_queue.conn.disconnect
67
58
  end
68
59
 
69
60
  def test_queue_instance
70
- queue = QC::Queue.new(:name => "queue_classic_jobs")
61
+ queue = QC::Queue.new("queue_classic_jobs")
71
62
  queue.enqueue("Klass.method")
72
63
  assert_equal(1, queue.count)
73
64
  queue.delete(queue.lock[:id])
74
65
  assert_equal(0, queue.count)
75
- ensure
76
- queue.conn.disconnect
66
+ end
67
+
68
+ def test_repair_after_error
69
+ queue = QC::Queue.new("queue_classic_jobs")
70
+ queue.conn_adapter = QC::ConnAdapter.new
71
+ queue.enqueue("Klass.method")
72
+ assert_equal(1, queue.count)
73
+ conn = queue.conn_adapter.connection
74
+ def conn.exec(*args); raise(PGError); end
75
+ def conn.reset(*args); raise(ResetError) end
76
+ # We ensure that the reset method is called on the connection.
77
+ assert_raises(PG::Error, ResetError) {queue.enqueue("Klass.other_method")}
78
+ queue.conn_adapter.disconnect
77
79
  end
78
80
 
79
81
  def test_custom_default_queue
@@ -97,17 +99,16 @@ class QueueTest < QCTest
97
99
  end
98
100
 
99
101
  def test_enqueue_triggers_notify
100
- c = QC::Conn.new
101
- c.execute('LISTEN "' + QC::Queue::QUEUE_NAME + '"')
102
- c.send(:drain_notify)
103
- msgs = c.send(:wait_for_notify, 0.25)
102
+ adapter = QC.default_conn_adapter
103
+ adapter.execute('LISTEN "' + QC::QUEUE + '"')
104
+ adapter.send(:drain_notify)
105
+
106
+ msgs = adapter.send(:wait_for_notify, 0.25)
104
107
  assert_equal(0, msgs.length)
105
108
 
106
109
  QC.enqueue("Klass.method")
107
- msgs = c.send(:wait_for_notify, 0.25)
110
+ msgs = adapter.send(:wait_for_notify, 0.25)
108
111
  assert_equal(1, msgs.length)
109
- ensure
110
- c.disconnect
111
112
  end
112
113
 
113
114
  end
data/test/worker_test.rb CHANGED
@@ -14,26 +14,19 @@ end
14
14
  class TestWorker < QC::Worker
15
15
  attr_accessor :failed_count
16
16
 
17
- def initialize(*args)
18
- super(*args)
17
+ def initialize(args={})
18
+ super(args.merge(:connection => QC.default_conn_adapter.connection))
19
19
  @failed_count = 0
20
20
  end
21
21
 
22
22
  def handle_failure(job,e)
23
23
  @failed_count += 1
24
+ super
24
25
  end
25
26
  end
26
27
 
27
28
  class WorkerTest < QCTest
28
29
 
29
- def setup
30
- init_db
31
- end
32
-
33
- def teardown
34
- QC.conn.disconnect
35
- end
36
-
37
30
  def test_work
38
31
  QC.enqueue("TestObject.no_args")
39
32
  worker = TestWorker.new
@@ -53,7 +46,7 @@ class WorkerTest < QCTest
53
46
  def test_failed_job_is_logged
54
47
  output = capture_debug_output do
55
48
  QC.enqueue("TestObject.not_a_method")
56
- QC::Worker.new.work
49
+ TestWorker.new.work
57
50
  end
58
51
  expected_output = /lib=queue-classic at=handle_failure job={:id=>"\d+", :method=>"TestObject.not_a_method", :args=>\[\]} error=#<NoMethodError: undefined method `not_a_method' for TestObject:Module>/
59
52
  assert_match(expected_output, output, "=== debug output ===\n #{output}")
@@ -102,40 +95,76 @@ class WorkerTest < QCTest
102
95
  end
103
96
 
104
97
  def test_work_custom_queue
105
- p_queue = QC::Queue.new(:name=> "priority_queue")
98
+ p_queue = QC::Queue.new("priority_queue")
106
99
  p_queue.enqueue("TestObject.two_args", "1", 2)
107
- worker = TestWorker.new(:queue => p_queue)
100
+ worker = TestWorker.new(q_name: "priority_queue")
108
101
  r = worker.work
109
102
  assert_equal(["1", 2], r)
110
103
  assert_equal(0, worker.failed_count)
111
- worker.stop
112
- p_queue.conn.disconnect
113
104
  end
114
105
 
115
106
  def test_worker_listens_on_chan
116
- p_queue = QC::Queue.new(:name => "priority_queue")
107
+ p_queue = QC::Queue.new("priority_queue")
108
+ # Use a new connection because the default connection
109
+ # will be locked by the sleeping worker.
110
+ p_queue.conn_adapter = QC::ConnAdapter.new
111
+ # The wait interval is extreme to demonstrate
112
+ # that the worker is in fact being activated by a NOTIFY.
113
+ worker = TestWorker.new(:q_name => "priority_queue", :wait_interval => 100)
114
+ t = Thread.new do
115
+ r = worker.work
116
+ assert_equal(["1", 2], r)
117
+ assert_equal(0, worker.failed_count)
118
+ end
119
+ sleep(0.5) #Give the thread some time to start the worker.
117
120
  p_queue.enqueue("TestObject.two_args", "1", 2)
118
- worker = TestWorker.new(
119
- :queue => p_queue,
120
- :listening_worker => true)
121
- r = worker.work
122
- assert_equal(["1", 2], r)
123
- assert_equal(0, worker.failed_count)
124
- worker.stop
125
- p_queue.conn.disconnect
121
+ p_queue.conn_adapter.disconnect
122
+ t.join
126
123
  end
127
124
 
128
125
  def test_worker_ueses_one_conn
129
126
  QC.enqueue("TestObject.no_args")
130
127
  worker = TestWorker.new
131
128
  worker.work
132
- s = "SELECT * from pg_stat_activity where datname=current_database()"
133
- s += " and application_name = '#{QC::Conn::APP_NAME}'"
134
- res = QC.conn.execute(s)
135
- num_conns = res.length if res.class == Array
136
- num_conns = 1 if res.class == Hash
137
- assert_equal(1, num_conns,
138
- "Multiple connections found -- are there open connections to" +
139
- " #{QC::Conf.db_url} in other terminals?\n res=#{res}")
129
+ assert_equal(
130
+ 1,
131
+ QC.default_conn_adapter.execute("SELECT count(*) from pg_stat_activity where datname = current_database()")["count"].to_i,
132
+ "Multiple connections found -- are there open connections to #{ QC.default_conn_adapter.send(:db_url) } in other terminals?"
133
+ )
134
+ end
135
+
136
+ def test_worker_can_work_multiple_queues
137
+ p_queue = QC::Queue.new("priority_queue")
138
+ p_queue.enqueue("TestObject.two_args", "1", 2)
139
+
140
+ s_queue = QC::Queue.new("secondary_queue")
141
+ s_queue.enqueue("TestObject.two_args", "1", 2)
142
+
143
+ worker = TestWorker.new(:q_names => ["priority_queue", "secondary_queue"])
144
+
145
+ 2.times do
146
+ r = worker.work
147
+ assert_equal(["1", 2], r)
148
+ assert_equal(0, worker.failed_count)
149
+ end
150
+ end
151
+
152
+ def test_worker_works_multiple_queue_left_to_right
153
+ l_queue = QC::Queue.new("left_queue")
154
+ r_queue = QC::Queue.new("right_queue")
155
+
156
+ 3.times { l_queue.enqueue("TestObject.two_args", "1", 2) }
157
+ 3.times { r_queue.enqueue("TestObject.two_args", "1", 2) }
158
+
159
+ worker = TestWorker.new(:q_names => ["left_queue", "right_queue"])
160
+
161
+ worker.work
162
+ assert_equal(2, l_queue.count)
163
+ assert_equal(3, r_queue.count)
164
+
165
+ worker.work
166
+ assert_equal(1, l_queue.count)
167
+ assert_equal(3, r_queue.count)
140
168
  end
169
+
141
170
  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: 2.3.0beta
4
+ version: 3.0.0beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Smith (♠ ace hacker)
@@ -16,18 +16,18 @@ dependencies:
16
16
  requirements:
17
17
  - - ~>
18
18
  - !ruby/object:Gem::Version
19
- version: 0.16.0
19
+ version: 0.17.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ~>
25
25
  - !ruby/object:Gem::Version
26
- version: 0.16.0
26
+ version: 0.17.0
27
27
  description: queue_classic is a queueing library for Ruby apps. (Rails, Sinatra, Etc...)
28
28
  queue_classic features asynchronous job polling, database maintained locks and no
29
29
  ridiculous dependencies. As a matter of fact, queue_classic only requires pg.
30
- email: ryan@heroku.com
30
+ email: r@32k.io
31
31
  executables: []
32
32
  extensions: []
33
33
  extra_rdoc_files: []
@@ -38,8 +38,7 @@ files:
38
38
  - sql/drop_ddl.sql
39
39
  - lib/generators/queue_classic/install_generator.rb
40
40
  - lib/generators/queue_classic/templates/add_queue_classic.rb
41
- - lib/queue_classic/conf.rb
42
- - lib/queue_classic/conn.rb
41
+ - lib/queue_classic/conn_adapter.rb
43
42
  - lib/queue_classic/queue.rb
44
43
  - lib/queue_classic/railtie.rb
45
44
  - lib/queue_classic/setup.rb
@@ -47,7 +46,6 @@ files:
47
46
  - lib/queue_classic/worker.rb
48
47
  - lib/queue_classic.rb
49
48
  - test/benchmark_test.rb
50
- - test/conf_test.rb
51
49
  - test/helper.rb
52
50
  - test/queue_test.rb
53
51
  - test/worker_test.rb
@@ -74,9 +72,8 @@ rubyforge_project:
74
72
  rubygems_version: 2.0.3
75
73
  signing_key:
76
74
  specification_version: 4
77
- summary: postgres backed queue
75
+ summary: Simple, efficient worker queue for Ruby & PostgreSQL.
78
76
  test_files:
79
77
  - test/benchmark_test.rb
80
- - test/conf_test.rb
81
78
  - test/queue_test.rb
82
79
  - test/worker_test.rb
@@ -1,33 +0,0 @@
1
- require 'uri'
2
-
3
- module QC
4
- module Conf
5
-
6
- def self.env(k); ENV[k]; end
7
- def self.env!(k); env(k) || raise("Must set #{k}."); end
8
-
9
- def self.debug?
10
- !env('DEBUG').nil?
11
- end
12
-
13
- def self.db_url
14
- url = env("QC_DATABASE_URL") ||
15
- env("DATABASE_URL") ||
16
- raise(ArgumentError, "Must set QC_DATABASE_URL or DATABASE_URL.")
17
- URI.parse(url)
18
- end
19
-
20
- def self.normalized_db_url(url=nil)
21
- url ||= db_url
22
- host = url.host
23
- host = host.gsub(/%2F/i, '/') if host
24
- [host, # host or percent-encoded socket path
25
- url.port || 5432,
26
- nil, '', #opts, tty
27
- url.path.gsub("/",""), # database name
28
- url.user,
29
- url.password]
30
- end
31
-
32
- end
33
- end
@@ -1,106 +0,0 @@
1
- require 'queue_classic/conf'
2
- require 'thread'
3
- require 'uri'
4
- require 'pg'
5
-
6
- module QC
7
- class Conn
8
- # Number of seconds to block on the listen chanel for new jobs.
9
- WAIT_TIME = (ENV["QC_LISTEN_TIME"] || 5).to_i
10
- # You can use the APP_NAME to query for
11
- # postgres related process information in the
12
- # pg_stat_activity table.
13
- APP_NAME = ENV["QC_APP_NAME"] || "queue_classic"
14
-
15
-
16
- def self.connect
17
- QC.log(:at => "establish_conn")
18
- conn = PGconn.connect(*Conf.normalized_db_url)
19
- if conn.status != PGconn::CONNECTION_OK
20
- log(:error => conn.error)
21
- end
22
- if !Conf.debug?
23
- conn.exec("SET client_min_messages TO 'warning'")
24
- end
25
- conn.exec("SET application_name = '#{APP_NAME}'")
26
- conn
27
- end
28
-
29
- def initialize
30
- @c = self.class.connect
31
- @max_attempts = 2
32
- end
33
-
34
- def execute(stmt, *params)
35
- QC.log(:measure => "conn.exec", :sql => stmt.inspect) do
36
- with_retry(@max_attempts) do
37
- params = nil if params.empty?
38
- r = @c.exec(stmt, params)
39
- result = []
40
- r.each {|t| result << t}
41
- result.length > 1 ? result : result.pop
42
- end
43
- end
44
- end
45
-
46
- def wait(chan)
47
- with_retry(@max_attempts) do
48
- execute('LISTEN "' + chan + '"')
49
- wait_for_notify(WAIT_TIME)
50
- execute('UNLISTEN "' + chan + '"')
51
- drain_notify
52
- end
53
- end
54
-
55
- def reconnect
56
- disconnect
57
- @c = self.class.connect
58
- end
59
-
60
- def disconnect
61
- begin @c && @c.finish
62
- ensure @c = nil
63
- end
64
- end
65
-
66
- def abort_open_transaction
67
- if @c.transaction_status != PGconn::PQTRANS_IDLE
68
- @c.exec('ROLLBACK')
69
- end
70
- end
71
-
72
- private
73
-
74
- def with_retry(n)
75
- completed = false
76
- attempts = 0
77
- result = nil
78
- last_error = nil
79
- until completed || attempts == n
80
- attempts += 1
81
- begin
82
- result = yield
83
- completed = true
84
- rescue => e
85
- QC.log(:error => e.class, :at => 'conn-retry', :attempts => attempts)
86
- last_error = e
87
- reconnect
88
- end
89
- end
90
- completed ? result : raise(last_error)
91
- end
92
-
93
- def wait_for_notify(t)
94
- Array.new.tap do |msgs|
95
- @c.wait_for_notify(t) {|event, pid, msg| msgs << msg}
96
- end
97
- end
98
-
99
- def drain_notify
100
- until @c.notifies.nil?
101
- QC.log(:at => "drain_notifications")
102
- end
103
- end
104
-
105
- end
106
- end
data/test/conf_test.rb DELETED
@@ -1,26 +0,0 @@
1
- require File.expand_path("../helper.rb", __FILE__)
2
-
3
- class ConfTest < QCTest
4
-
5
- def setup
6
- init_db
7
- end
8
-
9
- def test_extracts_the_segemnts_to_connect
10
- database_url = "postgres://ryan:secret@localhost:1234/application_db"
11
- normalized = QC::Conf.normalized_db_url(URI.parse(database_url))
12
- assert_equal ["localhost",
13
- 1234,
14
- nil, "",
15
- "application_db",
16
- "ryan",
17
- "secret"], normalized
18
- end
19
-
20
- def test_regression_database_url_without_host
21
- database_url = "postgres:///my_db"
22
- normalized = QC::Conf.normalized_db_url(URI.parse(database_url))
23
- assert_equal [nil, 5432, nil, "", "my_db", nil, nil], normalized
24
- end
25
-
26
- end