async-background 1.0.2 → 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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/README.md +36 -15
  4. data/async-background.gemspec +13 -7
  5. data/lib/async/background/job.rb +75 -28
  6. data/lib/async/background/metrics.rb +41 -33
  7. data/lib/async/background/min_heap.rb +16 -12
  8. data/lib/async/background/queue/client.rb +11 -8
  9. data/lib/async/background/queue/options.rb +3 -3
  10. data/lib/async/background/queue/schema.rb +19 -15
  11. data/lib/async/background/queue/socket_notifier.rb +14 -4
  12. data/lib/async/background/queue/socket_waker.rb +139 -56
  13. data/lib/async/background/queue/sql.rb +31 -31
  14. data/lib/async/background/queue/store.rb +44 -45
  15. data/lib/async/background/runner/lifecycle.rb +131 -0
  16. data/lib/async/background/runner/queue_execution.rb +101 -57
  17. data/lib/async/background/runner/schedule.rb +20 -17
  18. data/lib/async/background/runner.rb +98 -77
  19. data/lib/async/background/runtime/notification.rb +42 -0
  20. data/lib/async/background/runtime/semaphore.rb +151 -0
  21. data/lib/async/background/runtime/task.rb +146 -0
  22. data/lib/async/background/runtime/task_group.rb +81 -0
  23. data/lib/async/background/runtime.rb +246 -0
  24. data/lib/async/background/scheduler.rb +145 -0
  25. data/lib/async/background/version.rb +1 -1
  26. data/lib/async/background/web/app.rb +43 -40
  27. data/lib/async/background/web/assets/app.css +174 -0
  28. data/lib/async/background/web/assets/app.js +486 -0
  29. data/lib/async/background/web/assets/index.html +29 -0
  30. data/lib/async/background/web/assets.rb +4 -697
  31. data/lib/async/background/web/auth.rb +3 -3
  32. data/lib/async/background/web/configuration.rb +40 -93
  33. data/lib/async/background/web/cursor.rb +22 -26
  34. data/lib/async/background/web/event_hub.rb +12 -7
  35. data/lib/async/background/web/metrics_reader.rb +28 -36
  36. data/lib/async/background/web/response.rb +39 -51
  37. data/lib/async/background/web/serializer.rb +23 -63
  38. data/lib/async/background/web/snapshot.rb +35 -81
  39. data/lib/async/background/web/sql.rb +63 -69
  40. data/lib/async/background/web/stream.rb +52 -32
  41. data/lib/async/background/web.rb +1 -1
  42. data/lib/async/background.rb +1 -2
  43. metadata +30 -22
@@ -29,11 +29,9 @@ module Async
29
29
 
30
30
  enable_incremental_vacuum!(db) unless jobs_table?(db)
31
31
 
32
- with_migration_timeout(db) do
33
- immediate_transaction(db) do
34
- reject_future_version!(db)
35
- upgrade!(db) unless current?(db)
36
- end
32
+ synchronized_change(db) do
33
+ reject_future_version!(db)
34
+ upgrade!(db) unless current?(db)
37
35
  end
38
36
  end
39
37
 
@@ -41,20 +39,20 @@ module Async
41
39
  migrate!(db)
42
40
  return if dashboard_indexes_current?(db)
43
41
 
44
- with_migration_timeout(db) do
45
- immediate_transaction(db) do
46
- create_dashboard_indexes!(db) unless dashboard_indexes_current?(db)
47
- end
42
+ synchronized_change(db) do
43
+ create_dashboard_indexes!(db) unless dashboard_indexes_current?(db)
48
44
  end
49
45
  end
50
46
 
47
+ def synchronized_change(db, &change)
48
+ with_migration_timeout(db) { immediate_transaction(db, &change) }
49
+ end
50
+
51
51
  def current?(db)
52
52
  jobs_table?(db) && version(db) == VERSION && core_indexes_current?(db)
53
53
  end
54
54
 
55
- def dashboard_indexes_current?(db)
56
- (DASHBOARD_INDEXES - index_names(db)).empty?
57
- end
55
+ def dashboard_indexes_current?(db) = indexes_present?(db, DASHBOARD_INDEXES)
58
56
 
59
57
  def version(db)
60
58
  db.get_first_value(SQL::USER_VERSION).to_i
@@ -101,8 +99,10 @@ module Async
101
99
  SQL::CREATE_DASHBOARD_INDEXES.each { |statement| db.execute(statement) }
102
100
  end
103
101
 
104
- def core_indexes_current?(db)
105
- (CORE_INDEXES - index_names(db)).empty?
102
+ def core_indexes_current?(db) = indexes_present?(db, CORE_INDEXES)
103
+
104
+ def indexes_present?(db, names)
105
+ (names - index_names(db)).empty?
106
106
  end
107
107
 
108
108
  def jobs_table?(db)
@@ -156,7 +156,11 @@ module Async
156
156
  db.execute(SQL::COMMIT)
157
157
  result
158
158
  rescue StandardError
159
- db.execute(SQL::ROLLBACK) rescue nil
159
+ begin
160
+ db.execute(SQL::ROLLBACK)
161
+ rescue StandardError
162
+ nil
163
+ end
160
164
  raise
161
165
  end
162
166
  end
@@ -19,7 +19,7 @@ module Async
19
19
  ].freeze
20
20
 
21
21
  DEAD_WORKER_TTL = 5.0
22
- WAKE_BYTE = "\x01".freeze
22
+ WAKE_BYTE = "\x01"
23
23
 
24
24
  def initialize(socket_dir:, total_workers:)
25
25
  @socket_dir = socket_dir
@@ -64,7 +64,11 @@ module Async
64
64
  begin
65
65
  socket.write_nonblock(WAKE_BYTE)
66
66
  ensure
67
- socket.close rescue nil
67
+ begin
68
+ socket.close
69
+ rescue StandardError
70
+ nil
71
+ end
68
72
  end
69
73
  true
70
74
  rescue IO::WaitWritable
@@ -72,9 +76,15 @@ module Async
72
76
  rescue *UNAVAILABLE
73
77
  mark_dead(index, now)
74
78
  false
75
- rescue => e
79
+ rescue StandardError => e
76
80
  mark_dead(index, now)
77
- Console.logger.warn(self) { "SocketNotifier#notify_one(#{index + 1}) failed: #{e.class} #{e.message}" } rescue nil
81
+ begin
82
+ Console.logger.warn(self) do
83
+ "SocketNotifier#notify_one(#{index + 1}) failed: #{e.class} #{e.message}"
84
+ end
85
+ rescue StandardError
86
+ nil
87
+ end
78
88
  false
79
89
  end
80
90
 
@@ -1,113 +1,196 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'socket'
4
- require 'async/notification'
5
4
  require 'fileutils'
5
+ require_relative '../runtime'
6
6
 
7
7
  module Async
8
8
  module Background
9
9
  module Queue
10
10
  class SocketWaker
11
+ CLOSE_GRACE = 2
12
+
11
13
  attr_reader :path
12
14
 
13
15
  def initialize(path)
14
16
  @path = path
15
17
  @server = nil
16
- @notification = ::Async::Notification.new
18
+ @notification = Runtime::Notification.new
17
19
  @running = false
18
20
  @accept_task = nil
21
+ @clients = Runtime::TaskGroup.new
22
+ @sockets = {}
19
23
  end
20
24
 
21
25
  def open!
22
- cleanup_stale_socket
23
26
  ensure_directory
27
+ cleanup_stale_socket
24
28
  @server = UNIXServer.new(@path)
25
- File.chmod(0600, @path)
26
29
  @running = true
30
+ self
27
31
  rescue Errno::EADDRINUSE
28
32
  raise "Socket #{@path} is already in use by another process"
29
33
  end
30
34
 
31
- def start_accept_loop(parent_task)
32
- @accept_task = parent_task.async do |task|
33
- while @running
34
- begin
35
- client = @server.accept_nonblock
36
- handle_client(task, client)
37
- rescue IO::WaitReadable
38
- @server.wait_readable
39
- rescue Errno::EBADF, IOError
40
- break
41
- rescue => e
42
- Console.logger.error(self) { "SocketWaker accept error: #{e.class} #{e.message}" }
43
- end
44
- end
45
- rescue => e
46
- Console.logger.error(self) { "SocketWaker loop crashed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}" }
47
- ensure
48
- @accept_task = nil
49
- end
35
+ def start_accept_loop(_parent_task = nil)
36
+ @accept_task = Runtime.spawn(name: 'socket-waker-accept') { accept_loop }
50
37
  end
51
38
 
52
39
  def wait(timeout: nil)
53
- if timeout
54
- ::Async::Task.current.with_timeout(timeout) { @notification.wait }
55
- else
56
- @notification.wait
57
- end
58
- rescue ::Async::TimeoutError
59
- # Timeout is normal - listener will fall back to polling
40
+ @notification.wait(timeout)
60
41
  end
61
42
 
62
43
  def signal
63
- @notification.signal
44
+ @notification.signal_all
45
+ true
64
46
  end
65
47
 
66
48
  def close
49
+ return unless @running || @server
50
+
67
51
  @running = false
68
- if @accept_task && !@accept_task.finished?
69
- @accept_task.stop rescue nil
70
- end
52
+ stop_accept_loop
53
+ @notification.signal_all
54
+ hang_up_clients
55
+ stop_clients
71
56
 
72
- @server&.close rescue nil
57
+ begin
58
+ @server&.close
59
+ rescue StandardError
60
+ nil
61
+ end
73
62
  @server = nil
74
- File.unlink(@path) rescue nil
63
+ @accept_task = nil
64
+ begin
65
+ File.unlink(@path)
66
+ rescue StandardError
67
+ nil
68
+ end
75
69
  end
76
70
 
77
71
  private
78
72
 
79
- def handle_client(parent_task, client)
80
- parent_task.async do
73
+ def accept_loop
74
+ while @running
75
+ begin
76
+ client = @server.accept_nonblock
77
+ rescue IO::WaitReadable
78
+ @server.wait_readable
79
+ next
80
+ rescue Errno::EBADF, IOError
81
+ break
82
+ rescue StandardError => e
83
+ Console.logger.error(self) { "SocketWaker accept error: #{e.class} #{e.message}" }
84
+ next
85
+ end
86
+
87
+ break unless @running
88
+
89
+ handle_client(client)
90
+ end
91
+ rescue StandardError => e
92
+ Console.logger.error(self) { "SocketWaker loop crashed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}" }
93
+ end
94
+
95
+ def handle_client(client)
96
+ @sockets[client] = true
97
+
98
+ @clients.spawn(name: 'socket-waker-client') do
99
+ loop do
100
+ client.read_nonblock(256)
101
+ @notification.signal_all
102
+ rescue IO::WaitReadable
103
+ client.wait_readable
104
+ retry
105
+ rescue EOFError, Errno::ECONNRESET, Errno::EBADF, IOError
106
+ break
107
+ end
108
+ rescue StandardError => e
109
+ Console.logger.warn(self) { "SocketWaker client handler error: #{e.class} #{e.message}" }
110
+ ensure
111
+ @sockets.delete(client)
81
112
  begin
82
- loop do
83
- begin
84
- client.read_nonblock(256)
85
- @notification.signal
86
- rescue IO::WaitReadable
87
- client.wait_readable
88
- retry
89
- rescue EOFError, Errno::ECONNRESET
90
- break
91
- end
92
- end
93
- rescue => e
94
- Console.logger.warn(self) { "SocketWaker client handler error: #{e.class} #{e.message}" }
95
- ensure
96
- client.close rescue nil
97
- @notification.signal
113
+ client.close
114
+ rescue StandardError
115
+ nil
98
116
  end
117
+ @notification.signal_all
99
118
  end
100
119
  end
101
120
 
121
+ def stop_accept_loop
122
+ task = @accept_task or return
123
+
124
+ wake_accept_loop
125
+ return if await(task, CLOSE_GRACE)
126
+
127
+ task.stop
128
+ await(task, CLOSE_GRACE)
129
+ end
130
+
131
+ def wake_accept_loop
132
+ return unless @server
133
+
134
+ UNIXSocket.open(@path) do |s|
135
+ s.write_nonblock("\x00")
136
+ rescue StandardError
137
+ nil
138
+ end
139
+ rescue StandardError
140
+ nil
141
+ end
142
+
143
+ def hang_up_clients
144
+ @sockets.each_key do |socket|
145
+ socket.close
146
+ rescue StandardError
147
+ nil
148
+ end
149
+ end
150
+
151
+ def stop_clients
152
+ return if @clients.empty?
153
+
154
+ return if await_group(@clients, CLOSE_GRACE)
155
+
156
+ @clients.stop_all(CLOSE_GRACE)
157
+ end
158
+
159
+ def await(task, grace)
160
+ return true unless Runtime.scheduler
161
+
162
+ task.wait(grace)
163
+ true
164
+ rescue Runtime::TimeoutError
165
+ false
166
+ rescue Exception # rubocop:disable Lint/RescueException
167
+ true
168
+ end
169
+
170
+ def await_group(group, grace)
171
+ return true unless Runtime.scheduler
172
+
173
+ group.wait(grace)
174
+ true
175
+ rescue Runtime::TimeoutError
176
+ false
177
+ rescue StandardError
178
+ true
179
+ end
180
+
102
181
  def cleanup_stale_socket
103
182
  return unless File.exist?(@path)
104
183
 
105
184
  begin
106
- UNIXSocket.open(@path) { |s| s.close }
185
+ UNIXSocket.open(@path, &:close)
107
186
 
108
187
  raise "Socket #{@path} is already in use by another process (worker_index conflict?)"
109
188
  rescue Errno::ECONNREFUSED, Errno::ENOENT
110
- File.unlink(@path) rescue nil
189
+ begin
190
+ File.unlink(@path)
191
+ rescue StandardError
192
+ nil
193
+ end
111
194
  end
112
195
  end
113
196
 
@@ -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)
@@ -360,56 +372,43 @@ module Async
360
372
  Job::Options.new(**options)
361
373
  end
362
374
 
375
+ STATEMENTS = {
376
+ :@enqueue_stmt => SQL::INSERT_JOB,
377
+ :@fetch_stmt => SQL::FETCH_NEXT_JOB,
378
+ :@mark_started_stmt => SQL::MARK_STARTED,
379
+ :@complete_stmt => SQL::COMPLETE_JOB,
380
+ :@fail_stmt => SQL::FAIL_JOB,
381
+ :@retry_state_stmt => SQL::RETRY_STATE,
382
+ :@lease_check_stmt => SQL::LEASE_ALIVE,
383
+ :@retry_stmt => SQL::RETRY_JOB,
384
+ :@requeue_stmt => SQL::RECOVER_WORKER,
385
+ :@cleanup_done_stmt => SQL::CLEANUP_DONE,
386
+ :@cleanup_failed_stmt => SQL::CLEANUP_FAILED,
387
+ :@next_pending_stmt => SQL::NEXT_PENDING_RUN_AT,
388
+ :@begin_stmt => SQL::BEGIN_IMMEDIATE,
389
+ :@commit_stmt => SQL::COMMIT,
390
+ :@rollback_stmt => SQL::ROLLBACK
391
+ }.freeze
392
+
363
393
  def prepare_statements
364
- @enqueue_stmt = @db.prepare(SQL::INSERT_JOB)
365
- @fetch_stmt = @db.prepare(SQL::FETCH_NEXT_JOB)
366
- @mark_started_stmt = @db.prepare(SQL::MARK_STARTED)
367
- @complete_stmt = @db.prepare(SQL::COMPLETE_JOB)
368
- @fail_stmt = @db.prepare(SQL::FAIL_JOB)
369
- @retry_state_stmt = @db.prepare(SQL::RETRY_STATE)
370
- @lease_check_stmt = @db.prepare(SQL::LEASE_ALIVE)
371
- @retry_stmt = @db.prepare(SQL::RETRY_JOB)
372
- @requeue_stmt = @db.prepare(SQL::RECOVER_WORKER)
373
- @cleanup_done_stmt = @db.prepare(SQL::CLEANUP_DONE)
374
- @cleanup_failed_stmt = @db.prepare(SQL::CLEANUP_FAILED)
375
- @next_pending_stmt = @db.prepare(SQL::NEXT_PENDING_RUN_AT)
376
- @begin_stmt = @db.prepare(SQL::BEGIN_IMMEDIATE)
377
- @commit_stmt = @db.prepare(SQL::COMMIT)
378
- @rollback_stmt = @db.prepare(SQL::ROLLBACK)
394
+ STATEMENTS.each { |name, sql| instance_variable_set(name, @db.prepare(sql)) }
379
395
  end
380
396
 
381
397
  def finalize_statements
382
- statements.each { |statement| statement&.close rescue nil }
398
+ statements.each do |statement|
399
+ statement&.close
400
+ rescue StandardError
401
+ nil
402
+ end
383
403
  clear_statements
384
404
  end
385
405
 
386
406
  def statements
387
- [
388
- @enqueue_stmt,
389
- @fetch_stmt,
390
- @mark_started_stmt,
391
- @complete_stmt,
392
- @fail_stmt,
393
- @retry_state_stmt,
394
- @lease_check_stmt,
395
- @retry_stmt,
396
- @requeue_stmt,
397
- @cleanup_done_stmt,
398
- @cleanup_failed_stmt,
399
- @next_pending_stmt,
400
- @begin_stmt,
401
- @commit_stmt,
402
- @rollback_stmt
403
- ]
407
+ STATEMENTS.keys.map { |name| instance_variable_get(name) }
404
408
  end
405
409
 
406
410
  def clear_statements
407
- @enqueue_stmt = @fetch_stmt = @mark_started_stmt = nil
408
- @complete_stmt = @fail_stmt = @retry_state_stmt = @lease_check_stmt = nil
409
- @retry_stmt = @requeue_stmt = nil
410
- @cleanup_done_stmt = @cleanup_failed_stmt = nil
411
- @next_pending_stmt = nil
412
- @begin_stmt = @commit_stmt = @rollback_stmt = nil
411
+ STATEMENTS.each_key { |name| instance_variable_set(name, nil) }
413
412
  end
414
413
 
415
414
  def maybe_cleanup