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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'runtime'
4
+ require_relative 'runner/lifecycle'
4
5
  require_relative 'runner/queue_execution'
5
6
  require_relative 'runner/schedule'
6
7
 
@@ -21,9 +22,16 @@ module Async
21
22
 
22
23
  class Runner
23
24
  include Clock
25
+ include Lifecycle
24
26
  include QueueExecution
25
27
  include Schedule
26
28
 
29
+ Settings = Data.define(
30
+ :config_path, :job_count, :queue_socket_dir, :queue_db_path, :queue_mmap,
31
+ :drain_timeout, :metrics_shm_path
32
+ )
33
+ private_constant :Settings
34
+
27
35
  attr_reader :logger,
28
36
  :semaphore,
29
37
  :heap,
@@ -35,33 +43,43 @@ module Async
35
43
  :jobs,
36
44
  :services
37
45
 
38
- def initialize(
39
- config_path: nil,
40
- job_count: 2,
41
- worker_index:,
42
- total_workers:,
43
- queue_socket_dir: nil,
44
- queue_db_path: nil,
45
- queue_mmap: true,
46
- drain_timeout: DRAIN_GRACE,
47
- metrics_shm_path: Metrics.default_shm_path
48
- )
46
+ def initialize(worker_index:, total_workers:, **options)
47
+ settings = Settings.new(**default_settings.merge(options))
48
+ initialize_worker(worker_index, total_workers, settings)
49
+ end
50
+
51
+ def default_settings
52
+ {
53
+ config_path: nil,
54
+ job_count: 2,
55
+ queue_socket_dir: nil,
56
+ queue_db_path: nil,
57
+ queue_mmap: true,
58
+ drain_timeout: DRAIN_GRACE,
59
+ metrics_shm_path: Metrics.default_shm_path
60
+ }
61
+ end
62
+
63
+ def initialize_worker(worker_index, total_workers, settings)
64
+ config_path = settings.config_path
65
+ job_count = settings.job_count
49
66
  @logger = Console.logger
50
67
  @worker_index = worker_index
51
68
  @total_workers = total_workers
52
69
  @running = true
53
- @drain_timeout = drain_timeout
70
+ @drain_timeout = settings.drain_timeout
54
71
  @shutdown = Runtime::Notification.new
55
- @metrics = Metrics.new(worker_index: worker_index, total_workers: total_workers, shm_path: metrics_shm_path)
72
+ @metrics = Metrics.new(worker_index: worker_index, total_workers: total_workers, shm_path: settings.metrics_shm_path)
56
73
  logger.info { "Async::Background worker_index=#{worker_index}/#{total_workers}, job_count=#{job_count}" }
57
74
 
58
75
  @jobs = Runtime::TaskGroup.new(on_error: error_handler, on_release: method(:job_released))
59
76
  @services = Runtime::TaskGroup.new(on_error: error_handler)
60
77
  @semaphore = Runtime::Semaphore.new(job_count)
61
78
  @heap = config_path.nil? ? MinHeap.new : build_heap(config_path)
62
- setup_queue(queue_socket_dir, queue_db_path, queue_mmap)
79
+ setup_queue(settings.queue_socket_dir, settings.queue_db_path, settings.queue_mmap)
63
80
  validate_work_source!(config_path)
64
81
  end
82
+ private :default_settings, :initialize_worker
65
83
 
66
84
  def run
67
85
  Runtime.scheduler!
@@ -142,8 +160,8 @@ module Async
142
160
  end
143
161
  end
144
162
 
145
- def spawn_job(&block)
146
- @jobs.spawn(name: 'job') { |task| semaphore.acquire { block.call(task) } }
163
+ def spawn_job
164
+ @jobs.spawn(name: 'job') { |task| semaphore.acquire { yield(task) } }
147
165
  end
148
166
 
149
167
  def job_released(_task)
@@ -154,134 +172,47 @@ module Async
154
172
 
155
173
  def run_job(job_task, entry)
156
174
  metrics_started = false
157
- metrics.job_started(entry)
158
- metrics_started = true
159
- started_at = monotonic_now
160
- job_task.with_timeout(entry.timeout) { entry.job_class.perform_now }
161
-
162
- duration = monotonic_now - started_at
163
- metrics.job_succeeded(entry, duration)
164
- logger.info('Async::Background') { "#{entry.name}: completed in #{duration.round(2)}s" }
175
+ metrics_started = start_job_metrics(entry)
176
+ execute_entry_job(job_task, entry)
165
177
  rescue Runtime::TimeoutError
166
- metrics.job_timed_out(entry)
167
- logger.error('Async::Background') { "#{entry.name}: timed out after #{entry.timeout}s" }
168
- rescue StandardError => error
169
- metrics.job_failed(entry, error)
170
- logger.error('Async::Background') {
171
- "#{entry.name}: #{error.class} #{error.message}\n#{error.backtrace.join("\n")}"
172
- }
178
+ record_job_timeout(entry)
179
+ rescue StandardError => e
180
+ record_job_failure(entry, e)
173
181
  ensure
174
182
  metrics.job_stopped(entry) if metrics_started
175
183
  end
176
184
 
177
- def setup_signal_handlers
178
- @signal_r, @signal_w = IO.pipe
179
-
180
- %w[INT TERM].each do |signal|
181
- Signal.trap(signal) do
182
- @running = false
183
- @signal_w.write_nonblock('.') rescue nil
184
- end
185
- end
185
+ def start_job_metrics(entry)
186
+ metrics.job_started(entry)
187
+ true
186
188
  end
187
189
 
188
- def start_signal_watcher
189
- @services.spawn(name: 'signal-watcher') do
190
- while running?
191
- begin
192
- @signal_r.wait_readable
193
- @signal_r.read_nonblock(256)
194
- rescue IO::WaitReadable
195
- next
196
- rescue IOError, Errno::EBADF
197
- break
198
- end
199
-
200
- logger.info { 'Async::Background: stopping gracefully' } unless running?
201
-
202
- shutdown.signal_all
203
- @queue_waker&.signal
204
- end
205
- end
190
+ def execute_entry_job(job_task, entry)
191
+ started_at = monotonic_now
192
+ job_task.with_timeout(entry.timeout) { entry.job_class.perform_now }
193
+ record_job_success(entry, monotonic_now - started_at)
206
194
  end
207
195
 
208
- def error_handler
209
- @error_handler ||= begin
210
- log = logger
211
- lambda do |task, error|
212
- log.error('Async::Background') do
213
- "task #{task&.name || 'unnamed'} died: #{error.class} #{error.message}\n" \
214
- "#{Array(error.backtrace).join("\n")}"
215
- end
216
- end
217
- end
196
+ def record_job_success(entry, duration)
197
+ metrics.job_succeeded(entry, duration)
198
+ logger.info('Async::Background') { "#{entry.name}: completed in #{duration.round(2)}s" }
218
199
  end
219
200
 
220
- def warn_unsafe_timeouts
221
- return if Runtime.native_timeouts?
222
-
223
- logger.warn('Async::Background') do
224
- "#{Fiber.scheduler.class} does not implement #timeout_after; job timeouts " \
225
- 'fall back to stdlib Timeout and may interrupt an unrelated fiber'
226
- end
201
+ def record_job_timeout(entry)
202
+ metrics.job_timed_out(entry)
203
+ logger.error('Async::Background') { "#{entry.name}: timed out after #{entry.timeout}s" }
227
204
  end
228
205
 
229
- def wake_signal_watcher
230
- @signal_w&.write_nonblock('.')
231
- rescue StandardError
232
- nil
206
+ def record_job_failure(entry, error)
207
+ metrics.job_failed(entry, error)
208
+ logger.error('Async::Background') do
209
+ "#{entry.name}: #{error.class} #{error.message}\n#{error.backtrace.join("\n")}"
210
+ end
233
211
  end
234
212
 
235
213
  def wait_with_shutdown(duration)
236
214
  shutdown.wait(duration)
237
215
  end
238
-
239
- def shutdown_gracefully
240
- wake_services
241
- drain_jobs
242
- stop_services
243
- close_queue
244
- close_signal_pipe
245
- end
246
-
247
- def wake_services
248
- shutdown.signal_all
249
- wake_signal_watcher
250
- @queue_waker&.signal
251
- end
252
-
253
- def drain_jobs
254
- return if @jobs.empty?
255
-
256
- logger.info { "Async::Background: draining #{@jobs.size} in-flight job(s)" }
257
- @jobs.wait(@drain_timeout)
258
- rescue Runtime::TimeoutError
259
- logger.warn('Async::Background') do
260
- "#{@jobs.size} job(s) still running after #{@drain_timeout}s, cancelling"
261
- end
262
- @jobs.stop_all(JOB_CANCEL_GRACE)
263
- end
264
-
265
- def stop_services
266
- return if @services.empty?
267
-
268
- @services.wait(SERVICE_SHUTDOWN_GRACE)
269
- rescue Runtime::TimeoutError
270
- logger.warn('Async::Background') { 'service tasks did not exit in time, cancelling' }
271
- @services.stop_all(SERVICE_CANCEL_GRACE)
272
- end
273
-
274
- def close_queue
275
- @queue_waker&.close
276
- @queue_store&.close
277
- end
278
-
279
- def close_signal_pipe
280
- @signal_r&.close
281
- @signal_w&.close
282
- rescue IOError
283
- nil
284
- end
285
216
  end
286
217
  end
287
218
  end
@@ -31,7 +31,8 @@ module Async
31
31
  def signal_all
32
32
  return nil if @waiters.empty?
33
33
 
34
- pending, @waiters = @waiters, []
34
+ pending = @waiters
35
+ @waiters = []
35
36
  pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
36
37
  nil
37
38
  end
@@ -4,7 +4,7 @@ module Async
4
4
  module Background
5
5
  module Runtime
6
6
  class Semaphore
7
- attr_reader :limit
7
+ attr_reader :limit, :available
8
8
 
9
9
  def initialize(limit)
10
10
  @limit = Integer(limit)
@@ -14,7 +14,6 @@ module Async
14
14
  @waiting = WaitList.new
15
15
  end
16
16
 
17
- def available = @available
18
17
  def waiting = @waiting.size
19
18
 
20
19
  def acquire
@@ -47,17 +46,22 @@ module Async
47
46
  node = FiberNode.new(Fiber.current, self)
48
47
  @waiting.push(node)
49
48
  task&.enter_block(node.waiter)
49
+ wait_for_grant(node, task)
50
+ ensure
51
+ release_wait_state(node, task) if node
52
+ end
50
53
 
51
- begin
52
- until node.granted?
53
- node.suspend
54
- abandon_wait!(node, task) if task&.cancelled?
55
- end
54
+ def wait_for_grant(node, task)
55
+ until node.granted?
56
+ node.suspend
56
57
  abandon_wait!(node, task) if task&.cancelled?
57
- ensure
58
- task&.exit_block
59
- @waiting.remove(node) unless node.granted? || node.list.nil?
60
58
  end
59
+ abandon_wait!(node, task) if task&.cancelled?
60
+ end
61
+
62
+ def release_wait_state(node, task)
63
+ task&.exit_block
64
+ @waiting.remove(node) unless node.granted? || node.list.nil?
61
65
  end
62
66
 
63
67
  def abandon_wait!(node, task)
@@ -115,7 +119,7 @@ module Async
115
119
  @list = @prev = @next = nil
116
120
  @granted = false
117
121
  @scheduler = Runtime.scheduler!
118
- @waiter = {fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true}
122
+ @waiter = { fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true }
119
123
  end
120
124
 
121
125
  def granted? = @granted
@@ -29,20 +29,7 @@ module Async
29
29
  @scheduler = Runtime.scheduler!
30
30
  takes_task = !block.arity.zero?
31
31
 
32
- scheduled = Fiber.schedule do
33
- adopt_fiber(Fiber.current)
34
- previous = Runtime.current_task
35
- Runtime.current_task = self
36
- begin
37
- complete(takes_task ? block.call(self) : block.call, nil)
38
- rescue Cancel
39
- complete(nil, nil)
40
- rescue Exception => e # rubocop:disable Lint/RescueException
41
- complete(nil, e)
42
- ensure
43
- Runtime.current_task = previous
44
- end
45
- end
32
+ scheduled = Fiber.schedule { run_scheduled_task { takes_task ? yield(self) : yield } }
46
33
 
47
34
  @fiber ||= scheduled if scheduled.is_a?(Fiber)
48
35
  self
@@ -92,15 +79,28 @@ module Async
92
79
 
93
80
  private
94
81
 
82
+ def run_scheduled_task
83
+ adopt_fiber(Fiber.current)
84
+ previous = Runtime.current_task
85
+ Runtime.current_task = self
86
+ complete(yield, nil)
87
+ rescue Cancel
88
+ complete(nil, nil)
89
+ rescue Exception => e # rubocop:disable Lint/RescueException
90
+ complete(nil, e)
91
+ ensure
92
+ Runtime.current_task = previous
93
+ end
94
+
95
95
  def adopt_fiber(fiber)
96
- @fiber ||= fiber
96
+ @adopt_fiber ||= fiber
97
97
  end
98
98
 
99
99
  def interrupt_fiber
100
100
  fiber = @fiber
101
101
  scheduler = @scheduler
102
102
  return false unless fiber&.alive?
103
- return false unless scheduler&.respond_to?(:fiber_interrupt)
103
+ return false unless scheduler.respond_to?(:fiber_interrupt)
104
104
 
105
105
  result = scheduler.fiber_interrupt(fiber, Cancel.new('task stopped'))
106
106
  result != false
@@ -136,7 +136,8 @@ module Async
136
136
  def wake_waiters
137
137
  return if @waiters.nil? || @waiters.empty?
138
138
 
139
- pending, @waiters = @waiters, []
139
+ pending = @waiters
140
+ @waiters = []
140
141
  pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
141
142
  end
142
143
  end
@@ -72,8 +72,8 @@ module Async
72
72
  handler = @on_release or return
73
73
 
74
74
  handler.call(task)
75
- rescue StandardError => error
76
- Runtime.report_error(task, error, @on_error)
75
+ rescue StandardError => e
76
+ Runtime.report_error(task, e, @on_error)
77
77
  end
78
78
  end
79
79
  end
@@ -48,6 +48,40 @@ module Async
48
48
 
49
49
  module_function
50
50
 
51
+ module FiberLocals
52
+ def current_task
53
+ fiber_local(CURRENT_TASK_KEY)
54
+ end
55
+
56
+ def current_task=(task)
57
+ set_fiber_local(CURRENT_TASK_KEY, task)
58
+ end
59
+
60
+ def fiber_local(key)
61
+ Fiber[key]
62
+ rescue ArgumentError, FiberError
63
+ fiber_local_fallback[key]
64
+ end
65
+
66
+ def set_fiber_local(key, value)
67
+ Fiber[key] = value
68
+ rescue ArgumentError, FiberError
69
+ fiber_local_fallback[key] = value
70
+ end
71
+
72
+ def clear_fiber_local(key, expected)
73
+ current = fiber_local(key)
74
+ set_fiber_local(key, nil) if current.equal?(expected)
75
+ end
76
+
77
+ def fiber_local_fallback
78
+ fiber = Fiber.current
79
+ store = fiber.instance_variable_get(:@async_background_locals)
80
+ store || fiber.instance_variable_set(:@async_background_locals, {})
81
+ end
82
+ end
83
+ extend FiberLocals
84
+
51
85
  def spawn(name: nil, on_error: UNSET, &block)
52
86
  Task.spawn(name: name, on_error: on_error, &block)
53
87
  end
@@ -64,14 +98,6 @@ module Async
64
98
  !target.nil? && target.respond_to?(:timeout_after)
65
99
  end
66
100
 
67
- def current_task
68
- fiber_local(CURRENT_TASK_KEY)
69
- end
70
-
71
- def current_task=(task)
72
- set_fiber_local(CURRENT_TASK_KEY, task)
73
- end
74
-
75
101
  def build_waiter(blocker)
76
102
  existing = fiber_local(WAITER_KEY)
77
103
  if existing&.[](:blocker)
@@ -94,25 +120,29 @@ module Async
94
120
  scheduler = waiter[:scheduler]
95
121
  task = current_task
96
122
  task&.enter_block(waiter)
123
+ park_until_ready(scheduler, blocker, waiter, deadline, task) { yield }
124
+ ensure
125
+ waiter[:blocker] = nil
126
+ clear_fiber_local(WAITER_KEY, waiter)
127
+ task&.exit_block
128
+ end
97
129
 
130
+ def park_until_ready(scheduler, blocker, waiter, deadline, task)
98
131
  until waiter[:ready] || yield
99
- if deadline
100
- remaining = deadline - monotonic_now
101
- return false if remaining <= 0
102
-
103
- scheduler.block(blocker, remaining)
104
- else
105
- scheduler.block(blocker, nil)
106
- end
132
+ return false if wait_for_blocker(scheduler, blocker, deadline) == :expired
107
133
 
108
134
  task&.raise_if_cancelled!
109
135
  end
110
-
111
136
  true
112
- ensure
113
- waiter[:blocker] = nil
114
- clear_fiber_local(WAITER_KEY, waiter)
115
- task&.exit_block
137
+ end
138
+
139
+ def wait_for_blocker(scheduler, blocker, deadline)
140
+ return scheduler.block(blocker, nil) unless deadline
141
+
142
+ remaining = deadline - monotonic_now
143
+ return :expired if remaining <= 0
144
+
145
+ scheduler.block(blocker, remaining)
116
146
  end
117
147
 
118
148
  def with_waiter(blocker, waiters)
@@ -153,13 +183,9 @@ module Async
153
183
  monotonic_now + seconds
154
184
  end
155
185
 
156
- def error_handler=(handler)
157
- @error_handler = handler
158
- end
159
-
160
- def error_handler
161
- @error_handler
162
- end
186
+ attr_writer :error_handler
187
+ attr_reader :error_handler
188
+ module_function :error_handler=, :error_handler
163
189
 
164
190
  def with_error_handler(handler)
165
191
  previous = @error_handler
@@ -179,37 +205,14 @@ module Async
179
205
  false
180
206
  end
181
207
 
182
- def fiber_local(key)
183
- Fiber[key]
184
- rescue ArgumentError, FiberError
185
- fiber_local_fallback[key]
186
- end
187
-
188
- def set_fiber_local(key, value)
189
- Fiber[key] = value
190
- rescue ArgumentError, FiberError
191
- fiber_local_fallback[key] = value
192
- end
193
-
194
- def clear_fiber_local(key, expected)
195
- current = fiber_local(key)
196
- set_fiber_local(key, nil) if current.equal?(expected)
197
- end
198
-
199
- def fiber_local_fallback
200
- fiber = Fiber.current
201
- store = fiber.instance_variable_get(:@async_background_locals)
202
- store || fiber.instance_variable_set(:@async_background_locals, {})
203
- end
204
-
205
- def with_timeout(duration, on_timeout: UNSET)
208
+ def with_timeout(duration, on_timeout: UNSET, &block)
206
209
  return yield if duration.nil?
207
210
 
208
211
  seconds = Float(duration)
209
212
  raise ArgumentError, 'timeout must be positive and finite' unless seconds.finite? && seconds.positive?
210
213
 
211
214
  begin
212
- arm_deadline(seconds) { yield }
215
+ arm_deadline(seconds, &block)
213
216
  rescue Deadline => e
214
217
  raise TimeoutError, e.message if UNSET.equal?(on_timeout)
215
218
 
@@ -32,7 +32,7 @@ module Async
32
32
 
33
33
  def run(kind = nil, &block)
34
34
  raise ArgumentError, 'block required' unless block
35
- return block.call if installed?
35
+ return yield if installed?
36
36
 
37
37
  case resolve(kind)
38
38
  when :async then run_async(&block)
@@ -75,7 +75,7 @@ module Async
75
75
  require 'async'
76
76
 
77
77
  result = nil
78
- send(:Async) { result = block.call }
78
+ send(:Async) { result = yield }
79
79
  result
80
80
  end
81
81
 
@@ -101,7 +101,7 @@ module Async
101
101
 
102
102
  begin
103
103
  Fiber.schedule do
104
- result = block.call
104
+ result = yield
105
105
  rescue Exception => e # rubocop:disable Lint/RescueException
106
106
  failure = e
107
107
  ensure
@@ -130,7 +130,7 @@ module Async
130
130
  thread = Thread.new do
131
131
  Fiber.set_scheduler(scheduler_class.new)
132
132
  Fiber.schedule do
133
- result = block.call
133
+ result = yield
134
134
  rescue Exception => e # rubocop:disable Lint/RescueException
135
135
  failure = e
136
136
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Async
4
4
  module Background
5
- VERSION = '1.1.0'
5
+ VERSION = '1.1.1'
6
6
  end
7
7
  end
@@ -35,9 +35,18 @@ module Async
35
35
  LIST_ROUTES = {
36
36
  executing: [:executing, :executing, nil].freeze,
37
37
  claimed: [:claimed, :claimed, nil].freeze,
38
- done: [:recent_done, :done, :finished_cursor].freeze,
39
- failed: [:recent_failed, :failed, :finished_cursor].freeze,
40
- pending: [:pending, :pending, :pending_cursor].freeze
38
+ done: %i[recent_done done finished_cursor].freeze,
39
+ failed: %i[recent_failed failed finished_cursor].freeze,
40
+ pending: %i[pending pending pending_cursor].freeze
41
+ }.freeze
42
+ STATIC_ROUTES = {
43
+ index: :index_response,
44
+ javascript: :javascript_response,
45
+ stylesheet: :stylesheet_response,
46
+ overview: :overview_response,
47
+ metrics: :metrics_response,
48
+ config: :config_response,
49
+ stream: :stream_response
41
50
  }.freeze
42
51
 
43
52
  def handle(env, head:)
@@ -51,15 +60,15 @@ module Async
51
60
  end
52
61
 
53
62
  dispatch(route, env)
54
- rescue RequestError => error
55
- Response.bad_request(error.message)
63
+ rescue RequestError => e
64
+ Response.bad_request(e.message)
56
65
  rescue UnavailableError, ClosedError
57
66
  Response.unavailable
58
- rescue StandardError => error
67
+ rescue StandardError => e
59
68
  # Do not turn internal class names, paths or database errors into an
60
69
  # unauthenticated information disclosure channel — but do surface
61
70
  # them to the operator via the configured logger.
62
- log_internal_error(env, error)
71
+ log_internal_error(env, e)
63
72
  Response.internal_error
64
73
  end
65
74
 
@@ -78,25 +87,23 @@ module Async
78
87
  def dispatch(route, env)
79
88
  return list_response(route, env) if LIST_ROUTES.key?(route)
80
89
 
81
- case route
82
- when :index then Response.html(Assets.render_index(@config))
83
- when :javascript then Response.javascript(Assets::JS)
84
- when :stylesheet then Response.stylesheet(Assets::CSS)
85
- when :overview then overview_response
86
- when :metrics then metrics_response
87
- when :config then config_response
88
- when :stream then stream_response
89
- else Response.not_found
90
- end
90
+ handler = STATIC_ROUTES[route]
91
+ handler ? send(handler) : Response.not_found
91
92
  end
92
93
 
94
+ def index_response = Response.html(Assets.render_index(@config))
95
+
96
+ def javascript_response = Response.javascript(Assets::JS)
97
+
98
+ def stylesheet_response = Response.stylesheet(Assets::CSS)
99
+
93
100
  def list_response(route, env)
94
101
  reader, shape, cursor_kind = LIST_ROUTES.fetch(route)
95
102
  request = Request.new(env, @config)
96
103
 
97
104
  unless cursor_kind
98
105
  rows = @snapshot.public_send(reader, limit: request.limit)
99
- return Response.json({items: @serializer.public_send(shape, rows)})
106
+ return Response.json({ items: @serializer.public_send(shape, rows) })
100
107
  end
101
108
 
102
109
  rows = @snapshot.public_send(reader, limit: request.limit, cursor: request.public_send(cursor_kind))
@@ -144,7 +151,7 @@ module Async
144
151
 
145
152
  def log_internal_error(env, error)
146
153
  @logger&.error(
147
- "[async-background-web] internal error on " \
154
+ '[async-background-web] internal error on ' \
148
155
  "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}: " \
149
156
  "#{error.class}: #{error.message}"
150
157
  )