queue_classic 3.1.0 → 3.2.0.RC1

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.
@@ -0,0 +1,85 @@
1
+ module QC
2
+ module Config
3
+ # You can use the APP_NAME to query for
4
+ # postgres related process information in the
5
+ # pg_stat_activity table.
6
+ def app_name
7
+ @app_name ||= ENV["QC_APP_NAME"] || "queue_classic"
8
+ end
9
+
10
+ # Number of seconds to block on the listen chanel for new jobs.
11
+ def wait_time
12
+ @wait_time ||= (ENV["QC_LISTEN_TIME"] || 5).to_i
13
+ end
14
+
15
+ # Why do you want to change the table name?
16
+ # Just deal with the default OK?
17
+ # If you do want to change this, you will
18
+ # need to update the PL/pgSQL lock_head() function.
19
+ # Come on. Don't do it.... Just stick with the default.
20
+ def table_name
21
+ @table_name ||= "queue_classic_jobs"
22
+ end
23
+
24
+ def queue
25
+ @queue = ENV["QUEUE"] || "default"
26
+ end
27
+
28
+ # The default queue used by `QC.enqueue`.
29
+ def default_queue
30
+ @default_queue ||= Queue.new(QC.queue)
31
+ end
32
+
33
+ def default_queue=(queue)
34
+ @default_queue = queue
35
+ end
36
+
37
+ # Each row in the table will have a column that
38
+ # notes the queue. You can point your workers
39
+ # at different queues.
40
+ def queues
41
+ @queues ||= (ENV["QUEUES"] && ENV["QUEUES"].split(",").map(&:strip)) || []
42
+ end
43
+
44
+ # Set this to 1 for strict FIFO.
45
+ # There is nothing special about 9....
46
+ def top_bound
47
+ @top_bound ||= (ENV["QC_TOP_BOUND"] || 9).to_i
48
+ end
49
+
50
+ # Set this variable if you wish for
51
+ # the worker to fork a UNIX process for
52
+ # each locked job. Remember to re-establish
53
+ # any database connections. See the worker
54
+ # for more details.
55
+ def fork_worker?
56
+ @fork_worker ||= (!ENV["QC_FORK_WORKER"].nil?)
57
+ end
58
+
59
+ # The worker class instantiated by QC's rake tasks.
60
+ def default_worker_class
61
+
62
+ @worker_class ||= (ENV["QC_DEFAULT_WORKER_CLASS"] && Kernel.const_get(ENV["QC_DEFAULT_WORKER_CLASS"]) ||
63
+ QC::Worker)
64
+
65
+ end
66
+
67
+ def default_worker_class=(worker_class)
68
+ @worker_class = worker_class
69
+ end
70
+
71
+ # reset memoized configuration
72
+ def reset_config
73
+ # TODO: we might want to think about storing these in a Hash.
74
+ @app_name = nil
75
+ @wait_time = nil
76
+ @table_name = nil
77
+ @queue = nil
78
+ @default_queue = nil
79
+ @queues = nil
80
+ @top_bound = nil
81
+ @fork_worker = nil
82
+ @worker_class = nil
83
+ end
84
+ end
85
+ end
@@ -48,6 +48,13 @@ module QC
48
48
  end
49
49
  end
50
50
 
51
+ def server_version
52
+ @server_version ||= begin
53
+ version = execute("SHOW server_version_num;")["server_version_num"]
54
+ version && version.to_i
55
+ end
56
+ end
57
+
51
58
  private
52
59
 
53
60
  def wait_for_notify(t)
@@ -74,7 +81,7 @@ module QC
74
81
  if conn.status != PGconn::CONNECTION_OK
75
82
  QC.log(:error => conn.error)
76
83
  end
77
- conn.exec("SET application_name = '#{QC::APP_NAME}'")
84
+ conn.exec("SET application_name = '#{QC.app_name}'")
78
85
  conn
79
86
  end
80
87
 
@@ -9,7 +9,7 @@ module QC
9
9
  attr_reader :name, :top_bound
10
10
  def initialize(name, top_bound=nil)
11
11
  @name = name
12
- @top_bound = top_bound || QC::TOP_BOUND
12
+ @top_bound = top_bound || QC.top_bound
13
13
  end
14
14
 
15
15
  def conn_adapter=(a)
@@ -34,9 +34,10 @@ module QC
34
34
  # The args are stored as a collection and then splatted inside the worker.
35
35
  # Examples of args include: `'hello world'`, `['hello world']`,
36
36
  # `'hello', 'world'`.
37
+ # This method returns a hash with the id of the enqueued job.
37
38
  def enqueue(method, *args)
38
39
  QC.log_yield(:measure => 'queue.enqueue') do
39
- s = "INSERT INTO #{TABLE_NAME} (q_name, method, args) VALUES ($1, $2, $3)"
40
+ s = "INSERT INTO #{QC.table_name} (q_name, method, args) VALUES ($1, $2, $3) RETURNING id"
40
41
  conn_adapter.execute(s, name, method, JSON.dump(args))
41
42
  end
42
43
  end
@@ -46,8 +47,9 @@ module QC
46
47
  # The time argument must be a Time object or a float timestamp. The method
47
48
  # and args argument must be in the form described in the documentation for
48
49
  # the #enqueue method.
50
+ # This method returns a hash with the id of the enqueued job.
49
51
  def enqueue_at(timestamp, method, *args)
50
- offset = Time.at(timestamp) - Time.now
52
+ offset = Time.at(timestamp).to_i - Time.now.to_i
51
53
  enqueue_in(offset, method, *args)
52
54
  end
53
55
 
@@ -56,10 +58,12 @@ module QC
56
58
  # The seconds argument must be an integer. The method and args argument
57
59
  # must be in the form described in the documentation for the #enqueue
58
60
  # method.
61
+ # This method returns a hash with the id of the enqueued job.
59
62
  def enqueue_in(seconds, method, *args)
60
63
  QC.log_yield(:measure => 'queue.enqueue') do
61
- s = "INSERT INTO #{TABLE_NAME} (q_name, method, args, scheduled_at)
62
- VALUES ($1, $2, $3, now() + interval '#{seconds.to_i} seconds')"
64
+ s = "INSERT INTO #{QC.table_name} (q_name, method, args, scheduled_at)
65
+ VALUES ($1, $2, $3, now() + interval '#{seconds.to_i} seconds')
66
+ RETURNING id"
63
67
  conn_adapter.execute(s, name, method, JSON.dump(args))
64
68
  end
65
69
  end
@@ -85,27 +89,27 @@ module QC
85
89
 
86
90
  def unlock(id)
87
91
  QC.log_yield(:measure => 'queue.unlock') do
88
- s = "UPDATE #{TABLE_NAME} set locked_at = null where id = $1"
92
+ s = "UPDATE #{QC.table_name} set locked_at = null where id = $1"
89
93
  conn_adapter.execute(s, id)
90
94
  end
91
95
  end
92
96
 
93
97
  def delete(id)
94
98
  QC.log_yield(:measure => 'queue.delete') do
95
- conn_adapter.execute("DELETE FROM #{TABLE_NAME} where id = $1", id)
99
+ conn_adapter.execute("DELETE FROM #{QC.table_name} where id = $1", id)
96
100
  end
97
101
  end
98
102
 
99
103
  def delete_all
100
104
  QC.log_yield(:measure => 'queue.delete_all') do
101
- s = "DELETE FROM #{TABLE_NAME} WHERE q_name = $1"
105
+ s = "DELETE FROM #{QC.table_name} WHERE q_name = $1"
102
106
  conn_adapter.execute(s, name)
103
107
  end
104
108
  end
105
109
 
106
110
  def count
107
111
  QC.log_yield(:measure => 'queue.count') do
108
- s = "SELECT COUNT(*) FROM #{TABLE_NAME} WHERE q_name = $1"
112
+ s = "SELECT COUNT(*) FROM #{QC.table_name} WHERE q_name = $1"
109
113
  r = conn_adapter.execute(s, name)
110
114
  r["count"].to_i
111
115
  end
@@ -8,7 +8,7 @@ end
8
8
  namespace :qc do
9
9
  desc "Start a new worker for the (default or $QUEUE / $QUEUES) queue"
10
10
  task :work => :environment do
11
- @worker = QC::Worker.new
11
+ @worker = QC.default_worker_class.new
12
12
 
13
13
  trap('INT') do
14
14
  $stderr.puts("Received INT. Shutting down.")
@@ -0,0 +1,3 @@
1
+ module QC
2
+ VERSION = "3.2.0.RC1"
3
+ end
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  require_relative 'queue'
2
3
  require_relative 'conn_adapter'
3
4
 
@@ -16,8 +17,8 @@ module QC
16
17
  # q_names:: Names of queues to process. Will process left to right.
17
18
  # top_bound:: Offset to the head of the queue. 1 == strict FIFO.
18
19
  def initialize(args={})
19
- @fork_worker = args[:fork_worker] || QC::FORK_WORKER
20
- @wait_interval = args[:wait_interval] || QC::WAIT_TIME
20
+ @fork_worker = args[:fork_worker] || QC.fork_worker?
21
+ @wait_interval = args[:wait_interval] || QC.wait_time
21
22
 
22
23
  if args[:connection]
23
24
  @conn_adapter = ConnAdapter.new(args[:connection])
@@ -26,9 +27,9 @@ module QC
26
27
  end
27
28
 
28
29
  @queues = setup_queues(@conn_adapter,
29
- (args[:q_name] || QC::QUEUE),
30
- (args[:q_names] || QC::QUEUES),
31
- (args[:top_bound] || QC::TOP_BOUND))
30
+ (args[:q_name] || QC.queue),
31
+ (args[:q_names] || QC.queues),
32
+ (args[:top_bound] || QC.top_bound))
32
33
  log(args.merge(:at => "worker_initialized"))
33
34
  @running = true
34
35
  end
@@ -101,7 +102,7 @@ module QC
101
102
  # then it is deleted from the queue.
102
103
  # If the job has raised an exception the responsibility of what
103
104
  # to do with the job is delegated to Worker#handle_failure.
104
- # If the job is not finished and an INT signal is traped,
105
+ # If the job is not finished and an INT signal is trapped,
105
106
  # this method will unlock the job in the queue.
106
107
  def process(queue, job)
107
108
  start = Time.now
@@ -136,7 +137,7 @@ module QC
136
137
  # This method will be called when an exception
137
138
  # is raised during the execution of the job.
138
139
  def handle_failure(job,e)
139
- $stderr.puts("count#qc.job-error=1 job=#{job} error=#{e.inspect}")
140
+ $stderr.puts("count#qc.job-error=1 job=#{job} error=#{e.inspect} at=#{e.backtrace.first}")
140
141
  end
141
142
 
142
143
  # This method should be overriden if
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'queue_classic/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "queue_classic"
8
+ spec.email = "r@32k.io"
9
+ spec.version = QC::VERSION
10
+ spec.description = "queue_classic is a queueing library for Ruby apps. (Rails, Sinatra, Etc...) queue_classic features asynchronous job polling, database maintained locks and no ridiculous dependencies. As a matter of fact, queue_classic only requires pg."
11
+ spec.summary = "Simple, efficient worker queue for Ruby & PostgreSQL."
12
+ spec.authors = ["Ryan Smith (♠ ace hacker)"]
13
+ spec.homepage = "http://github.com/QueueClassic/queue_classic"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.require_paths = %w[lib]
22
+
23
+ spec.add_dependency "pg", ">= 0.17", "< 0.19"
24
+ end
@@ -11,10 +11,12 @@ CREATE TABLE queue_classic_jobs (
11
11
  scheduled_at timestamptz default now()
12
12
  );
13
13
 
14
- -- If json type is available, use it for the args column.
15
- perform * from pg_type where typname = 'json';
16
- if found then
17
- alter table queue_classic_jobs alter column args type json using (args::json);
14
+ -- If jsonb type is available, use it for the args column
15
+ if exists (select 1 from pg_type where typname = 'jsonb') then
16
+ alter table queue_classic_jobs alter column args type jsonb using args::jsonb;
17
+ -- Otherwise, use json type for the args column if available
18
+ elsif exists (select 1 from pg_type where typname = 'json') then
19
+ alter table queue_classic_jobs alter column args type json using args::json;
18
20
  end if;
19
21
 
20
22
  end $$ language plpgsql;
@@ -1,37 +1,38 @@
1
- require File.expand_path("../helper.rb", __FILE__)
1
+ require_relative 'helper'
2
2
 
3
3
  if ENV["QC_BENCHMARK"]
4
4
  class BenchmarkTest < QCTest
5
+ BENCHMARK_SIZE = Integer(ENV.fetch("QC_BENCHMARK_SIZE", 10_000))
6
+ BENCHMARK_MAX_TIME_DEQUEUE = Integer(ENV.fetch("QC_BENCHMARK_MAX_TIME_DEQUEUE", 30))
7
+ BENCHMARK_MAX_TIME_ENQUEUE = Integer(ENV.fetch("QC_BENCHMARK_MAX_TIME_ENQUEUE", 5))
5
8
 
6
9
  def test_enqueue
7
- n = 10_000
8
10
  start = Time.now
9
- n.times do
10
- QC.enqueue("1.odd?", [])
11
+ BENCHMARK_SIZE.times do
12
+ QC.enqueue("1.odd?")
11
13
  end
12
- assert_equal(n, QC.count)
14
+ assert_equal(BENCHMARK_SIZE, QC.count)
13
15
 
14
16
  elapsed = Time.now - start
15
- assert_in_delta(4, elapsed, 1)
17
+ assert_operator(elapsed, :<, BENCHMARK_MAX_TIME_ENQUEUE)
16
18
  end
17
19
 
18
20
  def test_dequeue
19
21
  worker = QC::Worker.new
20
22
  worker.running = true
21
- n = 10_000
22
- n.times do
23
- QC.enqueue("1.odd?", [])
23
+ BENCHMARK_SIZE.times do
24
+ QC.enqueue("1.odd?")
24
25
  end
25
- assert_equal(n, QC.count)
26
+ assert_equal(BENCHMARK_SIZE, QC.count)
26
27
 
27
28
  start = Time.now
28
- n.times do
29
+ BENCHMARK_SIZE.times do
29
30
  worker.work
30
31
  end
31
32
  elapsed = Time.now - start
32
33
 
33
34
  assert_equal(0, QC.count)
34
- assert_in_delta(10, elapsed, 3)
35
+ assert_operator(elapsed, :<, BENCHMARK_MAX_TIME_DEQUEUE)
35
36
  end
36
37
 
37
38
  end
@@ -0,0 +1,121 @@
1
+ require_relative 'helper'
2
+
3
+ class ConfigTest < QCTest
4
+ def setup
5
+ QC.reset_config
6
+ end
7
+
8
+ def teardown
9
+ QC.reset_config
10
+ end
11
+
12
+ def test_app_name_default
13
+ assert_equal "queue_classic", QC.app_name
14
+ end
15
+
16
+ def test_configure_app_name_with_env_var
17
+ with_env "QC_APP_NAME" => "zomg_qc" do
18
+ assert_equal "zomg_qc", QC.app_name
19
+ end
20
+ end
21
+
22
+ def test_wait_time_default
23
+ assert_equal 5, QC.wait_time
24
+ end
25
+
26
+ def test_configure_wait_time_with_env_var
27
+ with_env "QC_LISTEN_TIME" => "7" do
28
+ assert_equal 7, QC.wait_time
29
+ end
30
+ end
31
+
32
+ def test_table_name_default
33
+ assert_equal "queue_classic_jobs", QC.table_name
34
+ end
35
+
36
+ def test_queue_default
37
+ assert_equal "default", QC.queue
38
+ assert_equal "default", QC.default_queue.name
39
+ end
40
+
41
+ def test_configure_queue_with_env_var
42
+ with_env "QUEUE" => "priority" do
43
+ assert_equal "priority", QC.queue
44
+ assert_equal "priority", QC.default_queue.name
45
+ end
46
+ end
47
+
48
+ def test_assign_default_queue
49
+ QC.default_queue = QC::Queue.new "dispensable"
50
+ assert_equal "default", QC.queue
51
+ assert_equal "dispensable", QC.default_queue.name
52
+ end
53
+
54
+ def test_queues_default
55
+ assert_equal [], QC.queues
56
+ end
57
+
58
+ def test_configure_queues_with_env_var
59
+ with_env "QUEUES" => "first,second,third" do
60
+ assert_equal %w(first second third), QC.queues
61
+ end
62
+ end
63
+
64
+ def test_configure_queues_with_whitespace
65
+ with_env "QUEUES" => " one, two, three " do
66
+ assert_equal %w(one two three), QC.queues
67
+ end
68
+ end
69
+
70
+ def test_top_bound_default
71
+ assert_equal 9, QC.top_bound
72
+ end
73
+
74
+ def test_configure_top_bound_with_env_var
75
+ with_env "QC_TOP_BOUND" => "5" do
76
+ assert_equal 5, QC.top_bound
77
+ end
78
+ end
79
+
80
+ def test_fork_worker_default
81
+ refute QC.fork_worker?
82
+ end
83
+
84
+ def test_configure_fork_worker_with_env_var
85
+ with_env "QC_FORK_WORKER" => "yo" do
86
+ assert QC.fork_worker?
87
+ end
88
+ end
89
+
90
+ def test_configuration_constants_are_deprecated
91
+ warning = capture_stderr_output do
92
+ QC::FORK_WORKER
93
+ end
94
+ assert_match "QC::FORK_WORKER is deprecated", warning
95
+ assert_match "QC.fork_worker? instead", warning
96
+ end
97
+
98
+ class TestWorker < QC::Worker; end
99
+
100
+ def test_default_worker_class
101
+ assert_equal QC::Worker, QC.default_worker_class
102
+ end
103
+
104
+ def test_configure_default_worker_class_with_env_var
105
+ if RUBY_VERSION =~ /^1\.9\./
106
+ skip "Kernel.const_get in Ruby 1.9.x does not perform recursive lookups"
107
+ end
108
+ with_env "QC_DEFAULT_WORKER_CLASS" => "ConfigTest::TestWorker" do
109
+ assert_equal TestWorker, QC.default_worker_class
110
+ end
111
+ end
112
+
113
+ def test_assign_default_worker_class
114
+ original_worker = QC.default_worker_class
115
+ QC.default_worker_class = TestWorker
116
+
117
+ assert_equal TestWorker, QC.default_worker_class
118
+ ensure
119
+ QC.default_worker_class = original_worker
120
+ end
121
+ end