async-background 0.6.2 → 0.7.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/lib/async/background/job.rb +47 -18
- data/lib/async/background/queue/client.rb +18 -12
- data/lib/async/background/queue/store.rb +77 -37
- data/lib/async/background/runner.rb +49 -36
- data/lib/async/background/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 851bdc350f52df868547200fe10a0d93b06b692b605c6e761b28262e2691a4df
|
|
4
|
+
data.tar.gz: 8eccc004e5250df5349135a979a59e52f7450ee356245e8cab17b4b6e2a9a9f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ce63c6e58d96670add9cbfc438ab5cb33d2472c3ef6d4484cde4682f3416749f946cadc1fe00333e1e6e68b1b861df8d6d9cb88a7e520f48f73ecc9b4c86265
|
|
7
|
+
data.tar.gz: e4f7fc4fb560dba97580f0bd1ea446ade22edb219e1fba7d725ecb4b798b2feddf76b5266f3e408516b33d880046c481ca7b0746ae08accd70a1665466143019
|
data/lib/async/background/job.rb
CHANGED
|
@@ -4,9 +4,48 @@ module Async
|
|
|
4
4
|
module Background
|
|
5
5
|
module Job
|
|
6
6
|
DEFAULT_TIMEOUT = 120
|
|
7
|
+
BACKOFFS = %i[fixed linear exponential].freeze
|
|
8
|
+
DEFAULT_JITTER_FOR = { fixed: 0.0, linear: 0.0, exponential: 0.5 }.freeze
|
|
7
9
|
|
|
8
|
-
Options = Data.define(:timeout) do
|
|
9
|
-
def initialize(timeout: DEFAULT_TIMEOUT
|
|
10
|
+
Options = Data.define(:timeout, :retry, :retry_delay, :backoff, :attempt, :jitter) do
|
|
11
|
+
def initialize(timeout: DEFAULT_TIMEOUT, retry: 0, retry_delay: nil, backoff: :fixed, attempt: nil, jitter: nil)
|
|
12
|
+
timeout = Integer(timeout)
|
|
13
|
+
retries = Integer(binding.local_variable_get(:retry) || 0)
|
|
14
|
+
retry_delay = Float(retry_delay) unless retry_delay.nil?
|
|
15
|
+
backoff = backoff.to_sym
|
|
16
|
+
attempt = Integer(attempt) unless attempt.nil?
|
|
17
|
+
jitter = Float(jitter) unless jitter.nil?
|
|
18
|
+
|
|
19
|
+
raise ArgumentError, "timeout must be > 0" unless timeout.positive?
|
|
20
|
+
raise ArgumentError, "retry must be >= 0" if retries.negative?
|
|
21
|
+
raise ArgumentError, "attempt must be >= 0" if attempt && attempt.negative?
|
|
22
|
+
raise ArgumentError, "backoff must be one of #{BACKOFFS.inspect}" unless BACKOFFS.include?(backoff)
|
|
23
|
+
if retries.positive?
|
|
24
|
+
raise ArgumentError, "retry_delay is required when retry > 0" if retry_delay.nil?
|
|
25
|
+
raise ArgumentError, "retry_delay must be > 0" unless retry_delay.positive?
|
|
26
|
+
end
|
|
27
|
+
raise ArgumentError, "jitter must be in [0, 1]" if jitter && !(0.0..1.0).cover?(jitter)
|
|
28
|
+
|
|
29
|
+
super(timeout:, retry: retries, retry_delay:, backoff:, attempt:, jitter:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def retry? = self.retry.positive?
|
|
33
|
+
def attempts_made = attempt || 0
|
|
34
|
+
def next_attempt = attempts_made + 1
|
|
35
|
+
def with_attempt(n) = with(attempt: Integer(n))
|
|
36
|
+
def next_retry_delay(for_attempt = next_attempt, rng: Random)
|
|
37
|
+
raise ArgumentError, "attempt must be > 0" unless for_attempt.positive?
|
|
38
|
+
raise ArgumentError, "retry_delay must be configured" if retry_delay.nil?
|
|
39
|
+
|
|
40
|
+
base = case backoff
|
|
41
|
+
when :fixed then retry_delay
|
|
42
|
+
when :linear then retry_delay * for_attempt
|
|
43
|
+
when :exponential then retry_delay * (2**(for_attempt - 1))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
factor = jitter || DEFAULT_JITTER_FOR[backoff]
|
|
47
|
+
factor.zero? ? base : base * (1 + rng.rand * factor)
|
|
48
|
+
end
|
|
10
49
|
end
|
|
11
50
|
|
|
12
51
|
def self.included(base)
|
|
@@ -14,30 +53,20 @@ module Async
|
|
|
14
53
|
end
|
|
15
54
|
|
|
16
55
|
module ClassMethods
|
|
17
|
-
def perform_now(*args)
|
|
18
|
-
new.perform(*args)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def perform_async(*args, options: {})
|
|
22
|
-
Async::Background::Queue.enqueue(self, *args, options: options)
|
|
23
|
-
end
|
|
56
|
+
def perform_now(*args) = new.perform(*args)
|
|
24
57
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def perform_at(time, *args, options: {})
|
|
30
|
-
Async::Background::Queue.enqueue_at(time, self, *args, options: options)
|
|
31
|
-
end
|
|
58
|
+
def perform_async(*args, options: {}) = Async::Background::Queue.enqueue(self, *args, options: options)
|
|
59
|
+
def perform_in(delay, *args, options: {}) = Async::Background::Queue.enqueue_in(delay, self, *args, options: options)
|
|
60
|
+
def perform_at(time, *args, options: {}) = Async::Background::Queue.enqueue_at(time, self, *args, options: options)
|
|
32
61
|
|
|
33
62
|
def options(**values)
|
|
34
|
-
@options = Options.new(**values).to_h
|
|
63
|
+
@options = Options.new(**values).to_h.compact
|
|
35
64
|
end
|
|
36
65
|
|
|
37
66
|
def resolve_options = @options || {}
|
|
38
67
|
end
|
|
39
68
|
|
|
40
|
-
def perform(*
|
|
69
|
+
def perform(*)
|
|
41
70
|
raise NotImplementedError, "#{self.class} must implement #perform"
|
|
42
71
|
end
|
|
43
72
|
end
|
|
@@ -22,8 +22,7 @@ module Async
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def push_in(delay, class_name, args = [], options: {})
|
|
25
|
-
|
|
26
|
-
push(class_name, args, run_at, options: options)
|
|
25
|
+
push(class_name, args, realtime_now + delay.to_f, options: options)
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
def push_at(time, class_name, args = [], options: {})
|
|
@@ -37,29 +36,36 @@ module Async
|
|
|
37
36
|
|
|
38
37
|
def enqueue(job_class, *args, options: {})
|
|
39
38
|
ensure_configured!
|
|
40
|
-
|
|
41
|
-
default_client.push(resolve_class_name(job_class), args, nil, options: merged)
|
|
39
|
+
default_client.push(resolve_class_name(job_class), args, nil, options: build_options(job_class, options))
|
|
42
40
|
end
|
|
43
41
|
|
|
44
42
|
def enqueue_in(delay, job_class, *args, options: {})
|
|
45
43
|
ensure_configured!
|
|
46
|
-
|
|
47
|
-
default_client.push_in(delay, resolve_class_name(job_class), args, options: merged)
|
|
44
|
+
default_client.push_in(delay, resolve_class_name(job_class), args, options: build_options(job_class, options))
|
|
48
45
|
end
|
|
49
46
|
|
|
50
47
|
def enqueue_at(time, job_class, *args, options: {})
|
|
51
48
|
ensure_configured!
|
|
52
|
-
|
|
53
|
-
default_client.push_at(time, resolve_class_name(job_class), args, options: merged)
|
|
49
|
+
default_client.push_at(time, resolve_class_name(job_class), args, options: build_options(job_class, options))
|
|
54
50
|
end
|
|
55
51
|
|
|
56
52
|
private
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
RETRY_KEYS = %i[retry retry_delay backoff jitter].freeze
|
|
55
|
+
private_constant :RETRY_KEYS
|
|
56
|
+
|
|
57
|
+
def build_options(job_class, call_site)
|
|
58
|
+
call_site ||= {}
|
|
59
|
+
merged = resolve_options(job_class).merge(call_site.compact)
|
|
60
|
+
apply_retry_overrides!(merged, call_site)
|
|
61
|
+
|
|
62
|
+
merged.empty? ? EMPTY_OPTIONS : Job::Options.new(**merged).to_h.compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def apply_retry_overrides!(merged, call_site)
|
|
66
|
+
return unless RETRY_KEYS.any? { |k| call_site.key?(k) && !call_site[k].nil? }
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
RETRY_KEYS.each { |k| merged[k] = call_site[k] if call_site.key?(k) }
|
|
63
69
|
end
|
|
64
70
|
|
|
65
71
|
def ensure_configured!
|
|
@@ -44,19 +44,17 @@ module Async
|
|
|
44
44
|
attr_reader :path
|
|
45
45
|
|
|
46
46
|
def initialize(path: self.class.default_path, mmap: true)
|
|
47
|
-
@path
|
|
48
|
-
@mmap
|
|
49
|
-
@
|
|
50
|
-
@
|
|
51
|
-
@schema_checked = false
|
|
47
|
+
@path = path
|
|
48
|
+
@pragma_sql = PRAGMAS.call(mmap ? MMAP_SIZE : 0).freeze
|
|
49
|
+
@db = nil
|
|
50
|
+
@schema_checked = false
|
|
52
51
|
@last_cleanup_at = nil
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
def ensure_database!
|
|
56
55
|
require_sqlite3
|
|
57
56
|
db = SQLite3::Database.new(@path)
|
|
58
|
-
db
|
|
59
|
-
db.execute_batch(@pragma_sql)
|
|
57
|
+
configure_database(db)
|
|
60
58
|
db.execute_batch(SCHEMA)
|
|
61
59
|
db.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
62
60
|
db.close
|
|
@@ -65,34 +63,22 @@ module Async
|
|
|
65
63
|
|
|
66
64
|
def enqueue(class_name, args = [], run_at = nil, options: {})
|
|
67
65
|
ensure_connection
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@enqueue_stmt.execute(class_name, JSON.generate(args), options_json, realtime_now, run_at)
|
|
66
|
+
now = realtime_now
|
|
67
|
+
@enqueue_stmt.execute(class_name, JSON.generate(args), dump_options(options), now, run_at || now)
|
|
71
68
|
@db.last_insert_row_id
|
|
72
69
|
end
|
|
73
70
|
|
|
74
71
|
def fetch(worker_id)
|
|
75
72
|
ensure_connection
|
|
76
73
|
now = realtime_now
|
|
77
|
-
@db.execute("BEGIN IMMEDIATE")
|
|
78
|
-
|
|
79
|
-
begin
|
|
80
|
-
results = @fetch_stmt.execute(worker_id, now, now)
|
|
81
|
-
row = results.first
|
|
82
|
-
ensure
|
|
83
|
-
@fetch_stmt.reset! rescue nil
|
|
84
|
-
end
|
|
85
74
|
|
|
86
|
-
@
|
|
75
|
+
row = transaction { with_stmt(@fetch_stmt) { |s| s.execute(worker_id, now, now).first } }
|
|
87
76
|
return unless row
|
|
88
77
|
|
|
89
78
|
maybe_cleanup
|
|
90
|
-
|
|
91
|
-
{ id: row[0], class_name: row[1], args: JSON.parse(row[2]), options: options }
|
|
92
|
-
rescue
|
|
93
|
-
@db.execute("ROLLBACK") rescue nil
|
|
94
|
-
raise
|
|
79
|
+
{ id: row[0], class_name: row[1], args: JSON.parse(row[2]), options: load_options(row[3]) }
|
|
95
80
|
end
|
|
81
|
+
|
|
96
82
|
def complete(job_id)
|
|
97
83
|
ensure_connection
|
|
98
84
|
@complete_stmt.execute(job_id)
|
|
@@ -103,6 +89,24 @@ module Async
|
|
|
103
89
|
@fail_stmt.execute(job_id)
|
|
104
90
|
end
|
|
105
91
|
|
|
92
|
+
def retry_or_fail(job_id, fallback_options: nil)
|
|
93
|
+
ensure_connection
|
|
94
|
+
|
|
95
|
+
transaction do
|
|
96
|
+
stored = with_stmt(@retry_state_stmt) { |s| load_options(s.execute(job_id).first&.first) }
|
|
97
|
+
policy = stored.empty? ? normalize_options(fallback_options) : Job::Options.new(**stored)
|
|
98
|
+
|
|
99
|
+
if policy&.retry? && policy.next_attempt <= policy.retry
|
|
100
|
+
advanced = policy.with_attempt(policy.next_attempt)
|
|
101
|
+
@retry_stmt.execute(realtime_now + advanced.next_retry_delay(advanced.attempt), dump_options(advanced.to_h.compact), job_id)
|
|
102
|
+
:retried
|
|
103
|
+
else
|
|
104
|
+
@fail_stmt.execute(job_id)
|
|
105
|
+
:failed
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
106
110
|
def recover(worker_id)
|
|
107
111
|
ensure_connection
|
|
108
112
|
@requeue_stmt.execute(worker_id)
|
|
@@ -138,8 +142,7 @@ module Async
|
|
|
138
142
|
require_sqlite3
|
|
139
143
|
finalize_statements
|
|
140
144
|
@db = SQLite3::Database.new(@path)
|
|
141
|
-
@db
|
|
142
|
-
@db.execute_batch(@pragma_sql)
|
|
145
|
+
configure_database(@db)
|
|
143
146
|
|
|
144
147
|
unless @schema_checked
|
|
145
148
|
@db.execute_batch(SCHEMA)
|
|
@@ -151,6 +154,42 @@ module Async
|
|
|
151
154
|
@last_cleanup_at = monotonic_now
|
|
152
155
|
end
|
|
153
156
|
|
|
157
|
+
def configure_database(db)
|
|
158
|
+
db.execute("PRAGMA busy_timeout = 5000")
|
|
159
|
+
db.execute_batch(@pragma_sql)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def transaction
|
|
163
|
+
@db.execute("BEGIN IMMEDIATE")
|
|
164
|
+
result = yield
|
|
165
|
+
@db.execute("COMMIT")
|
|
166
|
+
result
|
|
167
|
+
rescue
|
|
168
|
+
@db.execute("ROLLBACK") rescue nil
|
|
169
|
+
raise
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def with_stmt(stmt)
|
|
173
|
+
yield stmt
|
|
174
|
+
ensure
|
|
175
|
+
stmt.reset! rescue nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def dump_options(options)
|
|
179
|
+
options.empty? ? nil : JSON.generate(options)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def load_options(json)
|
|
183
|
+
json ? JSON.parse(json, symbolize_names: true) : {}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def normalize_options(options)
|
|
187
|
+
return if options.nil?
|
|
188
|
+
return options if options.is_a?(Job::Options)
|
|
189
|
+
|
|
190
|
+
Job::Options.new(**options)
|
|
191
|
+
end
|
|
192
|
+
|
|
154
193
|
def prepare_statements
|
|
155
194
|
@enqueue_stmt = @db.prepare(
|
|
156
195
|
"INSERT INTO jobs (class_name, args, options, created_at, run_at) VALUES (?, ?, ?, ?, ?)"
|
|
@@ -168,23 +207,27 @@ module Async
|
|
|
168
207
|
RETURNING id, class_name, args, options
|
|
169
208
|
SQL
|
|
170
209
|
|
|
171
|
-
@complete_stmt
|
|
172
|
-
@fail_stmt
|
|
173
|
-
|
|
210
|
+
@complete_stmt = @db.prepare("UPDATE jobs SET status = 'done', locked_by = NULL, locked_at = NULL WHERE id = ?")
|
|
211
|
+
@fail_stmt = @db.prepare("UPDATE jobs SET status = 'failed', locked_by = NULL, locked_at = NULL WHERE id = ?")
|
|
212
|
+
@retry_state_stmt = @db.prepare("SELECT options FROM jobs WHERE id = ?")
|
|
213
|
+
@retry_stmt = @db.prepare(
|
|
214
|
+
"UPDATE jobs SET status = 'pending', locked_by = NULL, locked_at = NULL, run_at = ?, options = ? WHERE id = ?"
|
|
215
|
+
)
|
|
174
216
|
@requeue_stmt = @db.prepare(
|
|
175
217
|
"UPDATE jobs SET status = 'pending', locked_by = NULL, locked_at = NULL " \
|
|
176
218
|
"WHERE status = 'running' AND locked_by = ?"
|
|
177
219
|
)
|
|
178
|
-
|
|
179
220
|
@cleanup_stmt = @db.prepare("DELETE FROM jobs WHERE status = 'done' AND created_at < ?")
|
|
180
221
|
end
|
|
181
222
|
|
|
182
223
|
def finalize_statements
|
|
183
|
-
[@enqueue_stmt, @fetch_stmt, @complete_stmt, @fail_stmt,
|
|
184
|
-
|
|
224
|
+
[@enqueue_stmt, @fetch_stmt, @complete_stmt, @fail_stmt,
|
|
225
|
+
@retry_state_stmt, @retry_stmt, @requeue_stmt, @cleanup_stmt].each do |stmt|
|
|
226
|
+
stmt&.close rescue next
|
|
185
227
|
end
|
|
186
228
|
|
|
187
|
-
@enqueue_stmt = @fetch_stmt = @complete_stmt = @fail_stmt =
|
|
229
|
+
@enqueue_stmt = @fetch_stmt = @complete_stmt = @fail_stmt = nil
|
|
230
|
+
@retry_state_stmt = @retry_stmt = @requeue_stmt = @cleanup_stmt = nil
|
|
188
231
|
end
|
|
189
232
|
|
|
190
233
|
def maybe_cleanup
|
|
@@ -193,10 +236,7 @@ module Async
|
|
|
193
236
|
|
|
194
237
|
@last_cleanup_at = now
|
|
195
238
|
@cleanup_stmt.execute(realtime_now - CLEANUP_AGE)
|
|
196
|
-
|
|
197
|
-
if @db.changes > 100
|
|
198
|
-
@db.execute("PRAGMA incremental_vacuum")
|
|
199
|
-
end
|
|
239
|
+
@db.execute("PRAGMA incremental_vacuum") if @db.changes > 100
|
|
200
240
|
end
|
|
201
241
|
end
|
|
202
242
|
end
|
|
@@ -69,14 +69,13 @@ module Async
|
|
|
69
69
|
@listen_queue = false
|
|
70
70
|
return unless queue_socket_dir
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
isolated = ENV.fetch("ISOLATION_FORKS", "").split(",").map(&:to_i)
|
|
73
|
+
return if isolated.include?(worker_index)
|
|
74
|
+
|
|
73
75
|
require_relative 'queue/store'
|
|
74
76
|
require_relative 'queue/socket_waker'
|
|
75
77
|
require_relative 'queue/client'
|
|
76
78
|
|
|
77
|
-
isolated = ENV.fetch("ISOLATION_FORKS", "").split(",").map(&:to_i)
|
|
78
|
-
return if isolated.include?(worker_index)
|
|
79
|
-
|
|
80
79
|
@listen_queue = true
|
|
81
80
|
@queue_store = Queue::Store.new(
|
|
82
81
|
path: queue_db_path || Queue::Store.default_path,
|
|
@@ -112,40 +111,55 @@ module Async
|
|
|
112
111
|
|
|
113
112
|
def run_queue_job(job_task, job)
|
|
114
113
|
class_name = job[:class_name]
|
|
115
|
-
args = job[:args]
|
|
116
114
|
klass = resolve_job_class(class_name)
|
|
117
|
-
|
|
118
|
-
timeout = opts.timeout
|
|
115
|
+
options = parse_job_options(job[:options])
|
|
119
116
|
|
|
120
117
|
metrics.job_started(nil)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
started = monotonic_now
|
|
119
|
+
job_task.with_timeout(options.timeout) { klass.perform_now(*job[:args]) }
|
|
120
|
+
duration = monotonic_now - started
|
|
124
121
|
|
|
125
|
-
duration = monotonic_now - t
|
|
126
122
|
metrics.job_finished(nil, duration)
|
|
127
123
|
@queue_store.complete(job[:id])
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
logger.info('Async::Background') { "queue(#{class_name}): completed in #{duration.round(2)}s" }
|
|
125
|
+
rescue ConfigError => e
|
|
126
|
+
metrics.job_failed(nil, e) if options
|
|
127
|
+
@queue_store.fail(job[:id])
|
|
128
|
+
logger.error('Async::Background') { "queue(#{class_name}): #{e.class} #{e.message}" }
|
|
132
129
|
rescue ::Async::TimeoutError
|
|
133
130
|
metrics.job_timed_out(nil)
|
|
134
|
-
|
|
135
|
-
logger.error('Async::Background') { "queue(#{class_name}): timed out after #{timeout}s" }
|
|
131
|
+
handle_queue_failure(job, options, "timed out after #{options.timeout}s", backtrace: nil)
|
|
136
132
|
rescue => e
|
|
137
133
|
metrics.job_failed(nil, e)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
handle_queue_failure(job, options, "#{e.class} #{e.message}", backtrace: e.backtrace)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_job_options(raw)
|
|
138
|
+
Job::Options.new(**(raw || {}))
|
|
139
|
+
rescue ArgumentError, TypeError => e
|
|
140
|
+
raise ConfigError, "invalid queue options: #{e.message}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def handle_queue_failure(job, options, message, backtrace:)
|
|
144
|
+
result = @queue_store.retry_or_fail(job[:id], fallback_options: options)
|
|
145
|
+
class_name = job[:class_name]
|
|
146
|
+
|
|
147
|
+
if result == :retried
|
|
148
|
+
@queue_waker&.signal
|
|
149
|
+
attempt = options.next_attempt
|
|
150
|
+
logger.warn('Async::Background') do
|
|
151
|
+
"queue(#{class_name}): #{message}; retry #{attempt}/#{options.retry}"
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
tail = backtrace ? "\n#{backtrace.join("\n")}" : ''
|
|
155
|
+
logger.error('Async::Background') { "queue(#{class_name}): #{message}#{tail}" }
|
|
156
|
+
end
|
|
142
157
|
end
|
|
143
158
|
|
|
144
159
|
def resolve_job_class(class_name)
|
|
145
|
-
raise ConfigError, "empty class name in queue job" if class_name.nil? || class_name.strip.empty?
|
|
160
|
+
raise ConfigError, "empty class name in queue job" if class_name.nil? || class_name.to_s.strip.empty?
|
|
146
161
|
|
|
147
|
-
|
|
148
|
-
klass = names.reduce(Object) do |mod, name|
|
|
162
|
+
klass = class_name.split("::").reduce(Object) do |mod, name|
|
|
149
163
|
raise ConfigError, "unknown class: #{class_name}" unless mod.const_defined?(name, false)
|
|
150
164
|
mod.const_get(name, false)
|
|
151
165
|
end
|
|
@@ -259,14 +273,12 @@ module Async
|
|
|
259
273
|
class_name = config&.dig('class').to_s.strip
|
|
260
274
|
raise ConfigError, "[#{name}] missing class" if class_name.empty?
|
|
261
275
|
|
|
262
|
-
begin
|
|
263
|
-
|
|
264
|
-
rescue
|
|
265
|
-
raise ConfigError, "[#{name}]
|
|
276
|
+
job_class = begin
|
|
277
|
+
resolve_job_class(class_name)
|
|
278
|
+
rescue ConfigError => e
|
|
279
|
+
raise ConfigError, "[#{name}] #{e.message}"
|
|
266
280
|
end
|
|
267
281
|
|
|
268
|
-
raise ConfigError, "[#{name}] #{class_name} must include Async::Background::Job" unless job_class.respond_to?(:perform_now)
|
|
269
|
-
|
|
270
282
|
interval = config['every']&.then { |v|
|
|
271
283
|
int = v.to_i
|
|
272
284
|
raise ConfigError, "[#{name}] 'every' must be > 0" unless int.positive?
|
|
@@ -279,12 +291,13 @@ module Async
|
|
|
279
291
|
|
|
280
292
|
raise ConfigError, "[#{name}] specify 'every' or 'cron'" unless interval || cron
|
|
281
293
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
294
|
+
timeout = begin
|
|
295
|
+
Job::Options.new(timeout: config.fetch('timeout', DEFAULT_TIMEOUT)).timeout
|
|
296
|
+
rescue ArgumentError, TypeError => e
|
|
297
|
+
raise ConfigError, "[#{name}] #{e.message}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
{ job_class: job_class, interval: interval, cron: cron, timeout: timeout }
|
|
288
301
|
end
|
|
289
302
|
|
|
290
303
|
def run_job(job_task, entry)
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-background
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Hajdarov
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-23 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: async
|
|
@@ -114,7 +113,6 @@ metadata:
|
|
|
114
113
|
source_code_uri: https://github.com/roman-haidarov/async-background
|
|
115
114
|
changelog_uri: https://github.com/roman-haidarov/async-background/blob/main/CHANGELOG.md
|
|
116
115
|
bug_tracker_uri: https://github.com/roman-haidarov/async-background/issues
|
|
117
|
-
post_install_message:
|
|
118
116
|
rdoc_options: []
|
|
119
117
|
require_paths:
|
|
120
118
|
- lib
|
|
@@ -129,8 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
129
127
|
- !ruby/object:Gem::Version
|
|
130
128
|
version: '0'
|
|
131
129
|
requirements: []
|
|
132
|
-
rubygems_version: 3.
|
|
133
|
-
signing_key:
|
|
130
|
+
rubygems_version: 3.6.2
|
|
134
131
|
specification_version: 4
|
|
135
132
|
summary: Lightweight heap-based cron/interval scheduler for Async.
|
|
136
133
|
test_files: []
|