qc-additions 0.1.0

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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZjQ0ZWQ3OGM2OWY2ZjVkYmVmZThkNmM4ZTg0NTE1OTMzZDBjNzQ1OQ==
5
+ data.tar.gz: !binary |-
6
+ YzUxYmNmNWJmNTU0Y2U2NWQ1ZmI0ZWRhNGIwNWFiY2YxY2QyMWM4ZQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MzczZTc2MTE2ZTM0ZTlmMzAyMjc3MjMwMTkwNGUzMzcxOTU0ZTIxYWNhODNj
10
+ OTk2ZWNkNmU3ODdmYWY0NDZjYmMwNjcyYTU0N2U3YmEzNmJmMWY4NTE0MWUy
11
+ NWZjODMyOGM0NjMxZDhmOTVmMGY4ZjVkNzdiMzI2YmY3NmExYWI=
12
+ data.tar.gz: !binary |-
13
+ ZDEwYTAyN2NkYWI1ZmU1ZTM5ZjRjZmRhNmNmM2E3NzUxMTRkMDA5ZDIzOThi
14
+ NmUwYzBmNjE4YWMyZmVhODM0ODRiYjJiNjIyYTkyMDIwMTg1MzExZjYzZTEx
15
+ MTc1MzI0OTYzZWJhZDQwOTBkZDgxZDk1YmY5MGU2ZTNkNjJhMjY=
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # qc-additions
2
+
3
+
4
+ This gem adds some methods to [queue_classic](https://github.com/ryandotsmith/queue_classic) queues that I found helpful:
5
+
6
+ * `enqueue_if_not_queued(method, *args)`
7
+ * `job_count(method, *args)`
8
+ * `job_exists?(method, *args)`
9
+
10
+
11
+ An index might help to speed up the `job_count` and `job_exists?` queries. Although, I haven't really tested if there is much to gain by adding this:
12
+
13
+ ```SQL
14
+ CREATE INDEX idx_qc_unlocked_job_count ON queue_classic_jobs (q_name, method, args) WHERE locked_at IS NULL;
15
+ ```
16
+
17
+
18
+ ## Caveats when comparing args column
19
+
20
+ The method arguments are serialized to JSON. However, the comparison performed when looking for jobs in the database is a string comparison. Results might be incorrect if there is more than one way to serialize the arguments to a JSON string. It should be safe for simple things like passing a numeric id.
21
+
22
+
23
+ ---
24
+
25
+
26
+ I have some of the original code from there:
27
+
28
+ * https://github.com/ryandotsmith/queue_classic/pull/92
29
+
30
+ * https://github.com/GreenplumChorus/queue_classic/commit/2719301c2813717692169c1eeab42d317df0ac59
@@ -0,0 +1,4 @@
1
+ require "queue_classic"
2
+
3
+ require "qc-additions/queries"
4
+ require "qc-additions/queue"
@@ -0,0 +1,22 @@
1
+ module QC
2
+ module Queries
3
+ extend self
4
+
5
+ # TODO:
6
+ # Once PostgreSQL supports JSON comparison, use it.
7
+ # But older versions should be supported too (at least >= 9.0).
8
+
9
+ def job_count(q_name, method, args)
10
+ s = "SELECT COUNT(*) FROM #{TABLE_NAME} WHERE q_name = $1 AND method = $2 AND args::text = $3 AND locked_at IS NULL"
11
+ r = Conn.execute(s, q_name, method, JSON.dump(args))
12
+ r["count"].to_i
13
+ end
14
+
15
+ def job_exists?(q_name, method, args)
16
+ s = "SELECT 1 AS one FROM #{TABLE_NAME} WHERE q_name = $1 AND method = $2 AND args::text = $3 AND locked_at IS NULL LIMIT 1"
17
+ r = Conn.execute(s, q_name, method, JSON.dump(args))
18
+ !r.nil?
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module QC
2
+ class Queue
3
+
4
+ def enqueue_if_not_queued(method, *args)
5
+ enqueue(method, *args) unless job_exists?(method, *args)
6
+ end
7
+
8
+ def job_count(method, *args)
9
+ Queries.job_count(name, method, args)
10
+ end
11
+
12
+ def job_exists?(method, *args)
13
+ Queries.job_exists?(name, method, args)
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ class QueueTest < QCTest
4
+
5
+ def test_enqueue_if_not_queued
6
+ QC.enqueue_if_not_queued("Klass.method", "arg1", "arg2")
7
+ QC.enqueue_if_not_queued("Klass.method", "arg1", "arg2")
8
+ QC.lock
9
+ QC.enqueue_if_not_queued("Klass.method", "arg1", "arg2")
10
+ assert_equal(1, QC.job_count("Klass.method", "arg1", "arg2"))
11
+ end
12
+
13
+ def test_job_count
14
+ #Should return the count of unstarted jobs that match both method and arguments
15
+ QC.enqueue("Klass.method", "arg1", "arg2")
16
+ QC.enqueue("Klass.method", "arg1", "arg2")
17
+ QC.enqueue("Klass.method", "arg1", "arg2")
18
+ QC.enqueue("Klass.method", "arg3", "arg4")
19
+ QC.enqueue("Klass.other_method", "arg1", "arg2")
20
+ QC.lock #start the first job
21
+ assert_equal(2, QC.job_count("Klass.method", "arg1", "arg2"))
22
+ end
23
+
24
+ def test_job_exists
25
+ # Should return true if an unstarted job with same method and arguments exists
26
+ assert_equal(false, QC.job_exists?("Klass.method"))
27
+ QC.enqueue("Klass.method")
28
+ assert_equal(true, QC.job_exists?("Klass.method"))
29
+ assert_equal(false, QC.job_exists?("Klass.method", "arg1"))
30
+ assert_equal(false, QC.job_exists?("Klass.other_method"))
31
+ assert_equal(false, QC.job_exists?("Klass.other_method", "arg1"))
32
+ QC.lock # start the job
33
+ assert_equal(false, QC.job_exists?("Klass.method"))
34
+ end
35
+
36
+ end
@@ -0,0 +1,38 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ if ENV["QC_BENCHMARK"]
4
+ class BenchmarkTest < QCTest
5
+
6
+ def test_enqueue
7
+ n = 10_000
8
+ start = Time.now
9
+ n.times do
10
+ QC.enqueue("1.odd?", [])
11
+ end
12
+ assert_equal(n, QC.count)
13
+
14
+ elapsed = Time.now - start
15
+ assert_in_delta(4, elapsed, 1)
16
+ end
17
+
18
+ def test_dequeue
19
+ worker = QC::Worker.new
20
+ worker.running = true
21
+ n = 10_000
22
+ n.times do
23
+ QC.enqueue("1.odd?", [])
24
+ end
25
+ assert_equal(n, QC.count)
26
+
27
+ start = Time.now
28
+ n.times do
29
+ worker.work
30
+ end
31
+ elapsed = Time.now - start
32
+
33
+ assert_equal(0, QC.count)
34
+ assert_in_delta(10, elapsed, 3)
35
+ end
36
+
37
+ end
38
+ end
data/test/conn_test.rb ADDED
@@ -0,0 +1,22 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ class ConnTest < QCTest
4
+
5
+ def test_extracts_the_segemnts_to_connect
6
+ database_url = "postgres://ryan:secret@localhost:1234/application_db"
7
+ normalized = QC::Conn.normalize_db_url(URI.parse(database_url))
8
+ assert_equal ["localhost",
9
+ 1234,
10
+ nil, "",
11
+ "application_db",
12
+ "ryan",
13
+ "secret"], normalized
14
+ end
15
+
16
+ def test_regression_database_url_without_host
17
+ database_url = "postgres:///my_db"
18
+ normalized = QC::Conn.normalize_db_url(URI.parse(database_url))
19
+ assert_equal [nil, 5432, nil, "", "my_db", nil, nil], normalized
20
+ end
21
+
22
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,66 @@
1
+ $: << File.expand_path("lib")
2
+ $: << File.expand_path("test")
3
+
4
+ ENV["DATABASE_URL"] ||= "postgres:///queue_classic_test"
5
+
6
+ require "queue_classic"
7
+ require "qc-additions"
8
+ require "stringio"
9
+ require "minitest/autorun"
10
+
11
+ class QCTest < Minitest::Test
12
+
13
+ def setup
14
+ init_db
15
+ end
16
+
17
+ def teardown
18
+ QC.delete_all
19
+ end
20
+
21
+ def init_db(table_name="queue_classic_jobs")
22
+ QC::Conn.execute("SET client_min_messages TO 'warning'")
23
+ QC::Setup.drop
24
+ QC::Setup.create
25
+ QC::Conn.execute(<<EOS)
26
+ DO $$
27
+ -- Set initial sequence to a large number to test the entire toolchain
28
+ -- works on integers with higher bits set.
29
+ DECLARE
30
+ quoted_name text;
31
+ quoted_size text;
32
+ BEGIN
33
+ -- Find the name of the relevant sequence.
34
+ --
35
+ -- pg_get_serial_sequence quotes identifiers as part of its
36
+ -- behavior.
37
+ SELECT name
38
+ INTO STRICT quoted_name
39
+ FROM pg_get_serial_sequence('queue_classic_jobs', 'id') AS name;
40
+
41
+ -- Don't quote, because ALTER SEQUENCE RESTART doesn't like
42
+ -- general literals, only unquoted numeric literals.
43
+ SELECT pow(2, 34)::text AS size
44
+ INTO STRICT quoted_size;
45
+
46
+ EXECUTE 'ALTER SEQUENCE ' || quoted_name ||
47
+ ' RESTART ' || quoted_size || ';';
48
+ END;
49
+ $$;
50
+ EOS
51
+ end
52
+
53
+ def capture_debug_output
54
+ original_debug = ENV['DEBUG']
55
+ original_stdout = $stdout
56
+
57
+ ENV['DEBUG'] = "true"
58
+ $stdout = StringIO.new
59
+ yield
60
+ $stdout.string
61
+ ensure
62
+ ENV['DEBUG'] = original_debug
63
+ $stdout = original_stdout
64
+ end
65
+
66
+ end
@@ -0,0 +1,103 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ class QueueTest < QCTest
4
+
5
+ def test_enqueue
6
+ QC.enqueue("Klass.method")
7
+ end
8
+
9
+ def test_respond_to
10
+ assert_equal(true, QC.respond_to?(:enqueue))
11
+ end
12
+
13
+ def test_lock
14
+ QC.enqueue("Klass.method")
15
+
16
+ # See helper.rb for more information about the large initial id
17
+ # number.
18
+ expected = {:id=>(2**34).to_s, :method=>"Klass.method", :args=>[]}
19
+ assert_equal(expected, QC.lock)
20
+ end
21
+
22
+ def test_lock_when_empty
23
+ assert_nil(QC.lock)
24
+ end
25
+
26
+ def test_count
27
+ QC.enqueue("Klass.method")
28
+ assert_equal(1, QC.count)
29
+ end
30
+
31
+ def test_delete
32
+ QC.enqueue("Klass.method")
33
+ assert_equal(1, QC.count)
34
+ QC.delete(QC.lock[:id])
35
+ assert_equal(0, QC.count)
36
+ end
37
+
38
+ def test_delete_all
39
+ QC.enqueue("Klass.method")
40
+ QC.enqueue("Klass.method")
41
+ assert_equal(2, QC.count)
42
+ QC.delete_all
43
+ assert_equal(0, QC.count)
44
+ end
45
+
46
+ def test_delete_all_by_queue_name
47
+ p_queue = QC::Queue.new("priority_queue")
48
+ s_queue = QC::Queue.new("secondary_queue")
49
+ p_queue.enqueue("Klass.method")
50
+ s_queue.enqueue("Klass.method")
51
+ assert_equal(1, p_queue.count)
52
+ assert_equal(1, s_queue.count)
53
+ p_queue.delete_all
54
+ assert_equal(0, p_queue.count)
55
+ assert_equal(1, s_queue.count)
56
+ end
57
+
58
+ def test_queue_instance
59
+ queue = QC::Queue.new("queue_classic_jobs", false)
60
+ queue.enqueue("Klass.method")
61
+ assert_equal(1, queue.count)
62
+ queue.delete(queue.lock[:id])
63
+ assert_equal(0, queue.count)
64
+ end
65
+
66
+ def test_repair_after_error
67
+ queue = QC::Queue.new("queue_classic_jobs", false)
68
+ queue.enqueue("Klass.method")
69
+ assert_equal(1, queue.count)
70
+ connection = QC::Conn.connection
71
+ saved_method = connection.method(:exec)
72
+ def connection.exec(*args)
73
+ raise PGError
74
+ end
75
+ assert_raises(PG::Error) { queue.enqueue("Klass.other_method") }
76
+ assert_equal(1, queue.count)
77
+ queue.enqueue("Klass.other_method")
78
+ assert_equal(2, queue.count)
79
+ rescue PG::Error
80
+ QC::Conn.disconnect
81
+ assert false, "Expected to QC repair after connection error"
82
+ end
83
+
84
+ def test_custom_default_queue
85
+ queue_class = Class.new do
86
+ attr_accessor :jobs
87
+ def enqueue(method, *args)
88
+ @jobs ||= []
89
+ @jobs << method
90
+ end
91
+ end
92
+
93
+ queue_instance = queue_class.new
94
+ QC.default_queue = queue_instance
95
+
96
+ QC.enqueue("Klass.method1")
97
+ QC.enqueue("Klass.method2")
98
+
99
+ assert_equal ["Klass.method1", "Klass.method2"], queue_instance.jobs
100
+ ensure
101
+ QC.default_queue = nil
102
+ end
103
+ end
@@ -0,0 +1,125 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ module TestObject
4
+ extend self
5
+ def no_args; return nil; end
6
+ def one_arg(a); return a; end
7
+ def two_args(a,b); return [a,b]; end
8
+ end
9
+
10
+ # This not only allows me to test what happens
11
+ # when a failure occurs but it also demonstrates
12
+ # how to override the worker to handle failures the way
13
+ # you want.
14
+ class TestWorker < QC::Worker
15
+ attr_accessor :failed_count
16
+
17
+ def initialize(*args)
18
+ super(*args)
19
+ @failed_count = 0
20
+ end
21
+
22
+ def handle_failure(job,e)
23
+ @failed_count += 1
24
+ end
25
+ end
26
+
27
+ class WorkerTest < QCTest
28
+
29
+ def test_work
30
+ QC.enqueue("TestObject.no_args")
31
+ worker = TestWorker.new
32
+ assert_equal(1, QC.count)
33
+ worker.work
34
+ assert_equal(0, QC.count)
35
+ assert_equal(0, worker.failed_count)
36
+ end
37
+
38
+ def test_failed_job
39
+ QC.enqueue("TestObject.not_a_method")
40
+ worker = TestWorker.new
41
+ worker.work
42
+ assert_equal(1, worker.failed_count)
43
+ end
44
+
45
+ def test_failed_job_is_logged
46
+ output = capture_debug_output do
47
+ QC.enqueue("TestObject.not_a_method")
48
+ QC::Worker.new.work
49
+ end
50
+ expected_output = /lib=queue-classic at=handle_failure job={:id=>"\d+", :method=>"TestObject.not_a_method", :args=>\[\]} error=#<NoMethodError: undefined method `not_a_method' for TestObject:Module>/
51
+ assert_match(expected_output, output, "=== debug output ===\n #{output}")
52
+ end
53
+
54
+ def test_log_yield
55
+ output = capture_debug_output do
56
+ QC.log_yield(:action => "test") do
57
+ 0 == 1
58
+ end
59
+ end
60
+ expected_output = /lib=queue-classic action=test elapsed=\d*/
61
+ assert_match(expected_output, output, "=== debug output ===\n #{output}")
62
+ end
63
+
64
+ def test_log
65
+ output = capture_debug_output do
66
+ QC.log(:action => "test")
67
+ end
68
+ expected_output = /lib=queue-classic action=test/
69
+ assert_match(expected_output, output, "=== debug output ===\n #{output}")
70
+ end
71
+
72
+ def test_work_with_no_args
73
+ QC.enqueue("TestObject.no_args")
74
+ worker = TestWorker.new
75
+ r = worker.work
76
+ assert_nil(r)
77
+ assert_equal(0, worker.failed_count)
78
+ end
79
+
80
+ def test_work_with_one_arg
81
+ QC.enqueue("TestObject.one_arg", "1")
82
+ worker = TestWorker.new
83
+ r = worker.work
84
+ assert_equal("1", r)
85
+ assert_equal(0, worker.failed_count)
86
+ end
87
+
88
+ def test_work_with_two_args
89
+ QC.enqueue("TestObject.two_args", "1", 2)
90
+ worker = TestWorker.new
91
+ r = worker.work
92
+ assert_equal(["1", 2], r)
93
+ assert_equal(0, worker.failed_count)
94
+ end
95
+
96
+ def test_work_custom_queue
97
+ p_queue = QC::Queue.new("priority_queue")
98
+ p_queue.enqueue("TestObject.two_args", "1", 2)
99
+ worker = TestWorker.new(q_name: "priority_queue")
100
+ r = worker.work
101
+ assert_equal(["1", 2], r)
102
+ assert_equal(0, worker.failed_count)
103
+ end
104
+
105
+ def test_worker_listens_on_chan
106
+ p_queue = QC::Queue.new("priority_queue")
107
+ p_queue.enqueue("TestObject.two_args", "1", 2)
108
+ worker = TestWorker.new(q_name: "priority_queue", listening_worker: true)
109
+ r = worker.work
110
+ assert_equal(["1", 2], r)
111
+ assert_equal(0, worker.failed_count)
112
+ end
113
+
114
+ def test_worker_ueses_one_conn
115
+ QC.enqueue("TestObject.no_args")
116
+ worker = TestWorker.new
117
+ worker.work
118
+ assert_equal(
119
+ 1,
120
+ QC::Conn.execute("SELECT count(*) from pg_stat_activity where datname = current_database()")["count"].to_i,
121
+ "Multiple connections found -- are there open connections to #{ QC::Conn.db_url } in other terminals?"
122
+ )
123
+ end
124
+
125
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qc-additions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jakob Rath
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-07-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: queue_classic
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 2.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 2.2.0
27
+ description: Add some methods to queue_classic to determine whether a job exists or
28
+ how many times a given job is already queued.
29
+ email: mail@jakobrath.eu
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - lib/qc-additions/queries.rb
36
+ - lib/qc-additions/queue.rb
37
+ - lib/qc-additions.rb
38
+ - test/additions_test.rb
39
+ - test/benchmark_test.rb
40
+ - test/conn_test.rb
41
+ - test/helper.rb
42
+ - test/queue_test.rb
43
+ - test/worker_test.rb
44
+ homepage: https://github.com/JakobR/qc-additions
45
+ licenses: []
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubyforge_project:
63
+ rubygems_version: 2.0.3
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Add some methods to queue_classic.
67
+ test_files:
68
+ - test/additions_test.rb
69
+ - test/benchmark_test.rb
70
+ - test/conn_test.rb
71
+ - test/queue_test.rb
72
+ - test/worker_test.rb
73
+ has_rdoc: