async-background 1.1.0 → 1.1.1
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/CHANGELOG.md +14 -0
- data/lib/async/background/job.rb +75 -28
- data/lib/async/background/metrics.rb +41 -33
- data/lib/async/background/min_heap.rb +16 -12
- data/lib/async/background/queue/client.rb +11 -8
- data/lib/async/background/queue/options.rb +3 -3
- data/lib/async/background/queue/schema.rb +5 -1
- data/lib/async/background/queue/socket_notifier.rb +14 -4
- data/lib/async/background/queue/socket_waker.rb +27 -7
- data/lib/async/background/queue/sql.rb +31 -31
- data/lib/async/background/queue/store.rb +23 -7
- data/lib/async/background/runner/lifecycle.rb +131 -0
- data/lib/async/background/runner/queue_execution.rb +72 -52
- data/lib/async/background/runner/schedule.rb +4 -4
- data/lib/async/background/runner.rb +57 -126
- data/lib/async/background/runtime/notification.rb +2 -1
- data/lib/async/background/runtime/semaphore.rb +15 -11
- data/lib/async/background/runtime/task.rb +18 -17
- data/lib/async/background/runtime/task_group.rb +2 -2
- data/lib/async/background/runtime.rb +56 -53
- data/lib/async/background/scheduler.rb +4 -4
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +26 -19
- data/lib/async/background/web/assets/app.css +174 -0
- data/lib/async/background/web/assets/app.js +486 -0
- data/lib/async/background/web/assets/index.html +29 -0
- data/lib/async/background/web/assets.rb +4 -697
- data/lib/async/background/web/auth.rb +3 -3
- data/lib/async/background/web/configuration.rb +15 -15
- data/lib/async/background/web/cursor.rb +14 -7
- data/lib/async/background/web/metrics_reader.rb +4 -6
- data/lib/async/background/web/response.rb +1 -1
- data/lib/async/background/web/serializer.rb +2 -2
- data/lib/async/background/web/snapshot.rb +5 -5
- data/lib/async/background/web/sql.rb +7 -7
- data/lib/async/background/web/stream.rb +52 -32
- data/lib/async/background/web.rb +1 -1
- metadata +5 -1
|
@@ -4,17 +4,17 @@ module Async
|
|
|
4
4
|
module Background
|
|
5
5
|
module Queue
|
|
6
6
|
module SQL
|
|
7
|
-
USER_VERSION = 'PRAGMA user_version'
|
|
8
|
-
DATA_VERSION = 'PRAGMA data_version'
|
|
9
|
-
BUSY_TIMEOUT = 'PRAGMA busy_timeout'
|
|
10
|
-
TABLE_INFO = 'PRAGMA table_info(jobs)'
|
|
11
|
-
OPTIMIZE = 'PRAGMA optimize'
|
|
7
|
+
USER_VERSION = 'PRAGMA user_version'
|
|
8
|
+
DATA_VERSION = 'PRAGMA data_version'
|
|
9
|
+
BUSY_TIMEOUT = 'PRAGMA busy_timeout'
|
|
10
|
+
TABLE_INFO = 'PRAGMA table_info(jobs)'
|
|
11
|
+
OPTIMIZE = 'PRAGMA optimize'
|
|
12
12
|
INCREMENTAL_VACUUM_PAGES = 64
|
|
13
13
|
INCREMENTAL_VACUUM = "PRAGMA incremental_vacuum(#{INCREMENTAL_VACUUM_PAGES})".freeze
|
|
14
|
-
AUTO_VACUUM_INCREMENTAL = 'PRAGMA auto_vacuum = INCREMENTAL'
|
|
15
|
-
BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE'
|
|
16
|
-
COMMIT = 'COMMIT'
|
|
17
|
-
ROLLBACK = 'ROLLBACK'
|
|
14
|
+
AUTO_VACUUM_INCREMENTAL = 'PRAGMA auto_vacuum = INCREMENTAL'
|
|
15
|
+
BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE'
|
|
16
|
+
COMMIT = 'COMMIT'
|
|
17
|
+
ROLLBACK = 'ROLLBACK'
|
|
18
18
|
|
|
19
19
|
def self.busy_timeout(milliseconds)
|
|
20
20
|
"PRAGMA busy_timeout = #{Integer(milliseconds)}"
|
|
@@ -28,7 +28,7 @@ module Async
|
|
|
28
28
|
"ALTER TABLE jobs ADD COLUMN #{name} #{sql_type}"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
CREATE_SCHEMA = <<~SQL
|
|
31
|
+
CREATE_SCHEMA = <<~SQL
|
|
32
32
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
33
33
|
id INTEGER PRIMARY KEY,
|
|
34
34
|
class_name TEXT NOT NULL,
|
|
@@ -50,12 +50,12 @@ module Async
|
|
|
50
50
|
ON jobs(run_at) WHERE status = 'pending';
|
|
51
51
|
SQL
|
|
52
52
|
|
|
53
|
-
INSERT_JOB = <<~SQL
|
|
53
|
+
INSERT_JOB = <<~SQL
|
|
54
54
|
INSERT INTO jobs (class_name, args, options, created_at, run_at)
|
|
55
55
|
VALUES (?, ?, ?, ?, ?)
|
|
56
56
|
SQL
|
|
57
57
|
|
|
58
|
-
FETCH_NEXT_JOB = <<~SQL
|
|
58
|
+
FETCH_NEXT_JOB = <<~SQL
|
|
59
59
|
UPDATE jobs
|
|
60
60
|
SET status = 'running',
|
|
61
61
|
locked_by = ?,
|
|
@@ -73,13 +73,13 @@ module Async
|
|
|
73
73
|
RETURNING id, class_name, args, options
|
|
74
74
|
SQL
|
|
75
75
|
|
|
76
|
-
MARK_STARTED = <<~SQL
|
|
76
|
+
MARK_STARTED = <<~SQL
|
|
77
77
|
UPDATE jobs
|
|
78
78
|
SET started_at = ?
|
|
79
79
|
WHERE id = ? AND claim_token = ? AND status = 'running' AND started_at IS NULL
|
|
80
80
|
SQL
|
|
81
81
|
|
|
82
|
-
COMPLETE_JOB = <<~SQL
|
|
82
|
+
COMPLETE_JOB = <<~SQL
|
|
83
83
|
UPDATE jobs
|
|
84
84
|
SET status = 'done',
|
|
85
85
|
locked_by = NULL,
|
|
@@ -89,7 +89,7 @@ module Async
|
|
|
89
89
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
90
90
|
SQL
|
|
91
91
|
|
|
92
|
-
FAIL_JOB = <<~SQL
|
|
92
|
+
FAIL_JOB = <<~SQL
|
|
93
93
|
UPDATE jobs
|
|
94
94
|
SET status = 'failed',
|
|
95
95
|
locked_by = NULL,
|
|
@@ -101,19 +101,19 @@ module Async
|
|
|
101
101
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
102
102
|
SQL
|
|
103
103
|
|
|
104
|
-
RETRY_STATE = <<~SQL
|
|
104
|
+
RETRY_STATE = <<~SQL
|
|
105
105
|
SELECT options
|
|
106
106
|
FROM jobs
|
|
107
107
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
108
108
|
SQL
|
|
109
109
|
|
|
110
|
-
LEASE_ALIVE = <<~SQL
|
|
110
|
+
LEASE_ALIVE = <<~SQL
|
|
111
111
|
SELECT 1
|
|
112
112
|
FROM jobs
|
|
113
113
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
114
114
|
SQL
|
|
115
115
|
|
|
116
|
-
RETRY_JOB = <<~SQL
|
|
116
|
+
RETRY_JOB = <<~SQL
|
|
117
117
|
UPDATE jobs
|
|
118
118
|
SET status = 'pending',
|
|
119
119
|
locked_by = NULL,
|
|
@@ -129,7 +129,7 @@ module Async
|
|
|
129
129
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
130
130
|
SQL
|
|
131
131
|
|
|
132
|
-
RECOVER_WORKER = <<~SQL
|
|
132
|
+
RECOVER_WORKER = <<~SQL
|
|
133
133
|
UPDATE jobs
|
|
134
134
|
SET status = 'pending',
|
|
135
135
|
locked_by = NULL,
|
|
@@ -139,67 +139,67 @@ module Async
|
|
|
139
139
|
WHERE status = 'running' AND locked_by = ?
|
|
140
140
|
SQL
|
|
141
141
|
|
|
142
|
-
BACKFILL_FINISHED_AT = <<~SQL
|
|
142
|
+
BACKFILL_FINISHED_AT = <<~SQL
|
|
143
143
|
UPDATE jobs
|
|
144
144
|
SET finished_at = created_at
|
|
145
145
|
WHERE finished_at IS NULL AND status IN ('done', 'failed')
|
|
146
146
|
SQL
|
|
147
147
|
|
|
148
|
-
CLEANUP_DONE = <<~SQL
|
|
148
|
+
CLEANUP_DONE = <<~SQL
|
|
149
149
|
DELETE FROM jobs
|
|
150
150
|
WHERE status = 'done' AND finished_at IS NOT NULL AND finished_at < ?
|
|
151
151
|
SQL
|
|
152
152
|
|
|
153
|
-
CLEANUP_FAILED = <<~SQL
|
|
153
|
+
CLEANUP_FAILED = <<~SQL
|
|
154
154
|
DELETE FROM jobs
|
|
155
155
|
WHERE status = 'failed' AND finished_at IS NOT NULL AND finished_at < ?
|
|
156
156
|
SQL
|
|
157
157
|
|
|
158
|
-
NEXT_PENDING_RUN_AT = <<~SQL
|
|
158
|
+
NEXT_PENDING_RUN_AT = <<~SQL
|
|
159
159
|
SELECT MIN(run_at)
|
|
160
160
|
FROM jobs
|
|
161
161
|
WHERE status = 'pending'
|
|
162
162
|
SQL
|
|
163
163
|
|
|
164
|
-
JOBS_TABLE_EXISTS = <<~SQL
|
|
164
|
+
JOBS_TABLE_EXISTS = <<~SQL
|
|
165
165
|
SELECT 1
|
|
166
166
|
FROM sqlite_master
|
|
167
167
|
WHERE type = 'table' AND name = 'jobs'
|
|
168
168
|
LIMIT 1
|
|
169
169
|
SQL
|
|
170
170
|
|
|
171
|
-
JOB_INDEX_NAMES = <<~SQL
|
|
171
|
+
JOB_INDEX_NAMES = <<~SQL
|
|
172
172
|
SELECT name
|
|
173
173
|
FROM sqlite_master
|
|
174
174
|
WHERE type = 'index' AND tbl_name = 'jobs'
|
|
175
175
|
SQL
|
|
176
176
|
|
|
177
|
-
DROP_LEGACY_PENDING_INDEX = 'DROP INDEX IF EXISTS idx_jobs_status_run_at_id'
|
|
177
|
+
DROP_LEGACY_PENDING_INDEX = 'DROP INDEX IF EXISTS idx_jobs_status_run_at_id'
|
|
178
178
|
|
|
179
|
-
CREATE_PENDING_INDEX = <<~SQL
|
|
179
|
+
CREATE_PENDING_INDEX = <<~SQL
|
|
180
180
|
CREATE INDEX IF NOT EXISTS idx_jobs_pending
|
|
181
181
|
ON jobs(run_at) WHERE status = 'pending'
|
|
182
182
|
SQL
|
|
183
183
|
|
|
184
|
-
CREATE_DONE_INDEX = <<~SQL
|
|
184
|
+
CREATE_DONE_INDEX = <<~SQL
|
|
185
185
|
CREATE INDEX IF NOT EXISTS idx_jobs_done_finished_at
|
|
186
186
|
ON jobs(finished_at DESC, id DESC)
|
|
187
187
|
WHERE status = 'done'
|
|
188
188
|
SQL
|
|
189
189
|
|
|
190
|
-
CREATE_FAILED_INDEX = <<~SQL
|
|
190
|
+
CREATE_FAILED_INDEX = <<~SQL
|
|
191
191
|
CREATE INDEX IF NOT EXISTS idx_jobs_failed_finished_at
|
|
192
192
|
ON jobs(finished_at DESC, id DESC)
|
|
193
193
|
WHERE status = 'failed'
|
|
194
194
|
SQL
|
|
195
195
|
|
|
196
|
-
CREATE_EXECUTING_INDEX = <<~SQL
|
|
196
|
+
CREATE_EXECUTING_INDEX = <<~SQL
|
|
197
197
|
CREATE INDEX IF NOT EXISTS idx_jobs_executing_started_at
|
|
198
198
|
ON jobs(started_at)
|
|
199
199
|
WHERE status = 'running' AND started_at IS NOT NULL
|
|
200
200
|
SQL
|
|
201
201
|
|
|
202
|
-
CREATE_CLAIMED_INDEX = <<~SQL
|
|
202
|
+
CREATE_CLAIMED_INDEX = <<~SQL
|
|
203
203
|
CREATE INDEX IF NOT EXISTS idx_jobs_claimed_locked_at
|
|
204
204
|
ON jobs(locked_at)
|
|
205
205
|
WHERE status = 'running' AND started_at IS NULL
|
|
@@ -24,7 +24,7 @@ module Async
|
|
|
24
24
|
FAILED_RETENTION_AGE = 7 * 24 * 3600
|
|
25
25
|
CLEANUP_VACUUM_THRESHOLD = 100
|
|
26
26
|
ERROR_MESSAGE_MAX_LEN = 2_000
|
|
27
|
-
EMPTY_ARGS_JSON = '[]'
|
|
27
|
+
EMPTY_ARGS_JSON = '[]'
|
|
28
28
|
|
|
29
29
|
attr_reader :path, :options
|
|
30
30
|
|
|
@@ -131,7 +131,8 @@ module Async
|
|
|
131
131
|
@db.changes.positive?
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
def fail(job_id, claim_token:, error_class: nil, error_message: nil, finished_at: realtime_now,
|
|
134
|
+
def fail(job_id, claim_token:, error_class: nil, error_message: nil, finished_at: realtime_now,
|
|
135
|
+
duration_ms: nil)
|
|
135
136
|
ensure_connection
|
|
136
137
|
bind_failure(@fail_stmt, finished_at, duration_ms, error_class, error_message, job_id, claim_token)
|
|
137
138
|
@db.changes.positive?
|
|
@@ -153,8 +154,11 @@ module Async
|
|
|
153
154
|
next unless lease_alive?(job_id, claim_token)
|
|
154
155
|
|
|
155
156
|
policy = retry_policy(stored_options, fallback_options)
|
|
156
|
-
policy_retries?(policy)
|
|
157
|
+
if policy_retries?(policy)
|
|
158
|
+
retry_job!(job_id, claim_token, policy, error_class, error_message)
|
|
159
|
+
else
|
|
157
160
|
fail_job!(job_id, claim_token, error_class, error_message, finished_at, duration_ms)
|
|
161
|
+
end
|
|
158
162
|
end
|
|
159
163
|
end
|
|
160
164
|
|
|
@@ -178,7 +182,11 @@ module Async
|
|
|
178
182
|
return unless connected?
|
|
179
183
|
|
|
180
184
|
finalize_statements
|
|
181
|
-
|
|
185
|
+
begin
|
|
186
|
+
@db.execute(SQL::OPTIMIZE)
|
|
187
|
+
rescue StandardError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
182
190
|
@db.close
|
|
183
191
|
@db = nil
|
|
184
192
|
@schema_checked = false
|
|
@@ -211,7 +219,7 @@ module Async
|
|
|
211
219
|
require 'sqlite3'
|
|
212
220
|
rescue LoadError
|
|
213
221
|
raise LoadError,
|
|
214
|
-
|
|
222
|
+
'sqlite3 gem is required for Async::Background::Queue. ' \
|
|
215
223
|
"Add `gem 'sqlite3', '~> 2.0'` to your Gemfile."
|
|
216
224
|
end
|
|
217
225
|
|
|
@@ -338,7 +346,11 @@ module Async
|
|
|
338
346
|
yield statement if block_given?
|
|
339
347
|
statement.step
|
|
340
348
|
ensure
|
|
341
|
-
|
|
349
|
+
begin
|
|
350
|
+
statement.reset!
|
|
351
|
+
rescue StandardError
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
342
354
|
end
|
|
343
355
|
|
|
344
356
|
def dump_args(args)
|
|
@@ -383,7 +395,11 @@ module Async
|
|
|
383
395
|
end
|
|
384
396
|
|
|
385
397
|
def finalize_statements
|
|
386
|
-
statements.each
|
|
398
|
+
statements.each do |statement|
|
|
399
|
+
statement&.close
|
|
400
|
+
rescue StandardError
|
|
401
|
+
nil
|
|
402
|
+
end
|
|
387
403
|
clear_statements
|
|
388
404
|
end
|
|
389
405
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
class Runner
|
|
6
|
+
module Lifecycle
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def setup_signal_handlers
|
|
10
|
+
@signal_r, @signal_w = IO.pipe
|
|
11
|
+
|
|
12
|
+
%w[INT TERM].each do |signal|
|
|
13
|
+
Signal.trap(signal) { stop_from_signal }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stop_from_signal
|
|
18
|
+
@running = false
|
|
19
|
+
@signal_w.write_nonblock('.')
|
|
20
|
+
rescue StandardError
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def start_signal_watcher
|
|
25
|
+
@services.spawn(name: 'signal-watcher') { watch_signals }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def watch_signals
|
|
29
|
+
while running?
|
|
30
|
+
result = read_signal
|
|
31
|
+
next if result == :wait
|
|
32
|
+
break unless result
|
|
33
|
+
|
|
34
|
+
logger.info { 'Async::Background: stopping gracefully' } unless running?
|
|
35
|
+
shutdown.signal_all
|
|
36
|
+
@queue_waker&.signal
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def read_signal
|
|
41
|
+
@signal_r.wait_readable
|
|
42
|
+
@signal_r.read_nonblock(256)
|
|
43
|
+
rescue IO::WaitReadable
|
|
44
|
+
:wait
|
|
45
|
+
rescue IOError, Errno::EBADF
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def error_handler
|
|
50
|
+
@error_handler ||= build_error_handler
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_error_handler
|
|
54
|
+
log = logger
|
|
55
|
+
lambda do |task, error|
|
|
56
|
+
log.error('Async::Background') do
|
|
57
|
+
"task #{task&.name || 'unnamed'} died: #{error.class} #{error.message}\n" \
|
|
58
|
+
"#{Array(error.backtrace).join("\n")}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def warn_unsafe_timeouts
|
|
64
|
+
return if Runtime.native_timeouts?
|
|
65
|
+
|
|
66
|
+
logger.warn('Async::Background') do
|
|
67
|
+
"#{Fiber.scheduler.class} does not implement #timeout_after; job timeouts " \
|
|
68
|
+
'fall back to stdlib Timeout and may interrupt an unrelated fiber'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def wake_signal_watcher
|
|
73
|
+
@signal_w&.write_nonblock('.')
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def shutdown_gracefully
|
|
79
|
+
wake_services
|
|
80
|
+
drain_jobs
|
|
81
|
+
stop_services
|
|
82
|
+
close_queue
|
|
83
|
+
close_signal_pipe
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def wake_services
|
|
87
|
+
shutdown.signal_all
|
|
88
|
+
wake_signal_watcher
|
|
89
|
+
@queue_waker&.signal
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def drain_jobs
|
|
93
|
+
return if @jobs.empty?
|
|
94
|
+
|
|
95
|
+
logger.info { "Async::Background: draining #{@jobs.size} in-flight job(s)" }
|
|
96
|
+
@jobs.wait(@drain_timeout)
|
|
97
|
+
rescue Runtime::TimeoutError
|
|
98
|
+
cancel_draining_jobs
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def cancel_draining_jobs
|
|
102
|
+
logger.warn('Async::Background') do
|
|
103
|
+
"#{@jobs.size} job(s) still running after #{@drain_timeout}s, cancelling"
|
|
104
|
+
end
|
|
105
|
+
@jobs.stop_all(JOB_CANCEL_GRACE)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def stop_services
|
|
109
|
+
return if @services.empty?
|
|
110
|
+
|
|
111
|
+
@services.wait(SERVICE_SHUTDOWN_GRACE)
|
|
112
|
+
rescue Runtime::TimeoutError
|
|
113
|
+
logger.warn('Async::Background') { 'service tasks did not exit in time, cancelling' }
|
|
114
|
+
@services.stop_all(SERVICE_CANCEL_GRACE)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def close_queue
|
|
118
|
+
@queue_waker&.close
|
|
119
|
+
@queue_store&.close
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def close_signal_pipe
|
|
123
|
+
@signal_r&.close
|
|
124
|
+
@signal_w&.close
|
|
125
|
+
rescue IOError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -6,11 +6,15 @@ module Async
|
|
|
6
6
|
# Queue-only lifecycle. Keeping it separate from recurring scheduling makes
|
|
7
7
|
# the different delivery guarantees visible without adding runtime objects.
|
|
8
8
|
module QueueExecution
|
|
9
|
+
QueueRun = Data.define(:job, :class_name, :claim_token, :options, :started_at, :job_class)
|
|
10
|
+
QueueFailure = Data.define(:job, :options, :message, :error, :duration, :timeout, :backtrace)
|
|
11
|
+
private_constant :QueueRun, :QueueFailure
|
|
12
|
+
|
|
9
13
|
private
|
|
10
14
|
|
|
11
15
|
def setup_queue(queue_socket_dir, queue_db_path, queue_mmap)
|
|
12
16
|
@queue_saturated = false
|
|
13
|
-
@listen_queue =
|
|
17
|
+
@listen_queue = !queue_socket_dir.nil? && !isolated_worker?
|
|
14
18
|
return unless @listen_queue
|
|
15
19
|
|
|
16
20
|
require_relative '../queue/client'
|
|
@@ -19,7 +23,7 @@ module Async
|
|
|
19
23
|
|
|
20
24
|
@queue_store = Queue::Store.new(
|
|
21
25
|
path: queue_db_path || Queue::Store.default_path,
|
|
22
|
-
options: {mmap: queue_mmap}
|
|
26
|
+
options: { mmap: queue_mmap }
|
|
23
27
|
)
|
|
24
28
|
|
|
25
29
|
@queue_waker = Queue::SocketWaker.new(queue_socket_path(queue_socket_dir))
|
|
@@ -42,11 +46,11 @@ module Async
|
|
|
42
46
|
|
|
43
47
|
dispatch_available_queue_jobs
|
|
44
48
|
failures = 0
|
|
45
|
-
rescue StandardError =>
|
|
49
|
+
rescue StandardError => e
|
|
46
50
|
failures += 1
|
|
47
51
|
backoff = [QUEUE_ERROR_BACKOFF * failures, QUEUE_POLL_INTERVAL].min
|
|
48
52
|
logger.error('Async::Background') do
|
|
49
|
-
"queue listener: #{
|
|
53
|
+
"queue listener: #{e.class} #{e.message}; retrying in #{backoff}s"
|
|
50
54
|
end
|
|
51
55
|
shutdown.wait(backoff)
|
|
52
56
|
end
|
|
@@ -67,49 +71,56 @@ module Async
|
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def run_queue_job(job_task, job)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
options = nil
|
|
73
|
-
started_at = nil
|
|
74
|
+
context = QueueRun.new(job, job[:class_name], job[:claim_token], nil, nil, nil)
|
|
75
|
+
context = resolve_queue_run(context)
|
|
74
76
|
metrics_started = false
|
|
77
|
+
metrics_started = start_queue_metrics(context)
|
|
78
|
+
return unless metrics_started
|
|
79
|
+
|
|
80
|
+
context = context.with(started_at: monotonic_now)
|
|
81
|
+
perform_queue_run(job_task, context)
|
|
82
|
+
complete_queue_job!(context)
|
|
83
|
+
rescue ConfigError => e
|
|
84
|
+
record_invalid_queue_job!(context, e)
|
|
85
|
+
rescue Runtime::TimeoutError => e
|
|
86
|
+
handle_queue_failure(queue_failure(context, e, timeout: true))
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
handle_queue_failure(queue_failure(context, e, timeout: false))
|
|
89
|
+
ensure
|
|
90
|
+
metrics.job_stopped(nil) if metrics_started
|
|
91
|
+
end
|
|
75
92
|
|
|
76
|
-
|
|
93
|
+
def resolve_queue_run(context)
|
|
94
|
+
job, class_name, = context.deconstruct
|
|
95
|
+
job_class = resolve_job_class(class_name)
|
|
77
96
|
options = parse_job_options(job[:options])
|
|
78
|
-
|
|
97
|
+
context.with(options: options, job_class: job_class)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def start_queue_metrics(context)
|
|
101
|
+
job, class_name, claim_token, = context.deconstruct
|
|
102
|
+
return false unless start_queue_job!(job, class_name, claim_token)
|
|
79
103
|
|
|
80
104
|
metrics.job_started(nil)
|
|
81
|
-
|
|
82
|
-
started_at = monotonic_now
|
|
83
|
-
job_task.with_timeout(options.timeout) { klass.perform_now(*job[:args]) }
|
|
84
|
-
|
|
85
|
-
complete_queue_job!(job, class_name, claim_token, started_at)
|
|
86
|
-
rescue ConfigError => error
|
|
87
|
-
record_invalid_queue_job!(job, class_name, claim_token, error)
|
|
88
|
-
rescue Runtime::TimeoutError => error
|
|
89
|
-
handle_queue_failure(
|
|
90
|
-
job,
|
|
91
|
-
options,
|
|
92
|
-
"timed out after #{options&.timeout}s",
|
|
93
|
-
error: error,
|
|
94
|
-
duration: started_at && (monotonic_now - started_at),
|
|
95
|
-
timeout: true,
|
|
96
|
-
backtrace: nil
|
|
97
|
-
)
|
|
98
|
-
rescue StandardError => error
|
|
99
|
-
handle_queue_failure(
|
|
100
|
-
job,
|
|
101
|
-
options,
|
|
102
|
-
"#{error.class} #{error.message}",
|
|
103
|
-
error: error,
|
|
104
|
-
duration: started_at && (monotonic_now - started_at),
|
|
105
|
-
timeout: false,
|
|
106
|
-
backtrace: error.backtrace
|
|
107
|
-
)
|
|
108
|
-
ensure
|
|
109
|
-
metrics.job_stopped(nil) if metrics_started
|
|
105
|
+
true
|
|
110
106
|
end
|
|
111
107
|
|
|
112
|
-
def
|
|
108
|
+
def perform_queue_run(job_task, context)
|
|
109
|
+
job, _class_name, _claim_token, options, _started_at, job_class = context.deconstruct
|
|
110
|
+
job_task.with_timeout(options.timeout) do
|
|
111
|
+
job_class.perform_now(*job[:args])
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def queue_failure(context, error, timeout:)
|
|
116
|
+
job, _class_name, _claim_token, options, started_at = context.deconstruct
|
|
117
|
+
message = timeout ? "timed out after #{options&.timeout}s" : "#{error.class} #{error.message}"
|
|
118
|
+
duration = started_at && (monotonic_now - started_at)
|
|
119
|
+
QueueFailure.new(job, options, message, error, duration, timeout, timeout ? nil : error.backtrace)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def handle_queue_failure(failure)
|
|
123
|
+
job, options, message, error, duration, timeout, backtrace = failure.deconstruct
|
|
113
124
|
class_name = job[:class_name]
|
|
114
125
|
result = @queue_store.retry_or_fail(
|
|
115
126
|
job[:id],
|
|
@@ -122,14 +133,17 @@ module Async
|
|
|
122
133
|
return log_stale_queue_failure(job, class_name, timeout) unless result
|
|
123
134
|
|
|
124
135
|
timeout ? metrics.job_timed_out(nil) : metrics.job_failed(nil, error)
|
|
125
|
-
result == :retried
|
|
136
|
+
if result == :retried
|
|
137
|
+
log_queue_retry(job, class_name, message, options)
|
|
138
|
+
else
|
|
126
139
|
log_queue_failure(class_name, message, backtrace)
|
|
140
|
+
end
|
|
127
141
|
end
|
|
128
142
|
|
|
129
143
|
def parse_job_options(raw)
|
|
130
144
|
Job::Options.new(**(raw || {}))
|
|
131
|
-
rescue ArgumentError, TypeError =>
|
|
132
|
-
raise ConfigError, "invalid queue options: #{
|
|
145
|
+
rescue ArgumentError, TypeError => e
|
|
146
|
+
raise ConfigError, "invalid queue options: #{e.message}"
|
|
133
147
|
end
|
|
134
148
|
|
|
135
149
|
def isolated_worker?
|
|
@@ -164,25 +178,29 @@ module Async
|
|
|
164
178
|
def start_queue_job!(job, class_name, claim_token)
|
|
165
179
|
return true if @queue_store.mark_started!(job[:id], claim_token: claim_token)
|
|
166
180
|
|
|
167
|
-
logger.warn('Async::Background')
|
|
181
|
+
logger.warn('Async::Background') do
|
|
168
182
|
"queue(#{class_name}): lost lease before start for job #{job[:id]}, ignored"
|
|
169
|
-
|
|
183
|
+
end
|
|
170
184
|
false
|
|
171
185
|
end
|
|
172
186
|
|
|
173
|
-
def complete_queue_job!(
|
|
187
|
+
def complete_queue_job!(context)
|
|
188
|
+
job, class_name, claim_token, _options, started_at = context.deconstruct
|
|
174
189
|
duration = monotonic_now - started_at
|
|
175
190
|
if @queue_store.complete(job[:id], claim_token: claim_token, duration_ms: duration_ms(duration))
|
|
176
191
|
metrics.job_succeeded(nil, duration)
|
|
177
|
-
logger.info('Async::Background')
|
|
192
|
+
logger.info('Async::Background') do
|
|
193
|
+
"queue(#{class_name}): completed in #{duration.round(2)}s"
|
|
194
|
+
end
|
|
178
195
|
else
|
|
179
|
-
logger.warn('Async::Background')
|
|
196
|
+
logger.warn('Async::Background') do
|
|
180
197
|
"queue(#{class_name}): complete: stale lease for job #{job[:id]}, ignored"
|
|
181
|
-
|
|
198
|
+
end
|
|
182
199
|
end
|
|
183
200
|
end
|
|
184
201
|
|
|
185
|
-
def record_invalid_queue_job!(
|
|
202
|
+
def record_invalid_queue_job!(context, error)
|
|
203
|
+
job, class_name, claim_token, = context.deconstruct
|
|
186
204
|
recorded = @queue_store.fail(
|
|
187
205
|
job[:id],
|
|
188
206
|
claim_token: claim_token,
|
|
@@ -190,7 +208,9 @@ module Async
|
|
|
190
208
|
error_message: error.message
|
|
191
209
|
)
|
|
192
210
|
metrics.job_failed(nil, error) if recorded
|
|
193
|
-
logger.error('Async::Background')
|
|
211
|
+
logger.error('Async::Background') do
|
|
212
|
+
"queue(#{class_name}): #{error.class} #{error.message}"
|
|
213
|
+
end
|
|
194
214
|
end
|
|
195
215
|
|
|
196
216
|
def log_stale_queue_failure(job, class_name, timeout)
|
|
@@ -200,7 +220,7 @@ module Async
|
|
|
200
220
|
end
|
|
201
221
|
end
|
|
202
222
|
|
|
203
|
-
def log_queue_retry(
|
|
223
|
+
def log_queue_retry(_job, class_name, message, options)
|
|
204
224
|
@queue_waker&.signal
|
|
205
225
|
logger.warn('Async::Background') do
|
|
206
226
|
"queue(#{class_name}): #{message}; retry #{options&.next_attempt}/#{options&.retry}"
|
|
@@ -96,8 +96,8 @@ module Async
|
|
|
96
96
|
|
|
97
97
|
def resolve_scheduled_job(name, class_name)
|
|
98
98
|
resolve_job_class(class_name)
|
|
99
|
-
rescue ConfigError =>
|
|
100
|
-
raise ConfigError, "[#{name}] #{
|
|
99
|
+
rescue ConfigError => e
|
|
100
|
+
raise ConfigError, "[#{name}] #{e.message}"
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def parse_interval(name, value)
|
|
@@ -117,8 +117,8 @@ module Async
|
|
|
117
117
|
|
|
118
118
|
def parse_timeout(name, config)
|
|
119
119
|
Job::Options.new(timeout: config.fetch('timeout', DEFAULT_TIMEOUT)).timeout
|
|
120
|
-
rescue ArgumentError, TypeError =>
|
|
121
|
-
raise ConfigError, "[#{name}] #{
|
|
120
|
+
rescue ArgumentError, TypeError => e
|
|
121
|
+
raise ConfigError, "[#{name}] #{e.message}"
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
def validate_schedule_frequency!(name, interval, cron)
|