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