queue_classic_pg2 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 +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +19 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +326 -0
- data/Rakefile +14 -0
- data/changelog +146 -0
- data/lib/generators/queue_classic/install_generator.rb +36 -0
- data/lib/generators/queue_classic/templates/add_queue_classic.rb +9 -0
- data/lib/generators/queue_classic/templates/update_queue_classic_3_0_0.rb +9 -0
- data/lib/generators/queue_classic/templates/update_queue_classic_3_0_2.rb +11 -0
- data/lib/generators/queue_classic/templates/update_queue_classic_3_1_0.rb +9 -0
- data/lib/queue_classic/config.rb +85 -0
- data/lib/queue_classic/conn_adapter.rb +111 -0
- data/lib/queue_classic/queue.rb +119 -0
- data/lib/queue_classic/railtie.rb +9 -0
- data/lib/queue_classic/setup.rb +58 -0
- data/lib/queue_classic/tasks.rb +49 -0
- data/lib/queue_classic/version.rb +3 -0
- data/lib/queue_classic/worker.rb +166 -0
- data/lib/queue_classic.rb +122 -0
- data/queue_classic.gemspec +24 -0
- data/sql/create_table.sql +25 -0
- data/sql/ddl.sql +78 -0
- data/sql/downgrade_from_3_0_0.sql +2 -0
- data/sql/downgrade_from_3_1_0.sql +1 -0
- data/sql/drop_ddl.sql +3 -0
- data/sql/update_to_3_0_0.sql +17 -0
- data/sql/update_to_3_1_0.sql +9 -0
- data/test/benchmark_test.rb +39 -0
- data/test/config_test.rb +121 -0
- data/test/helper.rb +61 -0
- data/test/helper.sql +25 -0
- data/test/lib/queue_classic_rails_connection_test.rb +43 -0
- data/test/lib/queue_classic_test.rb +42 -0
- data/test/queue_test.rb +208 -0
- data/test/worker_test.rb +219 -0
- metadata +112 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
require_relative "queue_classic/config"
|
2
|
+
|
3
|
+
module QC
|
4
|
+
extend QC::Config
|
5
|
+
|
6
|
+
# Assign constants for backwards compatibility.
|
7
|
+
# They should no longer be used. Prefer the corresponding methods.
|
8
|
+
# See +QC::Config+ for more details.
|
9
|
+
DEPRECATED_CONSTANTS = {
|
10
|
+
:APP_NAME => :app_name,
|
11
|
+
:WAIT_TIME => :wait_time,
|
12
|
+
:TABLE_NAME => :table_name,
|
13
|
+
:QUEUE => :queue,
|
14
|
+
:QUEUES => :queues,
|
15
|
+
:TOP_BOUND => :top_bound,
|
16
|
+
:FORK_WORKER => :fork_worker?,
|
17
|
+
}
|
18
|
+
|
19
|
+
def self.const_missing(const_name)
|
20
|
+
if DEPRECATED_CONSTANTS.key? const_name
|
21
|
+
config_method = DEPRECATED_CONSTANTS[const_name]
|
22
|
+
$stderr.puts <<-MSG
|
23
|
+
The constant QC::#{const_name} is deprecated and will be removed in the future.
|
24
|
+
Please use the method QC.#{config_method} instead.
|
25
|
+
MSG
|
26
|
+
QC.public_send config_method
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Defer method calls on the QC module to the
|
33
|
+
# default queue. This facilitates QC.enqueue()
|
34
|
+
def self.method_missing(sym, *args, &block)
|
35
|
+
if default_queue.respond_to? sym
|
36
|
+
default_queue.public_send(sym, *args, &block)
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Ensure QC.respond_to?(:enqueue) equals true (ruby 1.9 only)
|
43
|
+
def self.respond_to_missing?(method_name, include_private = false)
|
44
|
+
default_queue.respond_to?(method_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.has_connection?
|
48
|
+
!default_conn_adapter.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.default_conn_adapter
|
52
|
+
t = Thread.current
|
53
|
+
return t[:qc_conn_adapter] if t[:qc_conn_adapter]
|
54
|
+
adapter = if rails_connection_sharing_enabled?
|
55
|
+
ConnAdapter.new(ActiveRecord::Base.connection.raw_connection)
|
56
|
+
else
|
57
|
+
ConnAdapter.new
|
58
|
+
end
|
59
|
+
|
60
|
+
t[:qc_conn_adapter] = adapter
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.default_conn_adapter=(conn)
|
64
|
+
Thread.current[:qc_conn_adapter] = conn
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.log_yield(data)
|
68
|
+
t0 = Time.now
|
69
|
+
begin
|
70
|
+
yield
|
71
|
+
rescue => e
|
72
|
+
log({:at => "error", :error => e.inspect}.merge(data))
|
73
|
+
raise
|
74
|
+
ensure
|
75
|
+
t = Integer((Time.now - t0)*1000)
|
76
|
+
log(data.merge(:elapsed => t)) unless e
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.log(data)
|
81
|
+
result = nil
|
82
|
+
data = {:lib => "queue-classic"}.merge(data)
|
83
|
+
if block_given?
|
84
|
+
result = yield
|
85
|
+
data.merge(:elapsed => Integer((Time.now - t0)*1000))
|
86
|
+
end
|
87
|
+
data.reduce(out=String.new) do |s, tup|
|
88
|
+
s << [tup.first, tup.last].join("=") << " "
|
89
|
+
end
|
90
|
+
puts(out) if ENV["DEBUG"]
|
91
|
+
return result
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.measure(data)
|
95
|
+
if ENV['QC_MEASURE']
|
96
|
+
$stdout.puts("measure#qc.#{data}")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# This will unlock all jobs any postgres' PID that is not existing anymore
|
101
|
+
# to prevent any infinitely locked jobs
|
102
|
+
def self.unlock_jobs_of_dead_workers
|
103
|
+
pid_column = default_conn_adapter.server_version < 90200 ? "procpid" : "pid"
|
104
|
+
default_conn_adapter.execute("UPDATE #{QC.table_name} SET locked_at = NULL, locked_by = NULL WHERE locked_by NOT IN (SELECT #{pid_column} FROM pg_stat_activity);")
|
105
|
+
end
|
106
|
+
|
107
|
+
# private class methods
|
108
|
+
class << self
|
109
|
+
private
|
110
|
+
|
111
|
+
def rails_connection_sharing_enabled?
|
112
|
+
enabled = ENV.fetch('QC_RAILS_DATABASE', 'true') != 'false'
|
113
|
+
return false unless enabled
|
114
|
+
return Object.const_defined?("ActiveRecord") && ActiveRecord::Base.respond_to?("connection")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
require_relative "queue_classic/queue"
|
120
|
+
require_relative "queue_classic/worker"
|
121
|
+
require_relative "queue_classic/setup"
|
122
|
+
require_relative "queue_classic/railtie" if defined?(Rails)
|
@@ -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_pg2"
|
8
|
+
spec.email = "r@32k.io"
|
9
|
+
spec.version = QC::VERSION
|
10
|
+
spec.description = "Support for Postgres2 : 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 = "https://github.com/PatrickLef/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", "< 2.0"
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
DO $$ BEGIN
|
2
|
+
|
3
|
+
CREATE TABLE queue_classic_jobs (
|
4
|
+
id bigserial PRIMARY KEY,
|
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,
|
8
|
+
locked_at timestamptz,
|
9
|
+
locked_by integer,
|
10
|
+
created_at timestamptz DEFAULT now(),
|
11
|
+
scheduled_at timestamptz DEFAULT now()
|
12
|
+
);
|
13
|
+
|
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;
|
20
|
+
END IF;
|
21
|
+
|
22
|
+
END $$ LANGUAGE plpgsql;
|
23
|
+
|
24
|
+
CREATE INDEX idx_qc_on_name_only_unlocked ON queue_classic_jobs (q_name, id) WHERE locked_at IS NULL;
|
25
|
+
CREATE INDEX idx_qc_on_scheduled_at_only_unlocked ON queue_classic_jobs (scheduled_at, id) WHERE locked_at IS NULL;
|
data/sql/ddl.sql
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
-- We are declaring the return type to be queue_classic_jobs.
|
2
|
+
-- This is ok since I am assuming that all of the users added queues will
|
3
|
+
-- have identical columns to queue_classic_jobs.
|
4
|
+
-- When QC supports queues with columns other than the default, we will have to change this.
|
5
|
+
|
6
|
+
CREATE OR REPLACE FUNCTION lock_head(q_name varchar, top_boundary integer)
|
7
|
+
RETURNS SETOF queue_classic_jobs AS $$
|
8
|
+
DECLARE
|
9
|
+
unlocked bigint;
|
10
|
+
relative_top integer;
|
11
|
+
job_count integer;
|
12
|
+
BEGIN
|
13
|
+
-- The purpose is to release contention for the first spot in the table.
|
14
|
+
-- The select count(*) is going to slow down dequeue performance but allow
|
15
|
+
-- for more workers. Would love to see some optimization here...
|
16
|
+
|
17
|
+
EXECUTE 'SELECT count(*) FROM '
|
18
|
+
|| '(SELECT * FROM queue_classic_jobs '
|
19
|
+
|| ' WHERE locked_at IS NULL'
|
20
|
+
|| ' AND q_name = '
|
21
|
+
|| quote_literal(q_name)
|
22
|
+
|| ' AND scheduled_at <= '
|
23
|
+
|| quote_literal(now())
|
24
|
+
|| ' LIMIT '
|
25
|
+
|| quote_literal(top_boundary)
|
26
|
+
|| ') limited'
|
27
|
+
INTO job_count;
|
28
|
+
|
29
|
+
SELECT TRUNC(random() * (top_boundary - 1))
|
30
|
+
INTO relative_top;
|
31
|
+
|
32
|
+
IF job_count < top_boundary THEN
|
33
|
+
relative_top = 0;
|
34
|
+
END IF;
|
35
|
+
|
36
|
+
LOOP
|
37
|
+
BEGIN
|
38
|
+
EXECUTE 'SELECT id FROM queue_classic_jobs '
|
39
|
+
|| ' WHERE locked_at IS NULL'
|
40
|
+
|| ' AND q_name = '
|
41
|
+
|| quote_literal(q_name)
|
42
|
+
|| ' AND scheduled_at <= '
|
43
|
+
|| quote_literal(now())
|
44
|
+
|| ' ORDER BY id ASC'
|
45
|
+
|| ' LIMIT 1'
|
46
|
+
|| ' OFFSET ' || quote_literal(relative_top)
|
47
|
+
|| ' FOR UPDATE NOWAIT'
|
48
|
+
INTO unlocked;
|
49
|
+
EXIT;
|
50
|
+
EXCEPTION
|
51
|
+
WHEN lock_not_available THEN
|
52
|
+
-- do nothing. loop again and hope we get a lock
|
53
|
+
END;
|
54
|
+
END LOOP;
|
55
|
+
|
56
|
+
RETURN QUERY EXECUTE 'UPDATE queue_classic_jobs '
|
57
|
+
|| ' SET locked_at = (CURRENT_TIMESTAMP),'
|
58
|
+
|| ' locked_by = (select pg_backend_pid())'
|
59
|
+
|| ' WHERE id = $1'
|
60
|
+
|| ' AND locked_at is NULL'
|
61
|
+
|| ' RETURNING *'
|
62
|
+
USING unlocked;
|
63
|
+
|
64
|
+
RETURN;
|
65
|
+
END $$ LANGUAGE plpgsql;
|
66
|
+
|
67
|
+
CREATE OR REPLACE FUNCTION lock_head(tname varchar) RETURNS SETOF queue_classic_jobs AS $$ BEGIN
|
68
|
+
RETURN QUERY EXECUTE 'SELECT * FROM lock_head($1,10)' USING tname;
|
69
|
+
END $$ LANGUAGE plpgsql;
|
70
|
+
|
71
|
+
-- queue_classic_notify function and trigger
|
72
|
+
CREATE FUNCTION queue_classic_notify() RETURNS TRIGGER AS $$ BEGIN
|
73
|
+
perform pg_notify(new.q_name, ''); RETURN NULL;
|
74
|
+
END $$ LANGUAGE plpgsql;
|
75
|
+
|
76
|
+
CREATE TRIGGER queue_classic_notify
|
77
|
+
AFTER INSERT ON queue_classic_jobs FOR EACH ROW
|
78
|
+
EXECUTE PROCEDURE queue_classic_notify();
|
@@ -0,0 +1 @@
|
|
1
|
+
ALTER TABLE queue_classic_jobs DROP COLUMN scheduled_at;
|
data/sql/drop_ddl.sql
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
DO $$DECLARE r record;
|
2
|
+
BEGIN
|
3
|
+
BEGIN
|
4
|
+
ALTER TABLE queue_classic_jobs ADD COLUMN created_at timestamptz DEFAULT now();
|
5
|
+
EXCEPTION
|
6
|
+
WHEN duplicate_column THEN RAISE NOTICE 'column created_at already exists in queue_classic_jobs.';
|
7
|
+
END;
|
8
|
+
END$$;
|
9
|
+
|
10
|
+
DO $$DECLARE r record;
|
11
|
+
BEGIN
|
12
|
+
BEGIN
|
13
|
+
ALTER TABLE queue_classic_jobs ADD COLUMN locked_by integer;
|
14
|
+
EXCEPTION
|
15
|
+
WHEN duplicate_column THEN RAISE NOTICE 'column locked_by already exists in queue_classic_jobs.';
|
16
|
+
END;
|
17
|
+
END$$;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
DO $$DECLARE r record;
|
2
|
+
BEGIN
|
3
|
+
BEGIN
|
4
|
+
ALTER TABLE queue_classic_jobs ADD COLUMN scheduled_at timestamptz DEFAULT now();
|
5
|
+
CREATE INDEX idx_qc_on_scheduled_at_only_unlocked ON queue_classic_jobs (scheduled_at, id) WHERE locked_at IS NULL;
|
6
|
+
EXCEPTION
|
7
|
+
WHEN duplicate_column THEN RAISE NOTICE 'column scheduled_at already exists in queue_classic_jobs.';
|
8
|
+
END;
|
9
|
+
END$$;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
if ENV["QC_BENCHMARK"]
|
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))
|
8
|
+
|
9
|
+
def test_enqueue
|
10
|
+
start = Time.now
|
11
|
+
BENCHMARK_SIZE.times do
|
12
|
+
QC.enqueue("1.odd?")
|
13
|
+
end
|
14
|
+
assert_equal(BENCHMARK_SIZE, QC.count)
|
15
|
+
|
16
|
+
elapsed = Time.now - start
|
17
|
+
assert_operator(elapsed, :<, BENCHMARK_MAX_TIME_ENQUEUE)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_dequeue
|
21
|
+
worker = QC::Worker.new
|
22
|
+
worker.running = true
|
23
|
+
BENCHMARK_SIZE.times do
|
24
|
+
QC.enqueue("1.odd?")
|
25
|
+
end
|
26
|
+
assert_equal(BENCHMARK_SIZE, QC.count)
|
27
|
+
|
28
|
+
start = Time.now
|
29
|
+
BENCHMARK_SIZE.times do
|
30
|
+
worker.work
|
31
|
+
end
|
32
|
+
elapsed = Time.now - start
|
33
|
+
|
34
|
+
assert_equal(0, QC.count)
|
35
|
+
assert_operator(elapsed, :<, BENCHMARK_MAX_TIME_DEQUEUE)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
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
|
data/test/helper.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require "bundler"
|
2
|
+
Bundler.setup :default, :test
|
3
|
+
|
4
|
+
ENV["DATABASE_URL"] ||= "postgres:///queue_classic_test"
|
5
|
+
|
6
|
+
require_relative '../lib/queue_classic'
|
7
|
+
require "stringio"
|
8
|
+
require "minitest/autorun"
|
9
|
+
|
10
|
+
class QCTest < Minitest::Test
|
11
|
+
|
12
|
+
def setup
|
13
|
+
init_db
|
14
|
+
end
|
15
|
+
|
16
|
+
def teardown
|
17
|
+
QC.delete_all
|
18
|
+
end
|
19
|
+
|
20
|
+
def init_db
|
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)
|
25
|
+
c.execute(File.read('./test/helper.sql'))
|
26
|
+
c.disconnect
|
27
|
+
end
|
28
|
+
|
29
|
+
def capture_stderr_output
|
30
|
+
original_stderr = $stderr
|
31
|
+
$stderr = StringIO.new
|
32
|
+
yield
|
33
|
+
$stderr.string
|
34
|
+
ensure
|
35
|
+
$stderr = original_stderr
|
36
|
+
end
|
37
|
+
|
38
|
+
def capture_debug_output
|
39
|
+
original_debug = ENV['DEBUG']
|
40
|
+
original_stdout = $stdout
|
41
|
+
|
42
|
+
ENV['DEBUG'] = "true"
|
43
|
+
$stdout = StringIO.new
|
44
|
+
yield
|
45
|
+
$stdout.string
|
46
|
+
ensure
|
47
|
+
ENV['DEBUG'] = original_debug
|
48
|
+
$stdout = original_stdout
|
49
|
+
end
|
50
|
+
|
51
|
+
def with_env(temporary_environment)
|
52
|
+
original_environment = {}
|
53
|
+
temporary_environment.each do |name, value|
|
54
|
+
original_environment[name] = ENV[name]
|
55
|
+
ENV[name] = value
|
56
|
+
end
|
57
|
+
yield
|
58
|
+
ensure
|
59
|
+
original_environment.each { |name, value| ENV[name] = value }
|
60
|
+
end
|
61
|
+
end
|
data/test/helper.sql
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
DO $$
|
2
|
+
-- Set initial sequence to a large number to test the entire toolchain
|
3
|
+
-- works on integers with higher bits set.
|
4
|
+
DECLARE
|
5
|
+
quoted_name text;
|
6
|
+
quoted_size text;
|
7
|
+
BEGIN
|
8
|
+
-- Find the name of the relevant sequence.
|
9
|
+
--
|
10
|
+
-- pg_get_serial_sequence quotes identifiers as part of its
|
11
|
+
-- behavior.
|
12
|
+
SELECT name
|
13
|
+
INTO STRICT quoted_name
|
14
|
+
FROM pg_get_serial_sequence('queue_classic_jobs', 'id') AS name;
|
15
|
+
|
16
|
+
-- Don't quote, because ALTER SEQUENCE RESTART doesn't like
|
17
|
+
-- general literals, only unquoted numeric literals.
|
18
|
+
SELECT pow(2, 34)::text AS size
|
19
|
+
INTO STRICT quoted_size;
|
20
|
+
|
21
|
+
EXECUTE 'ALTER SEQUENCE ' || quoted_name ||
|
22
|
+
' RESTART ' || quoted_size || ';';
|
23
|
+
END;
|
24
|
+
$$;
|
25
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.expand_path("../../helper.rb", __FILE__)
|
2
|
+
|
3
|
+
class QueueClassicRailsConnectionTest < QCTest
|
4
|
+
def before_setup
|
5
|
+
Object.send :const_set, :ActiveRecord, Module.new
|
6
|
+
ActiveRecord.const_set :Base, Module.new
|
7
|
+
|
8
|
+
@original_conn_adapter = QC.default_conn_adapter
|
9
|
+
QC.default_conn_adapter = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def after_teardown
|
13
|
+
ActiveRecord.send :remove_const, :Base
|
14
|
+
Object.send :remove_const, :ActiveRecord
|
15
|
+
|
16
|
+
QC.default_conn_adapter = @original_conn_adapter
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_uses_active_record_connection_if_exists
|
20
|
+
connection = get_connection
|
21
|
+
assert connection.verify
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_does_not_use_active_record_connection_if_env_var_set
|
25
|
+
with_env 'QC_RAILS_DATABASE' => 'false' do
|
26
|
+
connection = get_connection
|
27
|
+
assert_raises(MockExpectationError) { connection.verify }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def get_connection
|
33
|
+
connection = Minitest::Mock.new
|
34
|
+
connection.expect(:raw_connection, QC::ConnAdapter.new.connection)
|
35
|
+
|
36
|
+
ActiveRecord::Base.define_singleton_method(:connection) do
|
37
|
+
connection
|
38
|
+
end
|
39
|
+
|
40
|
+
QC.default_conn_adapter
|
41
|
+
connection
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path("../../helper.rb", __FILE__)
|
2
|
+
|
3
|
+
class QueueClassicTest < QCTest
|
4
|
+
def test_only_delegate_calls_to_queue_it_understands
|
5
|
+
e = assert_raises(NoMethodError) do
|
6
|
+
QC.probably_not
|
7
|
+
end
|
8
|
+
assert_match "undefined method `probably_not' for QC:Module", e.message
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_default_conn_adapter_default_value
|
12
|
+
assert(QC.default_conn_adapter.is_a?(QC::ConnAdapter))
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_assigning_a_default_conn_adapter
|
16
|
+
original_conn_adapter = QC.default_conn_adapter
|
17
|
+
connection = QC::ConnAdapter.new
|
18
|
+
QC.default_conn_adapter = connection
|
19
|
+
assert_equal(QC.default_conn_adapter, connection)
|
20
|
+
ensure
|
21
|
+
QC.default_conn_adapter = original_conn_adapter
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_unlock_jobs_of_dead_workers
|
25
|
+
# Insert a locked job
|
26
|
+
adapter = QC::ConnAdapter.new
|
27
|
+
query = "INSERT INTO #{QC.table_name} (q_name, method, args, locked_by, locked_at) VALUES ('whatever', 'Kernel.puts', '[\"ok?\"]', 0, (CURRENT_TIMESTAMP))"
|
28
|
+
adapter.execute(query)
|
29
|
+
|
30
|
+
# We should have no unlocked jobs
|
31
|
+
query_locked_jobs = "SELECT * FROM #{QC.table_name} WHERE locked_at IS NULL"
|
32
|
+
res = adapter.connection.exec(query_locked_jobs)
|
33
|
+
assert_equal(0, res.count)
|
34
|
+
|
35
|
+
# Unlock the job
|
36
|
+
QC.unlock_jobs_of_dead_workers
|
37
|
+
|
38
|
+
# We should have an unlocked job now
|
39
|
+
res = adapter.connection.exec(query_locked_jobs)
|
40
|
+
assert_equal(1, res.count)
|
41
|
+
end
|
42
|
+
end
|