queue_classic 3.1.0.RC1 → 4.0.0.pre.beta1

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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +192 -0
  3. data/.gitignore +11 -0
  4. data/CHANGELOG.md +192 -0
  5. data/CODE_OF_CONDUCT.md +46 -0
  6. data/CONTRIBUTING.md +17 -0
  7. data/Gemfile +12 -0
  8. data/LICENSE.txt +20 -0
  9. data/{readme.md → README.md} +120 -83
  10. data/Rakefile +16 -0
  11. data/lib/generators/queue_classic/install_generator.rb +6 -0
  12. data/lib/generators/queue_classic/templates/add_queue_classic.rb +3 -1
  13. data/lib/generators/queue_classic/templates/update_queue_classic_3_0_0.rb +3 -1
  14. data/lib/generators/queue_classic/templates/update_queue_classic_3_0_2.rb +3 -1
  15. data/lib/generators/queue_classic/templates/update_queue_classic_3_1_0.rb +3 -1
  16. data/lib/generators/queue_classic/templates/update_queue_classic_4_0_0.rb +11 -0
  17. data/lib/queue_classic/config.rb +86 -0
  18. data/lib/queue_classic/conn_adapter.rb +37 -16
  19. data/lib/queue_classic/queue.rb +76 -18
  20. data/lib/queue_classic/railtie.rb +2 -0
  21. data/lib/queue_classic/setup.rb +24 -7
  22. data/lib/queue_classic/tasks.rb +7 -8
  23. data/lib/queue_classic/version.rb +5 -0
  24. data/lib/queue_classic/worker.rb +18 -12
  25. data/lib/queue_classic.rb +50 -58
  26. data/queue_classic.gemspec +25 -0
  27. data/sql/create_table.sql +7 -14
  28. data/sql/ddl.sql +6 -82
  29. data/sql/downgrade_from_4_0_0.sql +88 -0
  30. data/sql/update_to_3_0_0.sql +5 -5
  31. data/sql/update_to_3_1_0.sql +6 -6
  32. data/sql/update_to_4_0_0.sql +6 -0
  33. data/test/benchmark_test.rb +15 -12
  34. data/test/config_test.rb +123 -0
  35. data/test/helper.rb +47 -3
  36. data/test/helper.sql +25 -0
  37. data/test/lib/queue_classic_rails_connection_test.rb +16 -10
  38. data/test/lib/queue_classic_test.rb +15 -3
  39. data/test/lib/queue_classic_test_with_activerecord_typecast.rb +21 -0
  40. data/test/queue_test.rb +127 -4
  41. data/test/rails-tests/.gitignore +2 -0
  42. data/test/rails-tests/rails523.sh +23 -0
  43. data/test/worker_test.rb +153 -35
  44. metadata +51 -7
@@ -0,0 +1,88 @@
1
+ DO $$DECLARE r record;
2
+ BEGIN
3
+ -- If jsonb type is available, do nothing as we're downgrading from 4.0.0
4
+ IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'jsonb') THEN
5
+ -- do nothing - it should already be already jsonb
6
+ -- Otherwise, use json type for the args column if available
7
+ ELSIF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'json') THEN
8
+ -- this should only happen if someone downgrades QC and their database < pg 9.4
9
+ ALTER TABLE queue_classic_jobs ALTER COLUMN args TYPE json USING args::json;
10
+ END IF;
11
+
12
+
13
+ END$$;
14
+
15
+
16
+ --
17
+ -- Re install the lock_head function
18
+ --
19
+
20
+ -- We are declaring the return type to be queue_classic_jobs.
21
+ -- This is ok since I am assuming that all of the users added queues will
22
+ -- have identical columns to queue_classic_jobs.
23
+ -- When QC supports queues with columns other than the default, we will have to change this.
24
+
25
+ CREATE OR REPLACE FUNCTION lock_head(q_name varchar, top_boundary integer)
26
+ RETURNS SETOF queue_classic_jobs AS $$
27
+ DECLARE
28
+ unlocked bigint;
29
+ relative_top integer;
30
+ job_count integer;
31
+ BEGIN
32
+ -- The purpose is to release contention for the first spot in the table.
33
+ -- The select count(*) is going to slow down dequeue performance but allow
34
+ -- for more workers. Would love to see some optimization here...
35
+
36
+ EXECUTE 'SELECT count(*) FROM '
37
+ || '(SELECT * FROM queue_classic_jobs '
38
+ || ' WHERE locked_at IS NULL'
39
+ || ' AND q_name = '
40
+ || quote_literal(q_name)
41
+ || ' AND scheduled_at <= '
42
+ || quote_literal(now())
43
+ || ' LIMIT '
44
+ || quote_literal(top_boundary)
45
+ || ') limited'
46
+ INTO job_count;
47
+
48
+ SELECT TRUNC(random() * (top_boundary - 1))
49
+ INTO relative_top;
50
+
51
+ IF job_count < top_boundary THEN
52
+ relative_top = 0;
53
+ END IF;
54
+
55
+ LOOP
56
+ BEGIN
57
+ EXECUTE 'SELECT id FROM queue_classic_jobs '
58
+ || ' WHERE locked_at IS NULL'
59
+ || ' AND q_name = '
60
+ || quote_literal(q_name)
61
+ || ' AND scheduled_at <= '
62
+ || quote_literal(now())
63
+ || ' ORDER BY id ASC'
64
+ || ' LIMIT 1'
65
+ || ' OFFSET ' || quote_literal(relative_top)
66
+ || ' FOR UPDATE NOWAIT'
67
+ INTO unlocked;
68
+ EXIT;
69
+ EXCEPTION
70
+ WHEN lock_not_available THEN
71
+ -- do nothing. loop again and hope we get a lock
72
+ END;
73
+ END LOOP;
74
+
75
+ RETURN QUERY EXECUTE 'UPDATE queue_classic_jobs '
76
+ || ' SET locked_at = (CURRENT_TIMESTAMP),'
77
+ || ' locked_by = (select pg_backend_pid())'
78
+ || ' WHERE id = $1'
79
+ || ' AND locked_at is NULL'
80
+ || ' RETURNING *'
81
+ USING unlocked;
82
+
83
+ RETURN;
84
+ END $$ LANGUAGE plpgsql;
85
+
86
+ CREATE OR REPLACE FUNCTION lock_head(tname varchar) RETURNS SETOF queue_classic_jobs AS $$ BEGIN
87
+ RETURN QUERY EXECUTE 'SELECT * FROM lock_head($1,10)' USING tname;
88
+ END $$ LANGUAGE plpgsql;
@@ -1,10 +1,10 @@
1
1
  DO $$DECLARE r record;
2
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;
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
8
  END$$;
9
9
 
10
10
  DO $$DECLARE r record;
@@ -1,9 +1,9 @@
1
1
  DO $$DECLARE r record;
2
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;
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
9
  END$$;
@@ -0,0 +1,6 @@
1
+ DO $$DECLARE r record;
2
+ BEGIN
3
+ ALTER TABLE queue_classic_jobs ALTER COLUMN args TYPE jsonb USING args::jsonb;
4
+ DROP FUNCTION IF EXISTS lock_head(tname varchar);
5
+ DROP FUNCTION IF EXISTS lock_head(q_name varchar, top_boundary integer);
6
+ END$$;
@@ -1,37 +1,40 @@
1
- require File.expand_path("../helper.rb", __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
2
4
 
3
5
  if ENV["QC_BENCHMARK"]
4
6
  class BenchmarkTest < QCTest
7
+ BENCHMARK_SIZE = Integer(ENV.fetch("QC_BENCHMARK_SIZE", 10_000))
8
+ BENCHMARK_MAX_TIME_DEQUEUE = Integer(ENV.fetch("QC_BENCHMARK_MAX_TIME_DEQUEUE", 30))
9
+ BENCHMARK_MAX_TIME_ENQUEUE = Integer(ENV.fetch("QC_BENCHMARK_MAX_TIME_ENQUEUE", 5))
5
10
 
6
11
  def test_enqueue
7
- n = 10_000
8
12
  start = Time.now
9
- n.times do
10
- QC.enqueue("1.odd?", [])
13
+ BENCHMARK_SIZE.times do
14
+ QC.enqueue("1.odd?")
11
15
  end
12
- assert_equal(n, QC.count)
16
+ assert_equal(BENCHMARK_SIZE, QC.count)
13
17
 
14
18
  elapsed = Time.now - start
15
- assert_in_delta(4, elapsed, 1)
19
+ assert_operator(elapsed, :<, BENCHMARK_MAX_TIME_ENQUEUE)
16
20
  end
17
21
 
18
22
  def test_dequeue
19
23
  worker = QC::Worker.new
20
24
  worker.running = true
21
- n = 10_000
22
- n.times do
23
- QC.enqueue("1.odd?", [])
25
+ BENCHMARK_SIZE.times do
26
+ QC.enqueue("1.odd?")
24
27
  end
25
- assert_equal(n, QC.count)
28
+ assert_equal(BENCHMARK_SIZE, QC.count)
26
29
 
27
30
  start = Time.now
28
- n.times do
31
+ BENCHMARK_SIZE.times do
29
32
  worker.work
30
33
  end
31
34
  elapsed = Time.now - start
32
35
 
33
36
  assert_equal(0, QC.count)
34
- assert_in_delta(10, elapsed, 3)
37
+ assert_operator(elapsed, :<, BENCHMARK_MAX_TIME_DEQUEUE)
35
38
  end
36
39
 
37
40
  end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class ConfigTest < QCTest
6
+ def setup
7
+ QC.reset_config
8
+ end
9
+
10
+ def teardown
11
+ QC.reset_config
12
+ end
13
+
14
+ def test_app_name_default
15
+ assert_equal "queue_classic", QC.app_name
16
+ end
17
+
18
+ def test_configure_app_name_with_env_var
19
+ with_env "QC_APP_NAME" => "zomg_qc" do
20
+ assert_equal "zomg_qc", QC.app_name
21
+ end
22
+ end
23
+
24
+ def test_wait_time_default
25
+ assert_equal 5, QC.wait_time
26
+ end
27
+
28
+ def test_configure_wait_time_with_env_var
29
+ with_env "QC_LISTEN_TIME" => "7" do
30
+ assert_equal 7, QC.wait_time
31
+ end
32
+ end
33
+
34
+ def test_table_name_default
35
+ assert_equal "queue_classic_jobs", QC.table_name
36
+ end
37
+
38
+ def test_queue_default
39
+ assert_equal "default", QC.queue
40
+ assert_equal "default", QC.default_queue.name
41
+ end
42
+
43
+ def test_configure_queue_with_env_var
44
+ with_env "QUEUE" => "priority" do
45
+ assert_equal "priority", QC.queue
46
+ assert_equal "priority", QC.default_queue.name
47
+ end
48
+ end
49
+
50
+ def test_assign_default_queue
51
+ QC.default_queue = QC::Queue.new "dispensable"
52
+ assert_equal "default", QC.queue
53
+ assert_equal "dispensable", QC.default_queue.name
54
+ end
55
+
56
+ def test_queues_default
57
+ assert_equal [], QC.queues
58
+ end
59
+
60
+ def test_configure_queues_with_env_var
61
+ with_env "QUEUES" => "first,second,third" do
62
+ assert_equal %w(first second third), QC.queues
63
+ end
64
+ end
65
+
66
+ def test_configure_queues_with_whitespace
67
+ with_env "QUEUES" => " one, two, three " do
68
+ assert_equal %w(one two three), QC.queues
69
+ end
70
+ end
71
+
72
+ def test_top_bound_default
73
+ assert_equal 9, QC.top_bound
74
+ end
75
+
76
+ def test_configure_top_bound_with_env_var
77
+ with_env "QC_TOP_BOUND" => "5" do
78
+ assert_equal 5, QC.top_bound
79
+ end
80
+ end
81
+
82
+ def test_fork_worker_default
83
+ refute QC.fork_worker?
84
+ end
85
+
86
+ def test_configure_fork_worker_with_env_var
87
+ with_env "QC_FORK_WORKER" => "yo" do
88
+ assert QC.fork_worker?
89
+ end
90
+ end
91
+
92
+ def test_configuration_constants_are_deprecated
93
+ warning = capture_stderr_output do
94
+ QC::FORK_WORKER
95
+ end
96
+ assert_match "QC::FORK_WORKER is deprecated", warning
97
+ assert_match "QC.fork_worker? instead", warning
98
+ end
99
+
100
+ class TestWorker < QC::Worker; end
101
+
102
+ def test_default_worker_class
103
+ assert_equal QC::Worker, QC.default_worker_class
104
+ end
105
+
106
+ def test_configure_default_worker_class_with_env_var
107
+ if RUBY_VERSION =~ /^1\.9\./
108
+ skip "Kernel.const_get in Ruby 1.9.x does not perform recursive lookups"
109
+ end
110
+ with_env "QC_DEFAULT_WORKER_CLASS" => "ConfigTest::TestWorker" do
111
+ assert_equal TestWorker, QC.default_worker_class
112
+ end
113
+ end
114
+
115
+ def test_assign_default_worker_class
116
+ original_worker = QC.default_worker_class
117
+ QC.default_worker_class = TestWorker
118
+
119
+ assert_equal TestWorker, QC.default_worker_class
120
+ ensure
121
+ QC.default_worker_class = original_worker
122
+ end
123
+ end
data/test/helper.rb CHANGED
@@ -1,9 +1,19 @@
1
- $: << File.expand_path("lib")
2
- $: << File.expand_path("test")
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ require "minitest/reporters"
5
+
6
+ Bundler.setup :default, :test
7
+
8
+ if ENV['CIRCLECI'] == "true"
9
+ Minitest::Reporters.use! Minitest::Reporters::JUnitReporter.new
10
+ else
11
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
12
+ end
3
13
 
4
14
  ENV["DATABASE_URL"] ||= "postgres:///queue_classic_test"
5
15
 
6
- require "queue_classic"
16
+ require_relative '../lib/queue_classic'
7
17
  require "stringio"
8
18
  require "minitest/autorun"
9
19
 
@@ -48,4 +58,38 @@ class QCTest < Minitest::Test
48
58
  $stdout = original_stdout
49
59
  end
50
60
 
61
+ def with_env(temporary_environment)
62
+ original_environment = {}
63
+ temporary_environment.each do |name, value|
64
+ original_environment[name] = ENV[name]
65
+ ENV[name] = value
66
+ end
67
+ yield
68
+ ensure
69
+ original_environment.each { |name, value| ENV[name] = value }
70
+ end
71
+
72
+ def stub_any_instance(class_name, method_name, definition)
73
+ new_method_name = "new_#{method_name}"
74
+ original_method_name = "original_#{method_name}"
75
+
76
+ method_present = class_name.instance_methods(false).include? method_name
77
+
78
+ if method_present
79
+ class_name.send(:alias_method, original_method_name, method_name)
80
+ class_name.send(:define_method, new_method_name, definition)
81
+ class_name.send(:alias_method, method_name, new_method_name)
82
+
83
+ yield
84
+ else
85
+ message = "#{class_name} does not have method #{method_name}."
86
+ message << "\nAvailable methods: #{class_name.instance_methods(false)}"
87
+ raise ArgumentError.new message
88
+ end
89
+ ensure
90
+ if method_present
91
+ class_name.send(:alias_method, method_name, original_method_name)
92
+ class_name.send(:undef_method, new_method_name)
93
+ end
94
+ end
51
95
  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
+
@@ -1,35 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path("../../helper.rb", __FILE__)
2
4
 
3
5
  class QueueClassicRailsConnectionTest < QCTest
4
6
  def before_setup
5
- Object.send :const_set, :ActiveRecord, Module.new
6
- ActiveRecord.const_set :Base, Module.new
7
-
7
+ @original_conn_adapter = QC.default_conn_adapter
8
8
  QC.default_conn_adapter = nil
9
9
  end
10
10
 
11
- def after_teardown
11
+ def before_teardown
12
12
  ActiveRecord.send :remove_const, :Base
13
13
  Object.send :remove_const, :ActiveRecord
14
+
15
+ QC.default_conn_adapter = @original_conn_adapter
14
16
  end
15
17
 
16
18
  def test_uses_active_record_connection_if_exists
17
19
  connection = get_connection
18
- assert connection.verify
20
+ QC.default_conn_adapter.execute('SELECT 1;')
21
+ connection.verify
19
22
  end
20
23
 
21
24
  def test_does_not_use_active_record_connection_if_env_var_set
22
- ENV['QC_RAILS_DATABASE'] = 'false'
23
- connection = get_connection
24
- assert_raises(MockExpectationError) { connection.verify }
25
- ENV['QC_RAILS_DATABASE'] = 'true'
25
+ with_env 'QC_RAILS_DATABASE' => 'false' do
26
+ connection = get_connection
27
+ QC.default_conn_adapter.execute('SELECT 1;')
28
+ assert_raises(MockExpectationError) { connection.verify }
29
+ end
26
30
  end
27
31
 
28
32
  private
29
33
  def get_connection
30
34
  connection = Minitest::Mock.new
31
- connection.expect(:raw_connection, QC::ConnAdapter.new.connection)
35
+ connection.expect(:raw_connection, QC::ConnAdapter.new(active_record_connection_share: true).connection)
32
36
 
37
+ Object.send :const_set, :ActiveRecord, Module.new
38
+ ActiveRecord.const_set :Base, Module.new
33
39
  ActiveRecord::Base.define_singleton_method(:connection) do
34
40
  connection
35
41
  end
@@ -1,24 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path("../../helper.rb", __FILE__)
2
4
 
3
5
  class QueueClassicTest < QCTest
6
+ def test_only_delegate_calls_to_queue_it_understands
7
+ e = assert_raises(NoMethodError) do
8
+ QC.probably_not
9
+ end
10
+ assert_match "undefined method `probably_not' for QC:Module", e.message
11
+ end
12
+
4
13
  def test_default_conn_adapter_default_value
5
14
  assert(QC.default_conn_adapter.is_a?(QC::ConnAdapter))
6
15
  end
7
16
 
8
- def test_default_conn_adapter=
17
+ def test_assigning_a_default_conn_adapter
18
+ original_conn_adapter = QC.default_conn_adapter
9
19
  connection = QC::ConnAdapter.new
10
20
  QC.default_conn_adapter = connection
11
21
  assert_equal(QC.default_conn_adapter, connection)
22
+ ensure
23
+ QC.default_conn_adapter = original_conn_adapter
12
24
  end
13
25
 
14
26
  def test_unlock_jobs_of_dead_workers
15
27
  # Insert a locked job
16
28
  adapter = QC::ConnAdapter.new
17
- query = "INSERT INTO #{QC::TABLE_NAME} (q_name, method, args, locked_by, locked_at) VALUES ('whatever', 'Kernel.puts', '[\"ok?\"]', 0, (CURRENT_TIMESTAMP))"
29
+ query = "INSERT INTO #{QC.table_name} (q_name, method, args, locked_by, locked_at) VALUES ('whatever', 'Kernel.puts', '[\"ok?\"]', 0, (CURRENT_TIMESTAMP))"
18
30
  adapter.execute(query)
19
31
 
20
32
  # We should have no unlocked jobs
21
- query_locked_jobs = "SELECT * FROM #{QC::TABLE_NAME} WHERE locked_at IS NULL"
33
+ query_locked_jobs = "SELECT * FROM #{QC.table_name} WHERE locked_at IS NULL"
22
34
  res = adapter.connection.exec(query_locked_jobs)
23
35
  assert_equal(0, res.count)
24
36
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../../helper.rb", __FILE__)
4
+
5
+ class QueueClassicTest < QCTest
6
+ def before_teardown
7
+ ActiveRecord.send :remove_const, :Base
8
+ Object.send :remove_const, :ActiveRecord
9
+
10
+ QC.default_conn_adapter = @original_conn_adapter
11
+ end
12
+
13
+ def test_lock_with_active_record_timestamp_type_cast
14
+ # Insert an unlocked job
15
+ p_queue = QC::Queue.new("priority_queue")
16
+ conn_adapter = Minitest::Mock.new
17
+ conn_adapter.expect(:execute, {"id" => '1', "q_name" => 'test', "method" => "Kernel.puts", "args" => "[]", "scheduled_at" => Time.now}, [String, String])
18
+ QC.default_conn_adapter = conn_adapter
19
+ assert_equal(p_queue.lock, {})
20
+ end
21
+ end