backburner 1.6.1 → 1.8.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +45 -0
  3. data/.github/workflows/github-release.yml +58 -0
  4. data/.github/workflows/release.yml +221 -0
  5. data/CHANGELOG.md +33 -0
  6. data/Gemfile +4 -1
  7. data/HOOKS.md +23 -14
  8. data/README.md +32 -9
  9. data/backburner.gemspec +6 -1
  10. data/bin/backburner +1 -1
  11. data/lib/active_job/queue_adapters/backburner_adapter.rb +41 -0
  12. data/lib/backburner/configuration.rb +7 -0
  13. data/lib/backburner/connection.rb +4 -2
  14. data/lib/backburner/enqueue_pool.rb +88 -0
  15. data/lib/backburner/helpers.rb +77 -22
  16. data/lib/backburner/job.rb +46 -7
  17. data/lib/backburner/logger.rb +9 -0
  18. data/lib/backburner/performable.rb +1 -1
  19. data/lib/backburner/queue.rb +37 -19
  20. data/lib/backburner/version.rb +1 -1
  21. data/lib/backburner/worker.rb +112 -21
  22. data/lib/backburner/workers/forking.rb +4 -2
  23. data/lib/backburner/workers/simple.rb +5 -0
  24. data/lib/backburner/workers/threading.rb +1 -7
  25. data/lib/backburner/workers/threads_on_fork.rb +58 -23
  26. data/lib/backburner.rb +7 -0
  27. data/test/active_job_adapter_test.rb +24 -0
  28. data/test/back_burner_test.rb +13 -0
  29. data/test/connection_test.rb +22 -0
  30. data/test/fixtures/active_job_app.rb +53 -0
  31. data/test/fixtures/active_jobs.rb +11 -0
  32. data/test/fixtures/autoload/test_broken_name_job.rb +4 -0
  33. data/test/fixtures/autoload/test_broken_no_method_job.rb +4 -0
  34. data/test/fixtures/autoload/test_broken_require_job.rb +4 -0
  35. data/test/fixtures/autoload/test_broken_runtime_job.rb +4 -0
  36. data/test/fixtures/autoload/test_broken_syntax_job.rb +4 -0
  37. data/test/fixtures/autoload/test_broken_timeout_job.rb +4 -0
  38. data/test/fixtures/autoload/test_same_name_job.rb +5 -0
  39. data/test/fixtures/test_fork_jobs.rb +5 -0
  40. data/test/fixtures/test_jobs.rb +53 -0
  41. data/test/fixtures/test_queue_settings.rb +41 -1
  42. data/test/helpers/templogger.rb +1 -1
  43. data/test/helpers_test.rb +184 -0
  44. data/test/job_test.rb +22 -0
  45. data/test/logger_test.rb +27 -0
  46. data/test/performable_test.rb +50 -0
  47. data/test/queue_test.rb +63 -0
  48. data/test/test_helper.rb +59 -5
  49. data/test/worker_test.rb +346 -1
  50. data/test/workers/forking_worker_test.rb +19 -1
  51. data/test/workers/simple_worker_test.rb +272 -0
  52. data/test/workers/threading_worker_test.rb +22 -1
  53. data/test/workers/threads_on_fork_worker_test.rb +279 -12
  54. metadata +87 -8
  55. data/.travis.yml +0 -29
  56. data/circle.yml +0 -3
@@ -0,0 +1,41 @@
1
+ # Rails' own adapter files require backburner too, for apps that don't require it themselves
2
+ require 'backburner' unless defined?(Backburner::Worker)
3
+
4
+ module ActiveJob
5
+ module QueueAdapters
6
+ # Rails 7.2 added AbstractAdapter; the adapters of older Rails have no superclass
7
+ rails_7_2_or_newer = const_defined?(:AbstractAdapter, false)
8
+
9
+ # Explicitly remove the implementation rails' 7.2+ ship. Older rails' keep theirs, unless their autoload
10
+ # loads this file instead (it shadows theirs when this gem's lib comes first on the load path).
11
+ remove_const(:BackburnerAdapter) if rails_7_2_or_newer && const_defined?(:BackburnerAdapter)
12
+
13
+ # = Backburner adapter for Active Job
14
+ #
15
+ # To use Backburner set the queue_adapter config to +:backburner+.
16
+ #
17
+ # Rails.application.config.active_job.queue_adapter = :backburner
18
+ class BackburnerAdapter < (rails_7_2_or_newer ? ::ActiveJob::QueueAdapters::AbstractAdapter : Object)
19
+ def enqueue(job)
20
+ response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
21
+ job.provider_job_id = response[:id] if response.is_a?(Hash)
22
+ response
23
+ end
24
+
25
+ def enqueue_at(job, timestamp)
26
+ delay = timestamp - Time.current.to_f
27
+ response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay)
28
+ job.provider_job_id = response[:id] if response.is_a?(Hash)
29
+ response
30
+ end
31
+
32
+ class JobWrapper
33
+ class << self
34
+ def perform(job_data)
35
+ Base.execute job_data
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -19,6 +19,7 @@ module Backburner
19
19
  attr_accessor :reserve_timeout # duration to wait to reserve on a single server
20
20
  attr_accessor :job_serializer_proc # proc to write the job body to a string
21
21
  attr_accessor :job_parser_proc # proc to parse a job body from a string
22
+ attr_reader :enqueue_connection_max_idle # seconds an idle enqueue connection is reused for (nil: no limit)
22
23
 
23
24
  def initialize
24
25
  @beanstalk_url = "beanstalk://127.0.0.1"
@@ -38,6 +39,12 @@ module Backburner
38
39
  @reserve_timeout = nil
39
40
  @job_serializer_proc = lambda { |body| body.to_json }
40
41
  @job_parser_proc = lambda { |body| JSON.parse(body) }
42
+ @enqueue_connection_max_idle = 60
43
+ end
44
+
45
+ # Accepts a number or a numeric string (from ENV, say), or nil for no limit
46
+ def enqueue_connection_max_idle=(seconds)
47
+ @enqueue_connection_max_idle = seconds.nil? ? nil : Float(seconds)
41
48
  end
42
49
 
43
50
  def namespace_separator=(val)
@@ -44,7 +44,9 @@ module Backburner
44
44
  # completely new connection)
45
45
  # @raise [Beaneater::NotConnected] If beanstalk fails to connect
46
46
  def reconnect!
47
- close
47
+ # Close the socket but keep the delegate: if connecting fails, the next call
48
+ # must still reach ensure_connected! and try again (unlike after #close)
49
+ @beanstalk.close if @beanstalk
48
50
  connect!
49
51
  @on_reconnect.call(self) if @on_reconnect.respond_to?(:call)
50
52
  end
@@ -67,7 +69,7 @@ module Backburner
67
69
  begin
68
70
  yield
69
71
 
70
- rescue Beaneater::NotConnected
72
+ rescue Beaneater::NotConnected => e
71
73
  if retry_count > 0
72
74
  reconnect!
73
75
  retry_count -= 1
@@ -0,0 +1,88 @@
1
+ module Backburner
2
+ # The connections Worker.enqueue puts jobs with, shared by the process's threads and fibers,
3
+ # so enqueueing doesn't open a TCP connection per job (that ran out of ports, #125).
4
+ # A connection is checked out for one enqueue and checked back in after it; up to
5
+ # MAX_IDLE_CONNECTIONS stay open between enqueues and the rest are closed.
6
+ #
7
+ # ceiling: idle connections are only closed when they are next checked out (or when the URL
8
+ # changes or the process exits); a reaper thread would close them sooner.
9
+ class EnqueuePool
10
+ MAX_IDLE_CONNECTIONS = 5
11
+
12
+ # The pool of this process: a forked child gets a new one and leaves its parent's
13
+ # sockets (and a lock another thread may have held at the fork) alone.
14
+ # ceiling: no lock, so threads that race on the first enqueue (or the first after a fork) may
15
+ # each make a pool; only the last one is kept, and the others' connections stay open until
16
+ # garbage collected. A class-level lock would need replacing after a fork, as a thread that
17
+ # doesn't exist in the child may hold it.
18
+ def self.current
19
+ pool = @current
20
+ pool && pool.pid == Process.pid ? pool : (@current = new)
21
+ end
22
+
23
+ attr_reader :pid
24
+
25
+ def initialize
26
+ @pid = Process.pid
27
+ @lock = Mutex.new
28
+ @url = nil
29
+ @idle = [] # [connection, idle since], most recently used last
30
+ end
31
+
32
+ # Yields a connection to the configured beanstalk_url. If the block doesn't finish (it raises,
33
+ # or its thread is killed, or it is left with throw), the connection is closed instead of
34
+ # checked back in, since an interrupted command may have left a reply unread on it.
35
+ def with_connection
36
+ url, connection = checkout
37
+ finished = false
38
+ begin
39
+ result = yield connection
40
+ finished = true
41
+ result
42
+ ensure
43
+ if finished
44
+ checkin(url, connection)
45
+ else
46
+ connection.close rescue nil
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def checkout
54
+ url = Backburner.configuration.beanstalk_url
55
+ max_idle = Backburner.configuration.enqueue_connection_max_idle
56
+ to_close = []
57
+ connection = @lock.synchronize do
58
+ unless @url == url # the URL changed: its old connections go
59
+ to_close.concat(@idle)
60
+ @idle = []
61
+ @url = url
62
+ end
63
+ # Connections idle for too long may have been dropped by a firewall or load balancer
64
+ # without either end knowing, and a put on one would hang until TCP gives up
65
+ now = monotonic_now
66
+ expired, @idle = @idle.partition { |_conn, since| max_idle && now - since > max_idle }
67
+ to_close.concat(expired)
68
+ conn, _since = @idle.pop
69
+ conn
70
+ end
71
+ to_close.each { |conn, _since| conn.close rescue nil }
72
+ [url, connection || Backburner::Connection.new(url)]
73
+ end
74
+
75
+ def checkin(url, connection)
76
+ kept = @lock.synchronize do
77
+ if @url == url && @idle.size < MAX_IDLE_CONNECTIONS
78
+ @idle.push([connection, monotonic_now])
79
+ end
80
+ end
81
+ connection.close rescue nil unless kept
82
+ end
83
+
84
+ def monotonic_now
85
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
86
+ end
87
+ end # EnqueuePool
88
+ end # Backburner
@@ -11,7 +11,7 @@ module Backburner
11
11
 
12
12
  base = File.expand_path(Dir.pwd) + '/'
13
13
  e.backtrace.each do |t|
14
- msg << " #{File.expand_path(t).gsub(/#{base}/, '')}"
14
+ msg << " #{File.expand_path(t).gsub(base, '')}"
15
15
  end if e.backtrace
16
16
 
17
17
  msg.join("\n")
@@ -44,20 +44,13 @@ module Backburner
44
44
  # constantize("Module") # => Module
45
45
  # constantize("Test::Unit") # => Test::Unit
46
46
  #
47
- # NameError is raised when the constant is unknown.
47
+ # NameError is raised when the constant is unknown. Given a block, yields each module and the name
48
+ # of the constant it is about to look up in it, so a caller can tell which one failed.
48
49
  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
50
  constant = Object
59
- names.each do |name|
51
+ constant_names(camel_cased_word).each do |name|
60
52
  args = Module.method(:const_get).arity != 1 ? [false] : []
53
+ yield constant, name if block_given?
61
54
 
62
55
  if constant.const_defined?(name, *args)
63
56
  constant = constant.const_get(name)
@@ -68,6 +61,23 @@ module Backburner
68
61
  constant
69
62
  end
70
63
 
64
+ # The names of the constants constantize looks up in turn
65
+ #
66
+ # @example
67
+ # constant_names("Test::Unit") # => ["Test", "Unit"]
68
+ #
69
+ def constant_names(camel_cased_word)
70
+ camel_cased_word = camel_cased_word.to_s
71
+
72
+ if camel_cased_word.include?('-')
73
+ camel_cased_word = classify(camel_cased_word)
74
+ end
75
+
76
+ names = camel_cased_word.split('::')
77
+ names.shift if names.empty? || names.first.empty?
78
+ names
79
+ end
80
+
71
81
  # Returns configuration options for backburner
72
82
  #
73
83
  # @example
@@ -85,7 +95,7 @@ module Backburner
85
95
  # expand_tube_name(FooJob) # => <prefix>.foo-job
86
96
  #
87
97
  def expand_tube_name(tube)
88
- prefix = queue_config.tube_namespace
98
+ prefix = queue_config.tube_namespace # nil or false mean no prefix; '' gives names that start with the separator, as in 1.7.0
89
99
  separator = queue_config.namespace_separator
90
100
  queue_name = if tube.is_a?(String)
91
101
  tube
@@ -99,7 +109,29 @@ module Backburner
99
109
  else # turn into a string
100
110
  tube.to_s
101
111
  end
102
- [prefix.gsub(/\.$/, ''), dasherize(queue_name).gsub(/^#{prefix}/, '')].join(separator).gsub(/#{Regexp::escape(separator)}+/, separator).split(':').first
112
+ tube_name = dasherize(queue_name)
113
+ tube_name = [separator.empty? ? prefix : prefix.gsub(/\.$/, ''), tube_name.gsub(namespace_pattern(prefix), '')].join(separator) if prefix
114
+ tube_name = tube_name.gsub(/#{Regexp::escape(separator)}+/, separator) unless separator.empty? # /+/ isn't a regexp
115
+ tube_name.split(':').first
116
+ end
117
+
118
+ # What every tube name expand_tube_name gives starts with: the namespace and its separator ('' for a nil or false one)
119
+ #
120
+ # @example
121
+ # expanded_tube_prefix # => "<prefix>."
122
+ #
123
+ def expanded_tube_prefix
124
+ expand_tube_name('').to_s
125
+ end
126
+
127
+ # The namespace as the pattern expand_tube_name strips from the start of a queue name.
128
+ # ceiling: a namespace that is a valid regexp is still matched as one ("app.prod" also strips "app-prod"), because
129
+ # matching it literally would move the jobs of the queue names it matches to other tubes; one that isn't ("app(")
130
+ # used to fail every enqueue and is matched literally.
131
+ def namespace_pattern(prefix)
132
+ Regexp.new("^#{prefix}")
133
+ rescue RegexpError
134
+ Regexp.new("^#{Regexp.escape(prefix)}")
103
135
  end
104
136
 
105
137
  # Resolves job priority based on the value given. Can be integer, a class or nothing
@@ -110,21 +142,31 @@ module Backburner
110
142
  # resolve_priority(nil) => <default priority>
111
143
  #
112
144
  def resolve_priority(pri)
145
+ priority = resolve_priority_value(pri)
146
+ return priority if priority
147
+ default = Backburner.configuration.default_priority # can be a label too
148
+ resolve_priority_value(default) || default # left as configured if it isn't a known label
149
+ end
150
+
151
+ # Follows a job class's queue_priority and priority labels to a number; nil for nothing,
152
+ # an unknown label or a cycle of labels
153
+ def resolve_priority_value(pri, seen_labels = [])
113
154
  if pri.respond_to?(:queue_priority)
114
- resolve_priority(pri.queue_priority)
155
+ resolve_priority_value(pri.queue_priority, seen_labels)
115
156
  elsif pri.is_a?(String) || pri.is_a?(Symbol) # named priority
116
- resolve_priority(Backburner.configuration.priority_labels[pri.to_sym])
157
+ label = pri.to_sym
158
+ resolve_priority_value(Backburner.configuration.priority_labels[label], seen_labels + [label]) unless seen_labels.include?(label)
117
159
  elsif pri.is_a?(Integer) # numerical
118
160
  pri
119
- else # default
120
- Backburner.configuration.default_priority
121
161
  end
122
162
  end
123
163
 
124
- # Resolves job respond timeout based on the value given. Can be integer, a class or nothing
164
+ # Resolves job respond timeout based on the value given, rounded up to whole seconds. Can be a number,
165
+ # a class or nothing
125
166
  #
126
167
  # @example
127
168
  # resolve_respond_timeout(1000) => 1000
169
+ # resolve_respond_timeout(1.5) => 2
128
170
  # resolve_respond_timeout(FooBar) => <queue respond_timeout>
129
171
  # resolve_respond_timeout(nil) => <default respond_timeout>
130
172
  #
@@ -133,8 +175,11 @@ module Backburner
133
175
  resolve_respond_timeout(ttr.queue_respond_timeout)
134
176
  elsif ttr.is_a?(Integer) # numerical
135
177
  ttr
178
+ elsif ttr.is_a?(Numeric) && ttr.finite? # fractional
179
+ whole_seconds(ttr)
136
180
  else # default
137
- Backburner.configuration.respond_timeout
181
+ ttr = Backburner.configuration.respond_timeout
182
+ !ttr.is_a?(Integer) && ttr.is_a?(Numeric) && ttr.finite? ? whole_seconds(ttr) : ttr
138
183
  end
139
184
  end
140
185
 
@@ -155,7 +200,7 @@ module Backburner
155
200
  end
156
201
  end
157
202
 
158
- # Resolves retry delay based on the value given. Can be integer, a class or nothing
203
+ # Resolves retry delay based on the value given. Can be a number, a class or nothing
159
204
  #
160
205
  # @example
161
206
  # resolve_retry_delay(5) => 5
@@ -165,7 +210,7 @@ module Backburner
165
210
  def resolve_retry_delay(delay)
166
211
  if delay.respond_to?(:queue_retry_delay)
167
212
  resolve_retry_delay(delay.queue_retry_delay)
168
- elsif delay.is_a?(Integer) # numerical
213
+ elsif delay.is_a?(Numeric) && delay.finite? # numerical (the worker rounds the delay it computes from it up)
169
214
  delay
170
215
  else # default
171
216
  Backburner.configuration.retry_delay
@@ -189,5 +234,15 @@ module Backburner
189
234
  end
190
235
  end
191
236
 
237
+ # Rounds a delay up to the whole seconds beanstalkd takes, so a job is never ready early
238
+ #
239
+ # @example
240
+ # whole_seconds(1.5) => 2
241
+ # whole_seconds(nil) => 0
242
+ #
243
+ def whole_seconds(delay)
244
+ [0, delay.to_f.ceil].max
245
+ end
246
+
192
247
  end # Helpers
193
248
  end # Backburner
@@ -45,12 +45,15 @@ module Backburner
45
45
  def process
46
46
  # Invoke before hook and stop if false
47
47
  res = @hooks.invoke_hook_events(job_name, :before_perform, *args)
48
- return false unless res
48
+ unless res
49
+ task.delete # aborted: don't leave it reserved, or beanstalkd hands it out again after its TTR
50
+ return false
51
+ end
49
52
  # Execute the job
50
53
  @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
+ # We subtract one to ensure we timeout before beanstalkd does, except if
55
+ # ttr == 1 (beanstalkd's minimum: it stores a ttr of 0 as 1), so that we
56
+ # don't accidentally set it to never time out.
54
57
  # NB: A ttr of 1 will likely result in race conditions between
55
58
  # Backburner and beanstalkd and should probably be avoided
56
59
  timeout_job_after(task.ttr > 1 ? task.ttr - 1 : task.ttr) { job_class.perform(*args) }
@@ -80,10 +83,16 @@ module Backburner
80
83
  #
81
84
  def job_class
82
85
  handler = try_job_class
83
- raise(JobNotFound, self.name) unless handler
86
+ raise(JobNotFound, @job_class_error ? "#{name} (#{@job_class_error.class}: #{@job_class_error.message.to_s.lines.first.to_s.chomp})" : name) unless handler
84
87
  handler
85
88
  end
86
89
 
90
+ # Whether the last lookup of the job's class found it missing (not defined, or an autoload whose
91
+ # file isn't there) rather than failing to load it (its file raised, or required a missing gem)
92
+ def job_class_missing?
93
+ !!@job_class_missing
94
+ end
95
+
87
96
  protected
88
97
 
89
98
  # Attempts to return a constantized job name, otherwise reverts to the name string
@@ -96,11 +105,41 @@ module Backburner
96
105
  end
97
106
 
98
107
  def try_job_class
99
- constantize(self.name)
100
- rescue NameError
108
+ @job_class_error = nil
109
+ @job_class_missing = false
110
+ scope = segment = autoload_path = nil
111
+ invalid_name = !constant_names(self.name).all? { |name| constant_name?(name) }
112
+ constantize(self.name) do |mod, name|
113
+ scope, segment = mod, name
114
+ autoload_path = mod.autoload?(name) rescue nil # a module, or the value of a constant that isn't one
115
+ end
116
+ rescue StandardError, ScriptError => e # its file may raise anything; an autoloader may raise LoadError for a file that isn't there
117
+ @job_class_error = e
118
+ @job_class_missing = !invalid_name && missing_constant_error?(e, scope, segment, autoload_path)
101
119
  nil
102
120
  end
103
121
 
122
+ # Whether a name can be a constant's: one that can't ("not a class", "123") will never be found
123
+ def constant_name?(name)
124
+ Module.new.const_defined?(name, false) # raises NameError for one that can't
125
+ true
126
+ rescue NameError
127
+ false
128
+ end
129
+
130
+ # Whether an error raised while looking up `segment` in `scope` says that constant is missing,
131
+ # rather than coming from inside the file that defines it (a NameError for another constant, or for
132
+ # one of the same name in another module, a NoMethodError, a require of a missing gem, any other
133
+ # error or a syntax error)
134
+ def missing_constant_error?(e, scope, segment, autoload_path)
135
+ case e
136
+ when NoMethodError then false
137
+ when NameError then e.name.to_s == segment.to_s && (e.receiver.equal?(scope) rescue true) # no receiver: by name only
138
+ when LoadError then e.path.nil? || e.path == autoload_path
139
+ else false
140
+ end
141
+ end
142
+
104
143
  # Timeout job within specified block after given time.
105
144
  #
106
145
  # @example
@@ -45,6 +45,15 @@ module Backburner
45
45
  logger ? logger.error(msg) : $stderr.puts(msg)
46
46
  end
47
47
 
48
+ # Print a warning to stderr
49
+ #
50
+ # @example
51
+ # log_warn("Task released!")
52
+ #
53
+ def log_warn(msg)
54
+ logger ? (logger.respond_to?(:warn) ? logger.warn(msg) : logger.error(msg)) : $stderr.puts(msg)
55
+ end
56
+
48
57
  # Return logger if specified
49
58
  def logger
50
59
  Backburner.configuration.logger
@@ -79,7 +79,7 @@ module Backburner
79
79
  with_async_name = :"#{aliased_method}_with_async#{punctuation}"
80
80
  without_async_name = :"#{aliased_method}_without_async#{punctuation}"
81
81
 
82
- klass.send(:include, Performable) unless included_modules.include?(Performable)
82
+ klass.send(:include, Performable) unless klass.included_modules.include?(Performable)
83
83
  klass_eval_scope.class_eval do
84
84
  define_method with_async_name do |*args|
85
85
  async(opts).__send__ without_async_name, *args
@@ -1,17 +1,14 @@
1
1
  module Backburner
2
2
  module Queue
3
3
  def self.included(base)
4
- base.instance_variable_set(:@queue_name, nil)
5
- base.instance_variable_set(:@queue_priority, nil)
6
- base.instance_variable_set(:@queue_respond_timeout, nil)
7
- base.instance_variable_set(:@queue_max_job_retries, nil)
8
- base.instance_variable_set(:@queue_retry_delay, nil)
9
- base.instance_variable_set(:@queue_retry_delay_proc, nil)
10
- base.instance_variable_set(:@queue_jobs_limit, nil)
11
- base.instance_variable_set(:@queue_garbage_limit, nil)
12
- base.instance_variable_set(:@queue_retry_limit, nil)
4
+ # Initialize the settings, but keep any already set: including Queue again (as including
5
+ # Performable does) must not reset them
6
+ [:@queue_name, :@queue_priority, :@queue_respond_timeout, :@queue_max_job_retries, :@queue_retry_delay,
7
+ :@queue_retry_delay_proc, :@queue_jobs_limit, :@queue_garbage_limit, :@queue_retry_limit].each do |setting|
8
+ base.instance_variable_set(setting, nil) unless base.instance_variable_defined?(setting)
9
+ end
13
10
  base.extend ClassMethods
14
- Backburner::Worker.known_queue_classes << base
11
+ Backburner::Worker.known_queue_classes << base unless Backburner::Worker.known_queue_classes.include?(base)
15
12
  end
16
13
 
17
14
  module ClassMethods
@@ -25,7 +22,8 @@ module Backburner
25
22
  if name
26
23
  @queue_name = name
27
24
  else # accessor
28
- (@queue_name.is_a?(Proc) ? @queue_name.call(self) : @queue_name) || Backburner.configuration.primary_queue
25
+ queue_name = inherited_queue_setting(:@queue_name)
26
+ (queue_name.is_a?(Proc) ? queue_name.call(self) : queue_name) || Backburner.configuration.primary_queue
29
27
  end
30
28
  end
31
29
 
@@ -39,7 +37,7 @@ module Backburner
39
37
  if pri
40
38
  @queue_priority = pri
41
39
  else # accessor
42
- @queue_priority
40
+ inherited_queue_setting(:@queue_priority)
43
41
  end
44
42
  end
45
43
 
@@ -53,7 +51,7 @@ module Backburner
53
51
  if ttr
54
52
  @queue_respond_timeout = ttr
55
53
  else # accessor
56
- @queue_respond_timeout
54
+ inherited_queue_setting(:@queue_respond_timeout)
57
55
  end
58
56
  end
59
57
 
@@ -67,7 +65,7 @@ module Backburner
67
65
  if delay
68
66
  @queue_max_job_retries = delay
69
67
  else # accessor
70
- @queue_max_job_retries
68
+ inherited_queue_setting(:@queue_max_job_retries)
71
69
  end
72
70
  end
73
71
 
@@ -81,7 +79,7 @@ module Backburner
81
79
  if delay
82
80
  @queue_retry_delay = delay
83
81
  else # accessor
84
- @queue_retry_delay
82
+ inherited_queue_setting(:@queue_retry_delay)
85
83
  end
86
84
  end
87
85
 
@@ -95,7 +93,7 @@ module Backburner
95
93
  if proc
96
94
  @queue_retry_delay_proc = proc
97
95
  else # accessor
98
- @queue_retry_delay_proc
96
+ inherited_queue_setting(:@queue_retry_delay_proc)
99
97
  end
100
98
  end
101
99
 
@@ -109,7 +107,7 @@ module Backburner
109
107
  if limit
110
108
  @queue_jobs_limit = limit
111
109
  else #accessor
112
- @queue_jobs_limit
110
+ inherited_queue_setting(:@queue_jobs_limit)
113
111
  end
114
112
  end
115
113
 
@@ -123,7 +121,7 @@ module Backburner
123
121
  if limit
124
122
  @queue_garbage_limit = limit
125
123
  else #accessor
126
- @queue_garbage_limit
124
+ inherited_queue_setting(:@queue_garbage_limit)
127
125
  end
128
126
  end
129
127
 
@@ -137,8 +135,28 @@ module Backburner
137
135
  if limit
138
136
  @queue_retry_limit = limit
139
137
  else #accessor
140
- @queue_retry_limit
138
+ inherited_queue_setting(:@queue_retry_limit)
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ # Registers subclasses as known queues too (they inherit the queue settings)
145
+ def inherited(subclass)
146
+ super
147
+ Backburner::Worker.known_queue_classes << subclass unless Backburner::Worker.known_queue_classes.include?(subclass)
148
+ end
149
+
150
+ # Returns the setting from this class or else from the closest superclass that has it,
151
+ # so a subclass shares its parent's queue settings unless it sets its own
152
+ def inherited_queue_setting(ivar)
153
+ klass = self
154
+ while klass.is_a?(ClassMethods)
155
+ value = klass.instance_variable_get(ivar) if klass.instance_variable_defined?(ivar)
156
+ return value unless value.nil?
157
+ klass = klass.respond_to?(:superclass) ? klass.superclass : nil
141
158
  end
159
+ nil
142
160
  end
143
161
  end # ClassMethods
144
162
  end # Queue
@@ -1,3 +1,3 @@
1
1
  module Backburner
2
- VERSION = "1.6.1"
2
+ VERSION = "1.8.0"
3
3
  end