uringmachine 0.19.1 → 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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +3 -4
  3. data/CHANGELOG.md +32 -1
  4. data/TODO.md +0 -39
  5. data/examples/bm_fileno.rb +33 -0
  6. data/examples/bm_mutex.rb +85 -0
  7. data/examples/bm_mutex_single.rb +33 -0
  8. data/examples/bm_queue.rb +29 -29
  9. data/examples/bm_send.rb +2 -5
  10. data/examples/bm_snooze.rb +20 -42
  11. data/examples/bm_write.rb +4 -1
  12. data/examples/fiber_scheduler_demo.rb +15 -51
  13. data/examples/fiber_scheduler_fork.rb +24 -0
  14. data/examples/nc_ssl.rb +71 -0
  15. data/ext/um/extconf.rb +5 -15
  16. data/ext/um/um.c +310 -74
  17. data/ext/um/um.h +66 -29
  18. data/ext/um/um_async_op.c +1 -1
  19. data/ext/um/um_async_op_class.c +2 -2
  20. data/ext/um/um_buffer.c +1 -1
  21. data/ext/um/um_class.c +178 -31
  22. data/ext/um/um_const.c +51 -3
  23. data/ext/um/um_mutex_class.c +1 -1
  24. data/ext/um/um_op.c +37 -0
  25. data/ext/um/um_queue_class.c +1 -1
  26. data/ext/um/um_stream.c +5 -5
  27. data/ext/um/um_stream_class.c +3 -0
  28. data/ext/um/um_sync.c +28 -39
  29. data/ext/um/um_utils.c +59 -19
  30. data/grant-2025/journal.md +353 -0
  31. data/grant-2025/tasks.md +135 -0
  32. data/lib/uringmachine/fiber_scheduler.rb +316 -57
  33. data/lib/uringmachine/version.rb +1 -1
  34. data/lib/uringmachine.rb +6 -0
  35. data/test/test_fiber_scheduler.rb +640 -0
  36. data/test/test_stream.rb +2 -2
  37. data/test/test_um.rb +722 -54
  38. data/uringmachine.gemspec +5 -5
  39. data/vendor/liburing/.github/workflows/ci.yml +94 -1
  40. data/vendor/liburing/.github/workflows/test_build.c +9 -0
  41. data/vendor/liburing/configure +27 -0
  42. data/vendor/liburing/examples/Makefile +6 -0
  43. data/vendor/liburing/examples/helpers.c +8 -0
  44. data/vendor/liburing/examples/helpers.h +5 -0
  45. data/vendor/liburing/liburing.spec +1 -1
  46. data/vendor/liburing/src/Makefile +9 -3
  47. data/vendor/liburing/src/include/liburing/barrier.h +11 -5
  48. data/vendor/liburing/src/include/liburing/io_uring/query.h +41 -0
  49. data/vendor/liburing/src/include/liburing/io_uring.h +51 -0
  50. data/vendor/liburing/src/include/liburing/sanitize.h +16 -4
  51. data/vendor/liburing/src/include/liburing.h +458 -121
  52. data/vendor/liburing/src/liburing-ffi.map +16 -0
  53. data/vendor/liburing/src/liburing.map +8 -0
  54. data/vendor/liburing/src/sanitize.c +4 -1
  55. data/vendor/liburing/src/setup.c +7 -4
  56. data/vendor/liburing/test/232c93d07b74.c +4 -16
  57. data/vendor/liburing/test/Makefile +15 -1
  58. data/vendor/liburing/test/accept.c +2 -13
  59. data/vendor/liburing/test/bind-listen.c +175 -13
  60. data/vendor/liburing/test/conn-unreach.c +132 -0
  61. data/vendor/liburing/test/fd-pass.c +32 -7
  62. data/vendor/liburing/test/fdinfo.c +39 -12
  63. data/vendor/liburing/test/fifo-futex-poll.c +114 -0
  64. data/vendor/liburing/test/fifo-nonblock-read.c +1 -12
  65. data/vendor/liburing/test/futex.c +1 -1
  66. data/vendor/liburing/test/helpers.c +99 -2
  67. data/vendor/liburing/test/helpers.h +9 -0
  68. data/vendor/liburing/test/io_uring_passthrough.c +6 -12
  69. data/vendor/liburing/test/mock_file.c +379 -0
  70. data/vendor/liburing/test/mock_file.h +47 -0
  71. data/vendor/liburing/test/nop.c +2 -2
  72. data/vendor/liburing/test/nop32-overflow.c +150 -0
  73. data/vendor/liburing/test/nop32.c +126 -0
  74. data/vendor/liburing/test/pipe.c +166 -0
  75. data/vendor/liburing/test/poll-race-mshot.c +13 -1
  76. data/vendor/liburing/test/read-write.c +4 -4
  77. data/vendor/liburing/test/recv-mshot-fair.c +81 -34
  78. data/vendor/liburing/test/recvsend_bundle.c +1 -1
  79. data/vendor/liburing/test/resize-rings.c +2 -0
  80. data/vendor/liburing/test/ring-query.c +322 -0
  81. data/vendor/liburing/test/ringbuf-loop.c +87 -0
  82. data/vendor/liburing/test/ringbuf-read.c +4 -4
  83. data/vendor/liburing/test/runtests.sh +2 -2
  84. data/vendor/liburing/test/send-zerocopy.c +43 -5
  85. data/vendor/liburing/test/send_recv.c +103 -32
  86. data/vendor/liburing/test/shutdown.c +2 -12
  87. data/vendor/liburing/test/socket-nb.c +3 -14
  88. data/vendor/liburing/test/socket-rw-eagain.c +2 -12
  89. data/vendor/liburing/test/socket-rw-offset.c +2 -12
  90. data/vendor/liburing/test/socket-rw.c +2 -12
  91. data/vendor/liburing/test/sqe-mixed-bad-wrap.c +87 -0
  92. data/vendor/liburing/test/sqe-mixed-nop.c +82 -0
  93. data/vendor/liburing/test/sqe-mixed-uring_cmd.c +153 -0
  94. data/vendor/liburing/test/timestamp.c +56 -19
  95. data/vendor/liburing/test/vec-regbuf.c +2 -4
  96. data/vendor/liburing/test/wq-aff.c +7 -0
  97. metadata +37 -15
@@ -1,32 +1,182 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'resolv'
4
+ require 'etc'
5
+
3
6
  class UringMachine
7
+ # Implements a thread pool for running blocking operations.
8
+ class BlockingOperationThreadPool
9
+ def initialize
10
+ @blocking_op_queue = UM::Queue.new
11
+ @pending_count = 0
12
+ @worker_count = 0
13
+ @max_workers = Etc.nprocessors
14
+ @worker_mutex = UM::Mutex.new
15
+ @workers = []
16
+ end
17
+
18
+ def process(machine, job)
19
+ queue = UM::Queue.new
20
+
21
+ if @worker_count == 0 || (@pending_count > 0 && @worker_count < @max_workers)
22
+ start_worker(machine)
23
+ end
24
+ machine.push(@blocking_op_queue, [queue, job])
25
+ machine.shift(queue)
26
+ end
27
+
28
+ private
29
+
30
+ def start_worker(machine)
31
+ machine.synchronize(@worker_mutex) do
32
+ return if @worker_count == @max_workers
33
+
34
+ @workers << Thread.new { run_worker_thread }
35
+ @worker_count += 1
36
+ end
37
+ end
38
+
39
+ def run_worker_thread
40
+ machine = UM.new(4).mark(1)
41
+ loop do
42
+ q, op = machine.shift(@blocking_op_queue)
43
+ @pending_count += 1
44
+ res = begin
45
+ op.()
46
+ rescue Exception => e
47
+ e
48
+ end
49
+ @pending_count -= 1
50
+ machine.push(q, res)
51
+ rescue => e
52
+ UM.debug("worker e: #{e.inspect}")
53
+ end
54
+ end
55
+ end
56
+
57
+ # UringMachine::FiberScheduler implements the Fiber::Scheduler interface for
58
+ # creating fiber-based concurrent applications in Ruby, in tight integration
59
+ # with the standard Ruby I/O and locking APIs.
4
60
  class FiberScheduler
5
- def initialize(machine)
6
- @machine = machine
61
+ @@blocking_operation_thread_pool = BlockingOperationThreadPool.new
62
+
63
+ attr_reader :machine, :fiber_map
64
+
65
+ # Instantiates a scheduler with the given UringMachine instance.
66
+ #
67
+ # machine = UM.new
68
+ # scheduler = UM::FiberScheduler.new(machine)
69
+ # Fiber.set_scheduler(scheduler)
70
+ #
71
+ # @param machine [UringMachine, nil] UringMachine instance
72
+ # @return [void]
73
+ def initialize(machine = nil)
74
+ @machine = machine || UM.new
75
+ @fiber_map = ObjectSpace::WeakMap.new
7
76
  end
8
77
 
9
- def p(o)
10
- @machine.write(1, "#{o.inspect}\n")
11
- rescue Errno::EINTR
12
- retry
78
+ def instance_variables_to_inspect
79
+ [:@machine]
80
+ end
81
+
82
+ # Should be called after a fork (eventually, we'll want Ruby to call this
83
+ # automatically after a fork).
84
+ #
85
+ # @return [self]
86
+ def process_fork
87
+ @machine = UM.new
88
+ @fiber_map = ObjectSpace::WeakMap.new
89
+ self
13
90
  end
14
91
 
15
- def join(*)
16
- @machine.join(*)
92
+ # For debugging purposes
93
+ def method_missing(sym, *a, **b)
94
+ @machine.write(1, "method_missing: #{sym.inspect} #{a.inspect} #{b.inspect}\n")
95
+ @machine.write(1, "#{caller.inspect}\n")
96
+ super
17
97
  end
18
98
 
19
- def block(blocker, timeout)
20
- p block: [blocker, timeout]
99
+ # scheduler_close hook: Waits for all fiber to terminate. Called upon thread
100
+ # termination or when the thread's fiber scheduler is changed.
101
+ #
102
+ # @return [void]
103
+ def scheduler_close
104
+ join()
105
+ end
106
+
107
+ # For debugging purposes
108
+ def p(o) = UM.debug(o.inspect)
109
+
110
+ # Waits for the given fibers to terminate. If no fibers are given, waits for
111
+ # all fibers to terminate.
112
+ #
113
+ # @param fibers [Array<Fiber>] fibers to terminate
114
+ # @return [void]
115
+ def join(*fibers)
116
+ if fibers.empty?
117
+ fibers = @fiber_map.keys
118
+ @fiber_map = ObjectSpace::WeakMap.new
119
+ end
120
+
121
+ @machine.join(*fibers)
122
+ end
123
+
124
+ # blocking_operation_wait hook: runs the given operation in a separate
125
+ # thread, so as not to block other fibers.
126
+ #
127
+ # @param blocking_operation [callable] blocking operation
128
+ # @return [void]
129
+ def blocking_operation_wait(blocking_operation)
130
+ # naive_blocking_peration_wait(blocking_operation)
131
+ @@blocking_operation_thread_pool.process(@machine, blocking_operation)
132
+ end
133
+
134
+ def naive_blocking_peration_wait(blocking_operation)
135
+ res = nil
136
+ Thread.new do
137
+ res = blocking_operation.()
138
+ rescue => e
139
+ res = e
140
+ end.join
141
+ res
142
+ end
143
+
144
+ # block hook: blocks the current fiber by yielding to the machine. This hook
145
+ # is called when a synchronization mechanism blocks, e.g. a mutex, a queue,
146
+ # etc.
147
+ #
148
+ # @param blocker [any] blocker object
149
+ # @param timeout [Number, nil] optional
150
+ # timeout @return [bool] was the operation successful
151
+ def block(blocker, timeout = nil)
152
+ if timeout
153
+ @machine.timeout(timeout, Timeout::Error) { @machine.yield }
154
+ else
155
+ @machine.yield
156
+ end
21
157
 
158
+ true
159
+ rescue Timeout::Error
160
+ false
22
161
  end
23
162
 
163
+ # unblock hook: unblocks the given fiber by scheduling it. This hook is
164
+ # called when a synchronization mechanism unblocks, e.g. a mutex, a queue,
165
+ # etc.
166
+ #
167
+ # @param blocker [any] blocker object
168
+ # @param fiber [Fiber] fiber to resume
169
+ # @return [void]
24
170
  def unblock(blocker, fiber)
25
- p unblock: [blocker, fiber]
171
+ @machine.schedule(fiber, nil)
172
+ @machine.wakeup
26
173
  end
27
174
 
175
+ # kernel_sleep hook: sleeps for the given duration.
176
+ #
177
+ # @param duration [Number, nil] sleep duration
178
+ # @return [void]
28
179
  def kernel_sleep(duration = nil)
29
- # p sleep: [duration]
30
180
  if duration
31
181
  @machine.sleep(duration)
32
182
  else
@@ -34,71 +184,180 @@ class UringMachine
34
184
  end
35
185
  end
36
186
 
187
+ # io_wait hook: waits for the given io to become ready.
188
+ #
189
+ # @param io [IO] IO object
190
+ # @param events [Number] readiness bitmask
191
+ # @param timeout [Number, nil] optional timeout
192
+ # @param return
37
193
  def io_wait(io, events, timeout = nil)
38
194
  timeout ||= io.timeout
39
- p timeout: timeout
40
195
  if timeout
41
- p 1
42
196
  @machine.timeout(timeout, Timeout::Error) {
43
- p 2
44
- @machine.poll(io.fileno, events).tap { p 3 }
45
- }.tap { p 4 }
197
+ @machine.poll(io.fileno, events)
198
+ }
46
199
  else
47
- p 5
48
- @machine.poll(io.fileno, events).tap { p 6 }
200
+ @machine.poll(io.fileno, events)
201
+ end
202
+ end
49
203
 
204
+ def io_select(rios, wios, eios, timeout = nil)
205
+ map_r = map_io_fds(rios)
206
+ map_w = map_io_fds(wios)
207
+ map_e = map_io_fds(eios)
208
+
209
+ r, w, e = nil
210
+ if timeout
211
+ @machine.timeout(timeout, Timeout::Error) {
212
+ r, w, e = @machine.select(map_r.keys, map_w.keys, map_e.keys)
213
+ }
214
+ else
215
+ r, w, e = @machine.select(map_r.keys, map_w.keys, map_e.keys)
50
216
  end
51
- rescue => e
52
- p e: e
53
- raise
217
+
218
+ [unmap_fds(r, map_r), unmap_fds(w, map_w), unmap_fds(e, map_e)]
54
219
  end
55
220
 
221
+ # fiber hook: creates a new fiber with the given block. The created fiber is
222
+ # added to the fiber map, scheduled on the scheduler machine, and started
223
+ # before this method returns (by calling snooze).
224
+ #
225
+ # @param block [Proc] fiber block @return [Fiber]
56
226
  def fiber(&block)
57
- f = @machine.spin(&block)
227
+ fiber = Fiber.new(blocking: false) { @machine.run(fiber, &block) }
228
+ @fiber_map[fiber] = true
229
+ @machine.schedule(fiber, nil)
58
230
  @machine.snooze
59
- f
231
+ fiber
60
232
  end
61
233
 
62
- 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)
234
+ # io_read hook: reads from the given IO.
235
+ #
236
+ # @param io [IO] IO object
237
+ # @param buffer [IO::Buffer] read buffer
238
+ # @param length [Integer] read length
239
+ # @param offset [Integer] buffer offset
240
+ # @return [Integer] bytes read
241
+ def io_read(io, buffer, length, offset)
242
+ length = buffer.size if length == 0
243
+
244
+ if (timeout = io.timeout)
245
+ @machine.timeout(timeout, Timeout::Error) do
246
+ @machine.read(io.fileno, buffer, length, offset)
247
+ rescue Errno::EINTR
248
+ retry
249
+ end
250
+ else
251
+ @machine.read(io.fileno, buffer, length, offset)
252
+ end
253
+ rescue Errno::EINTR
254
+ retry
65
255
  end
66
256
 
67
- def io_read(io, buffer, length, offset)
68
- # p io_read: [io, buffer, length, offset]
69
- s = +''
257
+ # io_pread hook: reads from the given IO at the given offset
258
+ #
259
+ # @param io [IO] IO object
260
+ # @param buffer [IO::Buffer] read buffer
261
+ # @param from [Integer] read offset
262
+ # @param length [Integer] read length
263
+ # @param offset [Integer] buffer offset
264
+ # @return [Integer] bytes read
265
+ def io_pread(io, buffer, from, length, offset)
266
+ length = buffer.size if length == 0
267
+
268
+ if (timeout = io.timeout)
269
+ @machine.timeout(timeout, Timeout::Error) do
270
+ @machine.read(io.fileno, buffer, length, offset, from)
271
+ rescue Errno::EINTR
272
+ retry
273
+ end
274
+ else
275
+ @machine.read(io.fileno, buffer, length, offset, from)
276
+ end
277
+ rescue Errno::EINTR
278
+ retry
279
+ end
280
+
281
+ # io_write hook: writes to the given IO.
282
+ #
283
+ # @param io [IO] IO object
284
+ # @param buffer [IO::Buffer] write buffer
285
+ # @param length [Integer] write length
286
+ # @param offset [Integer] write offset
287
+ # @return [Integer] bytes written
288
+ def io_write(io, buffer, length, offset)
289
+ # p(io_write: io, length:, offset:, timeout: io.timeout)
70
290
  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
291
+ buffer = buffer.slice(offset) if offset > 0
292
+
293
+ if (timeout = io.timeout)
294
+ @machine.timeout(timeout, Timeout::Error) do
295
+ @machine.write(io.fileno, buffer, length)
296
+ rescue Errno::EINTR
297
+ retry
298
+ end
299
+ else
300
+ @machine.write(io.fileno, buffer, length)
301
+ end
302
+ rescue Errno::EINTR
303
+ retry
76
304
  end
77
305
 
306
+ # io_pwrite hook: writes to the given IO at the given offset.
307
+ #
308
+ # @param io [IO] IO object
309
+ # @param buffer [IO::Buffer] write buffer
310
+ # @param length [Integer] file offset
311
+ # @param length [Integer] write length
312
+ # @param offset [Integer] buffer offset
313
+ # @return [Integer] bytes written
78
314
  def io_pwrite(io, buffer, from, length, offset)
79
- p io_pwrite: [io, buffer, from, length, offset]
315
+ # p(io_pwrite: io, from:, length:, offset:, timeout: io.timeout)
316
+ length = buffer.size if length == 0
317
+ buffer = buffer.slice(offset) if offset > 0
318
+
319
+ if (timeout = io.timeout)
320
+ @machine.timeout(timeout, Timeout::Error) do
321
+ @machine.write(io.fileno, buffer, length, from)
322
+ rescue Errno::EINTR
323
+ retry
324
+ end
325
+ else
326
+ @machine.write(io.fileno, buffer, length, from)
327
+ end
328
+ rescue Errno::EINTR
329
+ retry
330
+ end
331
+
332
+ if UM.method_defined?(:waitid_status)
333
+ def process_wait(pid, flags)
334
+ flags = UM::WEXITED if flags == 0
335
+ @machine.waitid_status(UM::P_PID, pid, flags)
336
+ end
337
+ end
338
+
339
+ def fiber_interrupt(fiber, exception)
340
+ @machine.schedule(fiber, exception)
341
+ @machine.wakeup
342
+ end
343
+
344
+ def address_resolve(hostname)
345
+ Resolv.getaddresses(hostname)
346
+ end
347
+
348
+ def timeout_after(duration, exception, message, &block)
349
+ @machine.timeout(duration, exception, &block)
350
+ end
351
+
352
+ private
353
+
354
+ def map_io_fds(ios)
355
+ ios.each_with_object({}) { |io, h| h[io.fileno] = io }
356
+ end
357
+
358
+ def unmap_fds(fds, map)
359
+ fds.map { map[it] }
80
360
  end
81
361
 
82
- def io_pread(io, buffer, from, length, offset)
83
- p io_pread: [io, buffer, from, length, offset]
84
- end
85
-
86
- # def fiber(&block)
87
- # fiber = Fiber.new(blocking: false, &block)
88
- # unblock(nil, fiber)
89
- # # fiber.resume
90
- # return fiber
91
- # end
92
-
93
- # def kernel_sleep(duration = nil)
94
- # block(:sleep, duration)
95
- # end
96
-
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
362
  end
104
363
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class UringMachine
4
- VERSION = '0.19.1'
4
+ VERSION = '0.21.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