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.
@@ -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,3 @@
1
+ module Backburner
2
+ VERSION = "0.0.1"
3
+ 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