backburner-allq 1.0.0

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