backburner-allq 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +29 -0
  4. data/CHANGELOG.md +133 -0
  5. data/CONTRIBUTING.md +37 -0
  6. data/Gemfile +4 -0
  7. data/HOOKS.md +99 -0
  8. data/LICENSE +22 -0
  9. data/README.md +658 -0
  10. data/Rakefile +17 -0
  11. data/TODO +4 -0
  12. data/backburner-allq.gemspec +26 -0
  13. data/bin/backburner +7 -0
  14. data/circle.yml +3 -0
  15. data/deploy.sh +3 -0
  16. data/examples/custom.rb +25 -0
  17. data/examples/demo.rb +60 -0
  18. data/examples/god.rb +46 -0
  19. data/examples/hooked.rb +87 -0
  20. data/examples/retried.rb +31 -0
  21. data/examples/simple.rb +43 -0
  22. data/examples/stress.rb +31 -0
  23. data/lib/backburner.rb +75 -0
  24. data/lib/backburner/allq_wrapper.rb +317 -0
  25. data/lib/backburner/async_proxy.rb +25 -0
  26. data/lib/backburner/cli.rb +53 -0
  27. data/lib/backburner/configuration.rb +48 -0
  28. data/lib/backburner/connection.rb +157 -0
  29. data/lib/backburner/helpers.rb +193 -0
  30. data/lib/backburner/hooks.rb +53 -0
  31. data/lib/backburner/job.rb +118 -0
  32. data/lib/backburner/logger.rb +53 -0
  33. data/lib/backburner/performable.rb +95 -0
  34. data/lib/backburner/queue.rb +145 -0
  35. data/lib/backburner/tasks.rb +54 -0
  36. data/lib/backburner/version.rb +3 -0
  37. data/lib/backburner/worker.rb +221 -0
  38. data/lib/backburner/workers/forking.rb +52 -0
  39. data/lib/backburner/workers/simple.rb +29 -0
  40. data/lib/backburner/workers/threading.rb +163 -0
  41. data/lib/backburner/workers/threads_on_fork.rb +263 -0
  42. data/test/async_proxy_test.rb +36 -0
  43. data/test/back_burner_test.rb +88 -0
  44. data/test/connection_test.rb +179 -0
  45. data/test/fixtures/hooked.rb +122 -0
  46. data/test/fixtures/test_fork_jobs.rb +72 -0
  47. data/test/fixtures/test_forking_jobs.rb +56 -0
  48. data/test/fixtures/test_jobs.rb +87 -0
  49. data/test/fixtures/test_queue_settings.rb +14 -0
  50. data/test/helpers/templogger.rb +22 -0
  51. data/test/helpers_test.rb +278 -0
  52. data/test/hooks_test.rb +112 -0
  53. data/test/job_test.rb +185 -0
  54. data/test/logger_test.rb +44 -0
  55. data/test/performable_test.rb +88 -0
  56. data/test/queue_test.rb +69 -0
  57. data/test/test_helper.rb +128 -0
  58. data/test/worker_test.rb +157 -0
  59. data/test/workers/forking_worker_test.rb +181 -0
  60. data/test/workers/simple_worker_test.rb +350 -0
  61. data/test/workers/threading_worker_test.rb +104 -0
  62. data/test/workers/threads_on_fork_worker_test.rb +484 -0
  63. metadata +217 -0
@@ -0,0 +1,193 @@
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
+ #
22
+ # @example
23
+ # classify('job-name') # => 'JobName'
24
+ #
25
+ def classify(dashed_word)
26
+ dashed_word.to_s.split('-').each { |part| part[0] = part[0].chr.upcase }.join
27
+ end
28
+
29
+ # Given a class, dasherizes the name, used for getting tube names
30
+ #
31
+ # @example
32
+ # dasherize('JobName') # => "job-name"
33
+ #
34
+ def dasherize(word)
35
+ classify(word).to_s.gsub(/::/, '/').
36
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
37
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
38
+ tr("_", "-").downcase
39
+ end
40
+
41
+ # Tries to find a constant with the name specified in the argument string:
42
+ #
43
+ # @example
44
+ # constantize("Module") # => Module
45
+ # constantize("Test::Unit") # => Test::Unit
46
+ #
47
+ # NameError is raised when the constant is unknown.
48
+ def constantize(camel_cased_word)
49
+ camel_cased_word = camel_cased_word.to_s
50
+
51
+ if camel_cased_word.include?('-')
52
+ camel_cased_word = classify(camel_cased_word)
53
+ end
54
+
55
+ names = camel_cased_word.split('::')
56
+ names.shift if names.empty? || names.first.empty?
57
+
58
+ constant = Object
59
+ names.each do |name|
60
+ args = Module.method(:const_get).arity != 1 ? [false] : []
61
+
62
+ if constant.const_defined?(name, *args)
63
+ constant = constant.const_get(name)
64
+ else
65
+ constant = constant.const_missing(name)
66
+ end
67
+ end
68
+ constant
69
+ end
70
+
71
+ # Returns configuration options for backburner
72
+ #
73
+ # @example
74
+ # queue_config.max_job_retries => 3
75
+ #
76
+ def queue_config
77
+ Backburner.configuration
78
+ end
79
+
80
+ # Expands a tube to include the prefix
81
+ #
82
+ # @example
83
+ # expand_tube_name("foo_with_settings:3:100:6") # => <prefix>.foo_with_settings
84
+ # expand_tube_name("foo") # => <prefix>.foo
85
+ # expand_tube_name(FooJob) # => <prefix>.foo-job
86
+ #
87
+ def expand_tube_name(tube)
88
+ prefix = queue_config.tube_namespace
89
+ separator = queue_config.namespace_separator
90
+ queue_name = if tube.is_a?(String)
91
+ tube
92
+ elsif tube.respond_to?(:queue) # use queue name
93
+ queue = tube.queue
94
+ queue.is_a?(Proc) ? queue.call(tube) : queue
95
+ elsif tube.is_a?(Proc)
96
+ tube.call
97
+ elsif tube.is_a?(Class) # no queue name, use default
98
+ queue_config.primary_queue # tube.name
99
+ else # turn into a string
100
+ tube.to_s
101
+ end
102
+ [prefix.gsub(/\.$/, ''), dasherize(queue_name).gsub(/^#{prefix}/, '')].join(separator).gsub(/#{Regexp::escape(separator)}+/, separator).split(':').first
103
+ end
104
+
105
+ # Resolves job priority based on the value given. Can be integer, a class or nothing
106
+ #
107
+ # @example
108
+ # resolve_priority(1000) => 1000
109
+ # resolve_priority(FooBar) => <queue priority>
110
+ # resolve_priority(nil) => <default priority>
111
+ #
112
+ def resolve_priority(pri)
113
+ if pri.respond_to?(:queue_priority)
114
+ resolve_priority(pri.queue_priority)
115
+ elsif pri.is_a?(String) || pri.is_a?(Symbol) # named priority
116
+ resolve_priority(Backburner.configuration.priority_labels[pri.to_sym])
117
+ elsif pri.is_a?(Integer) # numerical
118
+ pri
119
+ else # default
120
+ Backburner.configuration.default_priority
121
+ end
122
+ end
123
+
124
+ # Resolves job respond timeout based on the value given. Can be integer, a class or nothing
125
+ #
126
+ # @example
127
+ # resolve_respond_timeout(1000) => 1000
128
+ # resolve_respond_timeout(FooBar) => <queue respond_timeout>
129
+ # resolve_respond_timeout(nil) => <default respond_timeout>
130
+ #
131
+ def resolve_respond_timeout(ttr)
132
+ if ttr.respond_to?(:queue_respond_timeout)
133
+ resolve_respond_timeout(ttr.queue_respond_timeout)
134
+ elsif ttr.is_a?(Integer) # numerical
135
+ ttr
136
+ else # default
137
+ Backburner.configuration.respond_timeout
138
+ end
139
+ end
140
+
141
+ # Resolves max retries based on the value given. Can be integer, a class or nothing
142
+ #
143
+ # @example
144
+ # resolve_max_job_retries(5) => 5
145
+ # resolve_max_job_retries(FooBar) => <queue max_job_retries>
146
+ # resolve_max_job_retries(nil) => <default max_job_retries>
147
+ #
148
+ def resolve_max_job_retries(retries)
149
+ if retries.respond_to?(:queue_max_job_retries)
150
+ resolve_max_job_retries(retries.queue_max_job_retries)
151
+ elsif retries.is_a?(Integer) # numerical
152
+ retries
153
+ else # default
154
+ Backburner.configuration.max_job_retries
155
+ end
156
+ end
157
+
158
+ # Resolves retry delay based on the value given. Can be integer, a class or nothing
159
+ #
160
+ # @example
161
+ # resolve_retry_delay(5) => 5
162
+ # resolve_retry_delay(FooBar) => <queue retry_delay>
163
+ # resolve_retry_delay(nil) => <default retry_delay>
164
+ #
165
+ def resolve_retry_delay(delay)
166
+ if delay.respond_to?(:queue_retry_delay)
167
+ resolve_retry_delay(delay.queue_retry_delay)
168
+ elsif delay.is_a?(Integer) # numerical
169
+ delay
170
+ else # default
171
+ Backburner.configuration.retry_delay
172
+ end
173
+ end
174
+
175
+ # Resolves retry delay proc based on the value given. Can be proc, a class or nothing
176
+ #
177
+ # @example
178
+ # resolve_retry_delay_proc(proc) => proc
179
+ # resolve_retry_delay_proc(FooBar) => <queue retry_delay_proc>
180
+ # resolve_retry_delay_proc(nil) => <default retry_delay_proc>
181
+ #
182
+ def resolve_retry_delay_proc(proc)
183
+ if proc.respond_to?(:queue_retry_delay_proc)
184
+ resolve_retry_delay_proc(proc.queue_retry_delay_proc)
185
+ elsif proc.is_a?(Proc)
186
+ proc
187
+ else # default
188
+ Backburner.configuration.retry_delay_proc
189
+ end
190
+ end
191
+
192
+ end # Helpers
193
+ end # Backburner
@@ -0,0 +1,53 @@
1
+ module Backburner
2
+ class Hooks
3
+ class << self
4
+ # Triggers all method hooks that match the given event type with specified arguments.
5
+ #
6
+ # @example
7
+ # invoke_hook_events(hookable, :before_enqueue, 'some', 'args')
8
+ # invoke_hook_events(hookable, :after_perform, 5)
9
+ #
10
+ def invoke_hook_events(hookable, event, *args)
11
+ res = find_hook_events(hookable, event).map { |e| hookable.send(e, *args) }
12
+ return false if res.any? { |result| result == false }
13
+ res
14
+ end
15
+
16
+ # Triggers all method hooks that match given around event type. Used for 'around' hooks
17
+ # that stack over the original task cumulatively onto one another.
18
+ #
19
+ # The final block will be the one that actually invokes the
20
+ # original task after calling all other around blocks.
21
+ #
22
+ # @example
23
+ # around_hook_events(hookable, :around_perform) { hookable.perform }
24
+ #
25
+ def around_hook_events(hookable, event, *args, &block)
26
+ raise "Please pass a block to hook events!" unless block_given?
27
+ around_hooks = find_hook_events(hookable, event).reverse
28
+ aggregate_filter = Proc.new { |&blk| blk.call }
29
+ around_hooks.each do |ah|
30
+ prior_around_filter = aggregate_filter
31
+ aggregate_filter = Proc.new do |&blk|
32
+ hookable.method(ah).call(*args) do
33
+ prior_around_filter.call(&blk)
34
+ end
35
+ end
36
+ end
37
+ aggregate_filter.call(&block)
38
+ end
39
+
40
+ protected
41
+
42
+ # Returns all methods that match given hook type
43
+ #
44
+ # @example
45
+ # find_hook_events(:before_enqueue)
46
+ # # => ['before_enqueue_foo', 'before_enqueue_bar']
47
+ #
48
+ def find_hook_events(hookable, event)
49
+ (hookable.methods - Object.methods).grep(/^#{event}/).sort
50
+ end
51
+ end
52
+ end # Hooks
53
+ end # Backburner
@@ -0,0 +1,118 @@
1
+ module Backburner
2
+ # A single backburner job which can be processed and removed by the worker
3
+ class Job < SimpleDelegator
4
+ include Backburner::Helpers
5
+
6
+ # Raises when a job times out
7
+ class JobTimeout < RuntimeError; end
8
+ class JobNotFound < RuntimeError; end
9
+ class JobFormatInvalid < RuntimeError; end
10
+ class RetryJob < RuntimeError; end
11
+
12
+ attr_accessor :task, :body, :name, :args
13
+
14
+ # Construct a job to be parsed and processed
15
+ #
16
+ # task is a reserved object containing the json body in the form of
17
+ # { :class => "NewsletterSender", :args => ["foo@bar.com"] }
18
+ #
19
+ # @example
20
+ # Backburner::Job.new(payload)
21
+ #
22
+ def initialize(task)
23
+ @hooks = Backburner::Hooks
24
+ @task = task
25
+ @body = task.body.is_a?(Hash) ? task.body : Backburner.configuration.job_parser_proc.call(task.body)
26
+ @name = body["class"] || body[:class]
27
+ @args = body["args"] || body[:args]
28
+ rescue => ex # Job was not valid format
29
+ self.bury
30
+ raise JobFormatInvalid, "Job body could not be parsed: #{ex.inspect}"
31
+ end
32
+
33
+ # Sets the delegator object to the underlying beaneater job
34
+ # self.bury
35
+ def __getobj__
36
+ __setobj__(@task)
37
+ super
38
+ end
39
+
40
+ # Processes a job and handles any failure, deleting the job once complete
41
+ #
42
+ # @example
43
+ # @task.process
44
+ #
45
+ def process
46
+ # Invoke before hook and stop if false
47
+ res = @hooks.invoke_hook_events(job_name, :before_perform, *args)
48
+ return false unless res
49
+ # Execute the job
50
+ @hooks.around_hook_events(job_name, :around_perform, *args) do
51
+ # We subtract one to ensure we timeout before beanstalkd does, except if:
52
+ # a) ttr == 0, to support never timing out
53
+ # b) ttr == 1, so that we don't accidentally set it to never time out
54
+ # NB: A ttr of 1 will likely result in race conditions between
55
+ # Backburner and beanstalkd and should probably be avoided
56
+ timeout_job_after(task.ttr > 1 ? task.ttr - 1 : task.ttr) { job_class.perform(*args) }
57
+ end
58
+ task.delete
59
+ # Invoke after perform hook
60
+ @hooks.invoke_hook_events(job_name, :after_perform, *args)
61
+ rescue => e
62
+ @hooks.invoke_hook_events(job_name, :on_failure, e, *args)
63
+ raise e
64
+ end
65
+
66
+ def bury
67
+ @hooks.invoke_hook_events(job_name, :on_bury, *args)
68
+ task.bury
69
+ end
70
+
71
+ def retry(count, delay)
72
+ @hooks.invoke_hook_events(job_name, :on_retry, count, delay, *args)
73
+ task.release(delay: delay)
74
+ end
75
+
76
+ # Returns the class for the job handler
77
+ #
78
+ # @example
79
+ # job_class # => NewsletterSender
80
+ #
81
+ def job_class
82
+ handler = try_job_class
83
+ raise(JobNotFound, self.name) unless handler
84
+ handler
85
+ end
86
+
87
+ protected
88
+
89
+ # Attempts to return a constantized job name, otherwise reverts to the name string
90
+ #
91
+ # @example
92
+ # job_name # => "SomeUnknownJob"
93
+ def job_name
94
+ handler = try_job_class
95
+ handler ? handler : self.name
96
+ end
97
+
98
+ def try_job_class
99
+ constantize(self.name)
100
+ rescue NameError
101
+ nil
102
+ end
103
+
104
+ # Timeout job within specified block after given time.
105
+ #
106
+ # @example
107
+ # timeout_job_after(3) { do_something! }
108
+ #
109
+ def timeout_job_after(secs, &block)
110
+ begin
111
+ Timeout::timeout(secs) { yield }
112
+ rescue Timeout::Error => e
113
+ raise JobTimeout, "#{name}(#{(@args||[]).join(', ')}) hit #{secs}s timeout.\nbacktrace: #{e.backtrace}"
114
+ end
115
+ end
116
+
117
+ end # Job
118
+ end # Backburner
@@ -0,0 +1,53 @@
1
+ require 'logger'
2
+
3
+ module Backburner
4
+ module Logger
5
+ # Loads in instance and class levels
6
+ def self.included(base)
7
+ base.extend self
8
+ end
9
+
10
+ # Print out when a job is about to begin
11
+ def log_job_begin(name, args)
12
+ log_info "Work job #{name} with #{args.inspect}"
13
+ Thread.current[:job_started_at] = Time.now
14
+ end
15
+
16
+ # Print out when a job completed
17
+ # If message is nil, job is considered complete
18
+ def log_job_end(name, message = nil)
19
+ ellapsed = Time.now - job_started_at
20
+ ms = (ellapsed.to_f * 1000).to_i
21
+ action_word = message ? 'Finished' : 'Completed'
22
+ log_info("#{action_word} #{name} in #{ms}ms #{message}")
23
+ end
24
+
25
+ # Returns true if the job logging started
26
+ def job_started_at
27
+ Thread.current[:job_started_at]
28
+ end
29
+
30
+ # Print a message to stdout
31
+ #
32
+ # @example
33
+ # log_info("Working on task")
34
+ #
35
+ def log_info(msg)
36
+ logger ? logger.info(msg) : puts(msg)
37
+ end
38
+
39
+ # Print an error to stderr
40
+ #
41
+ # @example
42
+ # log_error("Task failed!")
43
+ #
44
+ def log_error(msg)
45
+ logger ? logger.error(msg) : $stderr.puts(msg)
46
+ end
47
+
48
+ # Return logger if specified
49
+ def logger
50
+ Backburner.configuration.logger
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,95 @@
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
+ # @example
15
+ # @model.async(:pri => 1000).do_something("foo")
16
+ #
17
+ def async(opts={})
18
+ Backburner::AsyncProxy.new(self.class, self.id, opts)
19
+ end
20
+ end # InstanceMethods
21
+
22
+ module ClassMethods
23
+ # Return proxy object to enqueue jobs for object
24
+ # Options: `pri` (priority), `delay` (delay in secs), `ttr` (time to respond), `queue` (queue name)
25
+ # @example
26
+ # Model.async(:ttr => 300).do_something("foo")
27
+ def async(opts={})
28
+ Backburner::AsyncProxy.new(self, nil, opts)
29
+ end
30
+
31
+ # Defines perform method for job processing
32
+ # @example
33
+ # perform(55, :do_something, "foo", "bar")
34
+ def perform(id, method, *args)
35
+ if id # instance
36
+ find(id).send(method, *args)
37
+ else # class method
38
+ send(method, *args)
39
+ end
40
+ end # perform
41
+
42
+ # Always handle an instance method asynchronously
43
+ # @example
44
+ # User.handle_asynchronously :send_welcome_email, queue: 'send-mail', delay: 10
45
+ def handle_asynchronously(method, opts={})
46
+ Backburner::Performable.handle_asynchronously(self, method, opts)
47
+ end
48
+
49
+ # Always handle a class method asynchronously
50
+ # @example
51
+ # User.handle_static_asynchronously :update_recent_visitors, ttr: 300
52
+ def handle_static_asynchronously(method, opts={})
53
+ Backburner::Performable.handle_static_asynchronously(self, method, opts)
54
+ end
55
+ end # ClassMethods
56
+
57
+
58
+ # Make all calls to an instance method asynchronous. The given opts will be passed
59
+ # to the async method.
60
+ # @example
61
+ # Backburner::Performable.handle_asynchronously(MyObject, :long_task, queue: 'long-tasks')
62
+ # NB: The method called on the async proxy will be ""#{method}_without_async". This
63
+ # will also be what's given to the Worker.enqueue method so your workers need
64
+ # to know about that. It shouldn't be a problem unless the producer and consumer are
65
+ # from different codebases (or anywhere they don't both call the handle_asynchronously
66
+ # method when booting up)
67
+ def self.handle_asynchronously(klass, method, opts={})
68
+ _handle_asynchronously(klass, klass, method, opts)
69
+ end
70
+
71
+ # Make all calls to a class method asynchronous. The given opts will be passed
72
+ # to the async method. Please see the NB on #handle_asynchronously
73
+ def self.handle_static_asynchronously(klass, method, opts={})
74
+ _handle_asynchronously(klass, klass.singleton_class, method, opts)
75
+ end
76
+
77
+ def self._handle_asynchronously(klass, klass_eval_scope, method, opts={})
78
+ aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
79
+ with_async_name = :"#{aliased_method}_with_async#{punctuation}"
80
+ without_async_name = :"#{aliased_method}_without_async#{punctuation}"
81
+
82
+ klass.send(:include, Performable) unless included_modules.include?(Performable)
83
+ klass_eval_scope.class_eval do
84
+ define_method with_async_name do |*args|
85
+ async(opts).__send__ without_async_name, *args
86
+ end
87
+ alias_method without_async_name, method.to_sym
88
+ alias_method method.to_sym, with_async_name
89
+ end
90
+ end
91
+ private_class_method :_handle_asynchronously
92
+
93
+
94
+ end # Performable
95
+ end # Backburner