async-background 1.0.2 → 1.1.0
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 +67 -0
- data/README.md +36 -15
- data/async-background.gemspec +13 -7
- data/lib/async/background/queue/schema.rb +14 -14
- data/lib/async/background/queue/socket_waker.rb +115 -52
- data/lib/async/background/queue/store.rb +21 -38
- data/lib/async/background/runner/queue_execution.rb +32 -8
- data/lib/async/background/runner/schedule.rb +16 -13
- data/lib/async/background/runner.rb +128 -38
- data/lib/async/background/runtime/notification.rb +41 -0
- data/lib/async/background/runtime/semaphore.rb +147 -0
- data/lib/async/background/runtime/task.rb +145 -0
- data/lib/async/background/runtime/task_group.rb +81 -0
- data/lib/async/background/runtime.rb +243 -0
- data/lib/async/background/scheduler.rb +145 -0
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +22 -26
- data/lib/async/background/web/configuration.rb +40 -93
- data/lib/async/background/web/cursor.rb +10 -21
- data/lib/async/background/web/event_hub.rb +12 -7
- data/lib/async/background/web/metrics_reader.rb +29 -35
- data/lib/async/background/web/response.rb +39 -51
- data/lib/async/background/web/serializer.rb +23 -63
- data/lib/async/background/web/snapshot.rb +32 -78
- data/lib/async/background/web/sql.rb +58 -64
- data/lib/async/background.rb +1 -2
- metadata +26 -22
|
@@ -59,12 +59,15 @@ module Async
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def build_entries(schedule, now)
|
|
62
|
-
schedule
|
|
63
|
-
|
|
62
|
+
schedule
|
|
63
|
+
.filter_map { |name, config| entry_for(name, config, now) }
|
|
64
|
+
.each_with_object(MinHeap.new) { |entry, heap| heap.push(entry) }
|
|
65
|
+
end
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
def entry_for(name, config, now)
|
|
68
|
+
return unless assigned_worker(config, name) == worker_index
|
|
69
|
+
|
|
70
|
+
build_entry(name, build_task_config(name, config), now)
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def assigned_worker(config, name)
|
|
@@ -98,18 +101,18 @@ module Async
|
|
|
98
101
|
end
|
|
99
102
|
|
|
100
103
|
def parse_interval(name, value)
|
|
101
|
-
|
|
102
|
-
interval = interval.to_i
|
|
103
|
-
raise ConfigError, "[#{name}] 'every' must be > 0" unless interval.positive?
|
|
104
|
+
return if value.nil?
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
interval = value.to_i
|
|
107
|
+
raise ConfigError, "[#{name}] 'every' must be > 0" unless interval.positive?
|
|
108
|
+
|
|
109
|
+
interval
|
|
107
110
|
end
|
|
108
111
|
|
|
109
112
|
def parse_cron(name, value)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
return if value.nil?
|
|
114
|
+
|
|
115
|
+
Fugit::Cron.new(value) || raise(ConfigError, "[#{name}] invalid cron: #{value}")
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
def parse_timeout(name, config)
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
require_relative 'runtime'
|
|
5
4
|
require_relative 'runner/queue_execution'
|
|
6
5
|
require_relative 'runner/schedule'
|
|
7
6
|
|
|
8
7
|
module Async
|
|
9
8
|
module Background
|
|
10
|
-
class ConfigError <
|
|
9
|
+
class ConfigError < Error; end
|
|
11
10
|
|
|
12
11
|
DEFAULT_TIMEOUT = 30
|
|
13
12
|
MIN_SLEEP_TIME = 0.1
|
|
14
13
|
MAX_JITTER = 5
|
|
15
14
|
QUEUE_POLL_INTERVAL = 5
|
|
16
15
|
MIN_QUEUE_WAIT = 0.001
|
|
16
|
+
QUEUE_ERROR_BACKOFF = 0.5
|
|
17
|
+
SERVICE_SHUTDOWN_GRACE = 5
|
|
18
|
+
SERVICE_CANCEL_GRACE = 2
|
|
19
|
+
DRAIN_GRACE = 30
|
|
20
|
+
JOB_CANCEL_GRACE = 5
|
|
17
21
|
|
|
18
22
|
class Runner
|
|
19
23
|
include Clock
|
|
@@ -27,11 +31,10 @@ module Async
|
|
|
27
31
|
:total_workers,
|
|
28
32
|
:shutdown,
|
|
29
33
|
:metrics,
|
|
30
|
-
:queue_store
|
|
34
|
+
:queue_store,
|
|
35
|
+
:jobs,
|
|
36
|
+
:services
|
|
31
37
|
|
|
32
|
-
# `config_path: nil` explicitly disables recurring jobs. This keeps the
|
|
33
|
-
# dynamic SQLite queue usable on its own; a supplied path remains strict
|
|
34
|
-
# so a typo cannot silently disable scheduled work.
|
|
35
38
|
def initialize(
|
|
36
39
|
config_path: nil,
|
|
37
40
|
job_count: 2,
|
|
@@ -40,31 +43,37 @@ module Async
|
|
|
40
43
|
queue_socket_dir: nil,
|
|
41
44
|
queue_db_path: nil,
|
|
42
45
|
queue_mmap: true,
|
|
46
|
+
drain_timeout: DRAIN_GRACE,
|
|
43
47
|
metrics_shm_path: Metrics.default_shm_path
|
|
44
48
|
)
|
|
45
49
|
@logger = Console.logger
|
|
46
50
|
@worker_index = worker_index
|
|
47
51
|
@total_workers = total_workers
|
|
48
52
|
@running = true
|
|
49
|
-
@
|
|
53
|
+
@drain_timeout = drain_timeout
|
|
54
|
+
@shutdown = Runtime::Notification.new
|
|
50
55
|
@metrics = Metrics.new(worker_index: worker_index, total_workers: total_workers, shm_path: metrics_shm_path)
|
|
51
56
|
logger.info { "Async::Background worker_index=#{worker_index}/#{total_workers}, job_count=#{job_count}" }
|
|
52
57
|
|
|
53
|
-
@
|
|
54
|
-
@
|
|
58
|
+
@jobs = Runtime::TaskGroup.new(on_error: error_handler, on_release: method(:job_released))
|
|
59
|
+
@services = Runtime::TaskGroup.new(on_error: error_handler)
|
|
60
|
+
@semaphore = Runtime::Semaphore.new(job_count)
|
|
55
61
|
@heap = config_path.nil? ? MinHeap.new : build_heap(config_path)
|
|
56
62
|
setup_queue(queue_socket_dir, queue_db_path, queue_mmap)
|
|
57
63
|
validate_work_source!(config_path)
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
def run
|
|
61
|
-
|
|
67
|
+
Runtime.scheduler!
|
|
68
|
+
warn_unsafe_timeouts
|
|
69
|
+
|
|
70
|
+
Runtime.with_error_handler(error_handler) do
|
|
62
71
|
setup_signal_handlers
|
|
63
|
-
start_signal_watcher
|
|
64
|
-
start_queue_listener
|
|
72
|
+
start_signal_watcher
|
|
73
|
+
start_queue_listener if @listen_queue
|
|
65
74
|
|
|
66
|
-
scheduler_loop
|
|
67
|
-
|
|
75
|
+
scheduler_loop
|
|
76
|
+
shutdown_gracefully
|
|
68
77
|
end
|
|
69
78
|
end
|
|
70
79
|
|
|
@@ -72,26 +81,21 @@ module Async
|
|
|
72
81
|
return unless @running
|
|
73
82
|
|
|
74
83
|
@running = false
|
|
75
|
-
|
|
76
|
-
shutdown.signal
|
|
77
|
-
@queue_waker&.signal
|
|
84
|
+
wake_signal_watcher
|
|
78
85
|
end
|
|
79
86
|
|
|
80
87
|
def running? = @running
|
|
81
88
|
|
|
82
89
|
private
|
|
83
90
|
|
|
84
|
-
def scheduler_loop
|
|
85
|
-
# Queue-only workers have no heap entry to sleep on. Keep the runner
|
|
86
|
-
# alive until #stop / SIGTERM wakes this condition; the queue listener
|
|
87
|
-
# continues independently in its own Async task.
|
|
91
|
+
def scheduler_loop
|
|
88
92
|
return shutdown.wait if heap.empty? && @listen_queue
|
|
89
93
|
|
|
90
94
|
while running?
|
|
91
95
|
entry = heap.peek
|
|
92
96
|
break unless entry
|
|
93
97
|
|
|
94
|
-
wait_for_next_entry(
|
|
98
|
+
wait_for_next_entry(entry)
|
|
95
99
|
break unless running?
|
|
96
100
|
|
|
97
101
|
dispatch_due_entries
|
|
@@ -104,9 +108,9 @@ module Async
|
|
|
104
108
|
raise ConfigError, 'Runner requires config_path or queue_socket_dir'
|
|
105
109
|
end
|
|
106
110
|
|
|
107
|
-
def wait_for_next_entry(
|
|
111
|
+
def wait_for_next_entry(entry)
|
|
108
112
|
wait = [entry.next_run_at - monotonic_now, MIN_SLEEP_TIME].max
|
|
109
|
-
wait_with_shutdown(
|
|
113
|
+
wait_with_shutdown(wait)
|
|
110
114
|
end
|
|
111
115
|
|
|
112
116
|
def dispatch_due_entries
|
|
@@ -131,13 +135,23 @@ module Async
|
|
|
131
135
|
|
|
132
136
|
def execute_entry(entry)
|
|
133
137
|
entry.running = true
|
|
134
|
-
|
|
138
|
+
spawn_job do |job_task|
|
|
135
139
|
run_job(job_task, entry)
|
|
136
140
|
ensure
|
|
137
141
|
entry.running = false
|
|
138
142
|
end
|
|
139
143
|
end
|
|
140
144
|
|
|
145
|
+
def spawn_job(&block)
|
|
146
|
+
@jobs.spawn(name: 'job') { |task| semaphore.acquire { block.call(task) } }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def job_released(_task)
|
|
150
|
+
return unless @queue_saturated
|
|
151
|
+
|
|
152
|
+
@queue_waker&.signal
|
|
153
|
+
end
|
|
154
|
+
|
|
141
155
|
def run_job(job_task, entry)
|
|
142
156
|
metrics_started = false
|
|
143
157
|
metrics.job_started(entry)
|
|
@@ -148,7 +162,7 @@ module Async
|
|
|
148
162
|
duration = monotonic_now - started_at
|
|
149
163
|
metrics.job_succeeded(entry, duration)
|
|
150
164
|
logger.info('Async::Background') { "#{entry.name}: completed in #{duration.round(2)}s" }
|
|
151
|
-
rescue ::
|
|
165
|
+
rescue Runtime::TimeoutError
|
|
152
166
|
metrics.job_timed_out(entry)
|
|
153
167
|
logger.error('Async::Background') { "#{entry.name}: timed out after #{entry.timeout}s" }
|
|
154
168
|
rescue StandardError => error
|
|
@@ -171,26 +185,102 @@ module Async
|
|
|
171
185
|
end
|
|
172
186
|
end
|
|
173
187
|
|
|
174
|
-
def start_signal_watcher
|
|
175
|
-
|
|
188
|
+
def start_signal_watcher
|
|
189
|
+
@services.spawn(name: 'signal-watcher') do
|
|
176
190
|
while running?
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
180
203
|
@queue_waker&.signal
|
|
181
204
|
end
|
|
182
205
|
end
|
|
183
206
|
end
|
|
184
207
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
188
218
|
end
|
|
189
219
|
|
|
190
|
-
def
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def wake_signal_watcher
|
|
230
|
+
@signal_w&.write_nonblock('.')
|
|
231
|
+
rescue StandardError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def wait_with_shutdown(duration)
|
|
236
|
+
shutdown.wait(duration)
|
|
237
|
+
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
|
|
193
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
|
|
194
284
|
end
|
|
195
285
|
end
|
|
196
286
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Runtime
|
|
6
|
+
class Notification
|
|
7
|
+
def initialize
|
|
8
|
+
@waiters = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def waiting = @waiters.size
|
|
12
|
+
|
|
13
|
+
def wait(timeout = nil)
|
|
14
|
+
wait_until(Runtime.deadline_for(timeout))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def wait_until(deadline = nil)
|
|
18
|
+
Runtime.with_waiter(self, @waiters) do |waiter|
|
|
19
|
+
Runtime.park(self, waiter, deadline) { false }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def signal
|
|
24
|
+
while (waiter = @waiters.shift)
|
|
25
|
+
return true if Runtime.wake_dequeued(waiter, self)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def signal_all
|
|
32
|
+
return nil if @waiters.empty?
|
|
33
|
+
|
|
34
|
+
pending, @waiters = @waiters, []
|
|
35
|
+
pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Runtime
|
|
6
|
+
class Semaphore
|
|
7
|
+
attr_reader :limit
|
|
8
|
+
|
|
9
|
+
def initialize(limit)
|
|
10
|
+
@limit = Integer(limit)
|
|
11
|
+
raise ArgumentError, 'limit must be >= 1' unless @limit.positive?
|
|
12
|
+
|
|
13
|
+
@available = @limit
|
|
14
|
+
@waiting = WaitList.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def available = @available
|
|
18
|
+
def waiting = @waiting.size
|
|
19
|
+
|
|
20
|
+
def acquire
|
|
21
|
+
wait
|
|
22
|
+
return @available unless block_given?
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
yield
|
|
26
|
+
ensure
|
|
27
|
+
release
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def release
|
|
32
|
+
while (node = @waiting.shift)
|
|
33
|
+
node.grant!
|
|
34
|
+
return @available if node.resume
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@available += 1
|
|
38
|
+
@available
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def wait
|
|
44
|
+
return (@available -= 1) if @available.positive?
|
|
45
|
+
|
|
46
|
+
task = Runtime.current_task
|
|
47
|
+
node = FiberNode.new(Fiber.current, self)
|
|
48
|
+
@waiting.push(node)
|
|
49
|
+
task&.enter_block(node.waiter)
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
until node.granted?
|
|
53
|
+
node.suspend
|
|
54
|
+
abandon_wait!(node, task) if task&.cancelled?
|
|
55
|
+
end
|
|
56
|
+
abandon_wait!(node, task) if task&.cancelled?
|
|
57
|
+
ensure
|
|
58
|
+
task&.exit_block
|
|
59
|
+
@waiting.remove(node) unless node.granted? || node.list.nil?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def abandon_wait!(node, task)
|
|
64
|
+
node.granted? ? release : @waiting.remove(node)
|
|
65
|
+
task.raise_if_cancelled!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class WaitList
|
|
69
|
+
attr_reader :size
|
|
70
|
+
|
|
71
|
+
def initialize
|
|
72
|
+
@head = @tail = nil
|
|
73
|
+
@size = 0
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def first = @head
|
|
77
|
+
def empty? = @head.nil?
|
|
78
|
+
|
|
79
|
+
def push(node)
|
|
80
|
+
raise ArgumentError, 'node already queued' if node.list
|
|
81
|
+
|
|
82
|
+
node.list = self
|
|
83
|
+
node.prev = @tail
|
|
84
|
+
node.next = nil
|
|
85
|
+
@tail ? (@tail.next = node) : (@head = node)
|
|
86
|
+
@tail = node
|
|
87
|
+
@size += 1
|
|
88
|
+
node
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def shift
|
|
92
|
+
node = @head or return nil
|
|
93
|
+
|
|
94
|
+
remove(node)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def remove(node)
|
|
98
|
+
return nil unless node.list.equal?(self)
|
|
99
|
+
|
|
100
|
+
node.prev ? (node.prev.next = node.next) : (@head = node.next)
|
|
101
|
+
node.next ? (node.next.prev = node.prev) : (@tail = node.prev)
|
|
102
|
+
node.list = node.prev = node.next = nil
|
|
103
|
+
@size -= 1
|
|
104
|
+
node
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class FiberNode
|
|
109
|
+
attr_accessor :list, :prev, :next
|
|
110
|
+
attr_reader :fiber, :waiter, :blocker
|
|
111
|
+
|
|
112
|
+
def initialize(fiber, blocker)
|
|
113
|
+
@fiber = fiber
|
|
114
|
+
@blocker = blocker
|
|
115
|
+
@list = @prev = @next = nil
|
|
116
|
+
@granted = false
|
|
117
|
+
@scheduler = Runtime.scheduler!
|
|
118
|
+
@waiter = {fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def granted? = @granted
|
|
122
|
+
|
|
123
|
+
def grant!
|
|
124
|
+
@granted = true
|
|
125
|
+
@waiter[:ready] = true
|
|
126
|
+
@waiter[:queued] = false
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def suspend
|
|
131
|
+
@scheduler.block(@blocker, nil)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def resume
|
|
135
|
+
fiber = @fiber
|
|
136
|
+
return false unless fiber.alive?
|
|
137
|
+
|
|
138
|
+
@scheduler.unblock(@blocker, fiber)
|
|
139
|
+
true
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private_constant :WaitList, :FiberNode
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Runtime
|
|
6
|
+
class Task
|
|
7
|
+
attr_reader :fiber, :name, :error
|
|
8
|
+
|
|
9
|
+
def self.spawn(name: nil, on_error: UNSET, &block)
|
|
10
|
+
task = new(name: name, on_error: on_error)
|
|
11
|
+
task.start(&block)
|
|
12
|
+
task
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(name: nil, group: nil, on_error: UNSET)
|
|
16
|
+
@fiber = @scheduler = @blocker = @result = @error = nil
|
|
17
|
+
@done = @cancelled = false
|
|
18
|
+
|
|
19
|
+
@name = name
|
|
20
|
+
@group = group
|
|
21
|
+
@on_error = on_error
|
|
22
|
+
@waiters = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start(&block)
|
|
26
|
+
raise ArgumentError, 'block required' unless block
|
|
27
|
+
raise Error, 'task already started' if @fiber || @done
|
|
28
|
+
|
|
29
|
+
@scheduler = Runtime.scheduler!
|
|
30
|
+
takes_task = !block.arity.zero?
|
|
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
|
|
46
|
+
|
|
47
|
+
@fiber ||= scheduled if scheduled.is_a?(Fiber)
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def wait(timeout = nil)
|
|
52
|
+
unless @done
|
|
53
|
+
deadline = Runtime.deadline_for(timeout)
|
|
54
|
+
raise TimeoutError, 'task did not finish in time' unless join(deadline)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
raise @error if @error
|
|
58
|
+
|
|
59
|
+
@result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def with_timeout(duration, &block)
|
|
63
|
+
Runtime.with_timeout(duration, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def stop
|
|
67
|
+
return false if @done
|
|
68
|
+
|
|
69
|
+
@cancelled = true
|
|
70
|
+
|
|
71
|
+
return true if interrupt_fiber
|
|
72
|
+
return true if release_blocker
|
|
73
|
+
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cancelled? = @cancelled
|
|
78
|
+
def finished? = @done
|
|
79
|
+
def waiting = @waiters ? @waiters.size : 0
|
|
80
|
+
|
|
81
|
+
def raise_if_cancelled!
|
|
82
|
+
raise Cancel, 'task stopped' if @cancelled
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def enter_block(waiter)
|
|
86
|
+
@blocker = waiter
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def exit_block
|
|
90
|
+
@blocker = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def adopt_fiber(fiber)
|
|
96
|
+
@fiber ||= fiber
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def interrupt_fiber
|
|
100
|
+
fiber = @fiber
|
|
101
|
+
scheduler = @scheduler
|
|
102
|
+
return false unless fiber&.alive?
|
|
103
|
+
return false unless scheduler&.respond_to?(:fiber_interrupt)
|
|
104
|
+
|
|
105
|
+
result = scheduler.fiber_interrupt(fiber, Cancel.new('task stopped'))
|
|
106
|
+
result != false
|
|
107
|
+
rescue FiberError
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def release_blocker
|
|
112
|
+
waiter = @blocker or return false
|
|
113
|
+
|
|
114
|
+
Runtime.wake(waiter, waiter[:blocker]) && waiter[:fiber].alive?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def complete(result, error)
|
|
118
|
+
return if @done
|
|
119
|
+
|
|
120
|
+
@result = result
|
|
121
|
+
@error = error
|
|
122
|
+
@done = true
|
|
123
|
+
|
|
124
|
+
Runtime.report_error(self, error, @on_error) if error && waiting.zero?
|
|
125
|
+
@group&.release(self)
|
|
126
|
+
wake_waiters
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def join(deadline)
|
|
130
|
+
waiters = (@waiters ||= [])
|
|
131
|
+
Runtime.with_waiter(self, waiters) do |waiter|
|
|
132
|
+
Runtime.park(self, waiter, deadline) { @done }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def wake_waiters
|
|
137
|
+
return if @waiters.nil? || @waiters.empty?
|
|
138
|
+
|
|
139
|
+
pending, @waiters = @waiters, []
|
|
140
|
+
pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|