backburner 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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