backburner-allq 1.0.0

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.
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