backburner-allq 1.0.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +29 -0
  4. data/CHANGELOG.md +133 -0
  5. data/CONTRIBUTING.md +37 -0
  6. data/Gemfile +4 -0
  7. data/HOOKS.md +99 -0
  8. data/LICENSE +22 -0
  9. data/README.md +658 -0
  10. data/Rakefile +17 -0
  11. data/TODO +4 -0
  12. data/backburner-allq.gemspec +26 -0
  13. data/bin/backburner +7 -0
  14. data/circle.yml +3 -0
  15. data/deploy.sh +3 -0
  16. data/examples/custom.rb +25 -0
  17. data/examples/demo.rb +60 -0
  18. data/examples/god.rb +46 -0
  19. data/examples/hooked.rb +87 -0
  20. data/examples/retried.rb +31 -0
  21. data/examples/simple.rb +43 -0
  22. data/examples/stress.rb +31 -0
  23. data/lib/backburner.rb +75 -0
  24. data/lib/backburner/allq_wrapper.rb +317 -0
  25. data/lib/backburner/async_proxy.rb +25 -0
  26. data/lib/backburner/cli.rb +53 -0
  27. data/lib/backburner/configuration.rb +48 -0
  28. data/lib/backburner/connection.rb +157 -0
  29. data/lib/backburner/helpers.rb +193 -0
  30. data/lib/backburner/hooks.rb +53 -0
  31. data/lib/backburner/job.rb +118 -0
  32. data/lib/backburner/logger.rb +53 -0
  33. data/lib/backburner/performable.rb +95 -0
  34. data/lib/backburner/queue.rb +145 -0
  35. data/lib/backburner/tasks.rb +54 -0
  36. data/lib/backburner/version.rb +3 -0
  37. data/lib/backburner/worker.rb +221 -0
  38. data/lib/backburner/workers/forking.rb +52 -0
  39. data/lib/backburner/workers/simple.rb +29 -0
  40. data/lib/backburner/workers/threading.rb +163 -0
  41. data/lib/backburner/workers/threads_on_fork.rb +263 -0
  42. data/test/async_proxy_test.rb +36 -0
  43. data/test/back_burner_test.rb +88 -0
  44. data/test/connection_test.rb +179 -0
  45. data/test/fixtures/hooked.rb +122 -0
  46. data/test/fixtures/test_fork_jobs.rb +72 -0
  47. data/test/fixtures/test_forking_jobs.rb +56 -0
  48. data/test/fixtures/test_jobs.rb +87 -0
  49. data/test/fixtures/test_queue_settings.rb +14 -0
  50. data/test/helpers/templogger.rb +22 -0
  51. data/test/helpers_test.rb +278 -0
  52. data/test/hooks_test.rb +112 -0
  53. data/test/job_test.rb +185 -0
  54. data/test/logger_test.rb +44 -0
  55. data/test/performable_test.rb +88 -0
  56. data/test/queue_test.rb +69 -0
  57. data/test/test_helper.rb +128 -0
  58. data/test/worker_test.rb +157 -0
  59. data/test/workers/forking_worker_test.rb +181 -0
  60. data/test/workers/simple_worker_test.rb +350 -0
  61. data/test/workers/threading_worker_test.rb +104 -0
  62. data/test/workers/threads_on_fork_worker_test.rb +484 -0
  63. metadata +217 -0
@@ -0,0 +1,128 @@
1
+ require 'rubygems'
2
+ require 'tempfile'
3
+ require 'minitest/autorun'
4
+ begin
5
+ require 'mocha/setup'
6
+ rescue LoadError
7
+ require 'mocha'
8
+ end
9
+ $LOAD_PATH.unshift File.expand_path("lib")
10
+ require 'backburner'
11
+ require File.expand_path('../helpers/templogger', __FILE__)
12
+
13
+ # Configure Backburner
14
+ Backburner.configure do |config|
15
+ config.beanstalk_url = "beanstalk://127.0.0.1"
16
+ config.tube_namespace = "demo.test"
17
+ end
18
+
19
+ ## Kernel Extensions
20
+ require 'stringio'
21
+
22
+ module Kernel
23
+ # Redirect standard out, standard error and the buffered logger for sprinkle to StringIO
24
+ # capture_stdout { any_commands; you_want } => "all output from the commands"
25
+ def capture_stdout
26
+ if ENV['DEBUG'] # Skip if debug mode
27
+ yield
28
+ return ""
29
+ end
30
+
31
+ out = StringIO.new
32
+ $stdout = out
33
+ $stderr = out
34
+ yield
35
+ return out.string
36
+ ensure
37
+ $stdout = STDOUT
38
+ $stderr = STDERR
39
+ end
40
+ end
41
+
42
+ class User
43
+ attr_accessor :id, :name
44
+
45
+ def initialize(id, name)
46
+ @id, @name = id, name
47
+ end
48
+ end
49
+
50
+ class MiniTest::Spec
51
+ class << self
52
+ alias :should :it
53
+ alias :context :describe
54
+ end
55
+ alias :assert_no_match :refute_match
56
+ alias :assert_not_nil :refute_nil
57
+ alias :assert_not_equal :refute_equal
58
+
59
+ # assert_same_elements([:a, :b, :c], [:c, :a, :b]) => passes
60
+ def assert_same_elements(a1, a2, msg = nil)
61
+ [:select, :inject, :size].each do |m|
62
+ [a1, a2].each {|a| assert_respond_to(a, m, "Are you sure that #{a.inspect} is an array? It doesn't respond to #{m}.") }
63
+ end
64
+
65
+ assert a1h = a1.inject({}) { |h,e| h[e] ||= a1.select { |i| i == e }.size; h }
66
+ assert a2h = a2.inject({}) { |h,e| h[e] ||= a2.select { |i| i == e }.size; h }
67
+
68
+ assert_equal(a1h, a2h, msg)
69
+ end
70
+
71
+ # assert_contains(['a', '1'], /\d/) => passes
72
+ # assert_contains(['a', '1'], 'a') => passes
73
+ # assert_contains(['a', '1'], /not there/) => fails
74
+ def assert_contains(collection, x, extra_msg = "")
75
+ collection = [collection] unless collection.is_a?(Array)
76
+ msg = "#{x.inspect} not found in #{collection.to_a.inspect} #{extra_msg}"
77
+ case x
78
+ when Regexp
79
+ assert(collection.detect { |e| e =~ x }, msg)
80
+ else
81
+ assert(collection.include?(x), msg)
82
+ end
83
+ end
84
+
85
+ # silenced(5) { ... }
86
+ def silenced(time=3, &block)
87
+ Timeout::timeout(time) { capture_stdout(&block) }
88
+ end
89
+
90
+ def beanstalk_connection
91
+ Backburner::Connection.new(Backburner.configuration.beanstalk_url)
92
+ end
93
+
94
+ # pop_one_job(tube_name)
95
+ def pop_one_job(tube_name=Backburner.configuration.primary_queue)
96
+ tube_name = [Backburner.configuration.tube_namespace, tube_name].join(".")
97
+ connection = beanstalk_connection
98
+ connection.tubes.watch!(tube_name)
99
+ silenced(3) { @res = connection.tubes.reserve }
100
+ yield @res, Backburner.configuration.job_parser_proc.call(@res.body)
101
+ ensure
102
+ connection.close if connection
103
+ end
104
+
105
+ # clear_jobs!('foo')
106
+ def clear_jobs!(*tube_names)
107
+ connection = beanstalk_connection
108
+ tube_names.each do |tube_name|
109
+ expanded_name = [Backburner.configuration.tube_namespace, tube_name].join(".")
110
+ connection.tubes.find(expanded_name).clear
111
+ end
112
+ ensure
113
+ connection.close if connection
114
+ end
115
+
116
+ # Simulates a broken connection for any Beaneater::Connection. Will
117
+ # simulate a restored connection after `reconnects_after`. This is expected
118
+ # to be used when ensuring a Beaneater connection is open, therefore
119
+ def simulate_disconnect(connection, reconnects_after = 2)
120
+ connection.beanstalk.connection.connection.expects(:closed? => true)
121
+ returns = Array.new(reconnects_after - 1, stub('TCPSocket'))
122
+ returns.each do |socket|
123
+ result = (socket != returns.last)
124
+ socket.stubs(:closed? => result)
125
+ end
126
+ TCPSocket.expects(:new).times(returns.size).returns(*returns)
127
+ end
128
+ end # MiniTest::Spec
@@ -0,0 +1,157 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+ require File.expand_path('../fixtures/test_jobs', __FILE__)
3
+ require File.expand_path('../fixtures/hooked', __FILE__)
4
+
5
+ describe "Backburner::Worker module" do
6
+ before do
7
+ Backburner.default_queues.clear
8
+ clear_jobs!(Backburner.configuration.primary_queue, "test-plain", "test.bar", "bar.baz.foo")
9
+ end
10
+
11
+ describe "for enqueue class method" do
12
+ it "should support enqueuing plain job" do
13
+ Backburner::Worker.enqueue TestPlainJob, [7, 9], :ttr => 100, :pri => 2000
14
+ pop_one_job("test-plain") do |job, body|
15
+ assert_equal "TestPlainJob", body["class"]
16
+ assert_equal [7, 9], body["args"]
17
+ assert_equal 100, job.ttr
18
+ assert_equal 2000, job.pri
19
+ end
20
+ end # plain
21
+
22
+ it "should support enqueuing job with class queue priority" do
23
+ Backburner::Worker.enqueue TestJob, [3, 4], :ttr => 100
24
+ pop_one_job do |job, body|
25
+ assert_equal "TestJob", body["class"]
26
+ assert_equal [3, 4], body["args"]
27
+ assert_equal 100, job.ttr
28
+ assert_equal 100, job.pri
29
+ end
30
+ end # queue priority
31
+
32
+ it "should support enqueuing job with specified named priority" do
33
+ Backburner::Worker.enqueue TestJob, [3, 4], :ttr => 100, :pri => 'high'
34
+ pop_one_job do |job, body|
35
+ assert_equal "TestJob", body["class"]
36
+ assert_equal [3, 4], body["args"]
37
+ assert_equal 100, job.ttr
38
+ assert_equal 0, job.pri
39
+ end
40
+ end # queue named priority
41
+
42
+ it "should support enqueuing job with class queue respond_timeout" do
43
+ Backburner::Worker.enqueue TestJob, [3, 4]
44
+ pop_one_job do |job, body|
45
+ assert_equal "TestJob", body["class"]
46
+ assert_equal [3, 4], body["args"]
47
+ assert_equal 300, job.ttr
48
+ assert_equal 100, job.pri
49
+ end
50
+ end # queue respond_timeout
51
+
52
+ it "should support enqueuing job with custom queue" do
53
+ Backburner::Worker.enqueue TestJob, [6, 7], :queue => "test.bar", :pri => 5000
54
+ pop_one_job("test.bar") do |job, body|
55
+ assert_equal "TestJob", body["class"]
56
+ assert_equal [6, 7], body["args"]
57
+ assert_equal 0, job.delay
58
+ assert_equal 5000, job.pri
59
+ assert_equal 300, job.ttr
60
+ end
61
+ end # custom
62
+
63
+ it "should support async job" do
64
+ TestAsyncJob.async(:ttr => 100, :queue => "bar.baz.foo").foo(10, 5)
65
+ pop_one_job("bar.baz.foo") do |job, body|
66
+ assert_equal "TestAsyncJob", body["class"]
67
+ assert_equal [nil, "foo", 10, 5], body["args"]
68
+ assert_equal 100, job.ttr
69
+ assert_equal Backburner.configuration.default_priority, job.pri
70
+ end
71
+ end # async
72
+
73
+ it "should support enqueueing job with lambda queue" do
74
+ expected_queue_name = TestLambdaQueueJob.calculated_queue_name
75
+ Backburner::Worker.enqueue TestLambdaQueueJob, [6, 7], :queue => lambda { |klass| klass.calculated_queue_name }
76
+ pop_one_job(expected_queue_name) do |job, body|
77
+ assert_equal "TestLambdaQueueJob", body["class"]
78
+ assert_equal [6, 7], body["args"]
79
+ end
80
+ end
81
+ end # enqueue
82
+
83
+ describe "for start class method" do
84
+ it "should initialize and start the worker instance" do
85
+ ech = stub
86
+ Backburner::Worker.expects(:new).with("foo").returns(ech)
87
+ ech.expects(:start)
88
+ Backburner::Worker.start("foo")
89
+ end
90
+ end # start
91
+
92
+ describe "for tube_names accessor" do
93
+ before do
94
+ Backburner.default_queues << "baz"
95
+ Backburner.default_queues << "bam"
96
+ end
97
+
98
+ it "supports retrieving tubes" do
99
+ worker = Backburner::Worker.new(["foo", "bar"])
100
+ assert_equal ["foo", "bar"], worker.tube_names
101
+ end
102
+
103
+ it "supports single tube array arg" do
104
+ worker = Backburner::Worker.new([["foo", "bar"]])
105
+ assert_equal ["foo", "bar"], worker.tube_names
106
+ end
107
+
108
+ it "supports empty nil array arg with default values" do
109
+ worker = Backburner::Worker.new([nil])
110
+ assert_equal ['baz', 'bam'], worker.tube_names
111
+ end
112
+
113
+ it "supports single tube arg" do
114
+ worker = Backburner::Worker.new("foo")
115
+ assert_equal ["foo"], worker.tube_names
116
+ end
117
+
118
+ it "supports empty array arg with default values" do
119
+ worker = Backburner::Worker.new([])
120
+ assert_equal ['baz', 'bam'], worker.tube_names
121
+ end
122
+
123
+ it "supports nil arg with default values" do
124
+ worker = Backburner::Worker.new(nil)
125
+ assert_equal ['baz', 'bam'], worker.tube_names
126
+ end
127
+ end # tube_names
128
+
129
+ describe "for custom serialization" do
130
+ before do
131
+ Backburner.configure do |config|
132
+ @old_parser = config.job_parser_proc
133
+ @old_serializer = config.job_serializer_proc
134
+ config.job_parser_proc = lambda { |body| Marshal.load(body) }
135
+ config.job_serializer_proc = lambda { |body| Marshal.dump(body) }
136
+ end
137
+ end
138
+
139
+ after do
140
+ clear_jobs!('test-plain')
141
+ Backburner.configure do |config|
142
+ config.job_parser_proc = @old_parser
143
+ config.job_serializer_proc = @old_serializer
144
+ end
145
+ end
146
+
147
+ it "should support enqueuing a job" do
148
+ Backburner::Worker.enqueue TestPlainJob, [7, 9], :ttr => 100, :pri => 2000
149
+ pop_one_job("test-plain") do |job, body|
150
+ assert_equal "TestPlainJob", body[:class]
151
+ assert_equal [7, 9], body[:args]
152
+ assert_equal 100, job.ttr
153
+ assert_equal 2000, job.pri
154
+ end
155
+ end
156
+ end # custom serialization
157
+ end # Backburner::Worker
@@ -0,0 +1,181 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+ require File.expand_path('../../fixtures/test_forking_jobs', __FILE__)
3
+
4
+ describe "Backburner::Workers::Forking module" do
5
+
6
+ before do
7
+ Backburner.default_queues.clear
8
+ @worker_class = Backburner::Workers::Forking
9
+ end
10
+
11
+ describe "for prepare method" do
12
+ it "should make tube names array always unique to avoid duplication" do
13
+ worker = @worker_class.new(["foo", "demo.test.foo"])
14
+ capture_stdout { worker.prepare }
15
+ assert_equal ["demo.test.foo"], worker.tube_names
16
+ end
17
+
18
+ it "should watch specified tubes" do
19
+ worker = @worker_class.new(["foo", "bar"])
20
+ out = capture_stdout { worker.prepare }
21
+ assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
22
+ assert_same_elements ["demo.test.foo", "demo.test.bar"], worker.connection.tubes.watched.map(&:name)
23
+ assert_match(/demo\.test\.foo/, out)
24
+ end # multiple
25
+
26
+ it "should watch single tube" do
27
+ worker = @worker_class.new("foo")
28
+ out = capture_stdout { worker.prepare }
29
+ assert_equal ["demo.test.foo"], worker.tube_names
30
+ assert_same_elements ["demo.test.foo"], worker.connection.tubes.watched.map(&:name)
31
+ assert_match(/demo\.test\.foo/, out)
32
+ end # single
33
+
34
+ it "should respect default_queues settings" do
35
+ Backburner.default_queues.concat(["foo", "bar"])
36
+ worker = @worker_class.new
37
+ out = capture_stdout { worker.prepare }
38
+ assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
39
+ assert_same_elements ["demo.test.foo", "demo.test.bar"], worker.connection.tubes.watched.map(&:name)
40
+ assert_match(/demo\.test\.foo/, out)
41
+ end
42
+
43
+ it "should assign based on all tubes" do
44
+ @worker_class.any_instance.expects(:all_existing_queues).once.returns("bar")
45
+ worker = @worker_class.new
46
+ out = capture_stdout { worker.prepare }
47
+ assert_equal ["demo.test.bar"], worker.tube_names
48
+ assert_same_elements ["demo.test.bar"], worker.connection.tubes.watched.map(&:name)
49
+ assert_match(/demo\.test\.bar/, out)
50
+ end # all assign
51
+
52
+ it "should properly retrieve all tubes" do
53
+ worker = @worker_class.new
54
+ out = capture_stdout { worker.prepare }
55
+ assert_contains worker.tube_names, "demo.test.backburner-jobs"
56
+ assert_contains worker.connection.tubes.watched.map(&:name), "demo.test.backburner-jobs"
57
+ assert_match(/demo\.test\.backburner-jobs/, out)
58
+ end # all read
59
+ end # prepare
60
+
61
+ describe "for fork_one_job method" do
62
+
63
+ it "should fork, work job, and exit" do
64
+ clear_jobs!("bar.foo")
65
+ @worker_class.enqueue TestJobForking, [1, 2], :queue => "bar.foo"
66
+
67
+ fake_pid = 45
68
+ Process.expects(:fork).returns(fake_pid).yields
69
+ @worker_class.any_instance.expects(:work_one_job)
70
+ @worker_class.any_instance.expects(:coolest_exit)
71
+ Process.expects(:wait).with(fake_pid)
72
+
73
+ silenced(2) do
74
+ worker = @worker_class.new('bar.foo')
75
+ worker.prepare
76
+ worker.fork_one_job
77
+ end
78
+ end
79
+
80
+ end # fork_one_job
81
+
82
+ describe "practical tests" do
83
+
84
+ before do
85
+ @templogger = Templogger.new('/tmp')
86
+ Backburner.configure { |config| config.logger = @templogger.logger }
87
+ $worker_test_count = 0
88
+ $worker_success = false
89
+ $worker_raise = false
90
+ clear_jobs!('response')
91
+ clear_jobs!('bar.foo.1', 'bar.foo.2', 'bar.foo.3', 'bar.foo.4', 'bar.foo.5')
92
+ silenced do
93
+ @response_worker = @worker_class.new('response')
94
+ end
95
+ end
96
+
97
+ after do
98
+ @templogger.close
99
+ Backburner.configuration.logger = nil
100
+ clear_jobs!('response')
101
+ clear_jobs!('bar.foo.1', 'bar.foo.2', 'bar.foo.3', 'bar.foo.4', 'bar.foo.5')
102
+ end
103
+
104
+
105
+ it "should work an enqueued job" do
106
+ @worker = @worker_class.new('bar.foo.1')
107
+ @worker_class.enqueue TestJobForking, [1, 2], :queue => "bar.foo.1"
108
+ @worker.prepare
109
+ silenced(2) do
110
+ @worker.fork_one_job
111
+ @templogger.wait_for_match(/Completed TestJobFork/m)
112
+ @response_worker.prepare
113
+ @response_worker.work_one_job
114
+ end
115
+ assert_equal 3, $worker_test_count
116
+ end # enqueue
117
+
118
+ it "should work for an async job" do
119
+ @worker = @worker_class.new('bar.foo.2')
120
+ TestAsyncJobForking.async(:queue => 'bar.foo.2').foo(3, 5)
121
+ @worker.prepare
122
+ silenced(4) do
123
+ @worker.fork_one_job
124
+ @templogger.wait_for_match(/Completed TestAsyncJobFork/m)
125
+ @response_worker.prepare
126
+ @response_worker.work_one_job
127
+ end
128
+ assert_equal 15, $worker_test_count
129
+ end # async
130
+
131
+ it "should fail quietly if there's an argument error" do
132
+ Backburner.configure { |config| config.max_job_retries = 0 }
133
+ @worker = @worker_class.new('bar.foo.3')
134
+ @worker_class.enqueue TestJobForking, ["bam", "foo", "bar"], :queue => "bar.foo.3"
135
+ @worker.prepare
136
+ silenced(5) do
137
+ @worker.fork_one_job
138
+ @templogger.wait_for_match(/Finished TestJobFork.*attempt 1 of 1/m)
139
+ end
140
+ assert_match(/Exception ArgumentError/, @templogger.body)
141
+ assert_equal 0, $worker_test_count
142
+ end # fail, argument
143
+
144
+ it "should support retrying jobs and burying" do
145
+ Backburner.configure { |config| config.max_job_retries = 1; config.retry_delay = 0 }
146
+ @worker = @worker_class.new('bar.foo.4')
147
+ @worker_class.enqueue TestRetryJobForking, ["bam", "foo"], :queue => 'bar.foo.4'
148
+ @worker.prepare
149
+ silenced(4) do
150
+ 2.times do
151
+ $worker_test_count += 1
152
+ @worker.fork_one_job
153
+ end
154
+ @templogger.wait_for_match(/Finished TestRetryJobFork.*attempt 2 of 2/m)
155
+ @response_worker.prepare
156
+ 2.times { @response_worker.work_one_job }
157
+ end
158
+ assert_equal 4, $worker_test_count
159
+ assert_equal false, $worker_success
160
+ end # retry, bury
161
+
162
+ it "should support retrying jobs and succeeds" do
163
+ Backburner.configure { |config| config.max_job_retries = 2; config.retry_delay = 0 }
164
+ @worker = @worker_class.new('bar.foo.5')
165
+ @worker_class.enqueue TestRetryJobForking, ["bam", "foo"], :queue => 'bar.foo.5'
166
+ @worker.prepare
167
+ silenced(4) do
168
+ 3.times do
169
+ $worker_test_count += 1
170
+ @worker.fork_one_job
171
+ end
172
+ @templogger.wait_for_match(/Completed TestRetryJobFork/m)
173
+ @response_worker.prepare
174
+ 3.times { @response_worker.work_one_job }
175
+ end
176
+ assert_equal 6, $worker_test_count
177
+ assert_equal true, $worker_success
178
+ end # retrying, succeeds
179
+
180
+ end # practical tests
181
+ end