uringmachine 0.19 → 0.20.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/TODO.md +40 -0
  4. data/examples/bm_fileno.rb +33 -0
  5. data/examples/bm_mutex.rb +85 -0
  6. data/examples/bm_mutex_single.rb +33 -0
  7. data/examples/bm_queue.rb +27 -28
  8. data/examples/bm_send.rb +2 -5
  9. data/examples/bm_snooze.rb +20 -42
  10. data/examples/fiber_scheduler_demo.rb +15 -51
  11. data/examples/fiber_scheduler_fork.rb +24 -0
  12. data/examples/nc_ssl.rb +71 -0
  13. data/ext/um/extconf.rb +5 -15
  14. data/ext/um/um.c +73 -42
  15. data/ext/um/um.h +21 -11
  16. data/ext/um/um_async_op_class.c +2 -2
  17. data/ext/um/um_buffer.c +1 -1
  18. data/ext/um/um_class.c +94 -23
  19. data/ext/um/um_const.c +51 -3
  20. data/ext/um/um_mutex_class.c +1 -1
  21. data/ext/um/um_queue_class.c +1 -1
  22. data/ext/um/um_stream.c +5 -5
  23. data/ext/um/um_stream_class.c +3 -0
  24. data/ext/um/um_sync.c +22 -27
  25. data/ext/um/um_utils.c +59 -19
  26. data/grant-2025/journal.md +229 -0
  27. data/grant-2025/tasks.md +66 -0
  28. data/lib/uringmachine/fiber_scheduler.rb +180 -48
  29. data/lib/uringmachine/version.rb +1 -1
  30. data/lib/uringmachine.rb +6 -0
  31. data/test/test_fiber_scheduler.rb +138 -0
  32. data/test/test_stream.rb +2 -2
  33. data/test/test_um.rb +451 -33
  34. data/vendor/liburing/.github/workflows/ci.yml +94 -1
  35. data/vendor/liburing/.github/workflows/test_build.c +9 -0
  36. data/vendor/liburing/configure +27 -0
  37. data/vendor/liburing/examples/Makefile +6 -0
  38. data/vendor/liburing/examples/helpers.c +8 -0
  39. data/vendor/liburing/examples/helpers.h +5 -0
  40. data/vendor/liburing/liburing.spec +1 -1
  41. data/vendor/liburing/src/Makefile +9 -3
  42. data/vendor/liburing/src/include/liburing/barrier.h +11 -5
  43. data/vendor/liburing/src/include/liburing/io_uring/query.h +41 -0
  44. data/vendor/liburing/src/include/liburing/io_uring.h +50 -0
  45. data/vendor/liburing/src/include/liburing/sanitize.h +16 -4
  46. data/vendor/liburing/src/include/liburing.h +445 -121
  47. data/vendor/liburing/src/liburing-ffi.map +15 -0
  48. data/vendor/liburing/src/liburing.map +8 -0
  49. data/vendor/liburing/src/sanitize.c +4 -1
  50. data/vendor/liburing/src/setup.c +7 -4
  51. data/vendor/liburing/test/232c93d07b74.c +4 -16
  52. data/vendor/liburing/test/Makefile +15 -1
  53. data/vendor/liburing/test/accept.c +2 -13
  54. data/vendor/liburing/test/conn-unreach.c +132 -0
  55. data/vendor/liburing/test/fd-pass.c +32 -7
  56. data/vendor/liburing/test/fdinfo.c +39 -12
  57. data/vendor/liburing/test/fifo-futex-poll.c +114 -0
  58. data/vendor/liburing/test/fifo-nonblock-read.c +1 -12
  59. data/vendor/liburing/test/futex.c +1 -1
  60. data/vendor/liburing/test/helpers.c +99 -2
  61. data/vendor/liburing/test/helpers.h +9 -0
  62. data/vendor/liburing/test/io_uring_passthrough.c +6 -12
  63. data/vendor/liburing/test/mock_file.c +379 -0
  64. data/vendor/liburing/test/mock_file.h +47 -0
  65. data/vendor/liburing/test/nop.c +2 -2
  66. data/vendor/liburing/test/nop32-overflow.c +150 -0
  67. data/vendor/liburing/test/nop32.c +126 -0
  68. data/vendor/liburing/test/pipe.c +166 -0
  69. data/vendor/liburing/test/poll-race-mshot.c +13 -1
  70. data/vendor/liburing/test/recv-mshot-fair.c +81 -34
  71. data/vendor/liburing/test/recvsend_bundle.c +1 -1
  72. data/vendor/liburing/test/resize-rings.c +2 -0
  73. data/vendor/liburing/test/ring-query.c +322 -0
  74. data/vendor/liburing/test/ringbuf-loop.c +87 -0
  75. data/vendor/liburing/test/runtests.sh +2 -2
  76. data/vendor/liburing/test/send-zerocopy.c +43 -5
  77. data/vendor/liburing/test/send_recv.c +102 -32
  78. data/vendor/liburing/test/shutdown.c +2 -12
  79. data/vendor/liburing/test/socket-nb.c +3 -14
  80. data/vendor/liburing/test/socket-rw-eagain.c +2 -12
  81. data/vendor/liburing/test/socket-rw-offset.c +2 -12
  82. data/vendor/liburing/test/socket-rw.c +2 -12
  83. data/vendor/liburing/test/sqe-mixed-bad-wrap.c +87 -0
  84. data/vendor/liburing/test/sqe-mixed-nop.c +82 -0
  85. data/vendor/liburing/test/sqe-mixed-uring_cmd.c +153 -0
  86. data/vendor/liburing/test/timestamp.c +56 -19
  87. data/vendor/liburing/test/vec-regbuf.c +2 -4
  88. data/vendor/liburing/test/wq-aff.c +7 -0
  89. metadata +24 -2
@@ -1,32 +1,120 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class UringMachine
4
+ # UringMachine::FiberScheduler implements the Fiber::Scheduler interface for
5
+ # creating fiber-based concurrent applications in Ruby, in tight integration
6
+ # with the standard Ruby I/O and locking APIs.
4
7
  class FiberScheduler
5
- def initialize(machine)
6
- @machine = machine
8
+ attr_reader :machine, :fiber_map
9
+
10
+ # Instantiates a scheduler with the given UringMachine instance.
11
+ #
12
+ # machine = UM.new
13
+ # scheduler = UM::FiberScheduler.new(machine)
14
+ # Fiber.set_scheduler(scheduler)
15
+ #
16
+ # @param machine [UringMachine, nil] UringMachine instance
17
+ # @return [void]
18
+ def initialize(machine = nil)
19
+ @machine = machine || UM.new
20
+ @ios = ObjectSpace::WeakMap.new
21
+ @fiber_map = ObjectSpace::WeakMap.new
7
22
  end
8
23
 
9
- def p(o)
10
- @machine.write(1, "#{o.inspect}\n")
11
- rescue Errno::EINTR
12
- retry
24
+ def instance_variables_to_inspect
25
+ [:@machine]
26
+ end
27
+
28
+ # Should be called after a fork (eventually, we'll want Ruby to call this
29
+ # automatically after a fork).
30
+ #
31
+ # @return [self]
32
+ def post_fork
33
+ @machine = UM.new
34
+ @ios = ObjectSpace::WeakMap.new
35
+ @fiber_map = ObjectSpace::WeakMap.new
36
+ self
37
+ end
38
+
39
+ # For debugging purposes
40
+ def method_missing(sym, *a, **b)
41
+ @machine.write(1, "method_missing: #{sym.inspect} #{a.inspect} #{b.inspect}\n")
42
+ @machine.write(1, "#{caller.inspect}\n")
43
+ super
44
+ end
45
+
46
+ # scheduler_close hook: Waits for all fiber to terminate. Called upon thread
47
+ # termination or when the thread's fiber scheduler is changed.
48
+ #
49
+ # @return [void]
50
+ def scheduler_close
51
+ join()
13
52
  end
14
53
 
15
- def join(*)
16
- @machine.join(*)
54
+ # fiber_interrupt hook: to be implemented.
55
+ def fiber_interrupt(fiber, exception)
56
+ raise NotImplementedError, "Implement me!"
17
57
  end
18
58
 
19
- def block(blocker, timeout)
20
- p block: [blocker, timeout]
59
+ # For debugging purposes
60
+ def p(o) = UM.debug(o.inspect)
61
+
62
+ # Waits for the given fibers to terminate. If no fibers are given, waits for
63
+ # all fibers to terminate.
64
+ #
65
+ # @param fibers [Array<Fiber>] fibers to terminate
66
+ # @return [void]
67
+ def join(*fibers)
68
+ if fibers.empty?
69
+ fibers = @fiber_map.keys
70
+ @fiber_map = ObjectSpace::WeakMap.new
71
+ end
72
+
73
+ @machine.join(*fibers)
74
+ end
21
75
 
76
+ # blocking_operation_wait hook: runs the given operation in a separate
77
+ # thread, so as not to block other fibers.
78
+ #
79
+ # @param blocking_operation [callable] blocking operation
80
+ # @return [void]
81
+ def blocking_operation_wait(blocking_operation)
82
+ start_blocking_operation_thread if !@blocking_op_queue
83
+
84
+ queue = UM::Queue.new
85
+ @machine.push(@blocking_op_queue, [queue, blocking_operation])
86
+ @machine.shift(queue)
22
87
  end
23
88
 
89
+ # block hook: blocks the current fiber by yielding to the machine. This hook
90
+ # is called when a synchronization mechanism blocks, e.g. a mutex, a queue,
91
+ # etc.
92
+ #
93
+ # @param blocker [any] blocker object
94
+ # @param timeout [Number, nil] optional
95
+ # timeout @return [void]
96
+ def block(blocker, timeout = nil)
97
+ raise NotImplementedError, "Implement me!" if timeout
98
+
99
+ @machine.yield
100
+ end
101
+
102
+ # unblock hook: unblocks the given fiber by scheduling it. This hook is
103
+ # called when a synchronization mechanism unblocks, e.g. a mutex, a queue,
104
+ # etc.
105
+ #
106
+ # @param blocker [any] blocker object
107
+ # @param fiber [Fiber] fiber to resume
108
+ # @return [void]
24
109
  def unblock(blocker, fiber)
25
- p unblock: [blocker, fiber]
110
+ @machine.schedule(fiber, nil)
26
111
  end
27
112
 
113
+ # kernel_sleep hook: sleeps for the given duration.
114
+ #
115
+ # @param duration [Number, nil] sleep duration
116
+ # @return [void]
28
117
  def kernel_sleep(duration = nil)
29
- # p sleep: [duration]
30
118
  if duration
31
119
  @machine.sleep(duration)
32
120
  else
@@ -34,17 +122,19 @@ class UringMachine
34
122
  end
35
123
  end
36
124
 
125
+ # io_wait hook: waits for the given io to become ready.
126
+ #
127
+ # @param io [IO] IO object
128
+ # @param events [Number] readiness bitmask
129
+ # @param timeout [Number, nil] optional timeout
130
+ # @param return
37
131
  def io_wait(io, events, timeout = nil)
38
132
  timeout ||= io.timeout
39
- p timeout: timeout
40
133
  if timeout
41
- p 1
42
134
  @machine.timeout(timeout, Timeout::Error) {
43
- p 2
44
135
  @machine.poll(io.fileno, events).tap { p 3 }
45
- }.tap { p 4 }
136
+ }
46
137
  else
47
- p 5
48
138
  @machine.poll(io.fileno, events).tap { p 6 }
49
139
 
50
140
  end
@@ -53,52 +143,94 @@ class UringMachine
53
143
  raise
54
144
  end
55
145
 
146
+ # fiber hook: creates a new fiber with the given block. The created fiber is
147
+ # added to the fiber map, scheduled on the scheduler machine, and started
148
+ # before this method returns (by calling snooze).
149
+ #
150
+ # @param block [Proc] fiber block @return [Fiber]
56
151
  def fiber(&block)
57
- f = @machine.spin(&block)
152
+ fiber = Fiber.new(blocking: false) { @machine.run(fiber, &block) }
153
+ @fiber_map[fiber] = true
154
+ @machine.schedule(fiber, nil)
58
155
  @machine.snooze
59
- f
156
+ fiber
60
157
  end
61
158
 
159
+ # io_write hook: writes to the given IO.
160
+ #
161
+ # @param io [IO] IO object
162
+ # @param buffer [IO::Buffer] write buffer
163
+ # @param length [Integer] write length
164
+ # @param offset [Integer] write offset
165
+ # @return [Integer] bytes written
62
166
  def io_write(io, buffer, length, offset)
63
- p io_write: [io, buffer.get_string, length, offset]
64
- @machine.write(io.fileno, buffer.get_string)
167
+ if offset > 0
168
+ raise NotImplementedError, "UringMachine currently does not support writing at an offset"
169
+ end
170
+
171
+ ensure_nonblock(io)
172
+ @machine.write(io.fileno, buffer)
173
+ rescue Errno::EINTR
174
+ retry
65
175
  end
66
176
 
177
+ # io_read hook: reads from the given IO.
178
+ #
179
+ # @param io [IO] IO object
180
+ # @param buffer [IO::Buffer] read buffer
181
+ # @param length [Integer] read length
182
+ # @param offset [Integer] read offset
183
+ # @return [Integer] bytes read
67
184
  def io_read(io, buffer, length, offset)
68
- # p io_read: [io, buffer, length, offset]
69
- s = +''
185
+ if offset > 0
186
+ raise NotImplementedError, "UringMachine currently does not support reading at an offset"
187
+ end
188
+
189
+ ensure_nonblock(io)
70
190
  length = buffer.size if length == 0
71
- bytes = @machine.read(io.fileno, s, length)
72
- buffer.set_string(s)
73
- bytes
74
- rescue SystemCallError => e
75
- -e.errno
191
+ @machine.read(io.fileno, buffer, length)
192
+ rescue Errno::EINTR
193
+ retry
76
194
  end
77
195
 
78
- def io_pwrite(io, buffer, from, length, offset)
79
- p io_pwrite: [io, buffer, from, length, offset]
196
+ if UM.instance_methods.include?(:waitid_status)
197
+ def process_wait(pid, flags)
198
+ flags = UM::WEXITED if flags == 0
199
+ @machine.waitid_status(UM::P_PID, pid, flags)
200
+ end
80
201
  end
81
202
 
82
- def io_pread(io, buffer, from, length, offset)
83
- p io_pread: [io, buffer, from, length, offset]
84
- end
203
+ private
204
+
205
+ # Ensures the given IO is in blocking mode.
206
+ #
207
+ # @param io [IO] IO object
208
+ # @return [void]
209
+ def ensure_nonblock(io)
210
+ return if @ios.key?(io)
85
211
 
86
- # def fiber(&block)
87
- # fiber = Fiber.new(blocking: false, &block)
88
- # unblock(nil, fiber)
89
- # # fiber.resume
90
- # return fiber
91
- # end
212
+ @ios[io] = true
213
+ UM.io_set_nonblock(io, false)
214
+ end
92
215
 
93
- # def kernel_sleep(duration = nil)
94
- # block(:sleep, duration)
95
- # end
216
+ # Starts a background thread for running blocking operations.
217
+ #
218
+ # @return [void]
219
+ def start_blocking_operation_thread
220
+ @blocking_op_queue = UM::Queue.new
221
+ @blocking_op_thread = Thread.new do
222
+ m = UM.new
223
+ loop do
224
+ q, op = m.shift(@blocking_op_queue)
225
+ res = begin
226
+ op.()
227
+ rescue Exception => e
228
+ e
229
+ end
230
+ m.push(q, res)
231
+ end
232
+ end
233
+ end
96
234
 
97
- # def process_wait(pid, flags)
98
- # # This is a very simple way to implement a non-blocking wait:
99
- # Thread.new do
100
- # Process::Status.wait(pid, flags)
101
- # end.value
102
- # end
103
235
  end
104
236
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class UringMachine
4
- VERSION = '0.19'
4
+ VERSION = '0.20.0'
5
5
  end
data/lib/uringmachine.rb CHANGED
@@ -23,6 +23,12 @@ class UringMachine
23
23
  @@fiber_map[fiber] = fiber
24
24
  end
25
25
 
26
+ def run(fiber, &block)
27
+ run_block_in_fiber(block, fiber, nil)
28
+ self.schedule(fiber, nil)
29
+ @@fiber_map[fiber] = fiber
30
+ end
31
+
26
32
  def join(*fibers)
27
33
  results = fibers.inject({}) { |h, f| h[f] = nil; h }
28
34
  queue = nil
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'uringmachine/fiber_scheduler'
5
+
6
+ class FiberSchedulerTest < UMBaseTest
7
+ def setup
8
+ super
9
+ @scheduler = UM::FiberScheduler.new(@machine)
10
+ Fiber.set_scheduler(@scheduler)
11
+ end
12
+
13
+ def teardown
14
+ Fiber.set_scheduler(nil)
15
+ GC.start
16
+ end
17
+
18
+ def test_fiber_scheduler_initialize_without_machine
19
+ s = UM::FiberScheduler.new
20
+ assert_kind_of UringMachine, s.machine
21
+ end
22
+
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
+ def test_fiber_scheduler_spinning
34
+ f1 = Fiber.schedule do
35
+ sleep 0.001
36
+ end
37
+
38
+ f2 = Fiber.schedule do
39
+ sleep 0.001
40
+ end
41
+
42
+ assert_kind_of Fiber, f1
43
+ assert_kind_of Fiber, f2
44
+ assert_equal 2, @scheduler.fiber_map.size
45
+
46
+ # close scheduler
47
+ Fiber.set_scheduler nil
48
+ GC.start
49
+ assert_equal 0, @scheduler.fiber_map.size
50
+ end
51
+
52
+ def test_fiber_scheduler_basic_io
53
+ i, o = IO.pipe
54
+ buffer = []
55
+
56
+ f1 = Fiber.schedule do
57
+ sleep 0.001
58
+ o.write 'foo'
59
+ buffer << :f1
60
+ end
61
+
62
+ f2 = Fiber.schedule do
63
+ sleep 0.002
64
+ o.write 'bar'
65
+ buffer << :f2
66
+ o.close
67
+ end
68
+
69
+ f3 = Fiber.schedule do
70
+ str = i.read
71
+ buffer << str
72
+ end
73
+
74
+ @scheduler.join
75
+ assert_equal [true] * 3, [f1, f2, f3].map(&:done?)
76
+ assert_equal [:f1, :f2, 'foobar'], buffer
77
+ ensure
78
+ i.close rescue nil
79
+ o.close rescue nil
80
+ end
81
+
82
+ def test_fiber_scheduler_sleep
83
+ t0 = monotonic_clock
84
+ assert_equal 0, machine.pending_count
85
+ Fiber.schedule do
86
+ sleep(0.01)
87
+ end
88
+ Fiber.schedule do
89
+ sleep(0.02)
90
+ end
91
+ assert_equal 2, machine.pending_count
92
+ @scheduler.join
93
+ t1 = monotonic_clock
94
+ assert_in_range 0.02..0.025, t1 - t0
95
+ end
96
+
97
+ def test_fiber_scheduler_lock
98
+ mutex = Mutex.new
99
+ buffer = []
100
+ t0 = monotonic_clock
101
+ Fiber.schedule do
102
+ 10.times { sleep 0.001; buffer << it }
103
+ end
104
+ Fiber.schedule do
105
+ mutex.synchronize { sleep(0.005) }
106
+ end
107
+ Fiber.schedule do
108
+ mutex.synchronize { sleep(0.005) }
109
+ end
110
+ @scheduler.join
111
+ t1 = monotonic_clock
112
+ assert_in_range 0.01..0.015, t1 - t0
113
+ end
114
+
115
+ def test_fiber_scheduler_process_wait
116
+ child_pid = nil
117
+ status = nil
118
+ f1 = Fiber.schedule do
119
+ child_pid = fork {
120
+ Fiber.scheduler.post_fork
121
+ Fiber.set_scheduler nil
122
+ sleep(0.01);
123
+ exit! 42
124
+ }
125
+ status = Process::Status.wait(child_pid)
126
+ rescue => e
127
+ p e
128
+ end
129
+ @scheduler.join(f1)
130
+ assert_kind_of Process::Status, status
131
+ assert_equal child_pid, status.pid
132
+ assert_equal 42, status.exitstatus
133
+ ensure
134
+ if child_pid
135
+ Process.wait(child_pid) rescue nil
136
+ end
137
+ end
138
+ end
data/test/test_stream.rb CHANGED
@@ -110,12 +110,12 @@ class StreamRespTest < StreamBaseTest
110
110
 
111
111
  machine.write(@wfd, "-foobar\r\n")
112
112
  o = @stream.resp_decode
113
- assert_kind_of RuntimeError, o
113
+ assert_kind_of UM::Stream::RESPError, o
114
114
  assert_equal "foobar", o.message
115
115
 
116
116
  machine.write(@wfd, "!3\r\nbaz\r\n")
117
117
  o = @stream.resp_decode
118
- assert_kind_of RuntimeError, o
118
+ assert_kind_of UM::Stream::RESPError, o
119
119
  assert_equal "baz", o.message
120
120
 
121
121
  machine.write(@wfd, ":123\r\n")