backburner 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
- ## Version 0.3.1 (Unreleased)
3
+ # Version 0.3.2 (Unreleased)
4
+
5
+ ## Version 0.3.1 (Dec 28 2012)
6
+
7
+ * Adds basic forking processing strategy and rake tasks (Thanks @danielfarrell)
4
8
 
5
9
  ## Version 0.3.0 (Nov 14 2012)
6
10
 
data/README.md CHANGED
@@ -254,21 +254,22 @@ This is why our examples use object IDs instead of passing around objects.
254
254
 
255
255
  ### Processing Strategies
256
256
 
257
- In Backburner, there are actually multiple different strategies for processing jobs
258
- which are reflected by multiple workers.
257
+ In Backburner, there are several different strategies for processing jobs
258
+ which are reflected by multiple worker subclasses.
259
259
  Custom workers can be [defined fairly easily](https://github.com/nesquena/backburner/wiki/Defining-Workers).
260
260
  By default, Backburner comes with the following workers built-in:
261
261
 
262
262
  | Worker | Description |
263
263
  | ------- | ------------------------------- |
264
264
  | `Backburner::Workers::Simple` | Single threaded, no forking worker. Simplest option. |
265
+ | `Backburner::Workers::Forking` | Basic forking worker that manages crashes and memory bloat. |
265
266
  | `Backburner::Workers::ThreadsOnFork` | Forking worker that utilizes threads for concurrent processing. |
266
267
 
267
268
  You can select the default worker for processing with:
268
269
 
269
270
  ```ruby
270
271
  Backburner.configure do |config|
271
- config.default_worker = Backburner::Workers::Simple
272
+ config.default_worker = Backburner::Workers::Forking
272
273
  end
273
274
  ```
274
275
 
@@ -400,6 +401,7 @@ jobs processed by your beanstalk workers. An excellent addition to your Backburn
400
401
  * [Tim Lee](https://github.com/timothy1ee), [Josh Hull](https://github.com/joshbuddy), [Nico Taing](https://github.com/Nico-Taing) - Helping me work through the idea
401
402
  * [Miso](http://gomiso.com) - Open-source friendly place to work
402
403
  * [Renan T. Fernandes](https://github.com/ShadowBelmolve) - Added threads_on_fork worker
404
+ * [Daniel Farrell](https://github.com/danielfarrell) - Added forking worker
403
405
 
404
406
  ## Contributing
405
407
 
@@ -26,5 +26,6 @@ end
26
26
  Backburner.enqueue TestJob, i
27
27
  end
28
28
 
29
- # Work tasks using threaded worker
30
- Backburner.work("test-job", :worker => Backburner::Workers::ThreadsOnFork)
29
+ # Work tasks using threads_on_fork worker
30
+ # twitter tube will have 10 threads, garbage after 1000 executions and retry jobs 1 times.
31
+ Backburner.work("test-job:10:100:1", :worker => Backburner::Workers::ThreadsOnFork)
@@ -11,6 +11,7 @@ require 'backburner/hooks'
11
11
  require 'backburner/performable'
12
12
  require 'backburner/worker'
13
13
  require 'backburner/workers/simple'
14
+ require 'backburner/workers/forking'
14
15
  require 'backburner/workers/threads_on_fork'
15
16
  require 'backburner/queue'
16
17
 
@@ -18,6 +18,15 @@ namespace :backburner do
18
18
  end
19
19
  end # simple
20
20
 
21
+ namespace :forking do
22
+ # QUEUE=foo,bar,baz rake backburner:forking:work
23
+ desc "Starts backburner worker using fork processing"
24
+ task :work => :environment do
25
+ queues = (ENV["QUEUE"] ? ENV["QUEUE"].split(',') : nil) rescue nil
26
+ Backburner.work queues, :worker => Backburner::Workers::Forking
27
+ end
28
+ end # forking
29
+
21
30
  namespace :threads_on_fork do
22
31
  # QUEUE=twitter:10:5000:5,parse_page,send_mail,verify_bithday THREADS=2 GARBAGE=1000 rake backburner:threads_on_fork:work
23
32
  # twitter tube will have 10 threads, garbage after 5k executions and retry 5 times.
@@ -1,3 +1,3 @@
1
1
  module Backburner
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
@@ -0,0 +1,48 @@
1
+ module Backburner
2
+ module Workers
3
+ class Forking < Worker
4
+ # Used to prepare job queues before processing jobs.
5
+ # Setup beanstalk tube_names and watch all specified tubes for jobs.
6
+ #
7
+ # @raise [Beaneater::NotConnected] If beanstalk fails to connect.
8
+ # @example
9
+ # @worker.prepare
10
+ #
11
+ def prepare
12
+ self.tube_names.map! { |name| expand_tube_name(name) }
13
+ log_info "Working #{tube_names.size} queues: [ #{tube_names.join(', ')} ]"
14
+ self.connection.tubes.watch!(*self.tube_names)
15
+ end
16
+
17
+ # Starts processing new jobs indefinitely.
18
+ # Primary way to consume and process jobs in specified tubes.
19
+ #
20
+ # @example
21
+ # @worker.start
22
+ #
23
+ def start
24
+ prepare
25
+ loop { fork_one_job }
26
+ end
27
+
28
+ # Need to re-establish the connection to the server(s) after forking
29
+ # Waits for a job, works the job, and exits
30
+ def fork_one_job
31
+ pid = Process.fork do
32
+ @connection = Connection.new(Backburner.configuration.beanstalk_url)
33
+ work_one_job
34
+ coolest_exit
35
+ end
36
+ Process.wait(pid)
37
+ end
38
+
39
+ # Exit with Kernel.exit! to avoid at_exit callbacks that should belongs to
40
+ # parent process
41
+ # We will use exitcode 99 that means the fork reached the garbage number
42
+ def coolest_exit
43
+ Kernel.exit! 99
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,56 @@
1
+ class ResponseForkingJob
2
+ include Backburner::Queue
3
+ queue_priority 1000
4
+ def self.perform(data)
5
+ $worker_test_count += data['worker_test_count'].to_i if data['worker_test_count']
6
+ $worker_success = data['worker_success'] if data['worker_success']
7
+ $worker_test_count = data['worker_test_count_set'].to_i if data['worker_test_count_set']
8
+ $worker_raise = data['worker_raise'] if data['worker_raise']
9
+ end
10
+ end
11
+
12
+ class TestJobForking
13
+ include Backburner::Queue
14
+ queue_priority 1000
15
+ def self.perform(x, y)
16
+ Backburner::Workers::Forking.enqueue ResponseForkingJob, [{
17
+ :worker_test_count_set => x + y
18
+ }], :queue => 'response'
19
+ end
20
+ end
21
+
22
+ class TestFailJobForking
23
+ include Backburner::Queue
24
+ def self.perform(x, y)
25
+ Backburner::Workers::Forking.enqueue ResponseForkingJob, [{
26
+ :worker_raise => true
27
+ }], :queue => 'response'
28
+ end
29
+ end
30
+
31
+ class TestRetryJobForking
32
+ include Backburner::Queue
33
+ def self.perform(x, y)
34
+ if $worker_test_count <= 2
35
+ Backburner::Workers::Forking.enqueue ResponseForkingJob, [{
36
+ :worker_test_count => 1
37
+ }], :queue => 'response'
38
+
39
+ raise RuntimeError
40
+ else # succeeds
41
+ Backburner::Workers::Forking.enqueue ResponseForkingJob, [{
42
+ :worker_test_count => 1,
43
+ :worker_success => true
44
+ }], :queue => 'response'
45
+ end
46
+ end
47
+ end
48
+
49
+ class TestAsyncJobForking
50
+ include Backburner::Performable
51
+ def self.foo(x, y)
52
+ Backburner::Workers::Forking.enqueue ResponseForkingJob, [{
53
+ :worker_test_count_set => x * y
54
+ }], :queue => 'response'
55
+ end
56
+ end
@@ -0,0 +1,179 @@
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 watch specified tubes" do
13
+ worker = @worker_class.new(["foo", "bar"])
14
+ out = capture_stdout { worker.prepare }
15
+ assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
16
+ assert_same_elements ["demo.test.foo", "demo.test.bar"], @worker_class.connection.tubes.watched.map(&:name)
17
+ assert_match /demo\.test\.foo/, out
18
+ end # multiple
19
+
20
+ it "should watch single tube" do
21
+ worker = @worker_class.new("foo")
22
+ out = capture_stdout { worker.prepare }
23
+ assert_equal ["demo.test.foo"], worker.tube_names
24
+ assert_same_elements ["demo.test.foo"], @worker_class.connection.tubes.watched.map(&:name)
25
+ assert_match /demo\.test\.foo/, out
26
+ end # single
27
+
28
+ it "should respect default_queues settings" do
29
+ Backburner.default_queues.concat(["foo", "bar"])
30
+ worker = @worker_class.new
31
+ out = capture_stdout { worker.prepare }
32
+ assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
33
+ assert_same_elements ["demo.test.foo", "demo.test.bar"], @worker_class.connection.tubes.watched.map(&:name)
34
+ assert_match /demo\.test\.foo/, out
35
+ end
36
+
37
+ it "should assign based on all tubes" do
38
+ @worker_class.any_instance.expects(:all_existing_queues).once.returns("bar")
39
+ worker = @worker_class.new
40
+ out = capture_stdout { worker.prepare }
41
+ assert_equal ["demo.test.bar"], worker.tube_names
42
+ assert_same_elements ["demo.test.bar"], @worker_class.connection.tubes.watched.map(&:name)
43
+ assert_match /demo\.test\.bar/, out
44
+ end # all assign
45
+
46
+ it "should properly retrieve all tubes" do
47
+ worker = @worker_class.new
48
+ out = capture_stdout { worker.prepare }
49
+ assert_contains worker.tube_names, "demo.test.test-job"
50
+ assert_contains @worker_class.connection.tubes.watched.map(&:name), "demo.test.test-job"
51
+ assert_match /demo\.test\.test-job/, out
52
+ end # all read
53
+ end # prepare
54
+
55
+ describe "for fork_one_job method" do
56
+
57
+ it "should fork, reconnect, work job, and exit" do
58
+ clear_jobs!("bar.foo")
59
+ @worker_class.enqueue TestJobForking, [1, 2], :queue => "bar.foo"
60
+
61
+ fake_pid = 45
62
+ Process.expects(:fork).returns(fake_pid) do |&block|
63
+ Connection.expects(:new).with(Backburner.configuration.beanstalk_url)
64
+ @worker_class.any_instance.expects(:work_one_job)
65
+ @worker_class.any_instance.expects(:coolest_exit)
66
+ block.call
67
+ end
68
+ Process.expects(:wait).with(fake_pid)
69
+
70
+ silenced(2) do
71
+ worker = @worker_class.new('bar.foo')
72
+ worker.prepare
73
+ worker.fork_one_job
74
+ end
75
+ end
76
+
77
+ end # fork_one_job
78
+
79
+ describe "practical tests" do
80
+
81
+ before do
82
+ @templogger = Templogger.new('/tmp')
83
+ Backburner.configure { |config| config.logger = @templogger.logger }
84
+ $worker_test_count = 0
85
+ $worker_success = false
86
+ $worker_raise = false
87
+ clear_jobs!('response')
88
+ clear_jobs!('bar.foo.1', 'bar.foo.2', 'bar.foo.3', 'bar.foo.4', 'bar.foo.5')
89
+ silenced do
90
+ @response_worker = @worker_class.new('response')
91
+ end
92
+ end
93
+
94
+ after do
95
+ @templogger.close
96
+ clear_jobs!('response')
97
+ clear_jobs!('bar.foo.1', 'bar.foo.2', 'bar.foo.3', 'bar.foo.4', 'bar.foo.5')
98
+ end
99
+
100
+
101
+ it "should work an enqueued job" do
102
+ @worker = @worker_class.new('bar.foo.1')
103
+ @worker_class.enqueue TestJobForking, [1, 2], :queue => "bar.foo.1"
104
+ @worker.prepare
105
+ silenced(2) do
106
+ @worker.fork_one_job
107
+ @templogger.wait_for_match(/Completed TestJobFork/m)
108
+ @response_worker.prepare
109
+ @response_worker.work_one_job
110
+ end
111
+ assert_equal 3, $worker_test_count
112
+ end # enqueue
113
+
114
+ it "should work for an async job" do
115
+ @worker = @worker_class.new('bar.foo.2')
116
+ TestAsyncJobForking.async(:queue => 'bar.foo.2').foo(3, 5)
117
+ @worker.prepare
118
+ silenced(4) do
119
+ @worker.fork_one_job
120
+ @templogger.wait_for_match(/Completed TestAsyncJobFork/m)
121
+ @response_worker.prepare
122
+ @response_worker.work_one_job
123
+ end
124
+ assert_equal 15, $worker_test_count
125
+ end # async
126
+
127
+ it "should fail quietly if there's an argument error" do
128
+ Backburner.configure { |config| config.max_job_retries = 0 }
129
+ @worker = @worker_class.new('bar.foo.3')
130
+ @worker_class.enqueue TestJobForking, ["bam", "foo", "bar"], :queue => "bar.foo.3"
131
+ @worker.prepare
132
+ silenced(5) do
133
+ @worker.fork_one_job
134
+ @templogger.wait_for_match(/Finished TestJobFork.*attempt 1 of 1/m)
135
+ end
136
+ assert_match(/Exception ArgumentError/, @templogger.body)
137
+ assert_equal 0, $worker_test_count
138
+ end # fail, argument
139
+
140
+ it "should support retrying jobs and burying" do
141
+ Backburner.configure { |config| config.max_job_retries = 1; config.retry_delay = 0 }
142
+ @worker = @worker_class.new('bar.foo.4')
143
+ @worker_class.enqueue TestRetryJobForking, ["bam", "foo"], :queue => 'bar.foo.4'
144
+ @worker.prepare
145
+ silenced(4) do
146
+ 2.times do
147
+ $worker_test_count += 1
148
+ @worker.fork_one_job
149
+ end
150
+ @templogger.wait_for_match(/Finished TestRetryJobFork.*attempt 2 of 2/m)
151
+ @response_worker.prepare
152
+ 2.times { @response_worker.work_one_job }
153
+ end
154
+ assert_equal 4, $worker_test_count
155
+ assert_equal false, $worker_success
156
+ end # retry, bury
157
+
158
+ it "should support retrying jobs and succeeds" do
159
+ Backburner.configure { |config| config.max_job_retries = 2; config.retry_delay = 0 }
160
+ @worker = @worker_class.new('bar.foo.5')
161
+ @worker_class.enqueue TestRetryJobForking, ["bam", "foo"], :queue => 'bar.foo.5'
162
+ @worker.prepare
163
+ silenced(4) do
164
+ 3.times do
165
+ $worker_test_count += 1
166
+ @worker.fork_one_job
167
+ end
168
+ @templogger.wait_for_match(/Completed TestRetryJobFork/m)
169
+ @response_worker.prepare
170
+ 3.times { @response_worker.work_one_job }
171
+ end
172
+ assert_equal 6, $worker_test_count
173
+ assert_equal true, $worker_success
174
+ end # retrying, succeeds
175
+
176
+ end # practical tests
177
+
178
+
179
+ end
@@ -50,6 +50,10 @@ describe "Backburner::Workers::ThreadsOnFork module" do
50
50
  end
51
51
 
52
52
  describe "for prepare method" do
53
+ before do
54
+ Backburner.configure { |config| config.logger = false }
55
+ end
56
+
53
57
  it "should watch specified tubes" do
54
58
  worker = @worker_class.new(["foo", "bar"])
55
59
  out = capture_stdout { worker.prepare }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backburner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-15 00:00:00.000000000 Z
12
+ date: 2012-12-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: beaneater
@@ -131,6 +131,7 @@ files:
131
131
  - lib/backburner/tasks.rb
132
132
  - lib/backburner/version.rb
133
133
  - lib/backburner/worker.rb
134
+ - lib/backburner/workers/forking.rb
134
135
  - lib/backburner/workers/simple.rb
135
136
  - lib/backburner/workers/threads_on_fork.rb
136
137
  - test/async_proxy_test.rb
@@ -138,6 +139,7 @@ files:
138
139
  - test/connection_test.rb
139
140
  - test/fixtures/hooked.rb
140
141
  - test/fixtures/test_fork_jobs.rb
142
+ - test/fixtures/test_forking_jobs.rb
141
143
  - test/fixtures/test_jobs.rb
142
144
  - test/helpers/templogger.rb
143
145
  - test/helpers_test.rb
@@ -148,8 +150,9 @@ files:
148
150
  - test/queue_test.rb
149
151
  - test/test_helper.rb
150
152
  - test/worker_test.rb
153
+ - test/workers/forking_worker_test.rb
151
154
  - test/workers/simple_worker_test.rb
152
- - test/workers/threads_on_fork_test.rb
155
+ - test/workers/threads_on_fork_worker_test.rb
153
156
  homepage: http://github.com/nesquena/backburner
154
157
  licenses: []
155
158
  post_install_message:
@@ -180,6 +183,7 @@ test_files:
180
183
  - test/connection_test.rb
181
184
  - test/fixtures/hooked.rb
182
185
  - test/fixtures/test_fork_jobs.rb
186
+ - test/fixtures/test_forking_jobs.rb
183
187
  - test/fixtures/test_jobs.rb
184
188
  - test/helpers/templogger.rb
185
189
  - test/helpers_test.rb
@@ -190,6 +194,6 @@ test_files:
190
194
  - test/queue_test.rb
191
195
  - test/test_helper.rb
192
196
  - test/worker_test.rb
197
+ - test/workers/forking_worker_test.rb
193
198
  - test/workers/simple_worker_test.rb
194
- - test/workers/threads_on_fork_test.rb
195
- has_rdoc:
199
+ - test/workers/threads_on_fork_worker_test.rb