backburner 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,17 +1,69 @@
1
1
  # Backburner
2
2
 
3
- Backburner is a beanstalkd-powered job queue designed to be as simple and easy to use as possible.
4
- You can create background jobs, place those on specialized queues and process them later.
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. Backburner works with any ruby-based
7
- web framework but is well-suited for use with [Sinatra](http://sinatrarb.com) and [Padrino](http://padrinorb.com).
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. Backburner is heavily inspired by Resque and DelayedJob.
10
- Backburner can be a persistent queue if the beanstalk persistence mode is enabled, supports priority, delays, and timeouts.
11
- Backburner stores jobs as simple JSON payloads.
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 must include the `Backburner::Queue` module and that you can set a `queue` name within the job automatically.
65
- Jobs can then be enqueued using:
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...', 5
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 for that user record.
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 Monitoring
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
- ## Why Backburner?
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
- To be filled in. DelayedJob, Resque, Stalker, et al.
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 adapted from a few excellent projects:
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
- * [Stalker](https://github.com/han/stalker)
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
@@ -11,5 +11,7 @@ task :test do
11
11
  end
12
12
  end
13
13
 
14
+ task :default => :test
15
+
14
16
  # task :doc do
15
17
  # YARD::CLI::Yardoc.new.run
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
- # Backburner.enqueue NewsletterSender, self.id, user.id
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
- # Backburner.work('newsletter_sender', 'test_job')
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
- # Backburner.configure do |config|
31
- # config.beanstalk_url = "beanstalk://..."
32
- # end
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
- # Backburner.configuration.beanstalk_url => false
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
- # default_queues << "foo"
46
- # default_queues => ["foo", "bar"]
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
- # beanstalk_addresses => ["localhost:11300"]
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
- # beanstalk_host_and_port("beanstalk://localhost") => "localhost:11300"
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'
@@ -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
- # classify('job-name') # => 'JobName'
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
- # dasherize('JobName') => "job-name"
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
- # constantize("Module") # => Module
38
- # constantize("Test::Unit") # => Test::Unit
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
- # tube_namespace => "some.namespace"
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
- # expand_tube_name("foo") => <prefix>.foo
83
- # expand_tube_name(FooJob) => <prefix>.foo-job
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
@@ -20,7 +20,9 @@ module Backburner
20
20
  end
21
21
 
22
22
  # Prints message about failure when beastalk cannot be connected
23
- # failed_connection(ex)
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
- # log("Working on task")
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
- # log_error("Task failed!")
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
- # @model.async(:pri => 1000).do_something("foo")
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
- # Model.async(:ttr => 300).do_something("foo")
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
- # perform(55, :do_something, "foo", "bar")
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)
@@ -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
- # queue "some.task.name"
12
- # queue => "some.task.name"
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
@@ -1,3 +1,3 @@
1
1
  module Backburner
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
- # Backburner::Worker.enqueue NewsletterSender, [self.id, user.id], :ttr => 1000
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
- # Backburner::Worker.start(["foo.tube.name"])
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
- # Backburner::Worker.connection => <Beanstalk::Pool>
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
- # Worker.new(['test.job'])
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
- # @worker.start
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
- # @worker.prepare
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
- # @worker.work_one_job
89
+ # @example
90
+ # @worker.work_one_job
84
91
  def work_one_job
85
- job = self.connection.reserve
86
- body = JSON.parse job.body
87
- name, args = body["class"], body["args"]
88
- self.class.log_job_begin(body)
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
- return yield if ENV['DEBUG'] # Skip if debug mode
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.1
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-26 00:00:00.000000000 Z
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: !ruby/object:Gem::Requirement
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: !ruby/object:Gem::Requirement
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: !ruby/object:Gem::Requirement
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: !ruby/object:Gem::Requirement
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: !ruby/object:Gem::Requirement
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: !ruby/object:Gem::Requirement
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.21
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