backburner 0.0.1 → 0.0.2
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.
- data/README.md +101 -18
- data/Rakefile +2 -0
- data/examples/god.rb +46 -0
- data/lib/backburner.rb +24 -9
- data/lib/backburner/async_proxy.rb +4 -1
- data/lib/backburner/connection.rb +8 -2
- data/lib/backburner/helpers.rb +20 -18
- data/lib/backburner/job.rb +68 -0
- data/lib/backburner/logger.rb +9 -3
- data/lib/backburner/performable.rb +7 -3
- data/lib/backburner/queue.rb +5 -3
- data/lib/backburner/version.rb +1 -1
- data/lib/backburner/worker.rb +24 -30
- data/test/job_test.rb +85 -0
- data/test/test_helper.rb +4 -1
- data/test/worker_test.rb +20 -2
- metadata +19 -45
data/README.md
CHANGED
@@ -1,17 +1,69 @@
|
|
1
1
|
# Backburner
|
2
2
|
|
3
|
-
Backburner is a beanstalkd-powered job queue
|
4
|
-
You
|
3
|
+
Backburner is a [beanstalkd](http://kr.github.com/beanstalkd/)-powered job queue which can handle a very high volume of jobs.
|
4
|
+
You create background jobs and place those on multiple work queues to be processed later.
|
5
5
|
|
6
|
-
Processing background jobs reliably has never been easier
|
7
|
-
web framework but is
|
6
|
+
Processing background jobs reliably has never been easier then with beanstalkd and Backburner. This gem works with any ruby-based
|
7
|
+
web framework but is especially suited for use with [Sinatra](http://sinatrarb.com), [Padrino](http://padrinorb.com) and Rails.
|
8
8
|
|
9
|
-
If you want to use beanstalk for job processing, consider using Backburner.
|
10
|
-
Backburner
|
11
|
-
Backburner
|
9
|
+
If you want to use beanstalk for your job processing, consider using Backburner.
|
10
|
+
Backburner is heavily inspired by Resque and DelayedJob. Backburner stores all jobs as simple JSON message payloads.
|
11
|
+
Backburner can be a persistent queue if the beanstalk persistence mode is enabled, supports multiple queues, priorities, delays, and timeouts.
|
12
|
+
|
13
|
+
## Why Backburner?
|
14
|
+
|
15
|
+
Backburner is well tested and has a familiar, no-nonsense approach to job processing but that is of secondary importance.
|
16
|
+
Let's face it; there are a lot of options for background job processing. [DelayedJob](https://github.com/collectiveidea/delayed_job),
|
17
|
+
and [Resque](https://github.com/defunkt/resque) are the first that come to mind immediately. So, how do we make sense
|
18
|
+
of which one to use? And why use Backburner over other alternatives?
|
19
|
+
|
20
|
+
The key to understanding the differences lies in understanding the different projects and protocols that power these popular queue
|
21
|
+
libraries under the hood. Every job queue requires a queue store that jobs are put into and pulled out of.
|
22
|
+
In the case of Resque, jobs are processed through **Redis**, a persistent key-value store. In the case of DelayedJob, jobs are processed through
|
23
|
+
**ActiveRecord** and a database such as PostgreSQL.
|
24
|
+
|
25
|
+
The work queue underlying these gems tells you infinitely more about the differences then anything else.
|
26
|
+
Beanstalk is probably the best solution for job queues available today for many reasons.
|
27
|
+
The real question then is... "Why Beanstalk?".
|
28
|
+
|
29
|
+
## Why Beanstalk?
|
30
|
+
|
31
|
+
Illya has an excellent blog post
|
32
|
+
[Scalable Work Queues with Beanstalk](http://www.igvita.com/2010/05/20/scalable-work-queues-with-beanstalk/) and
|
33
|
+
Adam Wiggins posted [an excellent comparison](http://adam.heroku.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/).
|
34
|
+
|
35
|
+
You will quickly see that **beanstalkd** is an underrated but incredible project that is extremely well-suited as a job queue.
|
36
|
+
Significantly better suited for this task then Redis or a database. Beanstalk is a simple,
|
37
|
+
and a very fast work queue service rolled into a single binary - it is the memcached of work queues.
|
38
|
+
Originally built to power the backend for the 'Causes' Facebook app, it is a mature and production ready open source project.
|
39
|
+
[PostRank](http://www.postrank.com) uses beanstalk to reliably process millions of jobs a day.
|
40
|
+
|
41
|
+
A single instance of Beanstalk is perfectly capable of handling thousands of jobs a second (or more, depending on your job size)
|
42
|
+
because it is an in-memory, event-driven system. Powered by libevent under the hood,
|
43
|
+
it requires zero setup (launch and forget, ala memcached), optional log based persistence, an easily parsed ASCII protocol,
|
44
|
+
and a rich set of tools for job management that go well beyond a simple FIFO work queue.
|
45
|
+
|
46
|
+
Beanstalk supports the following features natively, out of the box, without any questions asked:
|
47
|
+
|
48
|
+
* **Parallel Queues** - Supports multiple work queues, which are created and deleted on demand.
|
49
|
+
* **Reliable** - Beanstalk’s reserve, work, delete cycle, with a timeout on a job, means bad clients basically can't lose a job.
|
50
|
+
* **Scheduling** - Delay enqueuing jobs by a specified interval to schedule processing later.
|
51
|
+
* **Fast** - Beanstalkd is **significantly** [faster then alternatives](http://adam.heroku.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend). Easily processes thousands of jobs a second.
|
52
|
+
* **Priorities** - Specify a higher priority and those jobs will jump ahead to be processed first accordingly.
|
53
|
+
* **Persistence** - Jobs are stored in memory for speed (ala memcached), but also logged to disk for safe keeping.
|
54
|
+
* **Federation** - Fault-tolerance and horizontal scalability is provided the same way as Memcache - through federation by the client.
|
55
|
+
* **Buried jobs** - When a job causes an error, you can bury it which keeps it around for later debugging and inspection.
|
56
|
+
|
57
|
+
Keep in mind that these features are supported out of the box with beanstalk and require no special code within this gem to support.
|
58
|
+
In the end, **beanstalk is the ideal job queue** while also being ridiculously easy to install and setup.
|
12
59
|
|
13
60
|
## Installation
|
14
61
|
|
62
|
+
First, you probably want to [install beanstalkd](http://kr.github.com/beanstalkd/download.html), which powers the job queues.
|
63
|
+
Depending on your platform, this should be as simple as (for Ubuntu):
|
64
|
+
|
65
|
+
$ sudo apt-get install beanstalkd
|
66
|
+
|
15
67
|
Add this line to your application's Gemfile:
|
16
68
|
|
17
69
|
gem 'backburner'
|
@@ -61,11 +113,11 @@ class NewsletterJob
|
|
61
113
|
end
|
62
114
|
```
|
63
115
|
|
64
|
-
Notice that you
|
65
|
-
Jobs can
|
116
|
+
Notice that you can include the optional `Backburner::Queue` module so you can specify a `queue` name for this job.
|
117
|
+
Jobs can be enqueued with:
|
66
118
|
|
67
119
|
```ruby
|
68
|
-
Backburner.enqueue NewsletterJob, 'lorem ipsum...'
|
120
|
+
Backburner.enqueue NewsletterJob, 'foo@admin.com', 'lorem ipsum...'
|
69
121
|
```
|
70
122
|
|
71
123
|
`Backburner.enqueue` accepts first a ruby object that supports `perform` and then a series of parameters
|
@@ -75,7 +127,7 @@ if not otherwise specified.
|
|
75
127
|
### Simple Async Jobs ###
|
76
128
|
|
77
129
|
In addition to defining custom jobs, a job can also be enqueued by invoking the `async` method on any object which
|
78
|
-
includes `Backburner::Performable`.
|
130
|
+
includes `Backburner::Performable`. Async enqueuing works for both instance and class methods on any _performable_ object.
|
79
131
|
|
80
132
|
```ruby
|
81
133
|
class User
|
@@ -85,13 +137,20 @@ class User
|
|
85
137
|
@device = Device.find(device_id)
|
86
138
|
# ...
|
87
139
|
end
|
140
|
+
|
141
|
+
def self.reset_password(user_id)
|
142
|
+
# ...
|
143
|
+
end
|
88
144
|
end
|
89
145
|
|
146
|
+
# Async works for instance methods on a persisted model
|
90
147
|
@user = User.first
|
91
148
|
@user.async(:pri => 1000, :ttr => 100, :queue => "user.activate").activate(@device.id)
|
149
|
+
# ..as well as for class methods
|
150
|
+
User.async(:pri => 100, :delay => 10.seconds).reset_password(@user.id)
|
92
151
|
```
|
93
152
|
|
94
|
-
This will automatically enqueue a job that will run `activate` with the specified argument
|
153
|
+
This will automatically enqueue a job for that user record that will run `activate` with the specified argument.
|
95
154
|
The queue name used by default is the normalized class name (i.e `{namespace}.user`) if not otherwise specified.
|
96
155
|
Note you are able to pass `pri`, `ttr`, `delay` and `queue` directly as options into `async`.
|
97
156
|
|
@@ -174,17 +233,30 @@ If a job fails in beanstalk, the job is automatically buried and must be 'kicked
|
|
174
233
|
|
175
234
|
Right now, all logging happens to standard out and can be piped to a file or any other output manually. More on logging coming later.
|
176
235
|
|
177
|
-
### Front-end
|
236
|
+
### Front-end
|
178
237
|
|
179
238
|
To be completed is an admin dashboard that provides insight into beanstalk jobs via a simple Sinatra front-end. Coming soon.
|
180
239
|
|
181
|
-
|
240
|
+
### Workers in Production
|
241
|
+
|
242
|
+
Once you have Backburner setup in your application, starting workers is really easy. Once [beanstalkd](http://kr.github.com/beanstalkd/download.html)
|
243
|
+
is installed, your best bet is to use the built-in rake task that comes with Backburner. Simply add the task to your Rakefile:
|
182
244
|
|
183
|
-
|
245
|
+
# Rakefile
|
246
|
+
require 'backburner/tasks'
|
247
|
+
|
248
|
+
and then you can start the rake task with:
|
249
|
+
|
250
|
+
$ rake backburner:work
|
251
|
+
$ QUEUES=newsletter-sender,push-message rake backburner:work
|
252
|
+
|
253
|
+
The best way to deploy these rake tasks is using a monitoring library. We suggest [God](https://github.com/mojombo/god/)
|
254
|
+
which watches processes and ensures their stability. A simple God recipe for Backburner can be found in
|
255
|
+
[examples/god](https://github.com/nesquena/backburner/blob/master/examples/god.rb).
|
184
256
|
|
185
257
|
## Acknowledgements
|
186
258
|
|
187
|
-
* Nathan Esquenazi - Project maintainer
|
259
|
+
* [Nathan Esquenazi](https://github.com/nesquena) - Project maintainer
|
188
260
|
* Kristen Tucker - Coming up with the gem name
|
189
261
|
* [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
|
190
262
|
* [Miso](http://gomiso.com) - Open-source friendly place to work
|
@@ -199,7 +271,18 @@ To be filled in. DelayedJob, Resque, Stalker, et al.
|
|
199
271
|
|
200
272
|
## References
|
201
273
|
|
202
|
-
The code in this project has been
|
274
|
+
The code in this project has been made in light of a few excellent projects:
|
203
275
|
|
204
276
|
* [DelayedJob](https://github.com/collectiveidea/delayed_job)
|
205
|
-
* [
|
277
|
+
* [Resque](https://github.com/defunkt/resque)
|
278
|
+
* [Stalker](https://github.com/han/stalker)
|
279
|
+
|
280
|
+
Thanks to these projects for inspiration and certain design and implementation decisions.
|
281
|
+
|
282
|
+
## Links
|
283
|
+
|
284
|
+
* Code: `git clone git://github.com/nesquena/backburner.git`
|
285
|
+
* Home: <http://github.com/nesquena/backburner>
|
286
|
+
* Docs: <http://rdoc.info/github/nesquena/backburner/master/frames>
|
287
|
+
* Bugs: <http://github.com/nesquena/backburner/issues>
|
288
|
+
* Gems: <http://gemcutter.org/gems/backburner>
|
data/Rakefile
CHANGED
data/examples/god.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
God.watch do |w|
|
2
|
+
w.name = "backburner-worker-1"
|
3
|
+
w.dir = '/path/to/app/dir'
|
4
|
+
w.env = { 'PADRINO_ENV' => 'production', 'QUEUES' => 'newsletter-sender,push-message' }
|
5
|
+
w.group = 'backburner-workers'
|
6
|
+
w.interval = 30.seconds
|
7
|
+
w.start = "bundle exec rake -f Rakefile backburner:start"
|
8
|
+
w.log = "/var/log/god/backburner-worker-1.log"
|
9
|
+
|
10
|
+
# restart if memory gets too high
|
11
|
+
w.transition(:up, :restart) do |on|
|
12
|
+
on.condition(:memory_usage) do |c|
|
13
|
+
c.above = 50.megabytes
|
14
|
+
c.times = 3
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# determine the state on startup
|
19
|
+
w.transition(:init, { true => :up, false => :start }) do |on|
|
20
|
+
on.condition(:process_running) do |c|
|
21
|
+
c.running = true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# determine when process has finished starting
|
26
|
+
w.transition([:start, :restart], :up) do |on|
|
27
|
+
on.condition(:process_running) do |c|
|
28
|
+
c.running = true
|
29
|
+
c.interval = 5.seconds
|
30
|
+
end
|
31
|
+
|
32
|
+
# failsafe
|
33
|
+
on.condition(:tries) do |c|
|
34
|
+
c.times = 5
|
35
|
+
c.transition = :start
|
36
|
+
c.interval = 5.seconds
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# start if process is not running
|
41
|
+
w.transition(:up, :start) do |on|
|
42
|
+
on.condition(:process_running) do |c|
|
43
|
+
c.running = false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/backburner.rb
CHANGED
@@ -14,36 +14,51 @@ require 'backburner/queue'
|
|
14
14
|
module Backburner
|
15
15
|
class << self
|
16
16
|
|
17
|
-
# Enqueues a job to be performed with arguments
|
18
|
-
#
|
17
|
+
# Enqueues a job to be performed with given arguments.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# Backburner.enqueue NewsletterSender, self.id, user.id
|
21
|
+
#
|
19
22
|
def enqueue(job_class, *args)
|
20
23
|
Backburner::Worker.enqueue(job_class, args, {})
|
21
24
|
end
|
22
25
|
|
23
26
|
# Begins working on jobs enqueued with optional tubes specified
|
24
|
-
#
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# Backburner.work('newsletter_sender', 'test_job')
|
30
|
+
#
|
25
31
|
def work(*tubes)
|
26
32
|
Backburner::Worker.start(tubes)
|
27
33
|
end
|
28
34
|
|
29
35
|
# Yields a configuration block
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# Backburner.configure do |config|
|
39
|
+
# config.beanstalk_url = "beanstalk://..."
|
40
|
+
# end
|
41
|
+
#
|
33
42
|
def configure(&block)
|
34
43
|
yield(configuration)
|
35
44
|
configuration
|
36
45
|
end
|
37
46
|
|
38
47
|
# Returns the configuration options set for Backburner
|
39
|
-
#
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# Backburner.configuration.beanstalk_url => false
|
51
|
+
#
|
40
52
|
def configuration
|
41
53
|
@_configuration ||= Configuration.new
|
42
54
|
end
|
43
55
|
|
44
56
|
# Returns the queues that are processed by default if none are specified
|
45
|
-
#
|
46
|
-
#
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# Backburner.default_queues << "foo"
|
60
|
+
# Backburner.default_queues => ["foo", "bar"]
|
61
|
+
#
|
47
62
|
def default_queues
|
48
63
|
configuration.default_queues
|
49
64
|
end
|
@@ -8,8 +8,11 @@ module Backburner
|
|
8
8
|
|
9
9
|
# Class allows async task to be proxied
|
10
10
|
class AsyncProxy < BasicObject
|
11
|
-
# AsyncProxy(User, 10, :pri => 1000, :ttr => 1000)
|
12
11
|
# Options include `pri` (priority), `delay` (delay in secs), `ttr` (time to respond)
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# AsyncProxy(User, 10, :pri => 1000, :ttr => 1000)
|
15
|
+
#
|
13
16
|
def initialize(klazz, id=nil, opts={})
|
14
17
|
@klazz, @id, @opts = klazz, id, opts
|
15
18
|
end
|
@@ -26,14 +26,20 @@ module Backburner
|
|
26
26
|
end
|
27
27
|
|
28
28
|
# Returns the beanstalk queue addresses
|
29
|
-
#
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# beanstalk_addresses => ["localhost:11300"]
|
32
|
+
#
|
30
33
|
def beanstalk_addresses
|
31
34
|
uris = self.url.split(/[\s,]+/)
|
32
35
|
uris.map {|uri| beanstalk_host_and_port(uri)}
|
33
36
|
end
|
34
37
|
|
35
38
|
# Returns a host and port based on the uri_string given
|
36
|
-
#
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# beanstalk_host_and_port("beanstalk://localhost") => "localhost:11300"
|
42
|
+
#
|
37
43
|
def beanstalk_host_and_port(uri_string)
|
38
44
|
uri = URI.parse(uri_string)
|
39
45
|
raise(BadURL, uri_string) if uri.scheme != 'beanstalk'
|
data/lib/backburner/helpers.rb
CHANGED
@@ -18,13 +18,19 @@ module Backburner
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Given a word with dashes, returns a camel cased version of it.
|
21
|
-
#
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# classify('job-name') # => 'JobName'
|
24
|
+
#
|
22
25
|
def classify(dashed_word)
|
23
26
|
dashed_word.to_s.split('-').each { |part| part[0] = part[0].chr.upcase }.join
|
24
27
|
end
|
25
28
|
|
26
29
|
# Given a class, dasherizes the name, used for getting tube names
|
27
|
-
#
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# dasherize('JobName') # => "job-name"
|
33
|
+
#
|
28
34
|
def dasherize(word)
|
29
35
|
classify(word).to_s.gsub(/::/, '/').
|
30
36
|
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
@@ -34,19 +40,9 @@ module Backburner
|
|
34
40
|
|
35
41
|
# Tries to find a constant with the name specified in the argument string:
|
36
42
|
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# The name is assumed to be the one of a top-level constant, no matter
|
41
|
-
# whether it starts with "::" or not. No lexical context is taken into
|
42
|
-
# account:
|
43
|
-
#
|
44
|
-
# C = 'outside'
|
45
|
-
# module M
|
46
|
-
# C = 'inside'
|
47
|
-
# C # => 'inside'
|
48
|
-
# constantize("C") # => 'outside', same as ::C
|
49
|
-
# end
|
43
|
+
# @example
|
44
|
+
# constantize("Module") # => Module
|
45
|
+
# constantize("Test::Unit") # => Test::Unit
|
50
46
|
#
|
51
47
|
# NameError is raised when the constant is unknown.
|
52
48
|
def constantize(camel_cased_word)
|
@@ -73,14 +69,20 @@ module Backburner
|
|
73
69
|
end
|
74
70
|
|
75
71
|
# Returns tube_namespace for backburner
|
76
|
-
#
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# tube_namespace => "some.namespace"
|
75
|
+
#
|
77
76
|
def tube_namespace
|
78
77
|
Backburner.configuration.tube_namespace
|
79
78
|
end
|
80
79
|
|
81
80
|
# Expands a tube to include the prefix
|
82
|
-
#
|
83
|
-
#
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# expand_tube_name("foo") # => <prefix>.foo
|
84
|
+
# expand_tube_name(FooJob) # => <prefix>.foo-job
|
85
|
+
#
|
84
86
|
def expand_tube_name(tube)
|
85
87
|
prefix = tube_namespace
|
86
88
|
queue_name = if tube.is_a?(String)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Backburner
|
2
|
+
# A single backburner job which can be processed and removed by the worker
|
3
|
+
class Job
|
4
|
+
include Backburner::Helpers
|
5
|
+
|
6
|
+
# Raises when a job times out
|
7
|
+
class JobTimeout < RuntimeError; end
|
8
|
+
class JobNotFound < RuntimeError; end
|
9
|
+
|
10
|
+
attr_accessor :task, :body, :name, :args
|
11
|
+
|
12
|
+
# Construct a job to be parsed and processed
|
13
|
+
#
|
14
|
+
# task is a reserved object containing the json body in the form of
|
15
|
+
# { :class => "NewsletterSender", :args => ["foo@bar.com"] }
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# Backburner::Job.new(payload)
|
19
|
+
#
|
20
|
+
def initialize(task)
|
21
|
+
@task = task
|
22
|
+
@body = JSON.parse(task.body)
|
23
|
+
@name, @args = body["class"], body["args"]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Processes a job and handles any failure, deleting the job once complete
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# @task.process
|
30
|
+
#
|
31
|
+
def process
|
32
|
+
timeout_job_after(task.ttr - 1) { job_class.perform(*args) }
|
33
|
+
task.delete
|
34
|
+
end
|
35
|
+
|
36
|
+
# Bury a job out of the active queue if that job fails
|
37
|
+
def bury
|
38
|
+
task.bury
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# Returns the class for the job handler
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# job_class # => NewsletterSender
|
47
|
+
#
|
48
|
+
def job_class
|
49
|
+
handler = constantize(name) rescue nil
|
50
|
+
raise(JobNotFound, name) unless handler
|
51
|
+
handler
|
52
|
+
end
|
53
|
+
|
54
|
+
# Timeout job after given time
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# timeout_job_after(3) { do_something! }
|
58
|
+
#
|
59
|
+
def timeout_job_after(secs, &block)
|
60
|
+
begin
|
61
|
+
Timeout::timeout(secs) { yield }
|
62
|
+
rescue Timeout::Error
|
63
|
+
raise JobTimeout, "#{name} hit #{secs}s timeout"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end # Job
|
68
|
+
end # Backburner
|
data/lib/backburner/logger.rb
CHANGED
@@ -20,7 +20,9 @@ module Backburner
|
|
20
20
|
end
|
21
21
|
|
22
22
|
# Prints message about failure when beastalk cannot be connected
|
23
|
-
#
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# failed_connection(ex)
|
24
26
|
def failed_connection(e)
|
25
27
|
log_error exception_message(e)
|
26
28
|
log_error "*** Failed connection to #{connection.url}"
|
@@ -29,13 +31,17 @@ module Backburner
|
|
29
31
|
end
|
30
32
|
|
31
33
|
# Print a message to stdout
|
32
|
-
#
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# log("Working on task")
|
33
37
|
def log(msg)
|
34
38
|
puts msg
|
35
39
|
end
|
36
40
|
|
37
41
|
# Print an error to stderr
|
38
|
-
#
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# log_error("Task failed!")
|
39
45
|
def log_error(msg)
|
40
46
|
$stderr.puts msg
|
41
47
|
end
|
@@ -11,7 +11,9 @@ module Backburner
|
|
11
11
|
module InstanceMethods
|
12
12
|
# Return proxy object to enqueue jobs for object
|
13
13
|
# Options: `pri` (priority), `delay` (delay in secs), `ttr` (time to respond), `queue` (queue name)
|
14
|
-
# @
|
14
|
+
# @example
|
15
|
+
# @model.async(:pri => 1000).do_something("foo")
|
16
|
+
#
|
15
17
|
def async(opts={})
|
16
18
|
Backburner::AsyncProxy.new(self.class, self.id, opts)
|
17
19
|
end
|
@@ -20,13 +22,15 @@ module Backburner
|
|
20
22
|
module ClassMethods
|
21
23
|
# Return proxy object to enqueue jobs for object
|
22
24
|
# Options: `pri` (priority), `delay` (delay in secs), `ttr` (time to respond), `queue` (queue name)
|
23
|
-
#
|
25
|
+
# @example
|
26
|
+
# Model.async(:ttr => 300).do_something("foo")
|
24
27
|
def async(opts={})
|
25
28
|
Backburner::AsyncProxy.new(self, nil, opts)
|
26
29
|
end
|
27
30
|
|
28
31
|
# Defines perform method for job processing
|
29
|
-
#
|
32
|
+
# @example
|
33
|
+
# perform(55, :do_something, "foo", "bar")
|
30
34
|
def perform(id, method, *args)
|
31
35
|
if id # instance
|
32
36
|
find(id).send(method, *args)
|
data/lib/backburner/queue.rb
CHANGED
@@ -7,9 +7,11 @@ module Backburner
|
|
7
7
|
end
|
8
8
|
|
9
9
|
module ClassMethods
|
10
|
-
# Returns or assigns queue name for this job
|
11
|
-
#
|
12
|
-
#
|
10
|
+
# Returns or assigns queue name for this job.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# queue "some.task.name"
|
14
|
+
# queue => "some.task.name"
|
13
15
|
def queue(name=nil)
|
14
16
|
if name
|
15
17
|
@queue_name = name
|
data/lib/backburner/version.rb
CHANGED
data/lib/backburner/worker.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
|
+
require 'backburner/job'
|
2
|
+
|
1
3
|
module Backburner
|
2
4
|
class Worker
|
3
5
|
include Backburner::Helpers
|
4
6
|
include Backburner::Logger
|
5
7
|
|
6
|
-
class JobNotFound < RuntimeError; end
|
7
|
-
class JobTimeout < RuntimeError; end
|
8
|
-
class JobQueueNotSet < RuntimeError; end
|
9
|
-
|
10
8
|
# Backburner::Worker.known_queue_classes
|
11
9
|
# List of known_queue_classes
|
12
10
|
class << self
|
@@ -16,7 +14,10 @@ module Backburner
|
|
16
14
|
|
17
15
|
# Enqueues a job to be processed later by a worker
|
18
16
|
# Options: `pri` (priority), `delay` (delay in secs), `ttr` (time to respond), `queue` (queue name)
|
19
|
-
#
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# Backburner::Worker.enqueue NewsletterSender, [self.id, user.id], :ttr => 1000
|
20
|
+
#
|
20
21
|
def self.enqueue(job_class, args=[], opts={})
|
21
22
|
pri = opts[:pri] || Backburner.configuration.default_priority
|
22
23
|
delay = [0, opts[:delay].to_i].max
|
@@ -29,13 +30,15 @@ module Backburner
|
|
29
30
|
end
|
30
31
|
|
31
32
|
# Starts processing jobs in the specified tube_names
|
32
|
-
#
|
33
|
+
# @example
|
34
|
+
# Backburner::Worker.start(["foo.tube.name"])
|
33
35
|
def self.start(tube_names=nil)
|
34
36
|
self.new(tube_names).start
|
35
37
|
end
|
36
38
|
|
37
39
|
# Returns the worker connection
|
38
|
-
#
|
40
|
+
# @example
|
41
|
+
# Backburner::Worker.connection # => <Beanstalk::Pool>
|
39
42
|
def self.connection
|
40
43
|
@connection ||= Connection.new(Backburner.configuration.beanstalk_url)
|
41
44
|
end
|
@@ -43,7 +46,8 @@ module Backburner
|
|
43
46
|
# List of tube names to be watched and processed
|
44
47
|
attr_accessor :tube_names
|
45
48
|
|
46
|
-
#
|
49
|
+
# @example
|
50
|
+
# Worker.new(['test.job'])
|
47
51
|
def initialize(tube_names=nil)
|
48
52
|
@tube_names = begin
|
49
53
|
tube_names = tube_names.first if tube_names && tube_names.size == 1 && tube_names.first.is_a?(Array)
|
@@ -55,7 +59,8 @@ module Backburner
|
|
55
59
|
|
56
60
|
# Starts processing new jobs indefinitely
|
57
61
|
# Primary way to consume and process jobs in specified tubes
|
58
|
-
# @
|
62
|
+
# @example
|
63
|
+
# @worker.start
|
59
64
|
def start
|
60
65
|
prepare
|
61
66
|
loop { work_one_job }
|
@@ -63,7 +68,8 @@ module Backburner
|
|
63
68
|
|
64
69
|
# Setup beanstalk tube_names and watch all specified tubes for jobs.
|
65
70
|
# Used to prepare job queues before processing jobs.
|
66
|
-
# @
|
71
|
+
# @example
|
72
|
+
# @worker.prepare
|
67
73
|
def prepare
|
68
74
|
self.tube_names ||= Backburner.default_queues.any? ? Backburner.default_queues : all_existing_queues
|
69
75
|
self.tube_names = Array(self.tube_names)
|
@@ -80,25 +86,13 @@ module Backburner
|
|
80
86
|
# Reserves one job within the specified queues
|
81
87
|
# Pops the job off and serializes the job to JSON
|
82
88
|
# Each job is performed by invoking `perform` on the job class.
|
83
|
-
# @
|
89
|
+
# @example
|
90
|
+
# @worker.work_one_job
|
84
91
|
def work_one_job
|
85
|
-
job = self.connection.reserve
|
86
|
-
|
87
|
-
|
88
|
-
self.class.
|
89
|
-
handler = constantize(name)
|
90
|
-
raise(JobNotFound, name) unless handler
|
91
|
-
|
92
|
-
begin
|
93
|
-
Timeout::timeout(job.ttr - 1) do
|
94
|
-
handler.perform(*args)
|
95
|
-
end
|
96
|
-
rescue Timeout::Error
|
97
|
-
raise JobTimeout, "#{name} hit #{job.ttr-1}s timeout"
|
98
|
-
end
|
99
|
-
|
100
|
-
job.delete
|
101
|
-
self.class.log_job_end(name)
|
92
|
+
job = Backburner::Job.new(self.connection.reserve)
|
93
|
+
self.class.log_job_begin(job.body)
|
94
|
+
job.process
|
95
|
+
self.class.log_job_end(job.name)
|
102
96
|
rescue Beanstalk::NotConnected => e
|
103
97
|
failed_connection(e)
|
104
98
|
rescue SystemExit
|
@@ -106,8 +100,8 @@ module Backburner
|
|
106
100
|
rescue => e
|
107
101
|
job.bury
|
108
102
|
self.class.log_error self.class.exception_message(e)
|
109
|
-
self.class.log_job_end(name, 'failed') if @job_begun
|
110
|
-
handle_error(e, name, args)
|
103
|
+
self.class.log_job_end(job.name, 'failed') if @job_begun
|
104
|
+
handle_error(e, job.name, job.args)
|
111
105
|
end
|
112
106
|
|
113
107
|
protected
|
data/test/job_test.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
module NestedDemo
|
4
|
+
class TestJobC
|
5
|
+
include Backburner::Queue
|
6
|
+
def self.perform(x); puts "Performed #{x} in #{self}"; end
|
7
|
+
end
|
8
|
+
|
9
|
+
class TestJobD
|
10
|
+
include Backburner::Queue
|
11
|
+
def self.perform(x); raise RuntimeError; end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "Backburner::Job module" do
|
16
|
+
describe "for initialize method" do
|
17
|
+
before do
|
18
|
+
@task_body = { :class => "NewsletterSender", :args => ["foo@bar.com", "bar@foo.com"] }
|
19
|
+
@task = stub(:body => @task_body.to_json, :ttr => 120, :delete => true, :bury => true)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should create job with correct task data" do
|
23
|
+
@job = Backburner::Job.new(@task)
|
24
|
+
assert_equal @task, @job.task
|
25
|
+
assert_equal ["class", "args"], @job.body.keys
|
26
|
+
assert_equal @task_body[:class], @job.name
|
27
|
+
assert_equal @task_body[:args], @job.args
|
28
|
+
end
|
29
|
+
end # initialize
|
30
|
+
|
31
|
+
describe "for process method" do
|
32
|
+
describe "with valid task" do
|
33
|
+
before do
|
34
|
+
@task_body = { :class => "NestedDemo::TestJobC", :args => [56] }
|
35
|
+
@task = stub(:body => @task_body.to_json, :ttr => 120, :delete => true, :bury => true)
|
36
|
+
@task.expects(:delete).once
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should process task" do
|
40
|
+
@job = Backburner::Job.new(@task)
|
41
|
+
out = silenced(1) { @job.process }
|
42
|
+
assert_match /Performed 56 in NestedDemo::TestJobC/, out
|
43
|
+
end # process
|
44
|
+
end # valid
|
45
|
+
|
46
|
+
describe "with invalid task" do
|
47
|
+
before do
|
48
|
+
@task_body = { :class => "NestedDemo::TestJobD", :args => [56] }
|
49
|
+
@task = stub(:body => @task_body.to_json, :ttr => 120, :delete => true, :bury => true)
|
50
|
+
@task.expects(:delete).never
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should raise an exception" do
|
54
|
+
@job = Backburner::Job.new(@task)
|
55
|
+
assert_raises(RuntimeError) { @job.process }
|
56
|
+
end # error invalid
|
57
|
+
end # invalid
|
58
|
+
|
59
|
+
describe "with invalid class" do
|
60
|
+
before do
|
61
|
+
@task_body = { :class => "NestedDemo::TestJobY", :args => [56] }
|
62
|
+
@task = stub(:body => @task_body.to_json, :ttr => 120, :delete => true, :bury => true)
|
63
|
+
@task.expects(:delete).never
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should raise an exception" do
|
67
|
+
@job = Backburner::Job.new(@task)
|
68
|
+
assert_raises(Backburner::Job::JobNotFound) { @job.process }
|
69
|
+
end # error class
|
70
|
+
end # invalid
|
71
|
+
end # process
|
72
|
+
|
73
|
+
describe "for bury method" do
|
74
|
+
before do
|
75
|
+
@task_body = { :class => "NestedDemo::TestJobC", :args => [56] }
|
76
|
+
@task = stub(:body => @task_body.to_json, :ttr => 120, :delete => true, :bury => true)
|
77
|
+
@task.expects(:bury).once
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should call bury for task" do
|
81
|
+
@job = Backburner::Job.new(@task)
|
82
|
+
@job.bury
|
83
|
+
end # bury
|
84
|
+
end # bury
|
85
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -18,7 +18,10 @@ module Kernel
|
|
18
18
|
# Redirect standard out, standard error and the buffered logger for sprinkle to StringIO
|
19
19
|
# capture_stdout { any_commands; you_want } => "all output from the commands"
|
20
20
|
def capture_stdout
|
21
|
-
|
21
|
+
if ENV['DEBUG'] # Skip if debug mode
|
22
|
+
yield
|
23
|
+
""
|
24
|
+
end
|
22
25
|
|
23
26
|
out = StringIO.new
|
24
27
|
$stdout = out
|
data/test/worker_test.rb
CHANGED
@@ -7,6 +7,11 @@ class TestJob
|
|
7
7
|
def self.perform(x, y); $worker_test_count += x + y; end
|
8
8
|
end
|
9
9
|
|
10
|
+
class TestFailJob
|
11
|
+
include Backburner::Queue
|
12
|
+
def self.perform(x, y); raise RuntimeError; end
|
13
|
+
end
|
14
|
+
|
10
15
|
class TestAsyncJob
|
11
16
|
include Backburner::Performable
|
12
17
|
def self.foo(x, y); $worker_test_count = x * y; end
|
@@ -147,7 +152,20 @@ describe "Backburner::Worker module" do
|
|
147
152
|
worker.work_one_job
|
148
153
|
end
|
149
154
|
assert_equal 3, $worker_test_count
|
150
|
-
end
|
155
|
+
end # enqueue
|
156
|
+
|
157
|
+
it "should work an enqueued failing job" do
|
158
|
+
$worker_test_count = 0
|
159
|
+
Backburner::Worker.enqueue TestFailJob, [1, 2], :queue => "foo.bar.fail"
|
160
|
+
Backburner::Job.any_instance.expects(:bury).once
|
161
|
+
out = silenced(2) do
|
162
|
+
worker = Backburner::Worker.new('foo.bar.fail')
|
163
|
+
worker.prepare
|
164
|
+
worker.work_one_job
|
165
|
+
end
|
166
|
+
assert_match(/Exception RuntimeError/, out)
|
167
|
+
assert_equal 0, $worker_test_count
|
168
|
+
end # fail
|
151
169
|
|
152
170
|
it "should work for an async job" do
|
153
171
|
$worker_test_count = 0
|
@@ -158,6 +176,6 @@ describe "Backburner::Worker module" do
|
|
158
176
|
worker.work_one_job
|
159
177
|
end
|
160
178
|
assert_equal 15, $worker_test_count
|
161
|
-
end
|
179
|
+
end # async
|
162
180
|
end # work_one_job
|
163
181
|
end # Backburner::Worker
|
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.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-07-
|
12
|
+
date: 2012-07-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: beanstalk-client
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirement: &2152606340 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,15 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
25
|
-
none: false
|
26
|
-
requirements:
|
27
|
-
- - ! '>='
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '0'
|
24
|
+
version_requirements: *2152606340
|
30
25
|
- !ruby/object:Gem::Dependency
|
31
26
|
name: json_pure
|
32
|
-
requirement: !ruby/object:Gem::Requirement
|
27
|
+
requirement: &2152605640 !ruby/object:Gem::Requirement
|
33
28
|
none: false
|
34
29
|
requirements:
|
35
30
|
- - ! '>='
|
@@ -37,15 +32,10 @@ dependencies:
|
|
37
32
|
version: '0'
|
38
33
|
type: :runtime
|
39
34
|
prerelease: false
|
40
|
-
version_requirements:
|
41
|
-
none: false
|
42
|
-
requirements:
|
43
|
-
- - ! '>='
|
44
|
-
- !ruby/object:Gem::Version
|
45
|
-
version: '0'
|
35
|
+
version_requirements: *2152605640
|
46
36
|
- !ruby/object:Gem::Dependency
|
47
37
|
name: dante
|
48
|
-
requirement: !ruby/object:Gem::Requirement
|
38
|
+
requirement: &2152605220 !ruby/object:Gem::Requirement
|
49
39
|
none: false
|
50
40
|
requirements:
|
51
41
|
- - ! '>='
|
@@ -53,15 +43,10 @@ dependencies:
|
|
53
43
|
version: '0'
|
54
44
|
type: :runtime
|
55
45
|
prerelease: false
|
56
|
-
version_requirements:
|
57
|
-
none: false
|
58
|
-
requirements:
|
59
|
-
- - ! '>='
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
46
|
+
version_requirements: *2152605220
|
62
47
|
- !ruby/object:Gem::Dependency
|
63
48
|
name: rake
|
64
|
-
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirement: &2152604580 !ruby/object:Gem::Requirement
|
65
50
|
none: false
|
66
51
|
requirements:
|
67
52
|
- - ! '>='
|
@@ -69,15 +54,10 @@ dependencies:
|
|
69
54
|
version: '0'
|
70
55
|
type: :development
|
71
56
|
prerelease: false
|
72
|
-
version_requirements:
|
73
|
-
none: false
|
74
|
-
requirements:
|
75
|
-
- - ! '>='
|
76
|
-
- !ruby/object:Gem::Version
|
77
|
-
version: '0'
|
57
|
+
version_requirements: *2152604580
|
78
58
|
- !ruby/object:Gem::Dependency
|
79
59
|
name: minitest
|
80
|
-
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirement: &2152604000 !ruby/object:Gem::Requirement
|
81
61
|
none: false
|
82
62
|
requirements:
|
83
63
|
- - ! '>='
|
@@ -85,15 +65,10 @@ dependencies:
|
|
85
65
|
version: '0'
|
86
66
|
type: :development
|
87
67
|
prerelease: false
|
88
|
-
version_requirements:
|
89
|
-
none: false
|
90
|
-
requirements:
|
91
|
-
- - ! '>='
|
92
|
-
- !ruby/object:Gem::Version
|
93
|
-
version: '0'
|
68
|
+
version_requirements: *2152604000
|
94
69
|
- !ruby/object:Gem::Dependency
|
95
70
|
name: mocha
|
96
|
-
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirement: &2152603460 !ruby/object:Gem::Requirement
|
97
72
|
none: false
|
98
73
|
requirements:
|
99
74
|
- - ! '>='
|
@@ -101,12 +76,7 @@ dependencies:
|
|
101
76
|
version: '0'
|
102
77
|
type: :development
|
103
78
|
prerelease: false
|
104
|
-
version_requirements:
|
105
|
-
none: false
|
106
|
-
requirements:
|
107
|
-
- - ! '>='
|
108
|
-
- !ruby/object:Gem::Version
|
109
|
-
version: '0'
|
79
|
+
version_requirements: *2152603460
|
110
80
|
description: Beanstalk background job processing made easy
|
111
81
|
email:
|
112
82
|
- nesquena@gmail.com
|
@@ -126,12 +96,14 @@ files:
|
|
126
96
|
- bin/backburner
|
127
97
|
- examples/custom.rb
|
128
98
|
- examples/demo.rb
|
99
|
+
- examples/god.rb
|
129
100
|
- examples/simple.rb
|
130
101
|
- lib/backburner.rb
|
131
102
|
- lib/backburner/async_proxy.rb
|
132
103
|
- lib/backburner/configuration.rb
|
133
104
|
- lib/backburner/connection.rb
|
134
105
|
- lib/backburner/helpers.rb
|
106
|
+
- lib/backburner/job.rb
|
135
107
|
- lib/backburner/logger.rb
|
136
108
|
- lib/backburner/performable.rb
|
137
109
|
- lib/backburner/queue.rb
|
@@ -141,6 +113,7 @@ files:
|
|
141
113
|
- test/back_burner_test.rb
|
142
114
|
- test/connection_test.rb
|
143
115
|
- test/helpers_test.rb
|
116
|
+
- test/job_test.rb
|
144
117
|
- test/logger_test.rb
|
145
118
|
- test/performable_test.rb
|
146
119
|
- test/queue_test.rb
|
@@ -166,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
139
|
version: '0'
|
167
140
|
requirements: []
|
168
141
|
rubyforge_project:
|
169
|
-
rubygems_version: 1.8.
|
142
|
+
rubygems_version: 1.8.15
|
170
143
|
signing_key:
|
171
144
|
specification_version: 3
|
172
145
|
summary: Reliable beanstalk background job processing made easy for Ruby and Sinatra
|
@@ -174,6 +147,7 @@ test_files:
|
|
174
147
|
- test/back_burner_test.rb
|
175
148
|
- test/connection_test.rb
|
176
149
|
- test/helpers_test.rb
|
150
|
+
- test/job_test.rb
|
177
151
|
- test/logger_test.rb
|
178
152
|
- test/performable_test.rb
|
179
153
|
- test/queue_test.rb
|