uringmachine 0.21.0 → 0.22.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +14 -0
  4. data/TODO.md +144 -0
  5. data/benchmark/README.md +173 -0
  6. data/benchmark/bm_io_pipe.rb +70 -0
  7. data/benchmark/bm_io_socketpair.rb +71 -0
  8. data/benchmark/bm_mutex_cpu.rb +57 -0
  9. data/benchmark/bm_mutex_io.rb +64 -0
  10. data/benchmark/bm_pg_client.rb +109 -0
  11. data/benchmark/bm_queue.rb +76 -0
  12. data/benchmark/chart.png +0 -0
  13. data/benchmark/common.rb +135 -0
  14. data/benchmark/dns_client.rb +47 -0
  15. data/{examples/bm_http_parse.rb → benchmark/http_parse.rb} +1 -1
  16. data/benchmark/run_bm.rb +8 -0
  17. data/benchmark/sqlite.rb +108 -0
  18. data/{examples/bm_write.rb → benchmark/write.rb} +4 -4
  19. data/ext/um/um.c +189 -100
  20. data/ext/um/um.h +36 -10
  21. data/ext/um/um_async_op.c +1 -1
  22. data/ext/um/um_class.c +87 -13
  23. data/ext/um/um_op.c +6 -0
  24. data/ext/um/um_sync.c +2 -2
  25. data/ext/um/um_utils.c +16 -0
  26. data/grant-2025/journal.md +118 -1
  27. data/grant-2025/tasks.md +48 -22
  28. data/lib/uringmachine/actor.rb +8 -0
  29. data/lib/uringmachine/dns_resolver.rb +1 -2
  30. data/lib/uringmachine/fiber_scheduler.rb +127 -81
  31. data/lib/uringmachine/version.rb +1 -1
  32. data/lib/uringmachine.rb +32 -3
  33. data/test/helper.rb +7 -18
  34. data/test/test_actor.rb +12 -3
  35. data/test/test_async_op.rb +10 -10
  36. data/test/test_fiber.rb +84 -1
  37. data/test/test_fiber_scheduler.rb +950 -47
  38. data/test/test_um.rb +297 -120
  39. data/uringmachine.gemspec +2 -1
  40. metadata +38 -16
  41. data/examples/bm_fileno.rb +0 -33
  42. data/examples/bm_queue.rb +0 -111
  43. data/examples/bm_side_running.rb +0 -83
  44. data/examples/bm_sqlite.rb +0 -89
  45. data/examples/dns_client.rb +0 -12
  46. /data/{examples/bm_mutex.rb → benchmark/mutex.rb} +0 -0
  47. /data/{examples/bm_mutex_single.rb → benchmark/mutex_single.rb} +0 -0
  48. /data/{examples/bm_send.rb → benchmark/send.rb} +0 -0
  49. /data/{examples/bm_snooze.rb → benchmark/snooze.rb} +0 -0
@@ -4,6 +4,8 @@ require_relative 'helper'
4
4
  require 'uringmachine/fiber_scheduler'
5
5
  require 'securerandom'
6
6
  require 'socket'
7
+ require 'net/http'
8
+ require 'json'
7
9
 
8
10
  class MethodCallAuditor
9
11
  attr_reader :calls
@@ -19,7 +21,7 @@ class MethodCallAuditor
19
21
  res = @target.send(sym, *args, &block)
20
22
  @calls << ({ sym:, args:, res:})
21
23
  res
22
- rescue => e
24
+ rescue Exception => e
23
25
  @calls << ({ sym:, args:, res: e})
24
26
  raise
25
27
  end
@@ -39,7 +41,7 @@ class FiberSchedulerTest < UMBaseTest
39
41
 
40
42
  def teardown
41
43
  Fiber.set_scheduler(nil)
42
- GC.start
44
+ super
43
45
  end
44
46
 
45
47
  def test_fiber_scheduler_initialize_without_machine
@@ -101,7 +103,7 @@ class FiberSchedulerTest < UMBaseTest
101
103
  kernel_sleep: 2,
102
104
  io_write: 2,
103
105
  io_read: 3,
104
- blocking_operation_wait: 1,
106
+ io_close: 1,
105
107
  join: 1
106
108
  }, @scheduler.calls.map { it[:sym] }.tally)
107
109
  ensure
@@ -127,6 +129,9 @@ class FiberSchedulerTest < UMBaseTest
127
129
  io_read: 1,
128
130
  join: 1
129
131
  }, @scheduler.calls.map { it[:sym] }.tally)
132
+ ensure
133
+ i.close rescue nil
134
+ o.close rescue nil
130
135
  end
131
136
 
132
137
  def test_io_write_with_timeout
@@ -149,10 +154,34 @@ class FiberSchedulerTest < UMBaseTest
149
154
  io_write: 1,
150
155
  join: 1
151
156
  }, @scheduler.calls.map { it[:sym] }.tally)
157
+ ensure
158
+ i.close rescue nil
159
+ o.close rescue nil
160
+ end
161
+
162
+ def test_io_write_ioerror
163
+ i, o = IO.pipe
164
+ buf = []
165
+
166
+ Fiber.schedule do
167
+ buf << i.write('!')
168
+ rescue SystemCallError, IOError => e
169
+ buf << e
170
+ end
171
+ @scheduler.join
172
+ assert_kind_of IOError, buf.first
173
+
174
+ assert_equal({
175
+ fiber: 1,
176
+ join: 1
177
+ }, @scheduler.calls.map { it[:sym] }.tally)
178
+ ensure
179
+ i.close rescue nil
180
+ o.close rescue nil
152
181
  end
153
182
 
154
183
  def test_fiber_io_pread
155
- fn = "/tmp/#{SecureRandom.hex}"
184
+ fn = "/tmp/um_#{SecureRandom.hex}"
156
185
  IO.write(fn, 'foobar')
157
186
 
158
187
  buf = nil
@@ -170,12 +199,15 @@ class FiberSchedulerTest < UMBaseTest
170
199
  fiber: 1,
171
200
  blocking_operation_wait: 1,
172
201
  io_pread: 1,
202
+ io_close: 1,
173
203
  join: 1
174
204
  }, @scheduler.calls.map { it[:sym] }.tally)
205
+ ensure
206
+ FileUtils.rm(fn) rescue nil
175
207
  end
176
208
 
177
209
  def test_fiber_scheduler_io_pwrite
178
- fn = "/tmp/#{SecureRandom.hex}"
210
+ fn = "/tmp/um_#{SecureRandom.hex}"
179
211
  IO.write(fn, 'foobar')
180
212
 
181
213
  res = nil
@@ -191,22 +223,25 @@ class FiberSchedulerTest < UMBaseTest
191
223
  assert_equal 'fobazr', IO.read(fn)
192
224
  assert_equal({
193
225
  fiber: 1,
194
- blocking_operation_wait: 2,
226
+ blocking_operation_wait: 1,
195
227
  io_pwrite: 1,
228
+ io_close: 1,
196
229
  join: 1
197
230
  }, @scheduler.calls.map { it[:sym] }.tally)
231
+ ensure
232
+ FileUtils.rm(fn) rescue nil
198
233
  end
199
234
 
200
235
  def test_fiber_scheduler_sleep
201
236
  t0 = monotonic_clock
202
- assert_equal 0, machine.pending_count
237
+ assert_equal 0, machine.metrics[:ops_pending]
203
238
  Fiber.schedule do
204
239
  sleep(0.01)
205
240
  end
206
241
  Fiber.schedule do
207
242
  sleep(0.02)
208
243
  end
209
- assert_equal 2, machine.pending_count
244
+ assert_equal 2, machine.metrics[:ops_pending]
210
245
  @scheduler.join
211
246
  t1 = monotonic_clock
212
247
  assert_in_range 0.02..0.025, t1 - t0
@@ -244,7 +279,8 @@ class FiberSchedulerTest < UMBaseTest
244
279
  end
245
280
 
246
281
  def test_fiber_scheduler_process_wait
247
- skip if !@scheduler.respond_to?(:process_wait)
282
+ skip("Missing #process_wait hook (no rb_process_status_new)") \
283
+ if !@scheduler.respond_to?(:process_wait)
248
284
 
249
285
  child_pid = nil
250
286
  status = nil
@@ -282,7 +318,7 @@ class FiberSchedulerTest < UMBaseTest
282
318
  buf = +''
283
319
  sent = nil
284
320
 
285
- assert_equal 0, machine.total_op_count
321
+ assert_equal 0, machine.metrics[:total_ops]
286
322
  Fiber.schedule do
287
323
  buf = s1.recv(12)
288
324
  end
@@ -292,7 +328,7 @@ class FiberSchedulerTest < UMBaseTest
292
328
 
293
329
  # In Ruby, sockets are by default non-blocking. The recv will cause io_wait
294
330
  # to be invoked, the send should get through without needing to poll.
295
- assert_equal 1, machine.total_op_count
331
+ assert_equal 1, machine.metrics[:total_ops]
296
332
  @scheduler.join
297
333
 
298
334
  assert_equal 6, sent
@@ -308,48 +344,58 @@ class FiberSchedulerTest < UMBaseTest
308
344
  end
309
345
 
310
346
  def test_fiber_scheduler_io_write_io_read
311
- fn = "/tmp/#{SecureRandom.hex}"
347
+ fn = "/tmp/um_#{SecureRandom.hex}"
312
348
  Fiber.schedule do
313
349
  IO.write(fn, 'foobar')
314
350
  end
315
- assert_equal 1, machine.total_op_count
351
+ assert_equal 1, machine.metrics[:total_ops]
316
352
 
317
353
  buf = nil
318
354
  Fiber.schedule do
355
+ sleep 0.001
319
356
  buf = IO.read(fn)
320
357
  end
321
- assert_equal 2, machine.total_op_count
358
+ assert_equal 2, machine.metrics[:total_ops]
322
359
 
323
360
  @scheduler.join
324
361
  assert_equal 'foobar', buf
325
362
  assert_equal({
326
363
  fiber: 2,
327
- blocking_operation_wait: 3,
364
+ blocking_operation_wait: 2,
328
365
  io_read: 2,
366
+ io_close: 2,
367
+ kernel_sleep: 1,
329
368
  join: 1
330
369
  }, @scheduler.calls.map { it[:sym] }.tally)
370
+ ensure
371
+ FileUtils.rm(fn) rescue nil
331
372
  end
332
373
 
333
374
  def test_fiber_scheduler_file_io
334
- fn = "/tmp/#{SecureRandom.hex}"
375
+ fn = "/tmp/um_#{SecureRandom.hex}"
335
376
  Fiber.schedule do
336
377
  File.open(fn, 'w') { it.write 'foobar' }
337
378
  end
338
- assert_equal 1, machine.total_op_count
379
+ assert_equal 1, machine.metrics[:total_ops]
339
380
 
340
381
  buf = nil
341
382
  Fiber.schedule do
383
+ sleep 0.001
342
384
  File.open(fn, 'r') { buf = it.read }
343
385
  end
344
- assert_equal 2, machine.total_op_count
386
+ assert_equal 2, machine.metrics[:total_ops]
345
387
  @scheduler.join
346
388
  assert_equal 'foobar', buf
347
389
  assert_equal({
348
390
  fiber: 2,
349
- blocking_operation_wait: 3,
391
+ blocking_operation_wait: 2,
350
392
  io_read: 2,
393
+ io_close: 2,
394
+ kernel_sleep: 1,
351
395
  join: 1
352
396
  }, @scheduler.calls.map { it[:sym] }.tally)
397
+ ensure
398
+ FileUtils.rm(fn) rescue nil
353
399
  end
354
400
 
355
401
  def test_fiber_scheduler_mutex
@@ -359,24 +405,24 @@ class FiberSchedulerTest < UMBaseTest
359
405
  Fiber.schedule do
360
406
  buf << 11
361
407
  mutex.synchronize {
362
- buf << [12, machine.total_op_count]
408
+ buf << [12, machine.metrics[:total_ops]]
363
409
  sleep 0.01
364
- buf << [13, machine.total_op_count]
410
+ buf << [13, machine.metrics[:total_ops]]
365
411
  }
366
412
  buf << 14
367
413
  end
368
- assert_equal 1, machine.total_op_count
414
+ assert_equal 1, machine.metrics[:total_ops]
369
415
 
370
416
  Fiber.schedule do
371
417
  buf << 21
372
418
  mutex.synchronize {
373
- buf << [22, machine.total_op_count]
419
+ buf << [22, machine.metrics[:total_ops]]
374
420
  sleep 0.01
375
- buf << [23, machine.total_op_count]
421
+ buf << [23, machine.metrics[:total_ops]]
376
422
  }
377
423
  buf << 24
378
424
  end
379
- assert_equal 1, machine.total_op_count
425
+ assert_equal 1, machine.metrics[:total_ops]
380
426
 
381
427
  @scheduler.join
382
428
  assert_equal [11, [12, 0], 21, [13, 2], 14, [22, 2], [23, 4], 24], buf
@@ -394,16 +440,16 @@ class FiberSchedulerTest < UMBaseTest
394
440
 
395
441
  buf = []
396
442
  Fiber.schedule do
397
- buf << [11, machine.total_op_count]
443
+ buf << [11, machine.metrics[:total_ops]]
398
444
  buf << queue.shift
399
- buf << [12, machine.total_op_count]
445
+ buf << [12, machine.metrics[:total_ops]]
400
446
  end
401
447
  Fiber.schedule do
402
- buf << [21, machine.total_op_count]
448
+ buf << [21, machine.metrics[:total_ops]]
403
449
  queue << :foo
404
- buf << [22, machine.total_op_count]
450
+ buf << [22, machine.metrics[:total_ops]]
405
451
  end
406
- assert_equal 0, machine.total_op_count
452
+ assert_equal 0, machine.metrics[:total_ops]
407
453
  @scheduler.join
408
454
 
409
455
  assert_equal [[11, 0], [21, 0], [22, 0], :foo, [12, 1]], buf
@@ -420,14 +466,14 @@ class FiberSchedulerTest < UMBaseTest
420
466
 
421
467
  buf = []
422
468
  Fiber.schedule do
423
- buf << [11, machine.total_op_count]
469
+ buf << [11, machine.metrics[:total_ops]]
424
470
  buf << queue.shift(timeout: 0.01)
425
- buf << [12, machine.total_op_count]
471
+ buf << [12, machine.metrics[:total_ops]]
426
472
  end
427
473
  Fiber.schedule do
428
- buf << [21, machine.total_op_count]
474
+ buf << [21, machine.metrics[:total_ops]]
429
475
  end
430
- assert_equal 1, machine.total_op_count
476
+ assert_equal 1, machine.metrics[:total_ops]
431
477
  @scheduler.join
432
478
 
433
479
  assert_equal [[11, 0], [21, 1], nil, [12, 2]], buf
@@ -447,10 +493,10 @@ class FiberSchedulerTest < UMBaseTest
447
493
  end
448
494
 
449
495
  # No ops are issued, except for a NOP SQE used to wakeup the waiting thread.
450
- assert_equal 0, machine.total_op_count
496
+ assert_equal 0, machine.metrics[:total_ops]
451
497
 
452
498
  @scheduler.join
453
- assert_equal 1, machine.total_op_count
499
+ assert_equal 1, machine.metrics[:total_ops]
454
500
  assert_equal({
455
501
  fiber: 1,
456
502
  block: 1,
@@ -460,7 +506,8 @@ class FiberSchedulerTest < UMBaseTest
460
506
  end
461
507
 
462
508
  def test_fiber_scheduler_system
463
- skip if !@scheduler.respond_to?(:process_wait)
509
+ skip("Missing #process_wait hook (no rb_process_status_new)") \
510
+ if !@scheduler.respond_to?(:process_wait)
464
511
 
465
512
  buf = []
466
513
  Fiber.schedule do
@@ -478,13 +525,14 @@ class FiberSchedulerTest < UMBaseTest
478
525
  end
479
526
 
480
527
  def test_fiber_scheduler_cmd
481
- skip if !@scheduler.respond_to?(:process_wait)
528
+ skip("Missing #process_wait hook (no rb_process_status_new)") \
529
+ if !@scheduler.respond_to?(:process_wait)
482
530
 
483
531
  buf = []
484
532
  Fiber.schedule do
485
533
  buf << `echo 'foo'`
486
534
  end
487
- assert_equal 1, machine.total_op_count
535
+ assert_equal 1, machine.metrics[:total_ops]
488
536
  @scheduler.join
489
537
  assert_equal ["foo\n"], buf
490
538
  assert_equal({
@@ -498,19 +546,20 @@ class FiberSchedulerTest < UMBaseTest
498
546
  end
499
547
 
500
548
  def test_fiber_scheduler_popen
501
- skip if !@scheduler.respond_to?(:process_wait)
549
+ skip("Missing #process_wait hook (no rb_process_status_new)") \
550
+ if !@scheduler.respond_to?(:process_wait)
502
551
 
503
552
  buf = []
504
553
  Fiber.schedule do
505
554
  IO.popen('ruby', 'r+') do |pipe|
506
- buf << [11, machine.total_op_count]
555
+ buf << [11, machine.metrics[:total_ops]]
507
556
  pipe.puts 'puts "bar"'
508
- buf << [12, machine.total_op_count]
557
+ buf << [12, machine.metrics[:total_ops]]
509
558
  pipe.close_write
510
- buf << [13, pipe.gets.chomp, machine.total_op_count]
559
+ buf << [13, pipe.gets.chomp, machine.metrics[:total_ops]]
511
560
  end
512
561
  end
513
- assert_equal 1, machine.total_op_count
562
+ assert_equal 1, machine.metrics[:total_ops]
514
563
  @scheduler.join
515
564
  assert_equal [[11, 0], [12, 3], [13, "bar", 5]], buf
516
565
  assert_equal({
@@ -535,7 +584,7 @@ class FiberSchedulerTest < UMBaseTest
535
584
  rescue Exception => e
536
585
  exception = e
537
586
  end
538
- assert_equal 1, machine.total_op_count
587
+ assert_equal 1, machine.metrics[:total_ops]
539
588
  machine.snooze
540
589
  Thread.new {
541
590
  r.close
@@ -558,7 +607,7 @@ class FiberSchedulerTest < UMBaseTest
558
607
  Fiber.schedule do
559
608
  addrs = Addrinfo.getaddrinfo("localhost", 80, Socket::AF_INET, :STREAM)
560
609
  end
561
- assert_equal 1, machine.total_op_count
610
+ assert_equal 1, machine.metrics[:total_ops]
562
611
  @scheduler.join
563
612
  assert_kind_of Array, addrs
564
613
  addr = addrs.first
@@ -567,6 +616,7 @@ class FiberSchedulerTest < UMBaseTest
567
616
  assert_equal({
568
617
  fiber: 1,
569
618
  io_read: 2,
619
+ io_close: 1,
570
620
  blocking_operation_wait: 1,
571
621
  address_resolve: 1,
572
622
  join: 1
@@ -584,7 +634,7 @@ class FiberSchedulerTest < UMBaseTest
584
634
  res = e
585
635
  end
586
636
  @scheduler.join
587
- assert_equal 3, machine.total_op_count
637
+ assert_equal 3, machine.metrics[:total_ops]
588
638
  assert_kind_of Timeout::Error, res
589
639
  assert_equal({
590
640
  fiber: 1,
@@ -638,3 +688,856 @@ class FiberSchedulerTest < UMBaseTest
638
688
  assert_equal (1..20).map { it * 10 }, buf.sort
639
689
  end
640
690
  end
691
+
692
+ class FiberSchedulerIOClassMethodsTest < UMBaseTest
693
+ def setup
694
+ super
695
+ @raw_scheduler = UM::FiberScheduler.new(@machine)
696
+ @scheduler = MethodCallAuditor.new(@raw_scheduler)
697
+ Fiber.set_scheduler(@scheduler)
698
+ @fn = "/tmp/um_#{SecureRandom.hex}"
699
+ IO.write(@fn, '===')
700
+ end
701
+
702
+ def teardown
703
+ FileUtils.rm(@fn) rescue nil
704
+ Fiber.set_scheduler(nil)
705
+ super
706
+ end
707
+
708
+ def test_IO_s_binread
709
+ ret = nil
710
+ Fiber.schedule do
711
+ ret = IO.binread(@fn)
712
+ end
713
+ @scheduler.join
714
+ assert_equal '===', ret
715
+ assert_equal({
716
+ fiber: 1,
717
+ io_read: 2,
718
+ blocking_operation_wait: 1,
719
+ io_close: 1,
720
+ join: 1
721
+ }, @scheduler.calls.map { it[:sym] }.tally)
722
+ end
723
+
724
+ def test_IO_s_binwrite
725
+ ret = nil
726
+ Fiber.schedule do
727
+ ret = IO.binwrite(@fn, '***', 2)
728
+ end
729
+ @scheduler.join
730
+ assert_equal 3, ret
731
+ assert_equal '==***', IO.read(@fn)
732
+ assert_equal({
733
+ fiber: 1,
734
+ blocking_operation_wait: 1,
735
+ io_close: 1,
736
+ join: 1
737
+ }, @scheduler.calls.map { it[:sym] }.tally)
738
+ end
739
+
740
+ def test_IO_s_copy_stream
741
+ fn2 = "/tmp/um_#{SecureRandom.hex}"
742
+ ret = nil
743
+ Fiber.schedule do
744
+ ret = IO.copy_stream(@fn, fn2)
745
+ end
746
+ @scheduler.join
747
+ assert_equal 3, ret
748
+ assert_equal '===', IO.read(fn2)
749
+ assert_equal({
750
+ fiber: 1,
751
+ blocking_operation_wait: 3,
752
+ io_close: 2,
753
+ join: 1
754
+ }, @scheduler.calls.map { it[:sym] }.tally)
755
+ ensure
756
+ FileUtils.rm(fn2) rescue nil
757
+ end
758
+
759
+ def test_IO_s_foreach
760
+ buf = []
761
+ Fiber.schedule do
762
+ IO.foreach(@fn) { buf << it }
763
+ end
764
+ @scheduler.join
765
+ assert_equal ['==='], buf
766
+ assert_equal({
767
+ fiber: 1,
768
+ io_read: 3,
769
+ blocking_operation_wait: 1,
770
+ io_close: 1,
771
+ join: 1
772
+ }, @scheduler.calls.map { it[:sym] }.tally)
773
+ end
774
+
775
+ def test_IO_s_foreach
776
+ buf = []
777
+ Fiber.schedule do
778
+ IO.foreach(@fn) { buf << it }
779
+ end
780
+ @scheduler.join
781
+ assert_equal ['==='], buf
782
+ assert_equal({
783
+ fiber: 1,
784
+ io_read: 3,
785
+ blocking_operation_wait: 1,
786
+ io_close: 1,
787
+ join: 1
788
+ }, @scheduler.calls.map { it[:sym] }.tally)
789
+ end
790
+
791
+ def test_IO_s_popen
792
+ ret = nil
793
+ Fiber.schedule do
794
+ IO.popen("cat #{@fn}") { ret = it.read }
795
+ end
796
+ @scheduler.join
797
+ assert_equal '===', ret
798
+ assert_equal({
799
+ fiber: 1,
800
+ io_read: 2,
801
+ io_close: 1,
802
+ join: 1
803
+ }, @scheduler.calls.map { it[:sym] }.tally)
804
+ end
805
+
806
+ def test_IO_s_read
807
+ ret = nil
808
+ Fiber.schedule do
809
+ ret = IO.read(@fn)
810
+ end
811
+ @scheduler.join
812
+ assert_equal '===', ret
813
+ assert_equal({
814
+ fiber: 1,
815
+ io_read: 2,
816
+ blocking_operation_wait: 1,
817
+ io_close: 1,
818
+ join: 1
819
+ }, @scheduler.calls.map { it[:sym] }.tally)
820
+ end
821
+
822
+ def test_IO_s_readlines
823
+ ret = nil
824
+ Fiber.schedule do
825
+ ret = IO.readlines(@fn)
826
+ end
827
+ @scheduler.join
828
+ assert_equal ['==='], ret
829
+ assert_equal({
830
+ fiber: 1,
831
+ io_read: 3,
832
+ blocking_operation_wait: 1,
833
+ io_close: 1,
834
+ join: 1
835
+ }, @scheduler.calls.map { it[:sym] }.tally)
836
+ end
837
+
838
+ def test_IO_s_select
839
+ ret = nil
840
+ io = File.open(@fn, 'r+')
841
+ Fiber.schedule do
842
+ ret = IO.select([io], [io], [])
843
+ end
844
+ @scheduler.join
845
+ assert_equal [[io], [io], []], ret
846
+ assert_equal({
847
+ fiber: 1,
848
+ io_select: 1,
849
+ join: 1
850
+ }, @scheduler.calls.map { it[:sym] }.tally)
851
+ ensure
852
+ io.close rescue nil
853
+ end
854
+
855
+ def test_IO_s_write
856
+ ret = nil
857
+ Fiber.schedule do
858
+ ret = IO.write(@fn, '***', 2)
859
+ end
860
+ @scheduler.join
861
+ assert_equal 3, ret
862
+ assert_equal '==***', IO.read(@fn)
863
+ assert_equal({
864
+ fiber: 1,
865
+ blocking_operation_wait: 1,
866
+ io_close: 1,
867
+ join: 1
868
+ }, @scheduler.calls.map { it[:sym] }.tally)
869
+ end
870
+ end
871
+
872
+ class FiberSchedulerIOInstanceMethodsTest < UMBaseTest
873
+ def setup
874
+ super
875
+ @raw_scheduler = UM::FiberScheduler.new(@machine)
876
+ @scheduler = MethodCallAuditor.new(@raw_scheduler)
877
+ Fiber.set_scheduler(@scheduler)
878
+ @fn = "/tmp/um_#{SecureRandom.hex}"
879
+ IO.write(@fn, '===')
880
+ @io = File.open(@fn, 'r+')
881
+ @io.sync = true
882
+ end
883
+
884
+ def teardown
885
+ @io.close rescue nil
886
+ FileUtils.rm(@fn) rescue nil
887
+ Fiber.set_scheduler(nil)
888
+ super
889
+ end
890
+
891
+ def test_IO_i_double_left_chevron
892
+ Fiber.schedule do
893
+ @io.seek(2)
894
+ @io << '***'
895
+ end
896
+ @scheduler.join
897
+ assert_equal '==***', IO.read(@fn)
898
+ assert_equal({
899
+ fiber: 1,
900
+ io_write: 1,
901
+ join: 1
902
+ }, @scheduler.calls.map { it[:sym] }.tally)
903
+ end
904
+
905
+ def test_IO_i_close
906
+ ret = nil
907
+ Fiber.schedule do
908
+ ret = @io.close
909
+ end
910
+ @scheduler.join
911
+ assert_nil ret
912
+ assert_equal({
913
+ fiber: 1,
914
+ io_close: 1,
915
+ join: 1
916
+ }, @scheduler.calls.map { it[:sym] }.tally)
917
+ end
918
+
919
+ def test_IO_i_close_read
920
+ ret = nil
921
+ Fiber.schedule do
922
+ ret = @io.close_read
923
+ end
924
+ @scheduler.join
925
+ assert_nil ret
926
+ assert_equal({
927
+ fiber: 1,
928
+ join: 1
929
+ }, @scheduler.calls.map { it[:sym] }.tally)
930
+ end
931
+
932
+ def test_IO_i_close_write
933
+ ret = nil
934
+ Fiber.schedule do
935
+ ret = @io.close_write
936
+ end
937
+ @scheduler.join
938
+ assert_nil ret
939
+ assert_equal({
940
+ fiber: 1,
941
+ join: 1
942
+ }, @scheduler.calls.map { it[:sym] }.tally)
943
+ end
944
+
945
+ def test_IO_i_each_byte
946
+ buf = []
947
+ Fiber.schedule do
948
+ @io.each_byte { buf << it }
949
+ end
950
+ @scheduler.join
951
+ assert_equal [61, 61, 61], buf
952
+ assert_equal({
953
+ fiber: 1,
954
+ io_read: 2,
955
+ join: 1
956
+ }, @scheduler.calls.map { it[:sym] }.tally)
957
+ end
958
+
959
+ def test_IO_i_each_char
960
+ buf = []
961
+ Fiber.schedule do
962
+ @io.each_char { buf << it }
963
+ end
964
+ @scheduler.join
965
+ assert_equal ['=', '=', '='], buf
966
+ assert_equal({
967
+ fiber: 1,
968
+ io_read: 2,
969
+ join: 1
970
+ }, @scheduler.calls.map { it[:sym] }.tally)
971
+ end
972
+
973
+ def test_IO_i_each_line
974
+ buf = []
975
+ Fiber.schedule do
976
+ @io.each_line { buf << it }
977
+ end
978
+ @scheduler.join
979
+ assert_equal ['==='], buf
980
+ assert_equal({
981
+ fiber: 1,
982
+ io_read: 3,
983
+ join: 1
984
+ }, @scheduler.calls.map { it[:sym] }.tally)
985
+ end
986
+
987
+ def test_IO_i_getbyte
988
+ ret = nil
989
+ Fiber.schedule do
990
+ ret = @io.getbyte
991
+ end
992
+ @scheduler.join
993
+ assert_equal 61, ret
994
+ assert_equal({
995
+ fiber: 1,
996
+ io_read: 1,
997
+ join: 1
998
+ }, @scheduler.calls.map { it[:sym] }.tally)
999
+ end
1000
+
1001
+ def test_IO_i_getc
1002
+ ret = nil
1003
+ Fiber.schedule do
1004
+ ret = @io.getc
1005
+ end
1006
+ @scheduler.join
1007
+ assert_equal '=', ret
1008
+ assert_equal({
1009
+ fiber: 1,
1010
+ io_read: 1,
1011
+ join: 1
1012
+ }, @scheduler.calls.map { it[:sym] }.tally)
1013
+ end
1014
+
1015
+ def test_IO_i_gets
1016
+ ret = nil
1017
+ Fiber.schedule do
1018
+ ret = @io.gets
1019
+ end
1020
+ @scheduler.join
1021
+ assert_equal '===', ret
1022
+ assert_equal({
1023
+ fiber: 1,
1024
+ io_read: 2,
1025
+ join: 1
1026
+ }, @scheduler.calls.map { it[:sym] }.tally)
1027
+ end
1028
+
1029
+ def test_IO_i_pread
1030
+ ret = nil
1031
+ Fiber.schedule do
1032
+ ret = @io.pread(5, 2)
1033
+ end
1034
+ @scheduler.join
1035
+ assert_equal '=', ret
1036
+ assert_equal({
1037
+ fiber: 1,
1038
+ io_pread: 1,
1039
+ join: 1
1040
+ }, @scheduler.calls.map { it[:sym] }.tally)
1041
+ end
1042
+
1043
+ def test_IO_i_print
1044
+ ret = nil
1045
+ Fiber.schedule do
1046
+ ret = @io.print(1, 2.0, '3')
1047
+ end
1048
+ @scheduler.join
1049
+ assert_nil ret
1050
+ assert_equal '12.03', IO.read(@fn)
1051
+ assert_equal({
1052
+ fiber: 1,
1053
+ io_write: 3,
1054
+ join: 1
1055
+ }, @scheduler.calls.map { it[:sym] }.tally)
1056
+ end
1057
+
1058
+ def test_IO_i_printf
1059
+ ret = nil
1060
+ Fiber.schedule do
1061
+ ret = @io.printf('%08x', 4321)
1062
+ end
1063
+ @scheduler.join
1064
+ assert_nil ret
1065
+ assert_equal '000010e1', IO.read(@fn)
1066
+ assert_equal({
1067
+ fiber: 1,
1068
+ io_write: 1,
1069
+ join: 1
1070
+ }, @scheduler.calls.map { it[:sym] }.tally)
1071
+ end
1072
+
1073
+ def test_IO_i_putc
1074
+ ret = nil
1075
+ Fiber.schedule do
1076
+ ret = @io.putc('B')
1077
+ end
1078
+ @scheduler.join
1079
+ assert_equal 'B', ret
1080
+ assert_equal 'B==', IO.read(@fn)
1081
+ assert_equal({
1082
+ fiber: 1,
1083
+ io_write: 1,
1084
+ join: 1
1085
+ }, @scheduler.calls.map { it[:sym] }.tally)
1086
+ end
1087
+
1088
+ def test_IO_i_puts
1089
+ ret = nil
1090
+ Fiber.schedule do
1091
+ ret = @io.puts('abc')
1092
+ end
1093
+ @scheduler.join
1094
+ assert_nil ret
1095
+ assert_equal "abc\n", IO.read(@fn)
1096
+ assert_equal({
1097
+ fiber: 1,
1098
+ io_write: 2,
1099
+ join: 1
1100
+ }, @scheduler.calls.map { it[:sym] }.tally)
1101
+ end
1102
+
1103
+ def test_IO_i_pwrite
1104
+ ret = nil
1105
+ Fiber.schedule do
1106
+ ret = @io.pwrite('abc', 2)
1107
+ end
1108
+ @scheduler.join
1109
+ assert_equal 3, ret
1110
+ assert_equal "==abc", IO.read(@fn)
1111
+ assert_equal({
1112
+ fiber: 1,
1113
+ io_pwrite: 1,
1114
+ join: 1
1115
+ }, @scheduler.calls.map { it[:sym] }.tally)
1116
+ end
1117
+
1118
+ def test_IO_i_read
1119
+ ret = nil
1120
+ Fiber.schedule do
1121
+ ret = @io.read
1122
+ end
1123
+ @scheduler.join
1124
+ assert_equal "===", ret
1125
+ assert_equal({
1126
+ fiber: 1,
1127
+ io_read: 2,
1128
+ join: 1
1129
+ }, @scheduler.calls.map { it[:sym] }.tally)
1130
+ end
1131
+
1132
+ def test_IO_i_readbyte
1133
+ ret = nil
1134
+ Fiber.schedule do
1135
+ ret = @io.readbyte
1136
+ end
1137
+ @scheduler.join
1138
+ assert_equal 61, ret
1139
+ assert_equal({
1140
+ fiber: 1,
1141
+ io_read: 1,
1142
+ join: 1
1143
+ }, @scheduler.calls.map { it[:sym] }.tally)
1144
+ end
1145
+
1146
+ def test_IO_i_readchar
1147
+ ret = nil
1148
+ Fiber.schedule do
1149
+ ret = @io.readchar
1150
+ end
1151
+ @scheduler.join
1152
+ assert_equal '=', ret
1153
+ assert_equal({
1154
+ fiber: 1,
1155
+ io_read: 1,
1156
+ join: 1
1157
+ }, @scheduler.calls.map { it[:sym] }.tally)
1158
+ end
1159
+
1160
+ def test_IO_i_readline
1161
+ ret = nil
1162
+ Fiber.schedule do
1163
+ ret = @io.readline
1164
+ end
1165
+ @scheduler.join
1166
+ assert_equal '===', ret
1167
+ assert_equal({
1168
+ fiber: 1,
1169
+ io_read: 2,
1170
+ join: 1
1171
+ }, @scheduler.calls.map { it[:sym] }.tally)
1172
+ end
1173
+
1174
+ def test_IO_i_readlines
1175
+ ret = nil
1176
+ Fiber.schedule do
1177
+ ret = @io.readlines
1178
+ end
1179
+ @scheduler.join
1180
+ assert_equal ["==="], ret
1181
+ assert_equal({
1182
+ fiber: 1,
1183
+ io_read: 3,
1184
+ join: 1
1185
+ }, @scheduler.calls.map { it[:sym] }.tally)
1186
+ end
1187
+
1188
+ def test_IO_i_readpartial
1189
+ ret = nil
1190
+ Fiber.schedule do
1191
+ ret = @io.readpartial(7)
1192
+ end
1193
+ @scheduler.join
1194
+ assert_equal "===", ret
1195
+ assert_equal({
1196
+ fiber: 1,
1197
+ io_read: 1,
1198
+ join: 1
1199
+ }, @scheduler.calls.map { it[:sym] }.tally)
1200
+ end
1201
+
1202
+ def test_IO_i_sysread
1203
+ ret = nil
1204
+ Fiber.schedule do
1205
+ ret = @io.sysread(7)
1206
+ end
1207
+ @scheduler.join
1208
+ assert_equal "===", ret
1209
+ assert_equal({
1210
+ fiber: 1,
1211
+ io_read: 1,
1212
+ join: 1
1213
+ }, @scheduler.calls.map { it[:sym] }.tally)
1214
+ end
1215
+
1216
+ def test_IO_i_syswrite
1217
+ ret = nil
1218
+ Fiber.schedule do
1219
+ ret = @io.syswrite('2')
1220
+ end
1221
+ @scheduler.join
1222
+ assert_equal 1, ret
1223
+ assert_equal '2==', IO.read(@fn)
1224
+ assert_equal({
1225
+ fiber: 1,
1226
+ io_write: 1,
1227
+ join: 1
1228
+ }, @scheduler.calls.map { it[:sym] }.tally)
1229
+ end
1230
+
1231
+ def test_IO_i_wait
1232
+ ret = nil
1233
+ Fiber.schedule do
1234
+ ret = @io.wait(IO::READABLE | IO::WRITABLE)
1235
+ end
1236
+ @scheduler.join
1237
+ assert_equal @io, ret
1238
+ assert_equal({
1239
+ fiber: 1,
1240
+ io_wait: 1,
1241
+ join: 1
1242
+ }, @scheduler.calls.map { it[:sym] }.tally)
1243
+ end
1244
+
1245
+ def test_IO_i_wait_pipe_read_timeout
1246
+ r, w = IO.pipe
1247
+ ret = nil
1248
+ Fiber.schedule do
1249
+ ret = r.wait(IO::READABLE, 0.05)
1250
+ end
1251
+ @scheduler.join
1252
+ assert_nil ret
1253
+ assert_equal({
1254
+ fiber: 1,
1255
+ io_wait: 1,
1256
+ join: 1
1257
+ }, @scheduler.calls.map { it[:sym] }.tally)
1258
+ ensure
1259
+ r.close rescue nil
1260
+ w.close rescue nil
1261
+ end
1262
+
1263
+ def test_IO_i_wait_readable
1264
+ ret = nil
1265
+ Fiber.schedule do
1266
+ ret = @io.wait_readable
1267
+ end
1268
+ @scheduler.join
1269
+ assert_equal @io, ret
1270
+ assert_equal({
1271
+ fiber: 1,
1272
+ io_wait: 1,
1273
+ join: 1
1274
+ }, @scheduler.calls.map { it[:sym] }.tally)
1275
+ end
1276
+
1277
+ def test_IO_i_wait_writable
1278
+ ret = nil
1279
+ Fiber.schedule do
1280
+ ret = @io.wait_writable
1281
+ end
1282
+ @scheduler.join
1283
+ assert_equal @io, ret
1284
+ assert_equal({
1285
+ fiber: 1,
1286
+ io_wait: 1,
1287
+ join: 1
1288
+ }, @scheduler.calls.map { it[:sym] }.tally)
1289
+ end
1290
+
1291
+ def test_IO_i_write
1292
+ ret = nil
1293
+ Fiber.schedule do
1294
+ ret = @io.write('abcde')
1295
+ end
1296
+ @scheduler.join
1297
+ assert_equal 5, ret
1298
+ assert_equal 'abcde', IO.read(@fn)
1299
+ assert_equal({
1300
+ fiber: 1,
1301
+ io_write: 1,
1302
+ join: 1
1303
+ }, @scheduler.calls.map { it[:sym] }.tally)
1304
+ end
1305
+ end
1306
+
1307
+ class FiberSchedulerQueueTest < UMBaseTest
1308
+ def setup
1309
+ super
1310
+ @raw_scheduler = UM::FiberScheduler.new(@machine)
1311
+ @scheduler = MethodCallAuditor.new(@raw_scheduler)
1312
+ Fiber.set_scheduler(@scheduler)
1313
+ @queue = Queue.new
1314
+ end
1315
+
1316
+ def teardown
1317
+ Fiber.set_scheduler(nil)
1318
+ super
1319
+ end
1320
+
1321
+ I = 5
1322
+ C = 5
1323
+
1324
+ def test_fiber_scheduler_queue_multi_fiber
1325
+ C.times {
1326
+ Fiber.schedule do
1327
+ I.times { sleep 0.0001; @queue << rand(1000) }
1328
+ end
1329
+ }
1330
+ C.times {
1331
+ Fiber.schedule do
1332
+ I.times { @queue.shift; @scheduler.yield; }
1333
+ end
1334
+ }
1335
+ @scheduler.join
1336
+ assert_equal({
1337
+ fiber: 10,
1338
+ kernel_sleep: 25,
1339
+ yield: 25,
1340
+ block: 25,
1341
+ unblock: 25,
1342
+ join: 1
1343
+ }, @scheduler.calls.map { it[:sym] }.tally)
1344
+ end
1345
+ end
1346
+
1347
+ class FiberSchedulerNetHTTPTest < UMBaseTest
1348
+ def setup
1349
+ super
1350
+ @raw_scheduler = UM::FiberScheduler.new(@machine)
1351
+ @scheduler = MethodCallAuditor.new(@raw_scheduler)
1352
+ Fiber.set_scheduler(@scheduler)
1353
+ @uri = URI('https://ipinfo.io/')
1354
+ end
1355
+
1356
+ def teardown
1357
+ Fiber.set_scheduler(nil)
1358
+ super
1359
+ end
1360
+
1361
+ C = 10
1362
+
1363
+ def test_fiber_scheduler_net_http
1364
+ buf = []
1365
+ C.times {
1366
+ Fiber.schedule do
1367
+ buf << JSON.parse(Net::HTTP.get(@uri))
1368
+ end
1369
+ }
1370
+ @scheduler.join
1371
+ assert_equal C, buf.size
1372
+ assert_equal 1, buf.map { it['ip'] }.uniq.compact.size
1373
+ calls = @scheduler.calls.map { it[:sym] }.tally
1374
+ assert_equal C, calls[:fiber]
1375
+ assert_equal C, calls[:io_close]
1376
+ assert_in_range (C * 2)..(C * 4), calls[:io_wait]
1377
+ assert_in_range (C * 7)..(C * 17), calls[:blocking_operation_wait]
1378
+ end
1379
+ end
1380
+
1381
+ class FiberSchedulerMultiPipeTest < UMBaseTest
1382
+ def setup
1383
+ super
1384
+ @raw_scheduler = UM::FiberScheduler.new(@machine)
1385
+ @scheduler = MethodCallAuditor.new(@raw_scheduler)
1386
+ Fiber.set_scheduler(@scheduler)
1387
+ @uri = URI('https://ipinfo.io/')
1388
+ end
1389
+
1390
+ def teardown
1391
+ Fiber.set_scheduler(nil)
1392
+ super
1393
+ end
1394
+
1395
+ C = 10
1396
+ I = 100
1397
+ SIZE = 1024
1398
+ DATA = '*' * SIZE
1399
+
1400
+ def test_fiber_scheduler_multi_pipe
1401
+ count = 0
1402
+ ios = []
1403
+ C.times do
1404
+ r, w = IO.pipe
1405
+ r.sync = true
1406
+ w.sync = true
1407
+ ios << r << w
1408
+ Fiber.schedule do
1409
+ I.times { w.write(DATA); count += 1 }
1410
+ w.close
1411
+ end
1412
+ Fiber.schedule do
1413
+ I.times { r.read(SIZE); count += 1 }
1414
+ end
1415
+ end
1416
+ @scheduler.join
1417
+ assert_equal 2000, count
1418
+ assert_equal({
1419
+ fiber: 20,
1420
+ io_read: 1000,
1421
+ io_write: 1000,
1422
+ io_close: 10,
1423
+ join: 1
1424
+ }, @scheduler.calls.map { it[:sym] }.tally)
1425
+ ensure
1426
+ ios.each { it.close rescue nil }
1427
+ end
1428
+ end
1429
+
1430
+ class FiberSchedulerMultiTCPTest < UMBaseTest
1431
+ C = 10
1432
+ W = 10
1433
+ I = 10
1434
+ SIZE = 1024
1435
+ DATA = '*' * SIZE
1436
+
1437
+ def p(o)
1438
+ # UM.debug(o.inspect)
1439
+ end
1440
+
1441
+ def run_server(port)
1442
+ server = TCPServer.new('127.0.0.1', port)
1443
+ p(server:)
1444
+
1445
+ server_fiber = Fiber.schedule { accept_loop(server) }
1446
+
1447
+ fibers = []
1448
+ W.times {
1449
+ fibers << Fiber.schedule { run_client(port) }
1450
+ }
1451
+ @machine.await_fibers(fibers)
1452
+ server.close
1453
+ @machine.await_fibers(server_fiber)
1454
+ rescue Exception => e
1455
+ p test_run_server: e
1456
+ p e.backtrace
1457
+ exit!
1458
+ end
1459
+
1460
+ def accept_loop(server)
1461
+ loop do
1462
+ conn = server.accept
1463
+ p(accept: conn)
1464
+ Fiber.schedule { conn_loop(conn) }
1465
+ end
1466
+ rescue IOError
1467
+ # socket closed
1468
+ rescue Exception => e
1469
+ p accept_loop: e
1470
+ p e.backtrace
1471
+ exit!
1472
+ end
1473
+
1474
+ def conn_loop(conn)
1475
+ loop do
1476
+ msg = conn.recv(SIZE * 2)
1477
+ break if !msg
1478
+
1479
+ conn.send(msg, 0)
1480
+ end
1481
+ rescue Exception => e
1482
+ p conn_loop: e
1483
+ p e.backtrace
1484
+ ensure
1485
+ conn.close rescue nil
1486
+ end
1487
+
1488
+ def run_client(port)
1489
+ client = TCPSocket.new('127.0.0.1', port)
1490
+ p(client:)
1491
+ I.times do
1492
+ client.send(DATA, 0)
1493
+ client.recv(SIZE * 2)
1494
+ @msg_count += 1
1495
+ end
1496
+ p(:client_done)
1497
+ rescue Exception => e
1498
+ p client_loop: e
1499
+ p e.backtrace
1500
+ ensure
1501
+ client.close
1502
+ end
1503
+
1504
+ def run_fiber_scheduler_multi_tcp
1505
+ @msg_count = 0
1506
+ STDOUT.sync = true
1507
+ ios = []
1508
+ fibers = []
1509
+ C.times do |i|
1510
+ port = @port_base + i
1511
+ fibers << Fiber.schedule do
1512
+ run_server(port)
1513
+ end
1514
+ end
1515
+ @machine.await_fibers(fibers)
1516
+ @calls = @scheduler.calls.map { it[:sym] }.tally
1517
+ ensure
1518
+ # p ensure: ios
1519
+ ios.each { it.close rescue nil }
1520
+ end
1521
+
1522
+ def test_fiber_scheduler_multi_tcp
1523
+ Thread.new do
1524
+ @raw_scheduler = UM::FiberScheduler.new(@machine)
1525
+ @scheduler = MethodCallAuditor.new(@raw_scheduler)
1526
+ Fiber.set_scheduler(@scheduler)
1527
+ @port_base = assign_port
1528
+ run_fiber_scheduler_multi_tcp
1529
+ ensure
1530
+ Fiber.set_scheduler(nil)
1531
+ end.join
1532
+
1533
+ assert_equal 1000, @msg_count
1534
+ assert_equal({
1535
+ fiber: 220,
1536
+ io_wait: 2120,
1537
+ io_close: 210,
1538
+ fiber_interrupt: 10,
1539
+ unblock: 10,
1540
+ kernel_sleep: 10
1541
+ }, @calls)
1542
+ end
1543
+ end