synctest 0.0.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.
@@ -0,0 +1,1225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synctest
4
+ # Coordinates the threads, clock, and synchronization objects in one run.
5
+ class Bubble
6
+ NANOSECONDS = 1_000_000_000
7
+ TIMEOUT = Object.new.freeze
8
+ CLOSED = Object.new.freeze
9
+ THREAD_ABORT = Class.new(Exception) # standard:disable Lint/InheritException
10
+ private_constant :THREAD_ABORT
11
+
12
+ ThreadState = Struct.new(:status, :blocker)
13
+ Blocker = Struct.new(
14
+ :thread,
15
+ :kind,
16
+ :resource,
17
+ :deadline_ns,
18
+ :description,
19
+ :ready,
20
+ :result,
21
+ :error
22
+ )
23
+
24
+ attr_reader :start_at
25
+
26
+ def initialize(start_at:, timeout:)
27
+ unless start_at.is_a?(Time)
28
+ raise ArgumentError, "start_at must be a Time"
29
+ end
30
+ if timeout && (!timeout.is_a?(Numeric) || timeout <= 0)
31
+ raise ArgumentError, "timeout must be a positive number or nil"
32
+ end
33
+
34
+ @start_at = start_at.dup.freeze
35
+ @timeout = timeout&.to_f
36
+ @now_ns = 0
37
+
38
+ @lock = Mutex.new
39
+ @condition = ConditionVariable.new
40
+ @states = {}.compare_by_identity
41
+ @children = {}.compare_by_identity
42
+ @resource_waiters = {}.compare_by_identity
43
+ @mutex_owners = {}.compare_by_identity
44
+ @monitor_owners = {}.compare_by_identity
45
+ @grants = {}.compare_by_identity
46
+ @queue_grants = {}.compare_by_identity
47
+
48
+ @pending_children = 0
49
+ @root = nil
50
+ @root_active = false
51
+ @active_waiter = nil
52
+ @background_errors = {}.compare_by_identity
53
+ @observed_error_threads = {}.compare_by_identity
54
+ @fatal = nil
55
+ @aborting = false
56
+ @active = false
57
+ @progress_sequence = 0
58
+ end
59
+
60
+ def active?
61
+ @active
62
+ end
63
+
64
+ def run
65
+ @root = Thread.current
66
+ @active = true
67
+ with_lock do
68
+ @root_active = true
69
+ @states[@root] = state(:running)
70
+ end
71
+ Synctest.set_current_bubble(self)
72
+
73
+ result = nil
74
+ error = nil
75
+
76
+ begin
77
+ result = yield
78
+ rescue Exception => exception # rubocop:disable Lint/RescueException
79
+ error = exception
80
+ ensure
81
+ root_returned
82
+
83
+ begin
84
+ error ? abort_and_join : finish
85
+ rescue Exception => finish_error # rubocop:disable Lint/RescueException
86
+ error = finish_error if error.nil? || finish_error.is_a?(StalledError)
87
+ ensure
88
+ @active = false
89
+ Synctest.set_current_bubble(nil)
90
+ release_references
91
+ end
92
+ end
93
+
94
+ raise error if error
95
+
96
+ result
97
+ end
98
+
99
+ def wall_time
100
+ nanoseconds = with_lock { @now_ns }
101
+ @start_at + Rational(nanoseconds, NANOSECONDS)
102
+ end
103
+
104
+ def monotonic_time(unit = :float_second)
105
+ nanoseconds = with_lock { @now_ns }
106
+ convert_nanoseconds(nanoseconds, unit)
107
+ end
108
+
109
+ def realtime(unit = :float_second)
110
+ nanoseconds = (@start_at.to_r * NANOSECONDS).to_i + with_lock { @now_ns }
111
+ convert_nanoseconds(nanoseconds, unit)
112
+ end
113
+
114
+ def sleep(duration = nil)
115
+ start_ns = with_lock { @now_ns }
116
+ deadline = duration.nil? ? nil : start_ns + duration_to_nanoseconds(duration)
117
+ blocker = register_blocker(
118
+ kind: :sleep,
119
+ resource: Thread.current,
120
+ deadline_ns: deadline,
121
+ description: duration.nil? ? "sleep forever" : "sleep until #{format_deadline(deadline)}"
122
+ )
123
+ await(blocker)
124
+ Rational(with_lock { @now_ns } - start_ns, NANOSECONDS).round
125
+ end
126
+
127
+ def stop
128
+ blocker = register_blocker(
129
+ kind: :stop,
130
+ resource: Thread.current,
131
+ description: "Thread.stop"
132
+ )
133
+ await(blocker)
134
+ nil
135
+ end
136
+
137
+ def wake_thread(thread)
138
+ with_lock do
139
+ blocker = @states[thread]&.blocker
140
+ return false unless blocker && %i[sleep stop mutex_sleep].include?(blocker.kind)
141
+
142
+ wake_blocker_locked(blocker, result: true)
143
+ reconcile_locked
144
+ true
145
+ end
146
+ end
147
+
148
+ def interrupt_thread(thread)
149
+ with_lock do
150
+ current = @states[thread]
151
+ if current && %i[blocked waiting].include?(current.status)
152
+ @states[thread] = state(:runnable, current.blocker)
153
+ signal_locked
154
+ end
155
+ end
156
+ end
157
+
158
+ def wait
159
+ thread = Thread.current
160
+ blocker = with_lock do
161
+ if @active_waiter
162
+ raise ConcurrentWaitError, "only one Synctest.wait may be active in a bubble"
163
+ end
164
+
165
+ @active_waiter = thread
166
+ register_blocker_locked(
167
+ thread: thread,
168
+ kind: :wait,
169
+ description: "Synctest.wait",
170
+ status: :waiting
171
+ )
172
+ end
173
+
174
+ await(blocker)
175
+ raise_background_error
176
+ nil
177
+ ensure
178
+ with_lock { @active_waiter = nil if @active_waiter.equal?(thread) } if thread
179
+ end
180
+
181
+ # Thread lifecycle -----------------------------------------------------
182
+
183
+ def reserve_child
184
+ with_lock do
185
+ raise Error, "cannot create a thread in a finished Synctest bubble" unless @active && !@aborting
186
+
187
+ @pending_children += 1
188
+ signal_locked
189
+ end
190
+ end
191
+
192
+ def cancel_child_reservation
193
+ with_lock do
194
+ @pending_children -= 1
195
+ signal_locked
196
+ reconcile_locked
197
+ end
198
+ end
199
+
200
+ def attach_child(thread)
201
+ thread.instance_variable_set(Synctest::OBJECT_BUBBLE_IVAR, self)
202
+ with_lock do
203
+ @pending_children -= 1
204
+ @children[thread] = true
205
+ @states[thread] = state(:running)
206
+ signal_locked
207
+ reconcile_locked
208
+ end
209
+ end
210
+
211
+ # Thread.new returns before the new thread is guaranteed to have executed
212
+ # any Ruby code. Do not hand the Thread object to user code until its
213
+ # reservation has become a real member, or an immediate Thread#kill could
214
+ # strand a pending reservation forever.
215
+ def await_child_attachment(thread)
216
+ with_lock do
217
+ until @states.key?(thread)
218
+ unless thread.alive?
219
+ @pending_children -= 1
220
+ raise Error, "child thread exited before joining its Synctest bubble"
221
+ end
222
+
223
+ Raw::CONDITION_WAIT.bind_call(@condition, @lock, 0.01)
224
+ end
225
+ end
226
+ end
227
+
228
+ def child_finished(thread, error)
229
+ with_lock do
230
+ @background_errors[thread] = error if error && !@aborting
231
+ current = @states[thread]
232
+ @states[thread] = state(:exited)
233
+ remove_blocker_locked(current.blocker) if current&.blocker
234
+ release_owned_resources_locked(thread)
235
+ wake_resource_locked(thread, kinds: [:join], result: thread, all: true)
236
+ signal_locked
237
+ reconcile_locked unless @fatal
238
+ end
239
+ end
240
+
241
+ def join(thread, timeout)
242
+ raise ThreadError, "Target thread must not be current thread" if thread.equal?(Thread.current)
243
+
244
+ deadline = timeout.nil? ? nil : with_lock { @now_ns } + timeout_to_nanoseconds(timeout)
245
+ blocker = with_lock do
246
+ unless thread_finished_locked?(thread)
247
+ register_blocker_locked(
248
+ thread: Thread.current,
249
+ kind: :join,
250
+ resource: thread,
251
+ deadline_ns: deadline,
252
+ description: "join #{thread_label(thread)}"
253
+ )
254
+ end
255
+ end
256
+
257
+ result = blocker && await(blocker)
258
+ return nil if result.equal?(TIMEOUT)
259
+
260
+ raise_join_error(thread)
261
+ thread
262
+ end
263
+
264
+ # Mutex ---------------------------------------------------------------
265
+
266
+ def mutex_lock(mutex)
267
+ thread = Thread.current
268
+ with_lock do
269
+ loop do
270
+ owner = @mutex_owners[mutex]
271
+ raise ThreadError, "deadlock; recursive locking" if owner.equal?(thread)
272
+
273
+ granted = @grants[mutex]
274
+ if (!granted || granted.equal?(thread)) && Raw::MUTEX_TRY_LOCK.bind_call(mutex)
275
+ @grants.delete(mutex)
276
+ @mutex_owners[mutex] = thread
277
+ return mutex
278
+ end
279
+
280
+ blocker = register_blocker_locked(
281
+ thread: thread,
282
+ kind: :mutex_lock,
283
+ resource: mutex,
284
+ description: "Mutex#lock"
285
+ )
286
+ await_locked(blocker)
287
+ end
288
+ end
289
+ end
290
+
291
+ def mutex_try_lock(mutex)
292
+ thread = Thread.current
293
+ with_lock do
294
+ return false if @mutex_owners[mutex].equal?(thread)
295
+
296
+ granted = @grants[mutex]
297
+ return false if granted && !granted.equal?(thread)
298
+ return false unless Raw::MUTEX_TRY_LOCK.bind_call(mutex)
299
+
300
+ @grants.delete(mutex)
301
+ @mutex_owners[mutex] = thread
302
+ true
303
+ end
304
+ end
305
+
306
+ def mutex_unlock(mutex)
307
+ with_lock do
308
+ release_mutex_locked(mutex, Thread.current)
309
+ reconcile_locked
310
+ end
311
+ mutex
312
+ end
313
+
314
+ def mutex_owned?(mutex)
315
+ with_lock { @mutex_owners[mutex].equal?(Thread.current) }
316
+ end
317
+
318
+ def mutex_sleep(mutex, timeout)
319
+ released = false
320
+ start_ns = with_lock do
321
+ release_mutex_locked(mutex, Thread.current)
322
+ released = true
323
+ @now_ns
324
+ end
325
+
326
+ deadline = timeout.nil? ? nil : start_ns + duration_to_nanoseconds(timeout)
327
+ blocker = register_blocker(
328
+ kind: :mutex_sleep,
329
+ resource: Thread.current,
330
+ deadline_ns: deadline,
331
+ description: timeout.nil? ? "Mutex#sleep" : "Mutex#sleep until #{format_deadline(deadline)}"
332
+ )
333
+ await(blocker)
334
+ nil
335
+ ensure
336
+ mutex_lock(mutex) if released && !aborting?
337
+ end
338
+
339
+ # ConditionVariable ---------------------------------------------------
340
+
341
+ def condition_wait(condition, mutex, timeout)
342
+ blocker = nil
343
+ released = false
344
+
345
+ with_lock do
346
+ release_mutex_locked(mutex, Thread.current)
347
+ released = true
348
+ deadline = timeout.nil? ? nil : @now_ns + duration_to_nanoseconds(timeout)
349
+ blocker = register_blocker_locked(
350
+ thread: Thread.current,
351
+ kind: :condition,
352
+ resource: condition,
353
+ deadline_ns: deadline,
354
+ description: timeout.nil? ? "ConditionVariable#wait" : "ConditionVariable#wait until #{format_deadline(deadline)}"
355
+ )
356
+ end
357
+
358
+ result = await(blocker)
359
+ result.equal?(TIMEOUT) ? nil : 0
360
+ ensure
361
+ mutex_lock(mutex) if released && !aborting?
362
+ end
363
+
364
+ def condition_signal(condition, broadcast:)
365
+ with_lock do
366
+ wake_resource_locked(
367
+ condition,
368
+ kinds: [:condition],
369
+ result: true,
370
+ all: broadcast
371
+ )
372
+ reconcile_locked
373
+ end
374
+ condition
375
+ end
376
+
377
+ # Monitor -------------------------------------------------------------
378
+
379
+ def monitor_enter(monitor)
380
+ thread = Thread.current
381
+ with_lock do
382
+ loop do
383
+ owner, depth = @monitor_owners[monitor]
384
+ if owner.equal?(thread)
385
+ Raw::MONITOR_ENTER.bind_call(monitor)
386
+ @monitor_owners[monitor] = [thread, depth + 1]
387
+ return monitor
388
+ end
389
+
390
+ granted = @grants[monitor]
391
+ if (!granted || granted.equal?(thread)) && Raw::MONITOR_TRY_ENTER.bind_call(monitor)
392
+ @grants.delete(monitor)
393
+ @monitor_owners[monitor] = [thread, 1]
394
+ return monitor
395
+ end
396
+
397
+ blocker = register_blocker_locked(
398
+ thread: thread,
399
+ kind: :monitor_lock,
400
+ resource: monitor,
401
+ description: "Monitor#enter"
402
+ )
403
+ await_locked(blocker)
404
+ end
405
+ end
406
+ end
407
+
408
+ def monitor_try_enter(monitor)
409
+ thread = Thread.current
410
+ with_lock do
411
+ owner, depth = @monitor_owners[monitor]
412
+ if owner.equal?(thread)
413
+ Raw::MONITOR_ENTER.bind_call(monitor)
414
+ @monitor_owners[monitor] = [thread, depth + 1]
415
+ return true
416
+ end
417
+
418
+ granted = @grants[monitor]
419
+ return false if granted && !granted.equal?(thread)
420
+ return false unless Raw::MONITOR_TRY_ENTER.bind_call(monitor)
421
+
422
+ @grants.delete(monitor)
423
+ @monitor_owners[monitor] = [thread, 1]
424
+ true
425
+ end
426
+ end
427
+
428
+ def monitor_exit(monitor)
429
+ with_lock do
430
+ release_monitor_locked(monitor, Thread.current)
431
+ reconcile_locked
432
+ end
433
+ monitor
434
+ end
435
+
436
+ def monitor_owned?(monitor)
437
+ with_lock { @monitor_owners[monitor]&.first.equal?(Thread.current) }
438
+ end
439
+
440
+ def monitor_wait(monitor, condition, timeout)
441
+ depth = nil
442
+ blocker = nil
443
+
444
+ with_lock do
445
+ owner, depth = @monitor_owners[monitor]
446
+ raise ThreadError, "current thread not owner" unless owner.equal?(Thread.current)
447
+
448
+ depth.times { Raw::MONITOR_EXIT.bind_call(monitor) }
449
+ @monitor_owners.delete(monitor)
450
+ grant_next_locked(monitor, :monitor_lock)
451
+
452
+ deadline = timeout.nil? ? nil : @now_ns + duration_to_nanoseconds(timeout)
453
+ blocker = register_blocker_locked(
454
+ thread: Thread.current,
455
+ kind: :condition,
456
+ resource: condition,
457
+ deadline_ns: deadline,
458
+ description: timeout.nil? ? "Monitor condition wait" : "Monitor condition wait until #{format_deadline(deadline)}"
459
+ )
460
+ end
461
+
462
+ result = await(blocker)
463
+ !result.equal?(TIMEOUT)
464
+ ensure
465
+ depth&.times { monitor_enter(monitor) } unless aborting?
466
+ end
467
+
468
+ # Queue ---------------------------------------------------------------
469
+
470
+ def queue_pop(queue, non_block, timeout)
471
+ if non_block && !timeout.nil?
472
+ raise ArgumentError, "can't set a timeout if non_block is enabled"
473
+ end
474
+
475
+ with_lock do
476
+ loop do
477
+ grant = queue_grants_for_locked(queue)
478
+ begin
479
+ item = raw_queue_pop(queue, true)
480
+ if grant&.thread&.equal?(Thread.current)
481
+ consume_queue_grant_locked(queue, grant) if grant
482
+ elsif grant && raw_queue_length(queue).zero?
483
+ @queue_grants.delete(queue)
484
+ settle_queue_waiters_locked(queue)
485
+ end
486
+ fill_sized_queue_locked(queue) if queue.is_a?(SizedQueue)
487
+ reconcile_locked
488
+ return item
489
+ rescue ThreadError
490
+ consume_queue_grant_locked(queue, grant) if grant&.thread&.equal?(Thread.current)
491
+ return nil if Raw::QUEUE_CLOSED.bind_call(queue)
492
+ raise if non_block
493
+ end
494
+
495
+ deadline = timeout.nil? ? nil : @now_ns + timeout_to_nanoseconds(timeout)
496
+ blocker = register_blocker_locked(
497
+ thread: Thread.current,
498
+ kind: :queue_pop,
499
+ resource: queue,
500
+ deadline_ns: deadline,
501
+ description: timeout.nil? ? "Queue#pop" : "Queue#pop until #{format_deadline(deadline)}"
502
+ )
503
+ result = await_locked(blocker)
504
+ return nil if result.equal?(TIMEOUT) || result.equal?(CLOSED)
505
+ end
506
+ end
507
+ end
508
+
509
+ def queue_push(queue, item)
510
+ with_lock do
511
+ raise ClosedQueueError, "queue closed" if Raw::QUEUE_CLOSED.bind_call(queue)
512
+
513
+ Raw::QUEUE_PUSH.bind_call(queue, item)
514
+ grant_queued_item_locked(queue)
515
+ reconcile_locked
516
+ end
517
+ queue
518
+ end
519
+
520
+ def sized_queue_push(queue, item, non_block, timeout)
521
+ if non_block && !timeout.nil?
522
+ raise ArgumentError, "can't set a timeout if non_block is enabled"
523
+ end
524
+
525
+ blocker = with_lock do
526
+ raise ClosedQueueError, "queue closed" if Raw::QUEUE_CLOSED.bind_call(queue)
527
+
528
+ begin
529
+ Raw::SIZED_QUEUE_PUSH.bind_call(queue, item, true)
530
+ grant_queued_item_locked(queue)
531
+ reconcile_locked
532
+ return queue
533
+ rescue ThreadError
534
+ raise if non_block
535
+ end
536
+
537
+ deadline = timeout.nil? ? nil : @now_ns + timeout_to_nanoseconds(timeout)
538
+ register_blocker_locked(
539
+ thread: Thread.current,
540
+ kind: :queue_push,
541
+ resource: queue,
542
+ deadline_ns: deadline,
543
+ description: timeout.nil? ? "SizedQueue#push" : "SizedQueue#push until #{format_deadline(deadline)}",
544
+ result: item
545
+ )
546
+ end
547
+
548
+ result = await(blocker)
549
+ result.equal?(TIMEOUT) ? nil : queue
550
+ end
551
+
552
+ def queue_close(queue)
553
+ with_lock do
554
+ raw_queue_close(queue)
555
+ waiters_for_locked(queue).dup.each do |waiter|
556
+ case waiter.kind
557
+ when :queue_pop
558
+ settle_queue_waiters_locked(queue)
559
+ when :queue_push
560
+ wake_blocker_locked(waiter, error: ClosedQueueError.new("queue closed"))
561
+ end
562
+ end
563
+ reconcile_locked
564
+ end
565
+ queue
566
+ end
567
+
568
+ def queue_clear(queue)
569
+ with_lock do
570
+ raw_queue_clear(queue)
571
+ @queue_grants.delete(queue)
572
+ fill_sized_queue_locked(queue) if queue.is_a?(SizedQueue)
573
+ settle_queue_waiters_locked(queue)
574
+ reconcile_locked
575
+ end
576
+ queue
577
+ end
578
+
579
+ def sized_queue_set_max(queue, maximum)
580
+ with_lock do
581
+ result = Raw::SIZED_QUEUE_SET_MAX.bind_call(queue, maximum)
582
+ fill_sized_queue_locked(queue)
583
+ reconcile_locked
584
+ result
585
+ end
586
+ end
587
+
588
+ def queue_num_waiting(queue, native_count)
589
+ with_lock do
590
+ native_count + waiters_for_locked(queue).count { |waiter| !waiter.ready }
591
+ end
592
+ end
593
+
594
+ # Diagnostics and utilities ------------------------------------------
595
+
596
+ def duration_to_nanoseconds(duration)
597
+ interval_to_nanoseconds(duration, negative_as_zero: false)
598
+ end
599
+
600
+ def timeout_to_nanoseconds(timeout)
601
+ interval_to_nanoseconds(timeout, negative_as_zero: true)
602
+ end
603
+
604
+ def diagnostics
605
+ with_lock { diagnostics_locked }
606
+ end
607
+
608
+ private
609
+
610
+ def state(status, blocker = nil)
611
+ ThreadState.new(status:, blocker:)
612
+ end
613
+
614
+ def blocker(thread:, kind:, description:, resource: nil, deadline_ns: nil, ready: false, result: nil, error: nil)
615
+ Blocker.new(
616
+ thread:,
617
+ kind:,
618
+ resource:,
619
+ deadline_ns:,
620
+ description:,
621
+ ready:,
622
+ result:,
623
+ error:
624
+ )
625
+ end
626
+
627
+ def with_lock
628
+ Raw::MUTEX_LOCK.bind_call(@lock)
629
+ yield
630
+ ensure
631
+ Raw::MUTEX_UNLOCK.bind_call(@lock) if Raw::MUTEX_OWNED.bind_call(@lock)
632
+ end
633
+
634
+ def register_blocker(kind:, description:, resource: nil, deadline_ns: nil, result: nil)
635
+ with_lock do
636
+ register_blocker_locked(
637
+ thread: Thread.current,
638
+ kind:,
639
+ resource:,
640
+ deadline_ns:,
641
+ description:,
642
+ result:
643
+ )
644
+ end
645
+ end
646
+
647
+ def register_blocker_locked(thread:, kind:, description:, status: :blocked, resource: nil, deadline_ns: nil, result: nil)
648
+ item = blocker(
649
+ thread:,
650
+ kind:,
651
+ resource:,
652
+ deadline_ns:,
653
+ description:,
654
+ result:
655
+ )
656
+ @states[thread] = state(status, item)
657
+ waiters_for_locked(resource) << item if resource
658
+ signal_locked
659
+ if @fatal
660
+ wake_blocker_locked(item, error: @fatal)
661
+ elsif deadline_ns && deadline_ns <= @now_ns
662
+ wake_blocker_locked(item, result: TIMEOUT)
663
+ else
664
+ reconcile_locked
665
+ end
666
+ item
667
+ end
668
+
669
+ def await(item)
670
+ with_lock { await_locked(item) }
671
+ end
672
+
673
+ def await_locked(item)
674
+ real_deadline = @timeout && real_monotonic + @timeout
675
+ observed_progress = @progress_sequence
676
+ failed = true
677
+
678
+ begin
679
+ until item.ready
680
+ if real_deadline
681
+ remaining = real_deadline - real_monotonic
682
+ if remaining <= 0
683
+ fail_locked(StalledError.new(stalled_message_locked(item)))
684
+ next
685
+ end
686
+ Raw::CONDITION_WAIT.bind_call(@condition, @lock, remaining)
687
+ else
688
+ Raw::CONDITION_WAIT.bind_call(@condition, @lock)
689
+ end
690
+ current = @states[item.thread]
691
+ if !item.ready && current&.status == :runnable
692
+ @states[item.thread] = state(:blocked, item)
693
+ signal_locked
694
+ end
695
+ if real_deadline && @progress_sequence != observed_progress
696
+ real_deadline = real_monotonic + @timeout
697
+ observed_progress = @progress_sequence
698
+ end
699
+ reconcile_locked unless @fatal
700
+ end
701
+
702
+ raise item.error if item.error
703
+
704
+ failed = false
705
+ item.result
706
+ ensure
707
+ remove_blocker_locked(
708
+ item,
709
+ abandon_grant: failed || !item.ready || item.error
710
+ )
711
+ current = @states[item.thread]
712
+ @states[item.thread] = state(:running) if current && current.status != :exited
713
+ @active_waiter = nil if item.kind == :wait && @active_waiter.equal?(item.thread)
714
+ signal_locked
715
+ end
716
+ end
717
+
718
+ def reconcile_locked
719
+ return if @aborting || @fatal
720
+
721
+ if @active_waiter
722
+ waiter = @states[@active_waiter]&.blocker
723
+ if waiter && wait_quiescent_locked?(@active_waiter)
724
+ wake_blocker_locked(waiter, result: true)
725
+ return
726
+ end
727
+ end
728
+
729
+ return unless all_threads_durable_locked?
730
+
731
+ deadline = next_deadline_locked
732
+ if @root_active && deadline
733
+ @now_ns = deadline if deadline > @now_ns
734
+ due = blockers_locked.select { |item| item.deadline_ns && item.deadline_ns <= @now_ns }
735
+ due.each { |item| wake_blocker_locked(item, result: TIMEOUT) }
736
+ signal_locked
737
+ elsif active_thread_count_locked.positive?
738
+ error = unobserved_background_error_locked || DeadlockError.new(deadlock_message_locked)
739
+ fail_locked(error)
740
+ end
741
+ end
742
+
743
+ def wait_quiescent_locked?(waiter_thread)
744
+ return false unless @pending_children.zero?
745
+
746
+ @states.all? do |thread, thread_state|
747
+ thread.equal?(waiter_thread) ||
748
+ thread_state.status == :exited ||
749
+ thread_state.status == :blocked
750
+ end
751
+ end
752
+
753
+ def all_threads_durable_locked?
754
+ return false unless @pending_children.zero?
755
+ return false if active_thread_count_locked.zero?
756
+
757
+ @states.values.all? { |thread_state| %i[blocked waiting exited].include?(thread_state.status) }
758
+ end
759
+
760
+ def active_thread_count_locked
761
+ @states.values.count { |thread_state| thread_state.status != :exited } + @pending_children
762
+ end
763
+
764
+ def blockers_locked
765
+ @states.values.filter_map(&:blocker).reject(&:ready)
766
+ end
767
+
768
+ def next_deadline_locked
769
+ blockers_locked.filter_map(&:deadline_ns).min
770
+ end
771
+
772
+ def wake_blocker_locked(item, result: nil, error: nil)
773
+ return if item.ready
774
+
775
+ item.ready = true
776
+ item.result = result
777
+ item.error = error
778
+ current = @states[item.thread]
779
+ @states[item.thread] = state(:runnable, item) if current && current.status != :exited
780
+ signal_locked
781
+ end
782
+
783
+ def remove_blocker_locked(item, abandon_grant: true)
784
+ return unless item
785
+
786
+ if item.resource
787
+ waiters = @resource_waiters[item.resource]
788
+ waiters&.delete(item)
789
+ @resource_waiters.delete(item.resource) if waiters && waiters.empty?
790
+ end
791
+
792
+ if abandon_grant &&
793
+ @grants[item.resource].equal?(item.thread) &&
794
+ %i[mutex_lock monitor_lock].include?(item.kind)
795
+ @grants.delete(item.resource)
796
+ grant_next_locked(item.resource, item.kind)
797
+ end
798
+
799
+ abandon_queue_grant_locked(item) if abandon_grant && item.kind == :queue_pop
800
+ end
801
+
802
+ def waiters_for_locked(resource)
803
+ @resource_waiters[resource] ||= []
804
+ end
805
+
806
+ def first_waiter_locked(resource, *kinds)
807
+ @resource_waiters[resource]&.find { |item| !item.ready && kinds.include?(item.kind) }
808
+ end
809
+
810
+ def wake_resource_locked(resource, kinds:, result: nil, error: nil, all: false)
811
+ selected = Array(@resource_waiters[resource]).select { |item| !item.ready && kinds.include?(item.kind) }
812
+ selected = selected.first(1) unless all
813
+ selected.each { |item| wake_blocker_locked(item, result:, error:) }
814
+ selected
815
+ end
816
+
817
+ def grant_next_locked(resource, kind)
818
+ waiter = first_waiter_locked(resource, kind)
819
+ return unless waiter
820
+
821
+ @grants[resource] = waiter.thread
822
+ wake_blocker_locked(waiter, result: true)
823
+ end
824
+
825
+ def release_mutex_locked(mutex, thread)
826
+ unless @mutex_owners[mutex].equal?(thread)
827
+ raise ThreadError, "Attempt to unlock a mutex which is not locked by current thread"
828
+ end
829
+
830
+ Raw::MUTEX_UNLOCK.bind_call(mutex)
831
+ @mutex_owners.delete(mutex)
832
+ grant_next_locked(mutex, :mutex_lock)
833
+ end
834
+
835
+ def release_monitor_locked(monitor, thread)
836
+ owner, depth = @monitor_owners[monitor]
837
+ raise ThreadError, "current thread not owner" unless owner.equal?(thread)
838
+
839
+ Raw::MONITOR_EXIT.bind_call(monitor)
840
+ if depth == 1
841
+ @monitor_owners.delete(monitor)
842
+ grant_next_locked(monitor, :monitor_lock)
843
+ else
844
+ @monitor_owners[monitor] = [thread, depth - 1]
845
+ end
846
+ end
847
+
848
+ def fill_sized_queue_locked(queue)
849
+ loop do
850
+ waiter = first_waiter_locked(queue, :queue_push)
851
+ break unless waiter
852
+ break if Raw::QUEUE_CLOSED.bind_call(queue)
853
+
854
+ begin
855
+ Raw::SIZED_QUEUE_PUSH.bind_call(queue, waiter.result, true)
856
+ wake_blocker_locked(waiter, result: true)
857
+ grant_queued_item_locked(queue)
858
+ rescue ThreadError
859
+ break
860
+ end
861
+ end
862
+ end
863
+
864
+ def queue_grants_for_locked(queue)
865
+ @queue_grants[queue]
866
+ end
867
+
868
+ def grant_queued_item_locked(queue)
869
+ return if @queue_grants.key?(queue)
870
+ return unless raw_queue_length(queue).positive?
871
+
872
+ waiter = first_waiter_locked(queue, :queue_pop)
873
+ return unless waiter
874
+
875
+ @queue_grants[queue] = waiter
876
+ wake_blocker_locked(waiter, result: true)
877
+ end
878
+
879
+ def consume_queue_grant_locked(queue, grant)
880
+ return unless grant && @queue_grants[queue].equal?(grant)
881
+
882
+ @queue_grants.delete(queue)
883
+ settle_queue_waiters_locked(queue)
884
+ end
885
+
886
+ def abandon_queue_grant_locked(item)
887
+ queue = item.resource
888
+ return unless @queue_grants[queue].equal?(item)
889
+
890
+ @queue_grants.delete(queue)
891
+ settle_queue_waiters_locked(queue)
892
+ end
893
+
894
+ def settle_queue_waiters_locked(queue)
895
+ if raw_queue_length(queue).positive?
896
+ grant_queued_item_locked(queue)
897
+ elsif Raw::QUEUE_CLOSED.bind_call(queue)
898
+ wake_resource_locked(queue, kinds: [:queue_pop], result: CLOSED, all: true)
899
+ end
900
+ end
901
+
902
+ def raw_queue_pop(queue, non_block)
903
+ method = queue.is_a?(SizedQueue) ? Raw::SIZED_QUEUE_POP : Raw::QUEUE_POP
904
+ method.bind_call(queue, non_block)
905
+ end
906
+
907
+ def raw_queue_close(queue)
908
+ method = queue.is_a?(SizedQueue) ? Raw::SIZED_QUEUE_CLOSE : Raw::QUEUE_CLOSE
909
+ method.bind_call(queue)
910
+ end
911
+
912
+ def raw_queue_clear(queue)
913
+ method = queue.is_a?(SizedQueue) ? Raw::SIZED_QUEUE_CLEAR : Raw::QUEUE_CLEAR
914
+ method.bind_call(queue)
915
+ end
916
+
917
+ def raw_queue_length(queue)
918
+ method = queue.is_a?(SizedQueue) ? Raw::SIZED_QUEUE_LENGTH : Raw::QUEUE_LENGTH
919
+ method.bind_call(queue)
920
+ end
921
+
922
+ def release_owned_resources_locked(thread)
923
+ @queue_grants.keys.each do |queue|
924
+ item = @queue_grants[queue]
925
+ abandon_queue_grant_locked(item) if item.thread.equal?(thread)
926
+ end
927
+
928
+ @grants.keys.each do |resource|
929
+ next unless @grants[resource].equal?(thread)
930
+
931
+ kind = resource.is_a?(Monitor) ? :monitor_lock : :mutex_lock
932
+ @grants.delete(resource)
933
+ grant_next_locked(resource, kind)
934
+ end
935
+
936
+ @mutex_owners.keys.each do |mutex|
937
+ next unless @mutex_owners[mutex].equal?(thread)
938
+
939
+ begin
940
+ Raw::MUTEX_UNLOCK.bind_call(mutex)
941
+ rescue ThreadError
942
+ # The VM already released it while unwinding.
943
+ end
944
+ @mutex_owners.delete(mutex)
945
+ grant_next_locked(mutex, :mutex_lock)
946
+ end
947
+
948
+ @monitor_owners.keys.each do |monitor|
949
+ owner, depth = @monitor_owners[monitor]
950
+ next unless owner.equal?(thread)
951
+
952
+ depth.times do
953
+ Raw::MONITOR_EXIT.bind_call(monitor)
954
+ rescue ThreadError
955
+ break
956
+ end
957
+ @monitor_owners.delete(monitor)
958
+ grant_next_locked(monitor, :monitor_lock)
959
+ end
960
+ end
961
+
962
+ def thread_finished_locked?(thread)
963
+ @states[thread]&.status == :exited || !thread.alive?
964
+ end
965
+
966
+ def root_returned
967
+ with_lock do
968
+ @root_active = false
969
+ @states[@root] = state(:exited)
970
+ wake_resource_locked(@root, kinds: [:join], result: @root, all: true)
971
+ signal_locked
972
+ end
973
+ end
974
+
975
+ def finish
976
+ real_deadline = @timeout && real_monotonic + @timeout
977
+ observed_progress = with_lock { @progress_sequence }
978
+
979
+ loop do
980
+ done = with_lock do
981
+ reconcile_locked unless @fatal
982
+ break true if @pending_children.zero? && children_finished_locked?
983
+ break true if @fatal
984
+
985
+ if real_deadline
986
+ remaining = real_deadline - real_monotonic
987
+ if remaining <= 0
988
+ fail_locked(StalledError.new(stalled_finish_message_locked))
989
+ break true
990
+ end
991
+ Raw::CONDITION_WAIT.bind_call(@condition, @lock, remaining)
992
+ else
993
+ Raw::CONDITION_WAIT.bind_call(@condition, @lock)
994
+ end
995
+ if real_deadline && @progress_sequence != observed_progress
996
+ real_deadline = real_monotonic + @timeout
997
+ observed_progress = @progress_sequence
998
+ end
999
+ false
1000
+ end
1001
+ break if done
1002
+ end
1003
+
1004
+ error = with_lock { @fatal || unobserved_background_error_locked }
1005
+ abort_and_join if error
1006
+ join_finished_children
1007
+ raise error if error
1008
+ end
1009
+
1010
+ def children_finished_locked?
1011
+ @children.keys.all? { |thread| @states[thread]&.status == :exited }
1012
+ end
1013
+
1014
+ def abort_and_join
1015
+ children = with_lock do
1016
+ @aborting = true
1017
+ blockers_locked.each do |item|
1018
+ wake_blocker_locked(item, error: @fatal || Error.new("Synctest bubble aborted"))
1019
+ end
1020
+ signal_locked
1021
+ @children.keys
1022
+ end
1023
+
1024
+ children.each do |thread|
1025
+ next unless thread.alive?
1026
+
1027
+ Raw::THREAD_KILL.bind_call(thread)
1028
+ rescue ThreadError
1029
+ nil
1030
+ end
1031
+
1032
+ join_until(children, [@timeout || 0.5, 0.5].min)
1033
+ survivors = children.select(&:alive?)
1034
+ survivors.each do |thread|
1035
+ Raw::THREAD_RAISE.bind_call(thread, THREAD_ABORT, "Synctest bubble aborted")
1036
+ rescue ThreadError
1037
+ nil
1038
+ end
1039
+ join_until(survivors, 0.5)
1040
+
1041
+ survivors.select!(&:alive?)
1042
+ return if survivors.empty?
1043
+
1044
+ labels = survivors.map { |thread| thread_label(thread) }.join(", ")
1045
+ raise StalledError, "Synctest could not terminate bubble threads after abort: #{labels}"
1046
+ ensure
1047
+ @active = false
1048
+ end
1049
+
1050
+ def join_until(threads, timeout)
1051
+ deadline = real_monotonic + timeout
1052
+ threads.each do |thread|
1053
+ next unless thread.alive?
1054
+
1055
+ remaining = deadline - real_monotonic
1056
+ break unless remaining.positive?
1057
+
1058
+ Raw::THREAD_JOIN.bind_call(thread, remaining)
1059
+ rescue Exception # rubocop:disable Lint/RescueException
1060
+ nil
1061
+ end
1062
+ end
1063
+
1064
+ def join_finished_children
1065
+ @children.each_key do |thread|
1066
+ Raw::THREAD_JOIN.bind_call(thread)
1067
+ rescue
1068
+ # A child exception is reported through @background_errors.
1069
+ end
1070
+ end
1071
+
1072
+ def fail_locked(error)
1073
+ @fatal ||= error
1074
+ blockers_locked.each { |item| wake_blocker_locked(item, error: @fatal) }
1075
+ signal_locked
1076
+ end
1077
+
1078
+ def raise_background_error
1079
+ error = with_lock do
1080
+ source, background_error = unobserved_background_error_pair_locked
1081
+ if @fatal && (!source || !@fatal.equal?(background_error))
1082
+ @fatal
1083
+ elsif source
1084
+ observe_thread_error_locked(source)
1085
+ end
1086
+ end
1087
+ raise error if error
1088
+ end
1089
+
1090
+ def raise_join_error(thread)
1091
+ error = with_lock { observe_thread_error_locked(thread) }
1092
+ raise error if error
1093
+ end
1094
+
1095
+ def observe_thread_error_locked(thread)
1096
+ error = @background_errors[thread]
1097
+ return unless error
1098
+
1099
+ @observed_error_threads[thread] = true
1100
+ if @fatal.equal?(error)
1101
+ _source, @fatal = unobserved_background_error_pair_locked
1102
+ end
1103
+ error
1104
+ end
1105
+
1106
+ def unobserved_background_error_locked
1107
+ unobserved_background_error_pair_locked&.last
1108
+ end
1109
+
1110
+ def unobserved_background_error_pair_locked
1111
+ @background_errors.find { |thread, _error| !@observed_error_threads.key?(thread) }
1112
+ end
1113
+
1114
+ def interval_to_nanoseconds(value, negative_as_zero:)
1115
+ unless value.is_a?(Numeric) && !value.is_a?(Complex)
1116
+ raise TypeError, "can't convert #{value.class} into time interval"
1117
+ end
1118
+
1119
+ number = value.to_f
1120
+ unless number.finite?
1121
+ label = number.nan? ? "NaN" : "Inf"
1122
+ raise RangeError, "#{label} out of Time range"
1123
+ end
1124
+ if number.negative?
1125
+ return 0 if negative_as_zero
1126
+
1127
+ raise ArgumentError, "time interval must not be negative"
1128
+ end
1129
+
1130
+ rational = value.is_a?(Float) ? value.to_s.to_r : value.to_r
1131
+ (rational * NANOSECONDS).ceil
1132
+ rescue NoMethodError
1133
+ raise TypeError, "can't convert #{value.class} into time interval"
1134
+ end
1135
+
1136
+ def release_references
1137
+ with_lock do
1138
+ return if @children.each_key.any?(&:alive?)
1139
+
1140
+ @states.clear
1141
+ @children.clear
1142
+ @resource_waiters.clear
1143
+ @mutex_owners.clear
1144
+ @monitor_owners.clear
1145
+ @grants.clear
1146
+ @queue_grants.clear
1147
+ @background_errors.clear
1148
+ @observed_error_threads.clear
1149
+ @root = nil
1150
+ @active_waiter = nil
1151
+ end
1152
+ end
1153
+
1154
+ def aborting?
1155
+ with_lock { @aborting }
1156
+ end
1157
+
1158
+ def real_monotonic
1159
+ Raw::CLOCK_GETTIME.call(Process::CLOCK_MONOTONIC)
1160
+ end
1161
+
1162
+ def convert_nanoseconds(nanoseconds, unit)
1163
+ case unit
1164
+ when :nanosecond
1165
+ nanoseconds
1166
+ when :microsecond
1167
+ nanoseconds / 1_000
1168
+ when :millisecond
1169
+ nanoseconds / 1_000_000
1170
+ when :second
1171
+ nanoseconds / NANOSECONDS
1172
+ when :float_microsecond
1173
+ nanoseconds.fdiv(1_000)
1174
+ when :float_millisecond
1175
+ nanoseconds.fdiv(1_000_000)
1176
+ when :float_second
1177
+ nanoseconds.fdiv(NANOSECONDS)
1178
+ else
1179
+ raise ArgumentError, "unexpected unit: #{unit}"
1180
+ end
1181
+ end
1182
+
1183
+ def format_deadline(nanoseconds)
1184
+ "#{nanoseconds.fdiv(NANOSECONDS)}s"
1185
+ end
1186
+
1187
+ def deadlock_message_locked
1188
+ "Synctest bubble deadlocked: every active thread is durably blocked and no timer can fire\n" \
1189
+ "#{diagnostics_locked}"
1190
+ end
1191
+
1192
+ def stalled_message_locked(item)
1193
+ "Synctest bubble stalled while #{item.description}; a thread may be blocked on unsupported I/O or native code\n" \
1194
+ "#{diagnostics_locked}"
1195
+ end
1196
+
1197
+ def stalled_finish_message_locked
1198
+ "Synctest bubble stalled while waiting for child threads to exit; a thread may be blocked on unsupported I/O or native code\n" \
1199
+ "#{diagnostics_locked}"
1200
+ end
1201
+
1202
+ def diagnostics_locked
1203
+ lines = ["virtual time: #{format_deadline(@now_ns)}"]
1204
+ lines << "pending child threads: #{@pending_children}" if @pending_children.positive?
1205
+ @states.each do |thread, thread_state|
1206
+ next if thread_state.status == :exited
1207
+
1208
+ detail = thread_state.blocker&.description || thread_state.status
1209
+ lines << "- #{thread_label(thread)}: #{thread_state.status} (#{detail})"
1210
+ Array(thread.backtrace).first(5).each { |line| lines << " #{line}" }
1211
+ end
1212
+ lines.join("\n")
1213
+ end
1214
+
1215
+ def thread_label(thread)
1216
+ name = thread.name
1217
+ name ? "#{name} (0x#{thread.object_id.to_s(16)})" : "thread 0x#{thread.object_id.to_s(16)}"
1218
+ end
1219
+
1220
+ def signal_locked
1221
+ @progress_sequence += 1
1222
+ Raw::CONDITION_BROADCAST.bind_call(@condition)
1223
+ end
1224
+ end
1225
+ end