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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +8 -0
  3. data/.ruby-version +1 -0
  4. data/LICENSE +9 -0
  5. data/README.md +444 -0
  6. data/Rakefile +17 -0
  7. data/docs/images/example_boundary.png +0 -0
  8. data/docs/images/example_boundary_cancellation.png +0 -0
  9. data/docs/images/example_channel.png +0 -0
  10. data/docs/images/example_mutex.png +0 -0
  11. data/docs/images/example_promise.png +0 -0
  12. data/docs/images/example_semaphore.png +0 -0
  13. data/docs/images/example_trace.png +0 -0
  14. data/docs/images/example_trace_tag.png +0 -0
  15. data/lib/ori/channel.rb +148 -0
  16. data/lib/ori/lazy.rb +163 -0
  17. data/lib/ori/mutex.rb +9 -0
  18. data/lib/ori/out/index.html +146 -0
  19. data/lib/ori/out/script.js +3 -0
  20. data/lib/ori/promise.rb +39 -0
  21. data/lib/ori/reentrant_semaphore.rb +68 -0
  22. data/lib/ori/scope.rb +620 -0
  23. data/lib/ori/select.rb +35 -0
  24. data/lib/ori/selectable.rb +9 -0
  25. data/lib/ori/semaphore.rb +49 -0
  26. data/lib/ori/task.rb +78 -0
  27. data/lib/ori/timeout.rb +16 -0
  28. data/lib/ori/tracer.rb +335 -0
  29. data/lib/ori/version.rb +5 -0
  30. data/lib/ori.rb +68 -0
  31. data/mise-tasks/test +15 -0
  32. data/mise.toml +40 -0
  33. data/sorbet/config +8 -0
  34. data/sorbet/rbi/gems/.gitattributes +1 -0
  35. data/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
  36. data/sorbet/rbi/gems/benchmark@0.4.1.rbi +619 -0
  37. data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
  38. data/sorbet/rbi/gems/erb@5.1.1.rbi +845 -0
  39. data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
  40. data/sorbet/rbi/gems/io-console@0.8.1.rbi +9 -0
  41. data/sorbet/rbi/gems/json@2.15.1.rbi +2101 -0
  42. data/sorbet/rbi/gems/language_server-protocol@3.17.0.5.rbi +9 -0
  43. data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +240 -0
  44. data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
  45. data/sorbet/rbi/gems/minitest@5.26.0.rbi +2234 -0
  46. data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
  47. data/sorbet/rbi/gems/nio4r@2.7.4.rbi +293 -0
  48. data/sorbet/rbi/gems/parallel@1.27.0.rbi +291 -0
  49. data/sorbet/rbi/gems/parser@3.3.9.0.rbi +5535 -0
  50. data/sorbet/rbi/gems/pp@0.6.3.rbi +376 -0
  51. data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +477 -0
  52. data/sorbet/rbi/gems/prism@1.5.2.rbi +42056 -0
  53. data/sorbet/rbi/gems/psych@5.2.6.rbi +2469 -0
  54. data/sorbet/rbi/gems/racc@1.8.1.rbi +160 -0
  55. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
  56. data/sorbet/rbi/gems/rake@13.3.0.rbi +3036 -0
  57. data/sorbet/rbi/gems/rbi@0.3.7.rbi +7115 -0
  58. data/sorbet/rbi/gems/rbs@3.9.5.rbi +6978 -0
  59. data/sorbet/rbi/gems/rdoc@6.15.0.rbi +12777 -0
  60. data/sorbet/rbi/gems/regexp_parser@2.11.3.rbi +3845 -0
  61. data/sorbet/rbi/gems/reline@0.6.2.rbi +9 -0
  62. data/sorbet/rbi/gems/rexml@3.4.4.rbi +5285 -0
  63. data/sorbet/rbi/gems/rubocop-ast@1.47.1.rbi +7780 -0
  64. data/sorbet/rbi/gems/rubocop-shopify@2.17.1.rbi +9 -0
  65. data/sorbet/rbi/gems/rubocop-sorbet@0.11.0.rbi +2506 -0
  66. data/sorbet/rbi/gems/rubocop@1.81.1.rbi +63489 -0
  67. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1318 -0
  68. data/sorbet/rbi/gems/spoom@1.6.3.rbi +6985 -0
  69. data/sorbet/rbi/gems/stringio@3.1.7.rbi +9 -0
  70. data/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
  71. data/sorbet/rbi/gems/thor@1.4.0.rbi +4399 -0
  72. data/sorbet/rbi/gems/tsort@0.2.0.rbi +393 -0
  73. data/sorbet/rbi/gems/unicode-display_width@3.2.0.rbi +132 -0
  74. data/sorbet/rbi/gems/unicode-emoji@4.1.0.rbi +251 -0
  75. data/sorbet/rbi/gems/vernier@1.8.1-96ce5c739bfe6a18d2f4393f4219a1bf48674b87.rbi +904 -0
  76. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
  77. data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
  78. data/sorbet/rbi/gems/zeitwerk@2.7.3.rbi +1429 -0
  79. data/sorbet/shims/fiber.rbi +21 -0
  80. data/sorbet/shims/io.rbi +8 -0
  81. data/sorbet/shims/random.rbi +9 -0
  82. data/sorbet/shims/rdoc.rbi +3 -0
  83. data/sorbet/tapioca/require.rb +7 -0
  84. 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,9 @@
1
+ # typed: strict
2
+
3
+ module Ori
4
+ # @abstract
5
+ module Selectable
6
+ #: () -> untyped
7
+ def await; end
8
+ end
9
+ 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