queue_classic 3.1.0 → 3.2.0.RC1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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