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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +81 -0
- data/README.md +36 -15
- data/async-background.gemspec +13 -7
- 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 +19 -15
- data/lib/async/background/queue/socket_notifier.rb +14 -4
- data/lib/async/background/queue/socket_waker.rb +139 -56
- data/lib/async/background/queue/sql.rb +31 -31
- data/lib/async/background/queue/store.rb +44 -45
- data/lib/async/background/runner/lifecycle.rb +131 -0
- data/lib/async/background/runner/queue_execution.rb +101 -57
- data/lib/async/background/runner/schedule.rb +20 -17
- data/lib/async/background/runner.rb +98 -77
- data/lib/async/background/runtime/notification.rb +42 -0
- data/lib/async/background/runtime/semaphore.rb +151 -0
- data/lib/async/background/runtime/task.rb +146 -0
- data/lib/async/background/runtime/task_group.rb +81 -0
- data/lib/async/background/runtime.rb +246 -0
- data/lib/async/background/scheduler.rb +145 -0
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +43 -40
- 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 +40 -93
- data/lib/async/background/web/cursor.rb +22 -26
- data/lib/async/background/web/event_hub.rb +12 -7
- data/lib/async/background/web/metrics_reader.rb +28 -36
- 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 +35 -81
- data/lib/async/background/web/sql.rb +63 -69
- data/lib/async/background/web/stream.rb +52 -32
- data/lib/async/background/web.rb +1 -1
- data/lib/async/background.rb +1 -2
- metadata +30 -22
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
require_relative 'runtime'
|
|
4
|
+
require_relative 'runner/lifecycle'
|
|
5
5
|
require_relative 'runner/queue_execution'
|
|
6
6
|
require_relative 'runner/schedule'
|
|
7
7
|
|
|
8
8
|
module Async
|
|
9
9
|
module Background
|
|
10
|
-
class ConfigError <
|
|
10
|
+
class ConfigError < Error; end
|
|
11
11
|
|
|
12
12
|
DEFAULT_TIMEOUT = 30
|
|
13
13
|
MIN_SLEEP_TIME = 0.1
|
|
14
14
|
MAX_JITTER = 5
|
|
15
15
|
QUEUE_POLL_INTERVAL = 5
|
|
16
16
|
MIN_QUEUE_WAIT = 0.001
|
|
17
|
+
QUEUE_ERROR_BACKOFF = 0.5
|
|
18
|
+
SERVICE_SHUTDOWN_GRACE = 5
|
|
19
|
+
SERVICE_CANCEL_GRACE = 2
|
|
20
|
+
DRAIN_GRACE = 30
|
|
21
|
+
JOB_CANCEL_GRACE = 5
|
|
17
22
|
|
|
18
23
|
class Runner
|
|
19
24
|
include Clock
|
|
25
|
+
include Lifecycle
|
|
20
26
|
include QueueExecution
|
|
21
27
|
include Schedule
|
|
22
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
|
+
|
|
23
35
|
attr_reader :logger,
|
|
24
36
|
:semaphore,
|
|
25
37
|
:heap,
|
|
@@ -27,44 +39,59 @@ module Async
|
|
|
27
39
|
:total_workers,
|
|
28
40
|
:shutdown,
|
|
29
41
|
:metrics,
|
|
30
|
-
:queue_store
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
:queue_store,
|
|
43
|
+
:jobs,
|
|
44
|
+
:services
|
|
45
|
+
|
|
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
|
|
45
66
|
@logger = Console.logger
|
|
46
67
|
@worker_index = worker_index
|
|
47
68
|
@total_workers = total_workers
|
|
48
69
|
@running = true
|
|
49
|
-
@
|
|
50
|
-
@
|
|
70
|
+
@drain_timeout = settings.drain_timeout
|
|
71
|
+
@shutdown = Runtime::Notification.new
|
|
72
|
+
@metrics = Metrics.new(worker_index: worker_index, total_workers: total_workers, shm_path: settings.metrics_shm_path)
|
|
51
73
|
logger.info { "Async::Background worker_index=#{worker_index}/#{total_workers}, job_count=#{job_count}" }
|
|
52
74
|
|
|
53
|
-
@
|
|
54
|
-
@
|
|
75
|
+
@jobs = Runtime::TaskGroup.new(on_error: error_handler, on_release: method(:job_released))
|
|
76
|
+
@services = Runtime::TaskGroup.new(on_error: error_handler)
|
|
77
|
+
@semaphore = Runtime::Semaphore.new(job_count)
|
|
55
78
|
@heap = config_path.nil? ? MinHeap.new : build_heap(config_path)
|
|
56
|
-
setup_queue(queue_socket_dir, queue_db_path, queue_mmap)
|
|
79
|
+
setup_queue(settings.queue_socket_dir, settings.queue_db_path, settings.queue_mmap)
|
|
57
80
|
validate_work_source!(config_path)
|
|
58
81
|
end
|
|
82
|
+
private :default_settings, :initialize_worker
|
|
59
83
|
|
|
60
84
|
def run
|
|
61
|
-
|
|
85
|
+
Runtime.scheduler!
|
|
86
|
+
warn_unsafe_timeouts
|
|
87
|
+
|
|
88
|
+
Runtime.with_error_handler(error_handler) do
|
|
62
89
|
setup_signal_handlers
|
|
63
|
-
start_signal_watcher
|
|
64
|
-
start_queue_listener
|
|
90
|
+
start_signal_watcher
|
|
91
|
+
start_queue_listener if @listen_queue
|
|
65
92
|
|
|
66
|
-
scheduler_loop
|
|
67
|
-
|
|
93
|
+
scheduler_loop
|
|
94
|
+
shutdown_gracefully
|
|
68
95
|
end
|
|
69
96
|
end
|
|
70
97
|
|
|
@@ -72,26 +99,21 @@ module Async
|
|
|
72
99
|
return unless @running
|
|
73
100
|
|
|
74
101
|
@running = false
|
|
75
|
-
|
|
76
|
-
shutdown.signal
|
|
77
|
-
@queue_waker&.signal
|
|
102
|
+
wake_signal_watcher
|
|
78
103
|
end
|
|
79
104
|
|
|
80
105
|
def running? = @running
|
|
81
106
|
|
|
82
107
|
private
|
|
83
108
|
|
|
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.
|
|
109
|
+
def scheduler_loop
|
|
88
110
|
return shutdown.wait if heap.empty? && @listen_queue
|
|
89
111
|
|
|
90
112
|
while running?
|
|
91
113
|
entry = heap.peek
|
|
92
114
|
break unless entry
|
|
93
115
|
|
|
94
|
-
wait_for_next_entry(
|
|
116
|
+
wait_for_next_entry(entry)
|
|
95
117
|
break unless running?
|
|
96
118
|
|
|
97
119
|
dispatch_due_entries
|
|
@@ -104,9 +126,9 @@ module Async
|
|
|
104
126
|
raise ConfigError, 'Runner requires config_path or queue_socket_dir'
|
|
105
127
|
end
|
|
106
128
|
|
|
107
|
-
def wait_for_next_entry(
|
|
129
|
+
def wait_for_next_entry(entry)
|
|
108
130
|
wait = [entry.next_run_at - monotonic_now, MIN_SLEEP_TIME].max
|
|
109
|
-
wait_with_shutdown(
|
|
131
|
+
wait_with_shutdown(wait)
|
|
110
132
|
end
|
|
111
133
|
|
|
112
134
|
def dispatch_due_entries
|
|
@@ -131,66 +153,65 @@ module Async
|
|
|
131
153
|
|
|
132
154
|
def execute_entry(entry)
|
|
133
155
|
entry.running = true
|
|
134
|
-
|
|
156
|
+
spawn_job do |job_task|
|
|
135
157
|
run_job(job_task, entry)
|
|
136
158
|
ensure
|
|
137
159
|
entry.running = false
|
|
138
160
|
end
|
|
139
161
|
end
|
|
140
162
|
|
|
163
|
+
def spawn_job
|
|
164
|
+
@jobs.spawn(name: 'job') { |task| semaphore.acquire { yield(task) } }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def job_released(_task)
|
|
168
|
+
return unless @queue_saturated
|
|
169
|
+
|
|
170
|
+
@queue_waker&.signal
|
|
171
|
+
end
|
|
172
|
+
|
|
141
173
|
def run_job(job_task, entry)
|
|
142
174
|
metrics_started = false
|
|
175
|
+
metrics_started = start_job_metrics(entry)
|
|
176
|
+
execute_entry_job(job_task, entry)
|
|
177
|
+
rescue Runtime::TimeoutError
|
|
178
|
+
record_job_timeout(entry)
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
record_job_failure(entry, e)
|
|
181
|
+
ensure
|
|
182
|
+
metrics.job_stopped(entry) if metrics_started
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def start_job_metrics(entry)
|
|
143
186
|
metrics.job_started(entry)
|
|
144
|
-
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def execute_entry_job(job_task, entry)
|
|
145
191
|
started_at = monotonic_now
|
|
146
192
|
job_task.with_timeout(entry.timeout) { entry.job_class.perform_now }
|
|
193
|
+
record_job_success(entry, monotonic_now - started_at)
|
|
194
|
+
end
|
|
147
195
|
|
|
148
|
-
|
|
196
|
+
def record_job_success(entry, duration)
|
|
149
197
|
metrics.job_succeeded(entry, duration)
|
|
150
198
|
logger.info('Async::Background') { "#{entry.name}: completed in #{duration.round(2)}s" }
|
|
151
|
-
rescue ::Async::TimeoutError
|
|
152
|
-
metrics.job_timed_out(entry)
|
|
153
|
-
logger.error('Async::Background') { "#{entry.name}: timed out after #{entry.timeout}s" }
|
|
154
|
-
rescue StandardError => error
|
|
155
|
-
metrics.job_failed(entry, error)
|
|
156
|
-
logger.error('Async::Background') {
|
|
157
|
-
"#{entry.name}: #{error.class} #{error.message}\n#{error.backtrace.join("\n")}"
|
|
158
|
-
}
|
|
159
|
-
ensure
|
|
160
|
-
metrics.job_stopped(entry) if metrics_started
|
|
161
199
|
end
|
|
162
200
|
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
%w[INT TERM].each do |signal|
|
|
167
|
-
Signal.trap(signal) do
|
|
168
|
-
@running = false
|
|
169
|
-
@signal_w.write_nonblock('.') rescue nil
|
|
170
|
-
end
|
|
171
|
-
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" }
|
|
172
204
|
end
|
|
173
205
|
|
|
174
|
-
def
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
@signal_r.read_nonblock(256) rescue nil
|
|
179
|
-
shutdown.signal
|
|
180
|
-
@queue_waker&.signal
|
|
181
|
-
end
|
|
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")}"
|
|
182
210
|
end
|
|
183
211
|
end
|
|
184
212
|
|
|
185
|
-
def wait_with_shutdown(
|
|
186
|
-
|
|
187
|
-
rescue ::Async::TimeoutError
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def drain_and_close_queue
|
|
191
|
-
@drain_barrier.wait
|
|
192
|
-
@queue_store&.close
|
|
193
|
-
@queue_waker&.close
|
|
213
|
+
def wait_with_shutdown(duration)
|
|
214
|
+
shutdown.wait(duration)
|
|
194
215
|
end
|
|
195
216
|
end
|
|
196
217
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
|
35
|
+
@waiters = []
|
|
36
|
+
pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Runtime
|
|
6
|
+
class Semaphore
|
|
7
|
+
attr_reader :limit, :available
|
|
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 waiting = @waiting.size
|
|
18
|
+
|
|
19
|
+
def acquire
|
|
20
|
+
wait
|
|
21
|
+
return @available unless block_given?
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
yield
|
|
25
|
+
ensure
|
|
26
|
+
release
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def release
|
|
31
|
+
while (node = @waiting.shift)
|
|
32
|
+
node.grant!
|
|
33
|
+
return @available if node.resume
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@available += 1
|
|
37
|
+
@available
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def wait
|
|
43
|
+
return (@available -= 1) if @available.positive?
|
|
44
|
+
|
|
45
|
+
task = Runtime.current_task
|
|
46
|
+
node = FiberNode.new(Fiber.current, self)
|
|
47
|
+
@waiting.push(node)
|
|
48
|
+
task&.enter_block(node.waiter)
|
|
49
|
+
wait_for_grant(node, task)
|
|
50
|
+
ensure
|
|
51
|
+
release_wait_state(node, task) if node
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def wait_for_grant(node, task)
|
|
55
|
+
until node.granted?
|
|
56
|
+
node.suspend
|
|
57
|
+
abandon_wait!(node, task) if task&.cancelled?
|
|
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?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def abandon_wait!(node, task)
|
|
68
|
+
node.granted? ? release : @waiting.remove(node)
|
|
69
|
+
task.raise_if_cancelled!
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class WaitList
|
|
73
|
+
attr_reader :size
|
|
74
|
+
|
|
75
|
+
def initialize
|
|
76
|
+
@head = @tail = nil
|
|
77
|
+
@size = 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def first = @head
|
|
81
|
+
def empty? = @head.nil?
|
|
82
|
+
|
|
83
|
+
def push(node)
|
|
84
|
+
raise ArgumentError, 'node already queued' if node.list
|
|
85
|
+
|
|
86
|
+
node.list = self
|
|
87
|
+
node.prev = @tail
|
|
88
|
+
node.next = nil
|
|
89
|
+
@tail ? (@tail.next = node) : (@head = node)
|
|
90
|
+
@tail = node
|
|
91
|
+
@size += 1
|
|
92
|
+
node
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def shift
|
|
96
|
+
node = @head or return nil
|
|
97
|
+
|
|
98
|
+
remove(node)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def remove(node)
|
|
102
|
+
return nil unless node.list.equal?(self)
|
|
103
|
+
|
|
104
|
+
node.prev ? (node.prev.next = node.next) : (@head = node.next)
|
|
105
|
+
node.next ? (node.next.prev = node.prev) : (@tail = node.prev)
|
|
106
|
+
node.list = node.prev = node.next = nil
|
|
107
|
+
@size -= 1
|
|
108
|
+
node
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class FiberNode
|
|
113
|
+
attr_accessor :list, :prev, :next
|
|
114
|
+
attr_reader :fiber, :waiter, :blocker
|
|
115
|
+
|
|
116
|
+
def initialize(fiber, blocker)
|
|
117
|
+
@fiber = fiber
|
|
118
|
+
@blocker = blocker
|
|
119
|
+
@list = @prev = @next = nil
|
|
120
|
+
@granted = false
|
|
121
|
+
@scheduler = Runtime.scheduler!
|
|
122
|
+
@waiter = { fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def granted? = @granted
|
|
126
|
+
|
|
127
|
+
def grant!
|
|
128
|
+
@granted = true
|
|
129
|
+
@waiter[:ready] = true
|
|
130
|
+
@waiter[:queued] = false
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def suspend
|
|
135
|
+
@scheduler.block(@blocker, nil)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def resume
|
|
139
|
+
fiber = @fiber
|
|
140
|
+
return false unless fiber.alive?
|
|
141
|
+
|
|
142
|
+
@scheduler.unblock(@blocker, fiber)
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private_constant :WaitList, :FiberNode
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
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 { run_scheduled_task { takes_task ? yield(self) : yield } }
|
|
33
|
+
|
|
34
|
+
@fiber ||= scheduled if scheduled.is_a?(Fiber)
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def wait(timeout = nil)
|
|
39
|
+
unless @done
|
|
40
|
+
deadline = Runtime.deadline_for(timeout)
|
|
41
|
+
raise TimeoutError, 'task did not finish in time' unless join(deadline)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
raise @error if @error
|
|
45
|
+
|
|
46
|
+
@result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def with_timeout(duration, &block)
|
|
50
|
+
Runtime.with_timeout(duration, &block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stop
|
|
54
|
+
return false if @done
|
|
55
|
+
|
|
56
|
+
@cancelled = true
|
|
57
|
+
|
|
58
|
+
return true if interrupt_fiber
|
|
59
|
+
return true if release_blocker
|
|
60
|
+
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def cancelled? = @cancelled
|
|
65
|
+
def finished? = @done
|
|
66
|
+
def waiting = @waiters ? @waiters.size : 0
|
|
67
|
+
|
|
68
|
+
def raise_if_cancelled!
|
|
69
|
+
raise Cancel, 'task stopped' if @cancelled
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def enter_block(waiter)
|
|
73
|
+
@blocker = waiter
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def exit_block
|
|
77
|
+
@blocker = nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
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
|
+
def adopt_fiber(fiber)
|
|
96
|
+
@adopt_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
|
|
140
|
+
@waiters = []
|
|
141
|
+
pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Runtime
|
|
6
|
+
class TaskGroup
|
|
7
|
+
attr_accessor :on_release
|
|
8
|
+
|
|
9
|
+
def initialize(on_error: UNSET, on_release: nil)
|
|
10
|
+
@members = {}
|
|
11
|
+
@drained = Notification.new
|
|
12
|
+
@on_error = on_error
|
|
13
|
+
@on_release = on_release
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def size = @members.size
|
|
17
|
+
def empty? = @members.empty?
|
|
18
|
+
def tasks = @members.keys
|
|
19
|
+
|
|
20
|
+
def spawn(name: nil, &block)
|
|
21
|
+
raise ArgumentError, 'block required' unless block
|
|
22
|
+
|
|
23
|
+
task = Task.new(name: name, group: self, on_error: @on_error)
|
|
24
|
+
@members[task] = true
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
task.start(&block)
|
|
28
|
+
rescue Exception
|
|
29
|
+
release(task)
|
|
30
|
+
raise
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
task
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def wait(timeout = nil)
|
|
37
|
+
deadline = Runtime.deadline_for(timeout)
|
|
38
|
+
|
|
39
|
+
until @members.empty?
|
|
40
|
+
raise TimeoutError, 'tasks did not finish in time' unless @drained.wait_until(deadline)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stop_all(grace = nil)
|
|
47
|
+
tasks.each do |task|
|
|
48
|
+
task.stop
|
|
49
|
+
rescue StandardError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return @members.empty? if grace.nil?
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
wait(grace)
|
|
57
|
+
rescue TimeoutError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def release(task)
|
|
63
|
+
return unless @members.delete(task)
|
|
64
|
+
|
|
65
|
+
@drained.signal_all if @members.empty?
|
|
66
|
+
notify_release(task)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def notify_release(task)
|
|
72
|
+
handler = @on_release or return
|
|
73
|
+
|
|
74
|
+
handler.call(task)
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
Runtime.report_error(task, e, @on_error)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|