qc-additions 0.1.0

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