uringmachine 0.20.0 → 0.21.0

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.
@@ -2,11 +2,38 @@
2
2
 
3
3
  require_relative 'helper'
4
4
  require 'uringmachine/fiber_scheduler'
5
+ require 'securerandom'
6
+ require 'socket'
7
+
8
+ class MethodCallAuditor
9
+ attr_reader :calls
10
+
11
+ def initialize(target)
12
+ @target = target
13
+ @calls = []
14
+ end
15
+
16
+ def respond_to?(sym, include_all = false) = @target.respond_to?(sym, include_all)
17
+
18
+ def method_missing(sym, *args, &block)
19
+ res = @target.send(sym, *args, &block)
20
+ @calls << ({ sym:, args:, res:})
21
+ res
22
+ rescue => e
23
+ @calls << ({ sym:, args:, res: e})
24
+ raise
25
+ end
26
+
27
+ def last_call
28
+ calls.last
29
+ end
30
+ end
5
31
 
6
32
  class FiberSchedulerTest < UMBaseTest
7
33
  def setup
8
34
  super
9
- @scheduler = UM::FiberScheduler.new(@machine)
35
+ @raw_scheduler = UM::FiberScheduler.new(@machine)
36
+ @scheduler = MethodCallAuditor.new(@raw_scheduler)
10
37
  Fiber.set_scheduler(@scheduler)
11
38
  end
12
39
 
@@ -20,16 +47,6 @@ class FiberSchedulerTest < UMBaseTest
20
47
  assert_kind_of UringMachine, s.machine
21
48
  end
22
49
 
23
- def test_fiber_scheduler_post_fork
24
- Fiber.schedule {}
25
- assert_equal 1, @scheduler.fiber_map.size
26
-
27
- machine_before = @scheduler.machine
28
- @scheduler.post_fork
29
- refute_equal machine_before, @scheduler.machine
30
- assert_equal 0, @scheduler.fiber_map.size
31
- end
32
-
33
50
  def test_fiber_scheduler_spinning
34
51
  f1 = Fiber.schedule do
35
52
  sleep 0.001
@@ -41,26 +58,30 @@ class FiberSchedulerTest < UMBaseTest
41
58
 
42
59
  assert_kind_of Fiber, f1
43
60
  assert_kind_of Fiber, f2
61
+
62
+ assert_equal 2, @scheduler.calls.size
63
+ assert_equal [:fiber] * 2, @scheduler.calls.map { it[:sym] }
44
64
  assert_equal 2, @scheduler.fiber_map.size
45
65
 
46
66
  # close scheduler
47
67
  Fiber.set_scheduler nil
68
+ assert_equal :scheduler_close, @scheduler.last_call[:sym]
48
69
  GC.start
49
70
  assert_equal 0, @scheduler.fiber_map.size
50
71
  end
51
72
 
52
- def test_fiber_scheduler_basic_io
73
+ def test_fiber_scheduler_io_read_io_write
53
74
  i, o = IO.pipe
54
75
  buffer = []
55
76
 
56
77
  f1 = Fiber.schedule do
57
- sleep 0.001
78
+ sleep 0.01
58
79
  o.write 'foo'
59
80
  buffer << :f1
60
81
  end
61
82
 
62
83
  f2 = Fiber.schedule do
63
- sleep 0.002
84
+ sleep 0.02
64
85
  o.write 'bar'
65
86
  buffer << :f2
66
87
  o.close
@@ -74,11 +95,108 @@ class FiberSchedulerTest < UMBaseTest
74
95
  @scheduler.join
75
96
  assert_equal [true] * 3, [f1, f2, f3].map(&:done?)
76
97
  assert_equal [:f1, :f2, 'foobar'], buffer
98
+
99
+ assert_equal({
100
+ fiber: 3,
101
+ kernel_sleep: 2,
102
+ io_write: 2,
103
+ io_read: 3,
104
+ blocking_operation_wait: 1,
105
+ join: 1
106
+ }, @scheduler.calls.map { it[:sym] }.tally)
77
107
  ensure
78
108
  i.close rescue nil
79
109
  o.close rescue nil
80
110
  end
81
111
 
112
+ def test_io_read_with_timeout
113
+ i, o = IO.pipe
114
+ i.timeout = 0.01
115
+ buf = []
116
+
117
+ Fiber.schedule do
118
+ buf << i.read
119
+ rescue Timeout::Error
120
+ buf << :timeout
121
+ end
122
+ @scheduler.join
123
+ assert_equal [:timeout], buf
124
+
125
+ assert_equal({
126
+ fiber: 1,
127
+ io_read: 1,
128
+ join: 1
129
+ }, @scheduler.calls.map { it[:sym] }.tally)
130
+ end
131
+
132
+ def test_io_write_with_timeout
133
+ i, o = IO.pipe
134
+ o << ('*' * (1 << 16))
135
+ o.timeout = 0.01
136
+
137
+ buf = []
138
+
139
+ Fiber.schedule do
140
+ buf << o.write('!')
141
+ rescue Timeout::Error
142
+ buf << :timeout
143
+ end
144
+ @scheduler.join
145
+ assert_equal [:timeout], buf
146
+
147
+ assert_equal({
148
+ fiber: 1,
149
+ io_write: 1,
150
+ join: 1
151
+ }, @scheduler.calls.map { it[:sym] }.tally)
152
+ end
153
+
154
+ def test_fiber_io_pread
155
+ fn = "/tmp/#{SecureRandom.hex}"
156
+ IO.write(fn, 'foobar')
157
+
158
+ buf = nil
159
+ Fiber.schedule do
160
+ File.open(fn, 'r') do |f|
161
+ buf = f.pread(3, 2)
162
+ end
163
+ rescue => e
164
+ buf = e
165
+ end
166
+
167
+ @scheduler.join
168
+ assert_equal 'oba', buf
169
+ assert_equal({
170
+ fiber: 1,
171
+ blocking_operation_wait: 1,
172
+ io_pread: 1,
173
+ join: 1
174
+ }, @scheduler.calls.map { it[:sym] }.tally)
175
+ end
176
+
177
+ def test_fiber_scheduler_io_pwrite
178
+ fn = "/tmp/#{SecureRandom.hex}"
179
+ IO.write(fn, 'foobar')
180
+
181
+ res = nil
182
+ Fiber.schedule do
183
+ File.open(fn, 'r+') do |f|
184
+ res = f.pwrite('baz', 2)
185
+ end
186
+ end
187
+
188
+ @scheduler.join
189
+ assert_equal 3, res
190
+
191
+ assert_equal 'fobazr', IO.read(fn)
192
+ assert_equal({
193
+ fiber: 1,
194
+ blocking_operation_wait: 2,
195
+ io_pwrite: 1,
196
+ join: 1
197
+ }, @scheduler.calls.map { it[:sym] }.tally)
198
+ end
199
+
82
200
  def test_fiber_scheduler_sleep
83
201
  t0 = monotonic_clock
84
202
  assert_equal 0, machine.pending_count
@@ -92,9 +210,15 @@ class FiberSchedulerTest < UMBaseTest
92
210
  @scheduler.join
93
211
  t1 = monotonic_clock
94
212
  assert_in_range 0.02..0.025, t1 - t0
213
+
214
+ assert_equal({
215
+ fiber: 2,
216
+ kernel_sleep: 2,
217
+ join: 1
218
+ }, @scheduler.calls.map { it[:sym] }.tally)
95
219
  end
96
220
 
97
- def test_fiber_scheduler_lock
221
+ def test_fiber_scheduler_block
98
222
  mutex = Mutex.new
99
223
  buffer = []
100
224
  t0 = monotonic_clock
@@ -109,15 +233,24 @@ class FiberSchedulerTest < UMBaseTest
109
233
  end
110
234
  @scheduler.join
111
235
  t1 = monotonic_clock
112
- assert_in_range 0.01..0.015, t1 - t0
236
+ assert_in_range 0.01..0.020, t1 - t0
237
+ assert_equal({
238
+ fiber: 3,
239
+ kernel_sleep: 12,
240
+ block: 1,
241
+ unblock: 1,
242
+ join: 1
243
+ }, @scheduler.calls.map { it[:sym] }.tally)
113
244
  end
114
245
 
115
246
  def test_fiber_scheduler_process_wait
247
+ skip if !@scheduler.respond_to?(:process_wait)
248
+
116
249
  child_pid = nil
117
250
  status = nil
118
251
  f1 = Fiber.schedule do
119
252
  child_pid = fork {
120
- Fiber.scheduler.post_fork
253
+ Fiber.scheduler.process_fork
121
254
  Fiber.set_scheduler nil
122
255
  sleep(0.01);
123
256
  exit! 42
@@ -130,9 +263,378 @@ class FiberSchedulerTest < UMBaseTest
130
263
  assert_kind_of Process::Status, status
131
264
  assert_equal child_pid, status.pid
132
265
  assert_equal 42, status.exitstatus
266
+ assert_equal({
267
+ fiber: 1,
268
+ process_wait: 1,
269
+ join: 1
270
+ }, @scheduler.calls.map { it[:sym] }.tally)
133
271
  ensure
134
272
  if child_pid
135
273
  Process.wait(child_pid) rescue nil
136
274
  end
137
275
  end
276
+
277
+ # Currently the fiber scheduler doesn't have hooks for send/recv. The only
278
+ # hook that will be invoked is `io_wait`.
279
+ def test_fiber_scheduler_sockets
280
+ s1, s2 = UNIXSocket.pair(:STREAM)
281
+
282
+ buf = +''
283
+ sent = nil
284
+
285
+ assert_equal 0, machine.total_op_count
286
+ Fiber.schedule do
287
+ buf = s1.recv(12)
288
+ end
289
+ Fiber.schedule do
290
+ sent = s2.send('foobar', 0)
291
+ end
292
+
293
+ # In Ruby, sockets are by default non-blocking. The recv will cause io_wait
294
+ # to be invoked, the send should get through without needing to poll.
295
+ assert_equal 1, machine.total_op_count
296
+ @scheduler.join
297
+
298
+ assert_equal 6, sent
299
+ assert_equal 'foobar', buf
300
+ assert_equal({
301
+ fiber: 2,
302
+ io_wait: 1,
303
+ join: 1
304
+ }, @scheduler.calls.map { it[:sym] }.tally)
305
+ ensure
306
+ s1.close rescue nil
307
+ s2.close rescue nil
308
+ end
309
+
310
+ def test_fiber_scheduler_io_write_io_read
311
+ fn = "/tmp/#{SecureRandom.hex}"
312
+ Fiber.schedule do
313
+ IO.write(fn, 'foobar')
314
+ end
315
+ assert_equal 1, machine.total_op_count
316
+
317
+ buf = nil
318
+ Fiber.schedule do
319
+ buf = IO.read(fn)
320
+ end
321
+ assert_equal 2, machine.total_op_count
322
+
323
+ @scheduler.join
324
+ assert_equal 'foobar', buf
325
+ assert_equal({
326
+ fiber: 2,
327
+ blocking_operation_wait: 3,
328
+ io_read: 2,
329
+ join: 1
330
+ }, @scheduler.calls.map { it[:sym] }.tally)
331
+ end
332
+
333
+ def test_fiber_scheduler_file_io
334
+ fn = "/tmp/#{SecureRandom.hex}"
335
+ Fiber.schedule do
336
+ File.open(fn, 'w') { it.write 'foobar' }
337
+ end
338
+ assert_equal 1, machine.total_op_count
339
+
340
+ buf = nil
341
+ Fiber.schedule do
342
+ File.open(fn, 'r') { buf = it.read }
343
+ end
344
+ assert_equal 2, machine.total_op_count
345
+ @scheduler.join
346
+ assert_equal 'foobar', buf
347
+ assert_equal({
348
+ fiber: 2,
349
+ blocking_operation_wait: 3,
350
+ io_read: 2,
351
+ join: 1
352
+ }, @scheduler.calls.map { it[:sym] }.tally)
353
+ end
354
+
355
+ def test_fiber_scheduler_mutex
356
+ mutex = Mutex.new
357
+
358
+ buf = []
359
+ Fiber.schedule do
360
+ buf << 11
361
+ mutex.synchronize {
362
+ buf << [12, machine.total_op_count]
363
+ sleep 0.01
364
+ buf << [13, machine.total_op_count]
365
+ }
366
+ buf << 14
367
+ end
368
+ assert_equal 1, machine.total_op_count
369
+
370
+ Fiber.schedule do
371
+ buf << 21
372
+ mutex.synchronize {
373
+ buf << [22, machine.total_op_count]
374
+ sleep 0.01
375
+ buf << [23, machine.total_op_count]
376
+ }
377
+ buf << 24
378
+ end
379
+ assert_equal 1, machine.total_op_count
380
+
381
+ @scheduler.join
382
+ assert_equal [11, [12, 0], 21, [13, 2], 14, [22, 2], [23, 4], 24], buf
383
+ assert_equal({
384
+ fiber: 2,
385
+ kernel_sleep: 2,
386
+ block: 1,
387
+ unblock: 1,
388
+ join: 1
389
+ }, @scheduler.calls.map { it[:sym] }.tally)
390
+ end
391
+
392
+ def test_fiber_scheduler_queue_shift
393
+ queue = Queue.new
394
+
395
+ buf = []
396
+ Fiber.schedule do
397
+ buf << [11, machine.total_op_count]
398
+ buf << queue.shift
399
+ buf << [12, machine.total_op_count]
400
+ end
401
+ Fiber.schedule do
402
+ buf << [21, machine.total_op_count]
403
+ queue << :foo
404
+ buf << [22, machine.total_op_count]
405
+ end
406
+ assert_equal 0, machine.total_op_count
407
+ @scheduler.join
408
+
409
+ assert_equal [[11, 0], [21, 0], [22, 0], :foo, [12, 1]], buf
410
+ assert_equal({
411
+ fiber: 2,
412
+ block: 1,
413
+ unblock: 1,
414
+ join: 1
415
+ }, @scheduler.calls.map { it[:sym] }.tally)
416
+ end
417
+
418
+ def test_fiber_scheduler_queue_shift_with_timeout
419
+ queue = Queue.new
420
+
421
+ buf = []
422
+ Fiber.schedule do
423
+ buf << [11, machine.total_op_count]
424
+ buf << queue.shift(timeout: 0.01)
425
+ buf << [12, machine.total_op_count]
426
+ end
427
+ Fiber.schedule do
428
+ buf << [21, machine.total_op_count]
429
+ end
430
+ assert_equal 1, machine.total_op_count
431
+ @scheduler.join
432
+
433
+ assert_equal [[11, 0], [21, 1], nil, [12, 2]], buf
434
+ assert_equal({
435
+ fiber: 2,
436
+ block: 1,
437
+ join: 1
438
+ }, @scheduler.calls.map { it[:sym] }.tally)
439
+ end
440
+
441
+ def test_fiber_scheduler_thread_join
442
+ thread = Thread.new do
443
+ sleep 0.1
444
+ end
445
+ Fiber.schedule do
446
+ thread.join
447
+ end
448
+
449
+ # No ops are issued, except for a NOP SQE used to wakeup the waiting thread.
450
+ assert_equal 0, machine.total_op_count
451
+
452
+ @scheduler.join
453
+ assert_equal 1, machine.total_op_count
454
+ assert_equal({
455
+ fiber: 1,
456
+ block: 1,
457
+ unblock: 1,
458
+ join: 1
459
+ }, @scheduler.calls.map { it[:sym] }.tally)
460
+ end
461
+
462
+ def test_fiber_scheduler_system
463
+ skip if !@scheduler.respond_to?(:process_wait)
464
+
465
+ buf = []
466
+ Fiber.schedule do
467
+ buf << system('sleep 0.01')
468
+ end
469
+ @scheduler.join
470
+ assert_equal [true], buf
471
+ assert_equal({
472
+ fiber: 1,
473
+ process_wait: 1,
474
+ join: 1
475
+ }, @scheduler.calls.map { it[:sym] }.tally)
476
+ ensure
477
+ Process.wait(0, Process::WNOHANG) rescue nil
478
+ end
479
+
480
+ def test_fiber_scheduler_cmd
481
+ skip if !@scheduler.respond_to?(:process_wait)
482
+
483
+ buf = []
484
+ Fiber.schedule do
485
+ buf << `echo 'foo'`
486
+ end
487
+ assert_equal 1, machine.total_op_count
488
+ @scheduler.join
489
+ assert_equal ["foo\n"], buf
490
+ assert_equal({
491
+ fiber: 1,
492
+ io_read: 2,
493
+ process_wait: 1,
494
+ join: 1
495
+ }, @scheduler.calls.map { it[:sym] }.tally)
496
+ ensure
497
+ Process.wait(0, Process::WNOHANG) rescue nil
498
+ end
499
+
500
+ def test_fiber_scheduler_popen
501
+ skip if !@scheduler.respond_to?(:process_wait)
502
+
503
+ buf = []
504
+ Fiber.schedule do
505
+ IO.popen('ruby', 'r+') do |pipe|
506
+ buf << [11, machine.total_op_count]
507
+ pipe.puts 'puts "bar"'
508
+ buf << [12, machine.total_op_count]
509
+ pipe.close_write
510
+ buf << [13, pipe.gets.chomp, machine.total_op_count]
511
+ end
512
+ end
513
+ assert_equal 1, machine.total_op_count
514
+ @scheduler.join
515
+ assert_equal [[11, 0], [12, 3], [13, "bar", 5]], buf
516
+ assert_equal({
517
+ fiber: 1,
518
+ io_write: 2,
519
+ io_read: 1,
520
+ blocking_operation_wait: 1,
521
+ process_wait: 1,
522
+ join: 1
523
+ }, @scheduler.calls.map { it[:sym] }.tally)
524
+ ensure
525
+ Process.wait(0, Process::WNOHANG) rescue nil
526
+ end
527
+
528
+ def test_fiber_scheduler_fiber_interrupt
529
+ r, w = IO.pipe
530
+ w << 'foo'
531
+
532
+ exception = nil
533
+ Fiber.schedule do
534
+ r.read
535
+ rescue Exception => e
536
+ exception = e
537
+ end
538
+ assert_equal 1, machine.total_op_count
539
+ machine.snooze
540
+ Thread.new {
541
+ r.close
542
+ }
543
+ @scheduler.join
544
+ assert_kind_of IOError, exception
545
+ assert_equal({
546
+ fiber: 1,
547
+ io_read: 2,
548
+ fiber_interrupt: 1,
549
+ join: 1
550
+ }, @scheduler.calls.map { it[:sym] }.tally)
551
+ ensure
552
+ r.close rescue nil
553
+ w.close rescue nil
554
+ end
555
+
556
+ def test_fiber_scheduler_address_resolve
557
+ addrs = nil
558
+ Fiber.schedule do
559
+ addrs = Addrinfo.getaddrinfo("localhost", 80, Socket::AF_INET, :STREAM)
560
+ end
561
+ assert_equal 1, machine.total_op_count
562
+ @scheduler.join
563
+ assert_kind_of Array, addrs
564
+ addr = addrs.first
565
+ assert_kind_of Addrinfo, addr
566
+ assert_includes ['127.0.0.1', '::1'], addr.ip_address
567
+ assert_equal({
568
+ fiber: 1,
569
+ io_read: 2,
570
+ blocking_operation_wait: 1,
571
+ address_resolve: 1,
572
+ join: 1
573
+ }, @scheduler.calls.map { it[:sym] }.tally)
574
+ end
575
+
576
+ def test_fiber_scheduler_timeout_after
577
+ res = nil
578
+ Fiber.schedule do
579
+ Timeout.timeout(0.05) do
580
+ sleep 1
581
+ end
582
+ res = true
583
+ rescue => e
584
+ res = e
585
+ end
586
+ @scheduler.join
587
+ assert_equal 3, machine.total_op_count
588
+ assert_kind_of Timeout::Error, res
589
+ assert_equal({
590
+ fiber: 1,
591
+ timeout_after: 1,
592
+ kernel_sleep: 1,
593
+ join: 1
594
+ }, @scheduler.calls.map { it[:sym] }.tally)
595
+ end
596
+
597
+ def test_fiber_scheduler_io_select
598
+ r, w = IO.pipe
599
+ buf = []
600
+
601
+ Fiber.schedule do
602
+ buf << IO.select([r], [], [])
603
+ buf << IO.select([], [w], [])
604
+ end
605
+ @machine.snooze
606
+ w << 'foo'
607
+ @machine.snooze
608
+ assert_equal [[[r], [], []]], buf
609
+ @machine.snooze
610
+ @scheduler.join
611
+ assert_equal [[[r], [], []], [[], [w], []]], buf
612
+ ensure
613
+ r.close rescue nil
614
+ w.close rescue nil
615
+ end
616
+
617
+ def test_fiber_scheduler_blocking_operation_wait_single_issuer
618
+ buf = []
619
+ (1..10).each { |i|
620
+ op = -> { i * 10}
621
+ buf << @scheduler.blocking_operation_wait(op)
622
+ sleep 0.01
623
+ @machine.snooze
624
+ }
625
+ assert_equal (1..10).map { it * 10 }, buf
626
+
627
+ buf = []
628
+ (1..20).each { |i|
629
+ op = -> { i * 10}
630
+ Fiber.schedule do
631
+ sleep 0.001
632
+ buf << @scheduler.blocking_operation_wait(op)
633
+ sleep 0.001
634
+ end
635
+ }
636
+ @scheduler.join
637
+
638
+ assert_equal (1..20).map { it * 10 }, buf.sort
639
+ end
138
640
  end