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
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
146
|
-
@jobs.spawn(name: 'job') { |task| semaphore.acquire {
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
178
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
221
|
-
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
@@ -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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
|
139
|
+
pending = @waiters
|
|
140
|
+
@waiters = []
|
|
140
141
|
pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
|
|
141
142
|
end
|
|
142
143
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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)
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
133
|
+
result = yield
|
|
134
134
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
135
135
|
failure = e
|
|
136
136
|
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: [
|
|
39
|
-
failed: [
|
|
40
|
-
pending: [
|
|
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 =>
|
|
55
|
-
Response.bad_request(
|
|
63
|
+
rescue RequestError => e
|
|
64
|
+
Response.bad_request(e.message)
|
|
56
65
|
rescue UnavailableError, ClosedError
|
|
57
66
|
Response.unavailable
|
|
58
|
-
rescue StandardError =>
|
|
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,
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
154
|
+
'[async-background-web] internal error on ' \
|
|
148
155
|
"#{env['REQUEST_METHOD']} #{env['PATH_INFO']}: " \
|
|
149
156
|
"#{error.class}: #{error.message}"
|
|
150
157
|
)
|