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.
- checksums.yaml +4 -4
- data/.gitignore +9 -0
- data/.travis.yml +15 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +35 -7
- data/Rakefile +14 -0
- data/changelog +146 -0
- data/lib/queue_classic.rb +45 -50
- data/lib/queue_classic/config.rb +85 -0
- data/lib/queue_classic/conn_adapter.rb +8 -1
- data/lib/queue_classic/queue.rb +13 -9
- data/lib/queue_classic/tasks.rb +1 -1
- data/lib/queue_classic/version.rb +3 -0
- data/lib/queue_classic/worker.rb +8 -7
- data/queue_classic.gemspec +24 -0
- data/sql/create_table.sql +6 -4
- data/test/benchmark_test.rb +13 -12
- data/test/config_test.rb +121 -0
- data/test/helper.rb +11 -4
- data/test/helper.sql +25 -0
- data/test/lib/queue_classic_rails_connection_test.rb +4 -4
- data/test/lib/queue_classic_test.rb +9 -2
- data/test/queue_test.rb +65 -2
- data/test/worker_test.rb +26 -34
- metadata +20 -5
@@ -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
|
84
|
+
conn.exec("SET application_name = '#{QC.app_name}'")
|
78
85
|
conn
|
79
86
|
end
|
80
87
|
|
data/lib/queue_classic/queue.rb
CHANGED
@@ -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
|
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 #{
|
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 #{
|
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 #{
|
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 #{
|
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 #{
|
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 #{
|
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
|
data/lib/queue_classic/tasks.rb
CHANGED
@@ -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
|
11
|
+
@worker = QC.default_worker_class.new
|
12
12
|
|
13
13
|
trap('INT') do
|
14
14
|
$stderr.puts("Received INT. Shutting down.")
|
data/lib/queue_classic/worker.rb
CHANGED
@@ -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
|
20
|
-
@wait_interval = args[:wait_interval] || QC
|
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
|
30
|
-
(args[:q_names] || QC
|
31
|
-
(args[:top_bound] || QC
|
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
|
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
|
data/sql/create_table.sql
CHANGED
@@ -11,10 +11,12 @@ CREATE TABLE queue_classic_jobs (
|
|
11
11
|
scheduled_at timestamptz default now()
|
12
12
|
);
|
13
13
|
|
14
|
-
-- If
|
15
|
-
|
16
|
-
|
17
|
-
|
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;
|
data/test/benchmark_test.rb
CHANGED
@@ -1,37 +1,38 @@
|
|
1
|
-
|
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
|
-
|
10
|
-
QC.enqueue("1.odd?"
|
11
|
+
BENCHMARK_SIZE.times do
|
12
|
+
QC.enqueue("1.odd?")
|
11
13
|
end
|
12
|
-
assert_equal(
|
14
|
+
assert_equal(BENCHMARK_SIZE, QC.count)
|
13
15
|
|
14
16
|
elapsed = Time.now - start
|
15
|
-
|
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
|
-
|
22
|
-
|
23
|
-
QC.enqueue("1.odd?", [])
|
23
|
+
BENCHMARK_SIZE.times do
|
24
|
+
QC.enqueue("1.odd?")
|
24
25
|
end
|
25
|
-
assert_equal(
|
26
|
+
assert_equal(BENCHMARK_SIZE, QC.count)
|
26
27
|
|
27
28
|
start = Time.now
|
28
|
-
|
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
|
-
|
35
|
+
assert_operator(elapsed, :<, BENCHMARK_MAX_TIME_DEQUEUE)
|
35
36
|
end
|
36
37
|
|
37
38
|
end
|
data/test/config_test.rb
ADDED
@@ -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
|