ori-rb 0.4
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 +7 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/LICENSE +9 -0
- data/README.md +444 -0
- data/Rakefile +17 -0
- data/docs/images/example_boundary.png +0 -0
- data/docs/images/example_boundary_cancellation.png +0 -0
- data/docs/images/example_channel.png +0 -0
- data/docs/images/example_mutex.png +0 -0
- data/docs/images/example_promise.png +0 -0
- data/docs/images/example_semaphore.png +0 -0
- data/docs/images/example_trace.png +0 -0
- data/docs/images/example_trace_tag.png +0 -0
- data/lib/ori/channel.rb +148 -0
- data/lib/ori/lazy.rb +163 -0
- data/lib/ori/mutex.rb +9 -0
- data/lib/ori/out/index.html +146 -0
- data/lib/ori/out/script.js +3 -0
- data/lib/ori/promise.rb +39 -0
- data/lib/ori/reentrant_semaphore.rb +68 -0
- data/lib/ori/scope.rb +620 -0
- data/lib/ori/select.rb +35 -0
- data/lib/ori/selectable.rb +9 -0
- data/lib/ori/semaphore.rb +49 -0
- data/lib/ori/task.rb +78 -0
- data/lib/ori/timeout.rb +16 -0
- data/lib/ori/tracer.rb +335 -0
- data/lib/ori/version.rb +5 -0
- data/lib/ori.rb +68 -0
- data/mise-tasks/test +15 -0
- data/mise.toml +40 -0
- data/sorbet/config +8 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
- data/sorbet/rbi/gems/benchmark@0.4.1.rbi +619 -0
- data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
- data/sorbet/rbi/gems/erb@5.1.1.rbi +845 -0
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
- data/sorbet/rbi/gems/io-console@0.8.1.rbi +9 -0
- data/sorbet/rbi/gems/json@2.15.1.rbi +2101 -0
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.5.rbi +9 -0
- data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +240 -0
- data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
- data/sorbet/rbi/gems/minitest@5.26.0.rbi +2234 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
- data/sorbet/rbi/gems/nio4r@2.7.4.rbi +293 -0
- data/sorbet/rbi/gems/parallel@1.27.0.rbi +291 -0
- data/sorbet/rbi/gems/parser@3.3.9.0.rbi +5535 -0
- data/sorbet/rbi/gems/pp@0.6.3.rbi +376 -0
- data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +477 -0
- data/sorbet/rbi/gems/prism@1.5.2.rbi +42056 -0
- data/sorbet/rbi/gems/psych@5.2.6.rbi +2469 -0
- data/sorbet/rbi/gems/racc@1.8.1.rbi +160 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
- data/sorbet/rbi/gems/rake@13.3.0.rbi +3036 -0
- data/sorbet/rbi/gems/rbi@0.3.7.rbi +7115 -0
- data/sorbet/rbi/gems/rbs@3.9.5.rbi +6978 -0
- data/sorbet/rbi/gems/rdoc@6.15.0.rbi +12777 -0
- data/sorbet/rbi/gems/regexp_parser@2.11.3.rbi +3845 -0
- data/sorbet/rbi/gems/reline@0.6.2.rbi +9 -0
- data/sorbet/rbi/gems/rexml@3.4.4.rbi +5285 -0
- data/sorbet/rbi/gems/rubocop-ast@1.47.1.rbi +7780 -0
- data/sorbet/rbi/gems/rubocop-shopify@2.17.1.rbi +9 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.11.0.rbi +2506 -0
- data/sorbet/rbi/gems/rubocop@1.81.1.rbi +63489 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1318 -0
- data/sorbet/rbi/gems/spoom@1.6.3.rbi +6985 -0
- data/sorbet/rbi/gems/stringio@3.1.7.rbi +9 -0
- data/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
- data/sorbet/rbi/gems/thor@1.4.0.rbi +4399 -0
- data/sorbet/rbi/gems/tsort@0.2.0.rbi +393 -0
- data/sorbet/rbi/gems/unicode-display_width@3.2.0.rbi +132 -0
- data/sorbet/rbi/gems/unicode-emoji@4.1.0.rbi +251 -0
- data/sorbet/rbi/gems/vernier@1.8.1-96ce5c739bfe6a18d2f4393f4219a1bf48674b87.rbi +904 -0
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
- data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
- data/sorbet/rbi/gems/zeitwerk@2.7.3.rbi +1429 -0
- data/sorbet/shims/fiber.rbi +21 -0
- data/sorbet/shims/io.rbi +8 -0
- data/sorbet/shims/random.rbi +9 -0
- data/sorbet/shims/rdoc.rbi +3 -0
- data/sorbet/tapioca/require.rb +7 -0
- metadata +169 -0
data/lib/ori/scope.rb
ADDED
@@ -0,0 +1,620 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require "nio"
|
4
|
+
require "random/formatter"
|
5
|
+
require "ori/lazy"
|
6
|
+
|
7
|
+
module Ori
|
8
|
+
class Scope
|
9
|
+
# Add thread-local state management
|
10
|
+
class ThreadLocalState
|
11
|
+
attr_reader :fiber_ids,
|
12
|
+
:tasks,
|
13
|
+
:pending,
|
14
|
+
:readable,
|
15
|
+
:writable,
|
16
|
+
:waiting,
|
17
|
+
:blocked
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@fiber_ids = LazyHash.new
|
21
|
+
@tasks = LazyHash.new
|
22
|
+
@pending = LazyArray.new
|
23
|
+
@readable = LazyHashSet.new
|
24
|
+
@writable = LazyHashSet.new
|
25
|
+
@waiting = LazyHash.new
|
26
|
+
@blocked = LazyHash.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def child_scopes
|
30
|
+
@child_scopes ||= Set.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def child_scopes?
|
34
|
+
defined?(@child_scopes) && !@child_scopes.empty?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :tracer
|
39
|
+
|
40
|
+
HASH_SET_LAMBDA = ->(hash, key) { hash[key] = Set.new }
|
41
|
+
|
42
|
+
def initialize(parent_scope, name, deadline = nil, trace = false)
|
43
|
+
@name = name
|
44
|
+
@parent_scope = parent_scope
|
45
|
+
@parent_scope&.register_child_scope(self)
|
46
|
+
|
47
|
+
@tracer = if trace || parent_scope&.tracing?
|
48
|
+
parent_scope&.tracer || Tracer.new
|
49
|
+
end
|
50
|
+
|
51
|
+
@cancelled = false
|
52
|
+
@closed = false
|
53
|
+
|
54
|
+
# Instead, use thread-local storage
|
55
|
+
thread_local_state[object_id] = ThreadLocalState.new
|
56
|
+
|
57
|
+
inherit_or_set_deadline(deadline)
|
58
|
+
|
59
|
+
if @tracer
|
60
|
+
@scope_id = Random.uuid_v7(extra_timestamp_bits: 12)
|
61
|
+
creating_fiber_id = parent_scope.fiber_ids[Fiber.current] if parent_scope
|
62
|
+
@tracer.register_scope(@scope_id, parent_scope&.scope_id, creating_fiber_id, name: @name)
|
63
|
+
@tracer.record_scope(@scope_id, :opened)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Users are not expected to call this method directly
|
68
|
+
# This is the event loop for an Ori::Scope instance
|
69
|
+
def await
|
70
|
+
while pending_work?
|
71
|
+
process_available_work
|
72
|
+
Fiber.yield if parent_scope && pending_work?
|
73
|
+
end
|
74
|
+
ensure
|
75
|
+
close_scope
|
76
|
+
@parent_scope&.deregister_child_scope(self)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public API
|
80
|
+
|
81
|
+
def fork(&block)
|
82
|
+
task = create_task(&block)
|
83
|
+
resume_task_or_fiber(task) if task
|
84
|
+
task
|
85
|
+
end
|
86
|
+
|
87
|
+
def fork_each(enumerable)
|
88
|
+
return enum_for(:fork_each, enumerable) unless block_given?
|
89
|
+
|
90
|
+
enumerable.each { |item| fork { yield(item) } }
|
91
|
+
end
|
92
|
+
|
93
|
+
def tasks
|
94
|
+
task_queue.values
|
95
|
+
end
|
96
|
+
|
97
|
+
def closed? = @closed
|
98
|
+
|
99
|
+
def tracing? = !@tracer.nil?
|
100
|
+
|
101
|
+
def cancellation_error = @cancellation_error ||= CancellationError.new(self)
|
102
|
+
|
103
|
+
def shutdown!(cause = nil)
|
104
|
+
return if @cancelled
|
105
|
+
|
106
|
+
@cancelled = true
|
107
|
+
exn = cause.is_a?(CancellationError) ? cause : cancellation_error
|
108
|
+
|
109
|
+
@tracer&.record_scope(@scope_id, :cancelling, exn.message)
|
110
|
+
|
111
|
+
if child_scopes?
|
112
|
+
child_scopes.each do |scope|
|
113
|
+
scope.shutdown!(cause)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
pending.each { |fiber| cancel_fiber!(fiber, exn) }
|
118
|
+
waiting.each { |fiber, _| cancel_fiber!(fiber, exn) }
|
119
|
+
blocked.each { |fiber, _| cancel_fiber!(fiber, exn) }
|
120
|
+
|
121
|
+
cleanup_io_resources
|
122
|
+
|
123
|
+
@tracer&.record_scope(@scope_id, :cancelled)
|
124
|
+
|
125
|
+
raise(cause || exn)
|
126
|
+
end
|
127
|
+
|
128
|
+
def tag(name)
|
129
|
+
@tracer&.record_scope(@scope_id, :tag, name)
|
130
|
+
end
|
131
|
+
|
132
|
+
def print_ascii_trace
|
133
|
+
@tracer&.visualize
|
134
|
+
end
|
135
|
+
|
136
|
+
def write_html_trace(directory)
|
137
|
+
@tracer&.write_timeline_data(directory)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Ruby FiberScheduler interface implementation
|
141
|
+
|
142
|
+
def fiber(&block)
|
143
|
+
task = fork(&block)
|
144
|
+
task.fiber
|
145
|
+
end
|
146
|
+
|
147
|
+
def io_wait(io, events, timeout = nil)
|
148
|
+
return @parent_scope.io_wait(io, events, timeout) unless fiber_ids.key?(Fiber.current)
|
149
|
+
|
150
|
+
fiber = Fiber.current
|
151
|
+
id = fiber_ids[fiber]
|
152
|
+
@tracer&.record(id, :waiting_io, "#{io.inspect}:#{events}")
|
153
|
+
|
154
|
+
added = register_io_wait(fiber, io, events)
|
155
|
+
register_timeout(fiber, timeout)
|
156
|
+
|
157
|
+
Fiber.yield
|
158
|
+
|
159
|
+
if added[:readable] && added[:writable]
|
160
|
+
IO::READABLE | IO::WRITABLE
|
161
|
+
elsif added[:readable]
|
162
|
+
IO::READABLE
|
163
|
+
elsif added[:writable]
|
164
|
+
IO::WRITABLE
|
165
|
+
else
|
166
|
+
0
|
167
|
+
end
|
168
|
+
ensure
|
169
|
+
cleanup_io_wait(fiber, io, added)
|
170
|
+
cleanup_timeout(fiber) if timeout
|
171
|
+
end
|
172
|
+
|
173
|
+
def io_select(readables, writables, exceptables, timeout)
|
174
|
+
unless fiber_ids.key?(Fiber.current)
|
175
|
+
return @parent_scope.io_select(readables, writables, exceptables, timeout)
|
176
|
+
end
|
177
|
+
|
178
|
+
selector = NIO::Selector.new
|
179
|
+
|
180
|
+
readables&.each do |io|
|
181
|
+
selector.register(io, :r)
|
182
|
+
end
|
183
|
+
|
184
|
+
writables&.each do |io|
|
185
|
+
selector.register(io, :w)
|
186
|
+
end
|
187
|
+
|
188
|
+
begin
|
189
|
+
ready = selector.select(timeout)
|
190
|
+
return [], [], [] if ready.nil?
|
191
|
+
|
192
|
+
readable = []
|
193
|
+
writable = []
|
194
|
+
exceptional = []
|
195
|
+
|
196
|
+
ready.each do |monitor|
|
197
|
+
readable << monitor.io if monitor.readable?
|
198
|
+
writable << monitor.io if monitor.writable?
|
199
|
+
end
|
200
|
+
|
201
|
+
[readable, writable, exceptional]
|
202
|
+
ensure
|
203
|
+
selector.close
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def kernel_sleep(duration)
|
208
|
+
return @parent_scope.kernel_sleep(duration) unless fiber_ids.key?(Fiber.current)
|
209
|
+
|
210
|
+
fiber = Fiber.current
|
211
|
+
id = fiber_ids[fiber]
|
212
|
+
@tracer&.record(id, :sleeping, duration)
|
213
|
+
|
214
|
+
if duration > 0
|
215
|
+
register_timeout(fiber, duration)
|
216
|
+
Fiber.yield
|
217
|
+
end
|
218
|
+
ensure
|
219
|
+
cleanup_timeout(fiber)
|
220
|
+
end
|
221
|
+
|
222
|
+
def block(...)
|
223
|
+
unless fiber_ids.key?(Fiber.current)
|
224
|
+
return @parent_scope.block(...) if @parent_scope
|
225
|
+
end
|
226
|
+
|
227
|
+
Fiber.yield
|
228
|
+
end
|
229
|
+
|
230
|
+
def unblock(blocker, fiber)
|
231
|
+
unless fiber_ids.key?(Fiber.current)
|
232
|
+
return @parent_scope.unblock(blocker, fiber) if @parent_scope
|
233
|
+
end
|
234
|
+
|
235
|
+
resume_fiber(fiber)
|
236
|
+
end
|
237
|
+
|
238
|
+
# def io_write(...) = ()
|
239
|
+
# def io_pread(...) = ()
|
240
|
+
# def io_pwrite(...) = ()
|
241
|
+
|
242
|
+
# TODO: Implement these
|
243
|
+
# def process_wait(...) = ()
|
244
|
+
# def timeout_after(...) = ()
|
245
|
+
# def address_resolve(...) = ()
|
246
|
+
|
247
|
+
protected
|
248
|
+
|
249
|
+
attr_reader :scope_id
|
250
|
+
attr_reader :deadline_owner
|
251
|
+
|
252
|
+
#: () -> LazyHash
|
253
|
+
def fiber_ids = state.fiber_ids
|
254
|
+
|
255
|
+
def remaining_deadline
|
256
|
+
return unless @deadline_at
|
257
|
+
|
258
|
+
remaining = @deadline_at - current_time
|
259
|
+
remaining.positive? ? remaining : 0
|
260
|
+
end
|
261
|
+
|
262
|
+
def pending_work?
|
263
|
+
return false if closed?
|
264
|
+
|
265
|
+
return true if pending.any?(&:alive?)
|
266
|
+
return true if waiting.any? { |fiber, _| fiber.alive? }
|
267
|
+
return true if blocked.any? { |fiber, _| fiber.alive? }
|
268
|
+
return true if readable.any? { |_, fibers| fibers.any?(&:alive?) }
|
269
|
+
return true if writable.any? { |_, fibers| fibers.any?(&:alive?) }
|
270
|
+
return true if child_scopes? && child_scopes.any? { |scope| scope.pending_work? } # rubocop:disable Style/SymbolProc (protected method called)
|
271
|
+
|
272
|
+
false
|
273
|
+
end
|
274
|
+
|
275
|
+
def register_child_scope(scope)
|
276
|
+
child_scopes.add(scope)
|
277
|
+
end
|
278
|
+
|
279
|
+
def deregister_child_scope(scope)
|
280
|
+
child_scopes.delete(scope)
|
281
|
+
end
|
282
|
+
|
283
|
+
private
|
284
|
+
|
285
|
+
attr_reader :parent_scope
|
286
|
+
|
287
|
+
def thread_local_state
|
288
|
+
return @thread_local_state if defined?(@thread_local_state)
|
289
|
+
|
290
|
+
state = Thread.current.thread_variable_get(:ori_scope_states)
|
291
|
+
if state.nil?
|
292
|
+
state = {}
|
293
|
+
Thread.current.thread_variable_set(:ori_scope_states, state)
|
294
|
+
end
|
295
|
+
|
296
|
+
@thread_local_state = state
|
297
|
+
end
|
298
|
+
|
299
|
+
def child_scopes?
|
300
|
+
state.child_scopes?
|
301
|
+
end
|
302
|
+
|
303
|
+
# Scope lifecycle
|
304
|
+
|
305
|
+
def inherit_or_set_deadline(duration)
|
306
|
+
parent_deadline = parent_scope&.remaining_deadline
|
307
|
+
|
308
|
+
if parent_deadline && (duration.nil? || parent_deadline < duration)
|
309
|
+
# Inherit parent's deadline
|
310
|
+
@deadline_at = current_time + parent_deadline
|
311
|
+
@deadline_owner = parent_scope.deadline_owner
|
312
|
+
elsif duration
|
313
|
+
@deadline_at = current_time + duration
|
314
|
+
@deadline_owner = self
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def process_available_work
|
319
|
+
now = current_time
|
320
|
+
check_deadline!(now)
|
321
|
+
|
322
|
+
cleanup_dead_fibers
|
323
|
+
|
324
|
+
process_pending_fibers
|
325
|
+
process_blocked_fibers
|
326
|
+
process_io_operations(now)
|
327
|
+
process_timeouts(now)
|
328
|
+
end
|
329
|
+
|
330
|
+
def process_pending_fibers
|
331
|
+
pending.size.times do
|
332
|
+
fiber = pending.shift
|
333
|
+
# TODO???
|
334
|
+
next if waiting.key?(fiber)
|
335
|
+
|
336
|
+
resume_fiber(fiber)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def process_blocked_fibers
|
341
|
+
fibers_to_resume = []
|
342
|
+
|
343
|
+
# TODO: shuffle blocked before processing?
|
344
|
+
blocked.each do |fiber, resource|
|
345
|
+
case resource
|
346
|
+
when Ori::Channel
|
347
|
+
fibers_to_resume << fiber if resource.value?
|
348
|
+
when Ori::Promise
|
349
|
+
fibers_to_resume << fiber if resource.resolved?
|
350
|
+
when Ori::Semaphore, Ori::ReentrantSemaphore
|
351
|
+
fibers_to_resume << fiber if resource.available?
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# TODO: This doesn't work because it only looks in the current scope, not the parents
|
356
|
+
# check_stalled_fibers! if fibers_to_resume.empty?
|
357
|
+
|
358
|
+
fibers_to_resume.each do |fiber|
|
359
|
+
blocked.delete(fiber)
|
360
|
+
resume_fiber(fiber)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def process_io_operations(now = nil)
|
365
|
+
return if readable.none? && writable.none?
|
366
|
+
|
367
|
+
readable_out, writable_out = IO.select(readable.keys, writable.keys, [], next_timeout(now))
|
368
|
+
|
369
|
+
process_ready_io(readable_out, readable)
|
370
|
+
process_ready_io(writable_out, writable)
|
371
|
+
end
|
372
|
+
|
373
|
+
def process_ready_io(ready_ios, io_map)
|
374
|
+
return unless ready_ios
|
375
|
+
|
376
|
+
ready_ios.each do |io|
|
377
|
+
io_map[io].each { |fiber| resume_fiber(fiber) }
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def close_scope
|
382
|
+
@closed = true
|
383
|
+
@tracer&.record_scope(@scope_id, :closed)
|
384
|
+
thread_local_state&.delete(object_id)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Timeouts and deadlines
|
388
|
+
|
389
|
+
def current_time
|
390
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
391
|
+
end
|
392
|
+
|
393
|
+
def process_timeouts(now = current_time)
|
394
|
+
check_deadline!
|
395
|
+
|
396
|
+
fibers_to_resume = []
|
397
|
+
waiting.each do |fiber, deadline|
|
398
|
+
if deadline <= now
|
399
|
+
fibers_to_resume << fiber
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
fibers_to_resume.each do |fiber|
|
404
|
+
waiting.delete(fiber)
|
405
|
+
resume_fiber(fiber)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def check_deadline!(now = nil)
|
410
|
+
return unless @deadline_at
|
411
|
+
|
412
|
+
now ||= current_time
|
413
|
+
if now >= @deadline_at
|
414
|
+
error = CancellationError.new(@deadline_owner)
|
415
|
+
shutdown!(error)
|
416
|
+
raise(error)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def check_stalled_fibers!
|
421
|
+
return false if blocked.none?
|
422
|
+
|
423
|
+
if pending.empty? && waiting.empty? && readable.empty? && writable.empty?
|
424
|
+
error = CancellationError.new(self, "All fibers are blocked, impossible to proceed")
|
425
|
+
shutdown!(error)
|
426
|
+
raise(error)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def next_timeout(now = nil)
|
431
|
+
timeouts = T.let([], T::Array[Numeric])
|
432
|
+
timeouts.concat(waiting.values.compact) unless waiting.empty?
|
433
|
+
timeouts << @deadline_at if @deadline_at
|
434
|
+
|
435
|
+
return 0 if timeouts.empty?
|
436
|
+
|
437
|
+
now ||= current_time
|
438
|
+
nearest = timeouts.min #: as !nil
|
439
|
+
delay = nearest - now
|
440
|
+
|
441
|
+
# Return 0 if the timeout is in the past, otherwise return the delay
|
442
|
+
[0, delay].max
|
443
|
+
end
|
444
|
+
|
445
|
+
# Fiber management
|
446
|
+
|
447
|
+
def create_task(&block)
|
448
|
+
return false if @cancelled
|
449
|
+
raise "Scope is closed" if closed?
|
450
|
+
|
451
|
+
task = Task.new(&block)
|
452
|
+
register_task(task)
|
453
|
+
task
|
454
|
+
end
|
455
|
+
|
456
|
+
def register_task(task)
|
457
|
+
fiber_ids[task.fiber] = task.id
|
458
|
+
task_queue[task.fiber] = task
|
459
|
+
|
460
|
+
if @tracer
|
461
|
+
@tracer.register_fiber(task.id, @scope_id)
|
462
|
+
@tracer.record(task.id, :created)
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
def resume_fiber(fiber)
|
467
|
+
resume_task_or_fiber(task_queue.fetch(fiber, fiber))
|
468
|
+
end
|
469
|
+
|
470
|
+
def resume_task_or_fiber(task_or_fiber)
|
471
|
+
return unless task_or_fiber.alive?
|
472
|
+
|
473
|
+
fiber = task_or_fiber.is_a?(Task) ? task_or_fiber.fiber : task_or_fiber
|
474
|
+
id = fiber_ids[fiber]
|
475
|
+
|
476
|
+
begin
|
477
|
+
return if @cancelled # Early return if cancelled
|
478
|
+
|
479
|
+
result = task_or_fiber.resume
|
480
|
+
case result
|
481
|
+
when CancellationError
|
482
|
+
@tracer&.record(id, :cancelled, result.message)
|
483
|
+
task_or_fiber.kill
|
484
|
+
when Ori::Channel, Ori::Promise, Ori::Semaphore, Ori::ReentrantSemaphore
|
485
|
+
@tracer&.record(id, :resource_wait, result.class.name)
|
486
|
+
blocked[fiber] = result
|
487
|
+
when Task
|
488
|
+
pending << fiber
|
489
|
+
else
|
490
|
+
pending << fiber if fiber.alive?
|
491
|
+
end
|
492
|
+
rescue => error
|
493
|
+
@tracer&.record(id, :error, error.message)
|
494
|
+
shutdown!(error)
|
495
|
+
raise(error)
|
496
|
+
end
|
497
|
+
|
498
|
+
@tracer&.record(id, :completed) unless fiber.alive?
|
499
|
+
end
|
500
|
+
|
501
|
+
def cancel_fiber!(fiber, error)
|
502
|
+
return unless fiber.alive?
|
503
|
+
|
504
|
+
id = fiber_ids[fiber]
|
505
|
+
@tracer&.record(id, :cancelling, error.message)
|
506
|
+
|
507
|
+
if (task = task_queue[fiber])
|
508
|
+
task.cancel(error)
|
509
|
+
else
|
510
|
+
# For raw fibers, we still need to resume them one last time
|
511
|
+
# to give them a chance to cleanup
|
512
|
+
fiber.raise(error)
|
513
|
+
end
|
514
|
+
|
515
|
+
@tracer&.record(id, :cancelled, error.message)
|
516
|
+
end
|
517
|
+
|
518
|
+
# Registration
|
519
|
+
|
520
|
+
def register_timeout(fiber, deadline)
|
521
|
+
return unless deadline
|
522
|
+
|
523
|
+
waiting[fiber] = current_time + deadline
|
524
|
+
end
|
525
|
+
|
526
|
+
def register_io_wait(fiber, io, events)
|
527
|
+
added = {
|
528
|
+
readable: false,
|
529
|
+
writable: false,
|
530
|
+
} #: Hash[Symbol, bool]
|
531
|
+
|
532
|
+
if (events & IO::READABLE).nonzero?
|
533
|
+
readable[io].add(fiber)
|
534
|
+
added[:readable] = true
|
535
|
+
end
|
536
|
+
|
537
|
+
if (events & IO::WRITABLE).nonzero?
|
538
|
+
writable[io].add(fiber)
|
539
|
+
added[:writable] = true
|
540
|
+
end
|
541
|
+
|
542
|
+
added
|
543
|
+
end
|
544
|
+
|
545
|
+
# Cleanup
|
546
|
+
|
547
|
+
def cleanup_dead_fibers
|
548
|
+
dead_fibers = fiber_ids.reject { |fiber, _| fiber.alive? }.to_set
|
549
|
+
return if dead_fibers.empty?
|
550
|
+
|
551
|
+
readable.each { |_, fibers| fibers.subtract(dead_fibers) }
|
552
|
+
readable.delete_if { |_, fibers| fibers.empty? }
|
553
|
+
|
554
|
+
writable.each { |_, fibers| fibers.subtract(dead_fibers) }
|
555
|
+
writable.delete_if { |_, fibers| fibers.empty? }
|
556
|
+
|
557
|
+
waiting.delete_if { |fiber, _| dead_fibers.include?(fiber) }
|
558
|
+
|
559
|
+
dead_fibers.each do |fiber|
|
560
|
+
fiber_ids.delete(fiber)
|
561
|
+
task_queue.delete(fiber)
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
def cleanup_io_resources
|
566
|
+
readable.each do |io, _|
|
567
|
+
io.close unless io.closed?
|
568
|
+
rescue => e
|
569
|
+
@tracer&.record_scope(@scope_id, :error, "Failed to close readable: #{e.message}")
|
570
|
+
end
|
571
|
+
|
572
|
+
writable.each do |io, _|
|
573
|
+
io.close unless io.closed?
|
574
|
+
rescue => e
|
575
|
+
@tracer&.record_scope(@scope_id, :error, "Failed to close writable: #{e.message}")
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
def cleanup_io_wait(fiber, io, added)
|
580
|
+
readable[io].delete(fiber) if added[:readable]
|
581
|
+
writable[io].delete(fiber) if added[:writable]
|
582
|
+
|
583
|
+
readable.delete(io) if readable[io]&.empty?
|
584
|
+
writable.delete(io) if writable[io]&.empty?
|
585
|
+
end
|
586
|
+
|
587
|
+
def cleanup_timeout(fiber)
|
588
|
+
waiting.delete(fiber)
|
589
|
+
end
|
590
|
+
|
591
|
+
# Add helper method to access thread-local state
|
592
|
+
def state
|
593
|
+
thread_local_state&.[](object_id) or
|
594
|
+
raise "Scope accessed from wrong thread"
|
595
|
+
end
|
596
|
+
|
597
|
+
# Update all instance variable references to use state
|
598
|
+
|
599
|
+
#: () -> LazyHash
|
600
|
+
def task_queue = state.tasks
|
601
|
+
|
602
|
+
#: () -> LazyArray
|
603
|
+
def pending = state.pending
|
604
|
+
|
605
|
+
#: () -> LazyHashSet
|
606
|
+
def readable = state.readable
|
607
|
+
|
608
|
+
#: () -> LazyHashSet
|
609
|
+
def writable = state.writable
|
610
|
+
|
611
|
+
#: () -> LazyHash
|
612
|
+
def waiting = state.waiting
|
613
|
+
|
614
|
+
#: () -> LazyHash
|
615
|
+
def blocked = state.blocked
|
616
|
+
|
617
|
+
#: () -> Set[Scope]
|
618
|
+
def child_scopes = state.child_scopes
|
619
|
+
end
|
620
|
+
end
|
data/lib/ori/select.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Ori
|
4
|
+
module Select
|
5
|
+
class << self
|
6
|
+
#: [U] (Array[U & Selectable] resources) -> U
|
7
|
+
def await(resources)
|
8
|
+
# TODO: Check if any resources are already resolved
|
9
|
+
# before spawning fibers
|
10
|
+
winner = Promise.new
|
11
|
+
|
12
|
+
Ori.sync(name: "select") do |scope|
|
13
|
+
# TODO: use pattern match against Ori::Task here
|
14
|
+
# instead of Ori::Promise?
|
15
|
+
scope.fork_each(resources) do |resource|
|
16
|
+
case resource
|
17
|
+
when Ori::Timeout
|
18
|
+
# Timeout returns nil if it was cancelled
|
19
|
+
winner.resolve(resource) if resource.await
|
20
|
+
when Ori::Selectable # Ori::Promise, Ori::Task, Ori::Channel, Ori::Semaphore
|
21
|
+
resource.await
|
22
|
+
winner.resolve(resource)
|
23
|
+
else
|
24
|
+
raise "Unsupported await type: #{resource.class}"
|
25
|
+
end
|
26
|
+
|
27
|
+
scope.shutdown!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
winner.await
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
module Ori
|
4
|
+
class Semaphore
|
5
|
+
include(Ori::Selectable)
|
6
|
+
|
7
|
+
def initialize(num_tickets)
|
8
|
+
raise ArgumentError, "Number of tickets must be positive" if num_tickets <= 0
|
9
|
+
|
10
|
+
@tickets = num_tickets
|
11
|
+
@available = num_tickets
|
12
|
+
end
|
13
|
+
|
14
|
+
def sync
|
15
|
+
acquire
|
16
|
+
begin
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
release
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def release
|
24
|
+
raise "Semaphore overflow" if @available >= @tickets
|
25
|
+
|
26
|
+
@available += 1
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def acquire
|
31
|
+
Fiber.yield(self) until available?
|
32
|
+
@available -= 1
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def available?
|
37
|
+
@available > 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def count
|
41
|
+
@available
|
42
|
+
end
|
43
|
+
|
44
|
+
def await
|
45
|
+
Fiber.yield until available?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|