queue_classic 2.3.0beta → 3.0.0beta

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: 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