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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/lib/async/background/job.rb +75 -28
  4. data/lib/async/background/metrics.rb +41 -33
  5. data/lib/async/background/min_heap.rb +16 -12
  6. data/lib/async/background/queue/client.rb +11 -8
  7. data/lib/async/background/queue/options.rb +3 -3
  8. data/lib/async/background/queue/schema.rb +5 -1
  9. data/lib/async/background/queue/socket_notifier.rb +14 -4
  10. data/lib/async/background/queue/socket_waker.rb +27 -7
  11. data/lib/async/background/queue/sql.rb +31 -31
  12. data/lib/async/background/queue/store.rb +23 -7
  13. data/lib/async/background/runner/lifecycle.rb +131 -0
  14. data/lib/async/background/runner/queue_execution.rb +72 -52
  15. data/lib/async/background/runner/schedule.rb +4 -4
  16. data/lib/async/background/runner.rb +57 -126
  17. data/lib/async/background/runtime/notification.rb +2 -1
  18. data/lib/async/background/runtime/semaphore.rb +15 -11
  19. data/lib/async/background/runtime/task.rb +18 -17
  20. data/lib/async/background/runtime/task_group.rb +2 -2
  21. data/lib/async/background/runtime.rb +56 -53
  22. data/lib/async/background/scheduler.rb +4 -4
  23. data/lib/async/background/version.rb +1 -1
  24. data/lib/async/background/web/app.rb +26 -19
  25. data/lib/async/background/web/assets/app.css +174 -0
  26. data/lib/async/background/web/assets/app.js +486 -0
  27. data/lib/async/background/web/assets/index.html +29 -0
  28. data/lib/async/background/web/assets.rb +4 -697
  29. data/lib/async/background/web/auth.rb +3 -3
  30. data/lib/async/background/web/configuration.rb +15 -15
  31. data/lib/async/background/web/cursor.rb +14 -7
  32. data/lib/async/background/web/metrics_reader.rb +4 -6
  33. data/lib/async/background/web/response.rb +1 -1
  34. data/lib/async/background/web/serializer.rb +2 -2
  35. data/lib/async/background/web/snapshot.rb +5 -5
  36. data/lib/async/background/web/sql.rb +7 -7
  37. data/lib/async/background/web/stream.rb +52 -32
  38. data/lib/async/background/web.rb +1 -1
  39. 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'.freeze
8
- DATA_VERSION = 'PRAGMA data_version'.freeze
9
- BUSY_TIMEOUT = 'PRAGMA busy_timeout'.freeze
10
- TABLE_INFO = 'PRAGMA table_info(jobs)'.freeze
11
- OPTIMIZE = 'PRAGMA optimize'.freeze
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'.freeze
15
- BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE'.freeze
16
- COMMIT = 'COMMIT'.freeze
17
- ROLLBACK = 'ROLLBACK'.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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'.freeze
177
+ DROP_LEGACY_PENDING_INDEX = 'DROP INDEX IF EXISTS idx_jobs_status_run_at_id'
178
178
 
179
- CREATE_PENDING_INDEX = <<~SQL.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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 = '[]'.freeze
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, duration_ms: nil)
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) ? retry_job!(job_id, claim_token, policy, error_class, error_message) :
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
- @db.execute(SQL::OPTIMIZE) rescue nil
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
- "sqlite3 gem is required for Async::Background::Queue. " \
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
- statement.reset! rescue nil
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 { |statement| statement&.close rescue nil }
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 = !!queue_socket_dir && !isolated_worker?
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 => error
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: #{error.class} #{error.message}; retrying in #{backoff}s"
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
- class_name = job[:class_name]
71
- claim_token = job[:claim_token]
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
- klass = resolve_job_class(class_name)
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
- return unless start_queue_job!(job, class_name, claim_token)
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
- metrics_started = true
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 handle_queue_failure(job, options, message, error:, duration:, timeout:, backtrace:)
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 ? log_queue_retry(job, class_name, message, options) :
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 => error
132
- raise ConfigError, "invalid queue options: #{error.message}"
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!(job, class_name, claim_token, started_at)
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') { "queue(#{class_name}): completed in #{duration.round(2)}s" }
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!(job, class_name, claim_token, error)
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') { "queue(#{class_name}): #{error.class} #{error.message}" }
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(job, class_name, message, options)
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 => error
100
- raise ConfigError, "[#{name}] #{error.message}"
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 => error
121
- raise ConfigError, "[#{name}] #{error.message}"
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)