backburner 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|