backburner 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +205 -0
- data/Rakefile +15 -0
- data/TODO +5 -0
- data/backburner.gemspec +25 -0
- data/bin/backburner +10 -0
- data/examples/custom.rb +25 -0
- data/examples/demo.rb +60 -0
- data/examples/simple.rb +43 -0
- data/lib/backburner.rb +51 -0
- data/lib/backburner/async_proxy.rb +22 -0
- data/lib/backburner/configuration.rb +19 -0
- data/lib/backburner/connection.rb +43 -0
- data/lib/backburner/helpers.rb +99 -0
- data/lib/backburner/logger.rb +44 -0
- data/lib/backburner/performable.rb +40 -0
- data/lib/backburner/queue.rb +22 -0
- data/lib/backburner/tasks.rb +11 -0
- data/lib/backburner/version.rb +3 -0
- data/lib/backburner/worker.rb +140 -0
- data/test/back_burner_test.rb +61 -0
- data/test/connection_test.rb +35 -0
- data/test/helpers_test.rb +79 -0
- data/test/logger_test.rb +19 -0
- data/test/performable_test.rb +38 -0
- data/test/queue_test.rb +28 -0
- data/test/test_helper.rb +93 -0
- data/test/worker_test.rb +163 -0
- metadata +181 -0
data/lib/backburner.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'beanstalk-client'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
require 'timeout'
|
5
|
+
require 'backburner/version'
|
6
|
+
require 'backburner/helpers'
|
7
|
+
require 'backburner/configuration'
|
8
|
+
require 'backburner/logger'
|
9
|
+
require 'backburner/connection'
|
10
|
+
require 'backburner/performable'
|
11
|
+
require 'backburner/worker'
|
12
|
+
require 'backburner/queue'
|
13
|
+
|
14
|
+
module Backburner
|
15
|
+
class << self
|
16
|
+
|
17
|
+
# Enqueues a job to be performed with arguments
|
18
|
+
# Backburner.enqueue NewsletterSender, self.id, user.id
|
19
|
+
def enqueue(job_class, *args)
|
20
|
+
Backburner::Worker.enqueue(job_class, args, {})
|
21
|
+
end
|
22
|
+
|
23
|
+
# Begins working on jobs enqueued with optional tubes specified
|
24
|
+
# Backburner.work('newsletter_sender', 'test_job')
|
25
|
+
def work(*tubes)
|
26
|
+
Backburner::Worker.start(tubes)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Yields a configuration block
|
30
|
+
# Backburner.configure do |config|
|
31
|
+
# config.beanstalk_url = "beanstalk://..."
|
32
|
+
# end
|
33
|
+
def configure(&block)
|
34
|
+
yield(configuration)
|
35
|
+
configuration
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the configuration options set for Backburner
|
39
|
+
# Backburner.configuration.beanstalk_url => false
|
40
|
+
def configuration
|
41
|
+
@_configuration ||= Configuration.new
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the queues that are processed by default if none are specified
|
45
|
+
# default_queues << "foo"
|
46
|
+
# default_queues => ["foo", "bar"]
|
47
|
+
def default_queues
|
48
|
+
configuration.default_queues
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Backburner
|
2
|
+
# BasicObject for 1.8.7
|
3
|
+
class BasicObject
|
4
|
+
instance_methods.each do |m|
|
5
|
+
undef_method(m) if m.to_s !~ /(?:^__|^nil?$|^send$|^object_id$)/
|
6
|
+
end
|
7
|
+
end unless defined?(::BasicObject)
|
8
|
+
|
9
|
+
# Class allows async task to be proxied
|
10
|
+
class AsyncProxy < BasicObject
|
11
|
+
# AsyncProxy(User, 10, :pri => 1000, :ttr => 1000)
|
12
|
+
# Options include `pri` (priority), `delay` (delay in secs), `ttr` (time to respond)
|
13
|
+
def initialize(klazz, id=nil, opts={})
|
14
|
+
@klazz, @id, @opts = klazz, id, opts
|
15
|
+
end
|
16
|
+
|
17
|
+
# Enqueue as job when a method is invoked
|
18
|
+
def method_missing(method, *args, &block)
|
19
|
+
::Backburner::Worker.enqueue(@klazz, [@id, method, *args], @opts)
|
20
|
+
end
|
21
|
+
end # AsyncProxy
|
22
|
+
end # Backburner
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Backburner
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :beanstalk_url # beanstalk url connection
|
4
|
+
attr_accessor :tube_namespace # namespace prefix for every queue
|
5
|
+
attr_accessor :default_priority # default job priority
|
6
|
+
attr_accessor :respond_timeout # default job timeout
|
7
|
+
attr_accessor :on_error # error handler
|
8
|
+
attr_accessor :default_queues # default queues
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@beanstalk_url = "beanstalk://localhost"
|
12
|
+
@tube_namespace = "backburner.worker.queue"
|
13
|
+
@default_priority = 65536
|
14
|
+
@respond_timeout = 120
|
15
|
+
@on_error = nil
|
16
|
+
@default_queues = []
|
17
|
+
end
|
18
|
+
end # Configuration
|
19
|
+
end # Backburner
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Backburner
|
4
|
+
class Connection < SimpleDelegator
|
5
|
+
class BadURL < RuntimeError; end
|
6
|
+
|
7
|
+
attr_accessor :url, :beanstalk
|
8
|
+
|
9
|
+
def initialize(url)
|
10
|
+
@url = url
|
11
|
+
connect!
|
12
|
+
end
|
13
|
+
|
14
|
+
# Sets the delegator object to the underlying beanstalk connection
|
15
|
+
# self.put(...)
|
16
|
+
def __getobj__
|
17
|
+
__setobj__(@beanstalk)
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
# Connects to a beanstalk queue
|
24
|
+
def connect!
|
25
|
+
@beanstalk ||= Beanstalk::Pool.new(beanstalk_addresses)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the beanstalk queue addresses
|
29
|
+
# beanstalk_addresses => ["localhost:11300"]
|
30
|
+
def beanstalk_addresses
|
31
|
+
uris = self.url.split(/[\s,]+/)
|
32
|
+
uris.map {|uri| beanstalk_host_and_port(uri)}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a host and port based on the uri_string given
|
36
|
+
# beanstalk_host_and_port("beanstalk://localhost") => "localhost:11300"
|
37
|
+
def beanstalk_host_and_port(uri_string)
|
38
|
+
uri = URI.parse(uri_string)
|
39
|
+
raise(BadURL, uri_string) if uri.scheme != 'beanstalk'
|
40
|
+
"#{uri.host}:#{uri.port || 11300}"
|
41
|
+
end
|
42
|
+
end # Connection
|
43
|
+
end # Backburner
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Backburner
|
2
|
+
module Helpers
|
3
|
+
# Loads in instance and class levels
|
4
|
+
def self.included(base)
|
5
|
+
base.extend self
|
6
|
+
end
|
7
|
+
|
8
|
+
# Prints out exception_message based on specified e
|
9
|
+
def exception_message(e)
|
10
|
+
msg = [ "Exception #{e.class} -> #{e.message}" ]
|
11
|
+
|
12
|
+
base = File.expand_path(Dir.pwd) + '/'
|
13
|
+
e.backtrace.each do |t|
|
14
|
+
msg << " #{File.expand_path(t).gsub(/#{base}/, '')}"
|
15
|
+
end if e.backtrace
|
16
|
+
|
17
|
+
msg.join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Given a word with dashes, returns a camel cased version of it.
|
21
|
+
# classify('job-name') # => 'JobName'
|
22
|
+
def classify(dashed_word)
|
23
|
+
dashed_word.to_s.split('-').each { |part| part[0] = part[0].chr.upcase }.join
|
24
|
+
end
|
25
|
+
|
26
|
+
# Given a class, dasherizes the name, used for getting tube names
|
27
|
+
# dasherize('JobName') => "job-name"
|
28
|
+
def dasherize(word)
|
29
|
+
classify(word).to_s.gsub(/::/, '/').
|
30
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
31
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
32
|
+
tr("_", "-").downcase
|
33
|
+
end
|
34
|
+
|
35
|
+
# Tries to find a constant with the name specified in the argument string:
|
36
|
+
#
|
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
|
50
|
+
#
|
51
|
+
# NameError is raised when the constant is unknown.
|
52
|
+
def constantize(camel_cased_word)
|
53
|
+
camel_cased_word = camel_cased_word.to_s
|
54
|
+
|
55
|
+
if camel_cased_word.include?('-')
|
56
|
+
camel_cased_word = classify(camel_cased_word)
|
57
|
+
end
|
58
|
+
|
59
|
+
names = camel_cased_word.split('::')
|
60
|
+
names.shift if names.empty? || names.first.empty?
|
61
|
+
|
62
|
+
constant = Object
|
63
|
+
names.each do |name|
|
64
|
+
args = Module.method(:const_get).arity != 1 ? [false] : []
|
65
|
+
|
66
|
+
if constant.const_defined?(name, *args)
|
67
|
+
constant = constant.const_get(name)
|
68
|
+
else
|
69
|
+
constant = constant.const_missing(name)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
constant
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns tube_namespace for backburner
|
76
|
+
# tube_namespace => "some.namespace"
|
77
|
+
def tube_namespace
|
78
|
+
Backburner.configuration.tube_namespace
|
79
|
+
end
|
80
|
+
|
81
|
+
# Expands a tube to include the prefix
|
82
|
+
# expand_tube_name("foo") => <prefix>.foo
|
83
|
+
# expand_tube_name(FooJob) => <prefix>.foo-job
|
84
|
+
def expand_tube_name(tube)
|
85
|
+
prefix = tube_namespace
|
86
|
+
queue_name = if tube.is_a?(String)
|
87
|
+
tube
|
88
|
+
elsif tube.respond_to?(:queue) # use queue name
|
89
|
+
tube.queue
|
90
|
+
elsif tube.is_a?(Class) # no queue name, use job_class
|
91
|
+
tube.name
|
92
|
+
else # turn into a string
|
93
|
+
tube.to_s
|
94
|
+
end
|
95
|
+
[prefix.gsub(/\.$/, ''), dasherize(queue_name).gsub(/^#{prefix}/, '')].join(".")
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Backburner
|
2
|
+
module Logger
|
3
|
+
|
4
|
+
# Loads in instance and class levels
|
5
|
+
def self.included(base)
|
6
|
+
base.extend self
|
7
|
+
end
|
8
|
+
|
9
|
+
# Print out when a job is about to begin
|
10
|
+
def log_job_begin(body)
|
11
|
+
log [ "Working", body ].join(' ')
|
12
|
+
@job_begun = Time.now
|
13
|
+
end
|
14
|
+
|
15
|
+
# Print out when a job completed
|
16
|
+
def log_job_end(name, failed=false)
|
17
|
+
ellapsed = Time.now - @job_begun
|
18
|
+
ms = (ellapsed.to_f * 1000).to_i
|
19
|
+
log "Finished #{name} in #{ms}ms #{failed ? ' (failed)' : ''}"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Prints message about failure when beastalk cannot be connected
|
23
|
+
# failed_connection(ex)
|
24
|
+
def failed_connection(e)
|
25
|
+
log_error exception_message(e)
|
26
|
+
log_error "*** Failed connection to #{connection.url}"
|
27
|
+
log_error "*** Check that beanstalkd is running (or set a different beanstalk url)"
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
|
31
|
+
# Print a message to stdout
|
32
|
+
# log("Working on task")
|
33
|
+
def log(msg)
|
34
|
+
puts msg
|
35
|
+
end
|
36
|
+
|
37
|
+
# Print an error to stderr
|
38
|
+
# log_error("Task failed!")
|
39
|
+
def log_error(msg)
|
40
|
+
$stderr.puts msg
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'backburner/async_proxy'
|
2
|
+
|
3
|
+
module Backburner
|
4
|
+
module Performable
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:include, InstanceMethods)
|
7
|
+
base.send(:include, Backburner::Queue)
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
# Return proxy object to enqueue jobs for object
|
13
|
+
# Options: `pri` (priority), `delay` (delay in secs), `ttr` (time to respond), `queue` (queue name)
|
14
|
+
# @model.async(:pri => 1000).do_something("foo")
|
15
|
+
def async(opts={})
|
16
|
+
Backburner::AsyncProxy.new(self.class, self.id, opts)
|
17
|
+
end
|
18
|
+
end # InstanceMethods
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# Return proxy object to enqueue jobs for object
|
22
|
+
# Options: `pri` (priority), `delay` (delay in secs), `ttr` (time to respond), `queue` (queue name)
|
23
|
+
# Model.async(:ttr => 300).do_something("foo")
|
24
|
+
def async(opts={})
|
25
|
+
Backburner::AsyncProxy.new(self, nil, opts)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Defines perform method for job processing
|
29
|
+
# perform(55, :do_something, "foo", "bar")
|
30
|
+
def perform(id, method, *args)
|
31
|
+
if id # instance
|
32
|
+
find(id).send(method, *args)
|
33
|
+
else # class method
|
34
|
+
send(method, *args)
|
35
|
+
end
|
36
|
+
end # perform
|
37
|
+
end # ClassMethods
|
38
|
+
|
39
|
+
end # Performable
|
40
|
+
end # Backburner
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Backburner
|
2
|
+
module Queue
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:extend, Backburner::Helpers)
|
5
|
+
base.extend ClassMethods
|
6
|
+
Backburner::Worker.known_queue_classes << base
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Returns or assigns queue name for this job
|
11
|
+
# queue "some.task.name"
|
12
|
+
# queue => "some.task.name"
|
13
|
+
def queue(name=nil)
|
14
|
+
if name
|
15
|
+
@queue_name = name
|
16
|
+
else # accessor
|
17
|
+
@queue_name || dasherize(self.name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end # ClassMethods
|
21
|
+
end # Job
|
22
|
+
end # Backburner
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# require 'backburner/tasks'
|
2
|
+
# will give you the backburner tasks
|
3
|
+
|
4
|
+
namespace :backburner do
|
5
|
+
# QUEUE=foo,bar,baz rake backburner:work
|
6
|
+
desc "Start an backburner worker"
|
7
|
+
task :work => :environment do
|
8
|
+
queues = (ENV["QUEUE"] ? ENV["QUEUE"].split(',') : nil) rescue nil
|
9
|
+
Backburner.work queues
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Backburner
|
2
|
+
class Worker
|
3
|
+
include Backburner::Helpers
|
4
|
+
include Backburner::Logger
|
5
|
+
|
6
|
+
class JobNotFound < RuntimeError; end
|
7
|
+
class JobTimeout < RuntimeError; end
|
8
|
+
class JobQueueNotSet < RuntimeError; end
|
9
|
+
|
10
|
+
# Backburner::Worker.known_queue_classes
|
11
|
+
# List of known_queue_classes
|
12
|
+
class << self
|
13
|
+
attr_writer :known_queue_classes
|
14
|
+
def known_queue_classes; @known_queue_classes ||= []; end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Enqueues a job to be processed later by a worker
|
18
|
+
# 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
|
20
|
+
def self.enqueue(job_class, args=[], opts={})
|
21
|
+
pri = opts[:pri] || Backburner.configuration.default_priority
|
22
|
+
delay = [0, opts[:delay].to_i].max
|
23
|
+
ttr = opts[:ttr] || Backburner.configuration.respond_timeout
|
24
|
+
connection.use expand_tube_name(opts[:queue] || job_class)
|
25
|
+
data = { :class => job_class.name, :args => args }
|
26
|
+
connection.put data.to_json, pri, delay, ttr
|
27
|
+
rescue Beanstalk::NotConnected => e
|
28
|
+
failed_connection(e)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Starts processing jobs in the specified tube_names
|
32
|
+
# Backburner::Worker.start(["foo.tube.name"])
|
33
|
+
def self.start(tube_names=nil)
|
34
|
+
self.new(tube_names).start
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the worker connection
|
38
|
+
# Backburner::Worker.connection => <Beanstalk::Pool>
|
39
|
+
def self.connection
|
40
|
+
@connection ||= Connection.new(Backburner.configuration.beanstalk_url)
|
41
|
+
end
|
42
|
+
|
43
|
+
# List of tube names to be watched and processed
|
44
|
+
attr_accessor :tube_names
|
45
|
+
|
46
|
+
# Worker.new(['test.job'])
|
47
|
+
def initialize(tube_names=nil)
|
48
|
+
@tube_names = begin
|
49
|
+
tube_names = tube_names.first if tube_names && tube_names.size == 1 && tube_names.first.is_a?(Array)
|
50
|
+
tube_names = Array(tube_names).compact if tube_names && Array(tube_names).compact.size > 0
|
51
|
+
tube_names = nil if tube_names && tube_names.compact.empty?
|
52
|
+
tube_names
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Starts processing new jobs indefinitely
|
57
|
+
# Primary way to consume and process jobs in specified tubes
|
58
|
+
# @worker.start
|
59
|
+
def start
|
60
|
+
prepare
|
61
|
+
loop { work_one_job }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Setup beanstalk tube_names and watch all specified tubes for jobs.
|
65
|
+
# Used to prepare job queues before processing jobs.
|
66
|
+
# @worker.prepare
|
67
|
+
def prepare
|
68
|
+
self.tube_names ||= Backburner.default_queues.any? ? Backburner.default_queues : all_existing_queues
|
69
|
+
self.tube_names = Array(self.tube_names)
|
70
|
+
self.tube_names.map! { |name| expand_tube_name(name) }
|
71
|
+
log "Working #{tube_names.size} queues: [ #{tube_names.join(', ')} ]"
|
72
|
+
self.tube_names.uniq.each { |name| self.connection.watch(name) }
|
73
|
+
self.connection.list_tubes_watched.each do |server, tubes|
|
74
|
+
tubes.each { |tube| self.connection.ignore(tube) unless self.tube_names.include?(tube) }
|
75
|
+
end
|
76
|
+
rescue Beanstalk::NotConnected => e
|
77
|
+
failed_connection(e)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Reserves one job within the specified queues
|
81
|
+
# Pops the job off and serializes the job to JSON
|
82
|
+
# Each job is performed by invoking `perform` on the job class.
|
83
|
+
# @worker.work_one_job
|
84
|
+
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)
|
102
|
+
rescue Beanstalk::NotConnected => e
|
103
|
+
failed_connection(e)
|
104
|
+
rescue SystemExit
|
105
|
+
raise
|
106
|
+
rescue => e
|
107
|
+
job.bury
|
108
|
+
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)
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
# Returns a list of all tubes known within the system
|
116
|
+
# Filtered for tubes that match the known prefix
|
117
|
+
def all_existing_queues
|
118
|
+
known_queues = Backburner::Worker.known_queue_classes.map(&:queue)
|
119
|
+
existing_tubes = self.connection.list_tubes.values.flatten.uniq.select { |tube| tube =~ /^#{tube_namespace}/ }
|
120
|
+
known_queues + existing_tubes
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns a reference to the beanstalk connection
|
124
|
+
def connection
|
125
|
+
self.class.connection
|
126
|
+
end
|
127
|
+
|
128
|
+
# Handles an error according to custom definition
|
129
|
+
# Used when processing a job that errors out
|
130
|
+
def handle_error(e, name, args)
|
131
|
+
if error_handler = Backburner.configuration.on_error
|
132
|
+
if error_handler.arity == 1
|
133
|
+
error_handler.call(e)
|
134
|
+
else
|
135
|
+
error_handler.call(e, name, args)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end # Worker
|
140
|
+
end # Backburner
|