queue_classic 2.1.4 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  require "pg"
2
2
  require "uri"
3
- require "multi_json"
3
+ require "json"
4
4
 
5
5
  require "queue_classic/conn"
6
6
  require "queue_classic/queries"
@@ -16,11 +16,8 @@ module QC
16
16
 
17
17
  # You can use the APP_NAME to query for
18
18
  # postgres related process information in the
19
- # pg_stat_activity table. Don't set this unless
20
- # you are using PostgreSQL > 9.0
21
- if APP_NAME = ENV["QC_APP_NAME"]
22
- Conn.execute("SET application_name = '#{APP_NAME}'")
23
- end
19
+ # pg_stat_activity table.
20
+ APP_NAME = ENV["QC_APP_NAME"] || "queue_classic"
24
21
 
25
22
  # Why do you want to change the table name?
26
23
  # Just deal with the default OK?
@@ -66,6 +63,10 @@ module QC
66
63
  default_queue.respond_to?(method_name)
67
64
  end
68
65
 
66
+ def self.default_queue=(queue)
67
+ @default_queue = queue
68
+ end
69
+
69
70
  def self.default_queue
70
71
  @default_queue ||= begin
71
72
  Queue.new(QUEUE)
@@ -91,7 +92,7 @@ module QC
91
92
  if block_given?
92
93
  start = Time.now
93
94
  result = yield
94
- data.merge(:elapsed => Time.now - start)
95
+ data.merge(:elapsed => Integer((Time.now - t0)*1000))
95
96
  end
96
97
  data.reduce(out=String.new) do |s, tup|
97
98
  s << [tup.first, tup.last].join("=") << " "
@@ -99,5 +100,4 @@ module QC
99
100
  puts(out) if ENV["DEBUG"]
100
101
  return result
101
102
  end
102
-
103
103
  end
@@ -27,26 +27,11 @@ module QC
27
27
  execute('NOTIFY "' + chan + '"') #quotes matter
28
28
  end
29
29
 
30
- def listen(chan)
31
- log(:at => "LISTEN")
32
- execute('LISTEN "' + chan + '"') #quotes matter
33
- end
34
-
35
- def unlisten(chan)
36
- log(:at => "UNLISTEN")
37
- execute('UNLISTEN "' + chan + '"') #quotes matter
38
- end
39
-
40
- def drain_notify
41
- until connection.notifies.nil?
42
- log(:at => "drain_notifications")
43
- end
44
- end
45
-
46
- def wait_for_notify(t)
47
- connection.wait_for_notify(t) do |event, pid, msg|
48
- log(:at => "received_notification")
49
- end
30
+ def wait(chan, t)
31
+ listen(chan)
32
+ wait_for_notify(t)
33
+ unlisten(chan)
34
+ drain_notify
50
35
  end
51
36
 
52
37
  def transaction
@@ -69,7 +54,7 @@ module QC
69
54
  end
70
55
 
71
56
  def connection=(connection)
72
- unless connection.instance_of? PG::Connection
57
+ unless connection.is_a? PG::Connection
73
58
  c = connection.class
74
59
  err = "connection must be an instance of PG::Connection, but was #{c}"
75
60
  raise(ArgumentError, err)
@@ -85,20 +70,28 @@ module QC
85
70
 
86
71
  def connect
87
72
  log(:at => "establish_conn")
88
- conn = PGconn.connect(
89
- db_url.host.gsub(/%2F/i, '/'), # host or percent-encoded socket path
90
- db_url.port || 5432,
91
- nil, '', #opts, tty
92
- db_url.path.gsub("/",""), # database name
93
- db_url.user,
94
- db_url.password
95
- )
73
+ conn = PGconn.connect(*normalize_db_url(db_url))
96
74
  if conn.status != PGconn::CONNECTION_OK
97
75
  log(:error => conn.error)
98
76
  end
77
+ conn.exec("SET application_name = '#{QC::APP_NAME}'")
99
78
  conn
100
79
  end
101
80
 
81
+ def normalize_db_url(url)
82
+ host = url.host
83
+ host = host.gsub(/%2F/i, '/') if host
84
+
85
+ [
86
+ host, # host or percent-encoded socket path
87
+ url.port || 5432,
88
+ nil, '', #opts, tty
89
+ url.path.gsub("/",""), # database name
90
+ url.user,
91
+ url.password
92
+ ]
93
+ end
94
+
102
95
  def db_url
103
96
  return @db_url if @db_url
104
97
  url = ENV["QC_DATABASE_URL"] ||
@@ -107,9 +100,33 @@ module QC
107
100
  @db_url = URI.parse(url)
108
101
  end
109
102
 
103
+ private
104
+
110
105
  def log(msg)
111
106
  QC.log(msg)
112
107
  end
113
108
 
109
+ def listen(chan)
110
+ log(:at => "LISTEN")
111
+ execute('LISTEN "' + chan + '"') #quotes matter
112
+ end
113
+
114
+ def unlisten(chan)
115
+ log(:at => "UNLISTEN")
116
+ execute('UNLISTEN "' + chan + '"') #quotes matter
117
+ end
118
+
119
+ def wait_for_notify(t)
120
+ connection.wait_for_notify(t) do |event, pid, msg|
121
+ log(:at => "received_notification")
122
+ end
123
+ end
124
+
125
+ def drain_notify
126
+ until connection.notifies.nil?
127
+ log(:at => "drain_notifications")
128
+ end
129
+ end
130
+
114
131
  end
115
132
  end
@@ -5,7 +5,7 @@ module QC
5
5
  def insert(q_name, method, args, chan=nil)
6
6
  QC.log_yield(:action => "insert_job") do
7
7
  s = "INSERT INTO #{TABLE_NAME} (q_name, method, args) VALUES ($1, $2, $3)"
8
- res = Conn.execute(s, q_name, method, MultiJson.encode(args))
8
+ res = Conn.execute(s, q_name, method, JSON.dump(args))
9
9
  Conn.notify(chan) if chan
10
10
  end
11
11
  end
@@ -16,7 +16,7 @@ module QC
16
16
  {
17
17
  :id => r["id"],
18
18
  :method => r["method"],
19
- :args => MultiJson.decode(r["args"])
19
+ :args => JSON.parse(r["args"])
20
20
  }
21
21
  end
22
22
  end
@@ -19,7 +19,7 @@ namespace :qc do
19
19
  puts QC::Worker.new.queue.count
20
20
  end
21
21
 
22
- desc "Setup queue_classic tables and funtions in database"
22
+ desc "Setup queue_classic tables and functions in database"
23
23
  task :create => :environment do
24
24
  QC::Setup.create
25
25
  end
@@ -1,7 +1,7 @@
1
1
  module QC
2
2
  class Worker
3
3
 
4
- attr_reader :queue
4
+ attr_accessor :queue, :running
5
5
  # In the case no arguments are passed to the initializer,
6
6
  # the defaults are pulled from the environment variables.
7
7
  def initialize(args={})
@@ -113,21 +113,18 @@ module QC
113
113
  def wait(t)
114
114
  if @listening_worker
115
115
  log(:at => "listen_wait", :wait => t)
116
- Conn.listen(@queue.chan)
117
- Conn.wait_for_notify(t)
118
- Conn.unlisten(@queue.chan)
119
- Conn.drain_notify
120
- log(:at => "finished_listening")
116
+ Conn.wait(@queue.chan)
121
117
  else
122
118
  log(:at => "sleep_wait", :wait => t)
123
119
  Kernel.sleep(t)
124
120
  end
121
+ log(:at => "finished_listening")
125
122
  end
126
123
 
127
124
  # This method will be called when an exception
128
125
  # is raised during the execution of the job.
129
126
  def handle_failure(job,e)
130
- log(:at => "handle_failure")
127
+ log(:at => "handle_failure", :job => job, :error => e.inspect)
131
128
  end
132
129
 
133
130
  # This method should be overriden if
data/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # queue_classic
2
2
 
3
- v2.1.4
3
+ v2.2.0
4
4
 
5
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.
6
6
 
@@ -14,10 +14,11 @@ Features:
14
14
 
15
15
  Contents:
16
16
 
17
- * [Documentation](http://rubydoc.info/gems/queue_classic/2.1.4/frames)
17
+ * [Documentation](http://rubydoc.info/gems/queue_classic/2.2.0/frames)
18
18
  * [Usage](#usage)
19
19
  * [Setup](#setup)
20
20
  * [Configuration](#configuration)
21
+ * [Support](#support)
21
22
  * [Hacking](#hacking-on-queue_classic)
22
23
  * [License](#license)
23
24
 
@@ -30,6 +31,8 @@ There are 2 ways to use queue_classic.
30
31
 
31
32
  ### Producing Jobs
32
33
 
34
+ The first argument is a string which represents a ruby object and a method name. The second argument(s) will be passed along as arguments to the method invocation defined by the first argument. The set of arguments will be encoded as JSON in the database.
35
+
33
36
  ```ruby
34
37
  # This method has no arguments.
35
38
  QC.enqueue("Time.now")
@@ -51,12 +54,6 @@ p_queue = QC::Queue.new("priority_queue")
51
54
  p_queue.enqueue("Kernel.puts", ["hello", "world"])
52
55
  ```
53
56
 
54
- QueueClassic uses [MultiJSON](https://github.com/intridea/multi_json) to encode the job's payload.
55
-
56
- ```ruby
57
- MultiJson.dump({"test" => "test"})
58
- ```
59
-
60
57
  ### Working Jobs
61
58
 
62
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.
@@ -90,9 +87,6 @@ This example is probably not production ready; however, it serves as an example
90
87
  require 'timeout'
91
88
  require 'queue_classic'
92
89
 
93
- trap('INT') {exit}
94
- trap('TERM') {worker.stop}
95
-
96
90
  FailedQueue = QC::Queue.new("failed_jobs")
97
91
 
98
92
  class MyWorker < QC::Worker
@@ -103,6 +97,9 @@ end
103
97
 
104
98
  worker = MyWorker.new(max_attempts: 10, listening_worker: true)
105
99
 
100
+ trap('INT') {exit}
101
+ trap('TERM') {worker.stop}
102
+
106
103
  loop do
107
104
  job = worker.lock_job
108
105
  Thread.new do
@@ -132,7 +129,7 @@ Declare dependencies in Gemfile.
132
129
 
133
130
  ```ruby
134
131
  source "http://rubygems.org"
135
- gem "queue_classic", "2.1.4"
132
+ gem "queue_classic", "2.2.0"
136
133
  ```
137
134
 
138
135
  Require these files in your Rakefile so that you can run `rake qc:work`.
@@ -181,6 +178,13 @@ $ bundle exec rake qc:drop
181
178
 
182
179
  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.
183
180
 
181
+ ## JSON
182
+
183
+ If you are running PostgreSQL 9.2 or higher, queue_classic will use the [json](http://www.postgresql.org/docs/9.2/static/datatype-json.html) datatype for storing arguments. Versions 9.1 and lower will use the 'text' column. If you have installed queue_classic prior to version 2.1.4 and are running PostgreSQL >= 9.2, run the following to switch to using the json type:
184
+ ```
185
+ alter table queue_classic_jobs alter column args type json using (args::json);
186
+ ```
187
+
184
188
  ## Logging
185
189
 
186
190
  By default queue_classic does not talk very much.
@@ -191,8 +195,23 @@ you can enable the debug output by setting the `DEBUG` environment variable:
191
195
  export DEBUG="true"
192
196
  ```
193
197
 
198
+ ## Support
199
+
200
+ If you think you have found a bug, feel free to open an issue. Use the following template for the new issue:
201
+
202
+ 1. List Versions: Ruby, PostgreSQL, queue_classic.
203
+ 2. Define what you would have expcted to happen.
204
+ 3. List what actually happened.
205
+ 4. Provide sample codes & commands which will reproduce the problem.
206
+
207
+ If you have general questions about how to use queue_classic, send a message to the mailing list:
208
+
209
+ https://groups.google.com/d/forum/queue_classic
210
+
194
211
  ## Hacking on queue_classic
195
212
 
213
+ [![Build Status](https://drone.io/github.com/ryandotsmith/queue_classic/status.png)](https://drone.io/github.com/ryandotsmith/queue_classic/latest)
214
+
196
215
  ### Dependencies
197
216
 
198
217
  * Ruby 1.9.2 (tests work in 1.8.7 but compatibility is not guaranteed or supported)
@@ -1,9 +1,19 @@
1
+ do $$ begin
2
+
1
3
  CREATE TABLE queue_classic_jobs (
2
4
  id bigserial PRIMARY KEY,
3
- q_name varchar(255),
4
- method varchar(255),
5
- args text,
5
+ q_name text not null check (length(q_name) > 0),
6
+ method text not null check (length(method) > 0),
7
+ args text not null,
6
8
  locked_at timestamptz
7
9
  );
8
10
 
11
+ -- If json type is available, use it for the args column.
12
+ perform * from pg_type where typname = 'json';
13
+ if found then
14
+ alter table queue_classic_jobs alter column args type json using (args::json);
15
+ end if;
16
+
17
+ end $$ language plpgsql;
18
+
9
19
  CREATE INDEX idx_qc_on_name_only_unlocked ON queue_classic_jobs (q_name, id) WHERE locked_at IS NULL;
@@ -0,0 +1,38 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ if ENV["QC_BENCHMARK"]
4
+ class BenchmarkTest < QCTest
5
+
6
+ def test_enqueue
7
+ n = 10_000
8
+ start = Time.now
9
+ n.times do
10
+ QC.enqueue("1.odd?", [])
11
+ end
12
+ assert_equal(n, QC.count)
13
+
14
+ elapsed = Time.now - start
15
+ assert_in_delta(4, elapsed, 1)
16
+ end
17
+
18
+ def test_dequeue
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)
26
+
27
+ start = Time.now
28
+ n.times do
29
+ worker.work
30
+ end
31
+ elapsed = Time.now - start
32
+
33
+ assert_equal(0, QC.count)
34
+ assert_in_delta(10, elapsed, 3)
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ class ConnTest < QCTest
4
+
5
+ def test_extracts_the_segemnts_to_connect
6
+ database_url = "postgres://ryan:secret@localhost:1234/application_db"
7
+ normalized = QC::Conn.normalize_db_url(URI.parse(database_url))
8
+ assert_equal ["localhost",
9
+ 1234,
10
+ nil, "",
11
+ "application_db",
12
+ "ryan",
13
+ "secret"], normalized
14
+ end
15
+
16
+ def test_regression_database_url_without_host
17
+ database_url = "postgres:///my_db"
18
+ normalized = QC::Conn.normalize_db_url(URI.parse(database_url))
19
+ assert_equal [nil, 5432, nil, "", "my_db", nil, nil], normalized
20
+ end
21
+
22
+ end
@@ -4,10 +4,10 @@ $: << File.expand_path("test")
4
4
  ENV["DATABASE_URL"] ||= "postgres:///queue_classic_test"
5
5
 
6
6
  require "queue_classic"
7
- require "minitest/unit"
8
- MiniTest::Unit.autorun
7
+ require "stringio"
8
+ require "minitest/autorun"
9
9
 
10
- class QCTest < MiniTest::Unit::TestCase
10
+ class QCTest < Minitest::Test
11
11
 
12
12
  def setup
13
13
  init_db
@@ -47,7 +47,19 @@ BEGIN
47
47
  END;
48
48
  $$;
49
49
  EOS
50
- QC::Conn.disconnect
50
+ end
51
+
52
+ def capture_debug_output
53
+ original_debug = ENV['DEBUG']
54
+ original_stdout = $stdout
55
+
56
+ ENV['DEBUG'] = "true"
57
+ $stdout = StringIO.new
58
+ yield
59
+ $stdout.string
60
+ ensure
61
+ ENV['DEBUG'] = original_debug
62
+ $stdout = original_stdout
51
63
  end
52
64
 
53
65
  end
@@ -72,7 +72,7 @@ class QueueTest < QCTest
72
72
  def connection.exec(*args)
73
73
  raise PGError
74
74
  end
75
- assert_raises(PG::Error) { queue.enqueue("Klass.other_method") }
75
+ assert_raises(PG::Error) { queue.enqueue("Klass.other_method") }
76
76
  assert_equal(1, queue.count)
77
77
  queue.enqueue("Klass.other_method")
78
78
  assert_equal(2, queue.count)
@@ -80,4 +80,24 @@ class QueueTest < QCTest
80
80
  QC::Conn.disconnect
81
81
  assert false, "Expected to QC repair after connection error"
82
82
  end
83
+
84
+ def test_custom_default_queue
85
+ queue_class = Class.new do
86
+ attr_accessor :jobs
87
+ def enqueue(method, *args)
88
+ @jobs ||= []
89
+ @jobs << method
90
+ end
91
+ end
92
+
93
+ queue_instance = queue_class.new
94
+ QC.default_queue = queue_instance
95
+
96
+ QC.enqueue("Klass.method1")
97
+ QC.enqueue("Klass.method2")
98
+
99
+ assert_equal ["Klass.method1", "Klass.method2"], queue_instance.jobs
100
+ ensure
101
+ QC.default_queue = nil
102
+ end
83
103
  end
@@ -42,6 +42,33 @@ class WorkerTest < QCTest
42
42
  assert_equal(1, worker.failed_count)
43
43
  end
44
44
 
45
+ def test_failed_job_is_logged
46
+ output = capture_debug_output do
47
+ QC.enqueue("TestObject.not_a_method")
48
+ QC::Worker.new.work
49
+ end
50
+ 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>/
51
+ assert_match(expected_output, output, "=== debug output ===\n #{output}")
52
+ end
53
+
54
+ def test_log_yield
55
+ output = capture_debug_output do
56
+ QC.log_yield(:action => "test") do
57
+ 0 == 1
58
+ end
59
+ end
60
+ expected_output = /lib=queue-classic action=test elapsed=\d*/
61
+ assert_match(expected_output, output, "=== debug output ===\n #{output}")
62
+ end
63
+
64
+ def test_log
65
+ output = capture_debug_output do
66
+ QC.log(:action => "test")
67
+ end
68
+ expected_output = /lib=queue-classic action=test/
69
+ assert_match(expected_output, output, "=== debug output ===\n #{output}")
70
+ end
71
+
45
72
  def test_work_with_no_args
46
73
  QC.enqueue("TestObject.no_args")
47
74
  worker = TestWorker.new
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.1.4
4
+ version: 2.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -27,22 +27,6 @@ dependencies:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
29
  version: 0.15.1
30
- - !ruby/object:Gem::Dependency
31
- name: multi_json
32
- requirement: !ruby/object:Gem::Requirement
33
- none: false
34
- requirements:
35
- - - ~>
36
- - !ruby/object:Gem::Version
37
- version: 1.7.2
38
- type: :runtime
39
- prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ~>
44
- - !ruby/object:Gem::Version
45
- version: 1.7.2
46
30
  description: queue_classic is a queueing library for Ruby apps. (Rails, Sinatra, Etc...)
47
31
  queue_classic features asynchronous job polling, database maintained locks and no
48
32
  ridiculous dependencies. As a matter of fact, queue_classic only requires pg.
@@ -62,6 +46,8 @@ files:
62
46
  - lib/queue_classic/tasks.rb
63
47
  - lib/queue_classic/worker.rb
64
48
  - lib/queue_classic.rb
49
+ - test/benchmark_test.rb
50
+ - test/conn_test.rb
65
51
  - test/helper.rb
66
52
  - test/queue_test.rb
67
53
  - test/worker_test.rb
@@ -91,5 +77,7 @@ signing_key:
91
77
  specification_version: 3
92
78
  summary: postgres backed queue
93
79
  test_files:
80
+ - test/benchmark_test.rb
81
+ - test/conn_test.rb
94
82
  - test/queue_test.rb
95
83
  - test/worker_test.rb