backburner 0.3.0 → 0.3.1

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.
@@ -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