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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +235 -0
- data/lib/synctest/bubble.rb +1225 -0
- data/lib/synctest/errors.rb +27 -0
- data/lib/synctest/patches.rb +608 -0
- data/lib/synctest/raw.rb +46 -0
- data/lib/synctest/version.rb +5 -0
- data/lib/synctest.rb +113 -0
- metadata +135 -0
|
@@ -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
|