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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +45 -0
- data/.github/workflows/github-release.yml +58 -0
- data/.github/workflows/release.yml +221 -0
- data/CHANGELOG.md +33 -0
- data/Gemfile +4 -1
- data/HOOKS.md +23 -14
- data/README.md +32 -9
- data/backburner.gemspec +6 -1
- data/bin/backburner +1 -1
- data/lib/active_job/queue_adapters/backburner_adapter.rb +41 -0
- data/lib/backburner/configuration.rb +7 -0
- data/lib/backburner/connection.rb +4 -2
- data/lib/backburner/enqueue_pool.rb +88 -0
- data/lib/backburner/helpers.rb +77 -22
- data/lib/backburner/job.rb +46 -7
- data/lib/backburner/logger.rb +9 -0
- data/lib/backburner/performable.rb +1 -1
- data/lib/backburner/queue.rb +37 -19
- data/lib/backburner/version.rb +1 -1
- data/lib/backburner/worker.rb +112 -21
- data/lib/backburner/workers/forking.rb +4 -2
- data/lib/backburner/workers/simple.rb +5 -0
- data/lib/backburner/workers/threading.rb +1 -7
- data/lib/backburner/workers/threads_on_fork.rb +58 -23
- data/lib/backburner.rb +7 -0
- data/test/active_job_adapter_test.rb +24 -0
- data/test/back_burner_test.rb +13 -0
- data/test/connection_test.rb +22 -0
- data/test/fixtures/active_job_app.rb +53 -0
- data/test/fixtures/active_jobs.rb +11 -0
- data/test/fixtures/autoload/test_broken_name_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_no_method_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_require_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_runtime_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_syntax_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_timeout_job.rb +4 -0
- data/test/fixtures/autoload/test_same_name_job.rb +5 -0
- data/test/fixtures/test_fork_jobs.rb +5 -0
- data/test/fixtures/test_jobs.rb +53 -0
- data/test/fixtures/test_queue_settings.rb +41 -1
- data/test/helpers/templogger.rb +1 -1
- data/test/helpers_test.rb +184 -0
- data/test/job_test.rb +22 -0
- data/test/logger_test.rb +27 -0
- data/test/performable_test.rb +50 -0
- data/test/queue_test.rb +63 -0
- data/test/test_helper.rb +59 -5
- data/test/worker_test.rb +346 -1
- data/test/workers/forking_worker_test.rb +19 -1
- data/test/workers/simple_worker_test.rb +272 -0
- data/test/workers/threading_worker_test.rb +22 -1
- data/test/workers/threads_on_fork_worker_test.rb +279 -12
- metadata +87 -8
- data/.travis.yml +0 -29
- 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
|
-
|
|
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
|
data/lib/backburner/helpers.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
resolve_priority_value(pri.queue_priority, seen_labels)
|
|
115
156
|
elsif pri.is_a?(String) || pri.is_a?(Symbol) # named priority
|
|
116
|
-
|
|
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
|
|
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
|
|
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?(
|
|
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
|
data/lib/backburner/job.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
53
|
-
#
|
|
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,
|
|
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
|
-
|
|
100
|
-
|
|
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
|
data/lib/backburner/logger.rb
CHANGED
|
@@ -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
|
data/lib/backburner/queue.rb
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
module Backburner
|
|
2
2
|
module Queue
|
|
3
3
|
def self.included(base)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/backburner/version.rb
CHANGED