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
data/ext/um/um_const.c CHANGED
@@ -13,6 +13,7 @@
13
13
  #include <netdb.h>
14
14
  #include <net/if.h>
15
15
  #include <poll.h>
16
+ #include <signal.h>
16
17
 
17
18
  #define DEF_CONST_INT(mod, v) rb_define_const(mod, #v, INT2NUM(v))
18
19
 
@@ -88,12 +89,12 @@ void um_define_net_constants(VALUE mod) {
88
89
  DEF_CONST_INT(mod, O_TRUNC);
89
90
  DEF_CONST_INT(mod, O_WRONLY);
90
91
 
91
- DEF_CONST_INT(mod, WNOHANG);
92
- DEF_CONST_INT(mod, WUNTRACED);
93
92
  DEF_CONST_INT(mod, WCONTINUED);
94
93
  DEF_CONST_INT(mod, WEXITED);
95
- DEF_CONST_INT(mod, WSTOPPED);
94
+ DEF_CONST_INT(mod, WNOHANG);
96
95
  DEF_CONST_INT(mod, WNOWAIT);
96
+ DEF_CONST_INT(mod, WSTOPPED);
97
+ DEF_CONST_INT(mod, WUNTRACED);
97
98
 
98
99
  DEF_CONST_INT(mod, SOCK_STREAM);
99
100
  DEF_CONST_INT(mod, SOCK_DGRAM);
@@ -391,4 +392,51 @@ void um_define_net_constants(VALUE mod) {
391
392
  DEF_CONST_INT(mod, EKEYREJECTED);
392
393
  DEF_CONST_INT(mod, EOWNERDEAD);
393
394
  DEF_CONST_INT(mod, ENOTRECOVERABLE);
395
+
396
+ DEF_CONST_INT(mod, P_PID);
397
+ DEF_CONST_INT(mod, P_PIDFD);
398
+ DEF_CONST_INT(mod, P_PGID);
399
+ DEF_CONST_INT(mod, P_ALL);
400
+
401
+ DEF_CONST_INT(mod, CLD_EXITED);
402
+ DEF_CONST_INT(mod, CLD_KILLED);
403
+ DEF_CONST_INT(mod, CLD_DUMPED);
404
+ DEF_CONST_INT(mod, CLD_STOPPED);
405
+ DEF_CONST_INT(mod, CLD_TRAPPED);
406
+ DEF_CONST_INT(mod, CLD_CONTINUED);
407
+
408
+ DEF_CONST_INT(mod, SIGHUP);
409
+ DEF_CONST_INT(mod, SIGINT);
410
+ DEF_CONST_INT(mod, SIGQUIT);
411
+ DEF_CONST_INT(mod, SIGILL);
412
+ DEF_CONST_INT(mod, SIGTRAP);
413
+ DEF_CONST_INT(mod, SIGABRT);
414
+ DEF_CONST_INT(mod, SIGIOT);
415
+ DEF_CONST_INT(mod, SIGFPE);
416
+ DEF_CONST_INT(mod, SIGKILL);
417
+ DEF_CONST_INT(mod, SIGBUS);
418
+ DEF_CONST_INT(mod, SIGSEGV);
419
+ DEF_CONST_INT(mod, SIGSYS);
420
+ DEF_CONST_INT(mod, SIGPIPE);
421
+ DEF_CONST_INT(mod, SIGALRM);
422
+ DEF_CONST_INT(mod, SIGTERM);
423
+ DEF_CONST_INT(mod, SIGURG);
424
+ DEF_CONST_INT(mod, SIGSTOP);
425
+ DEF_CONST_INT(mod, SIGTSTP);
426
+ DEF_CONST_INT(mod, SIGCONT);
427
+ DEF_CONST_INT(mod, SIGCHLD);
428
+ DEF_CONST_INT(mod, SIGCLD);
429
+ DEF_CONST_INT(mod, SIGTTIN);
430
+ DEF_CONST_INT(mod, SIGTTOU);
431
+ DEF_CONST_INT(mod, SIGIO);
432
+ DEF_CONST_INT(mod, SIGXCPU);
433
+ DEF_CONST_INT(mod, SIGXFSZ);
434
+ DEF_CONST_INT(mod, SIGVTALRM);
435
+ DEF_CONST_INT(mod, SIGPROF);
436
+ DEF_CONST_INT(mod, SIGWINCH);
437
+ DEF_CONST_INT(mod, SIGUSR1);
438
+ DEF_CONST_INT(mod, SIGUSR2);
439
+ DEF_CONST_INT(mod, SIGPWR);
440
+ DEF_CONST_INT(mod, SIGPOLL);
441
+
394
442
  }
@@ -11,7 +11,7 @@ static const rb_data_type_t Mutex_type = {
11
11
  .dsize = NULL,
12
12
  .dcompact = NULL
13
13
  },
14
- .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE
14
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
15
15
  };
16
16
 
17
17
  static VALUE Mutex_allocate(VALUE klass) {
@@ -26,7 +26,7 @@ static const rb_data_type_t Queue_type = {
26
26
  .dsize = NULL,
27
27
  .dcompact = Queue_compact
28
28
  },
29
- .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE
29
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
30
30
  };
31
31
 
32
32
  static VALUE Queue_allocate(VALUE klass) {
data/ext/um/um_stream.c CHANGED
@@ -211,7 +211,7 @@ static inline VALUE resp_decode_simple_error(char *ptr, ulong len) {
211
211
  if (!ID_new) ID_new = rb_intern("new");
212
212
 
213
213
  VALUE msg = rb_str_new(ptr + 1, len - 1);
214
- VALUE err = rb_funcall(rb_eRuntimeError, ID_new, 1, msg);
214
+ VALUE err = rb_funcall(eStreamRESPError, ID_new, 1, msg);
215
215
  RB_GC_GUARD(msg);
216
216
  return err;
217
217
  }
@@ -221,7 +221,7 @@ static inline VALUE resp_decode_error(struct um_stream *stream, VALUE out_buffer
221
221
  if (!ID_new) ID_new = rb_intern("new");
222
222
 
223
223
  VALUE msg = resp_decode_string(stream, out_buffer, len);
224
- VALUE err = rb_funcall(rb_eRuntimeError, ID_new, 1, msg);
224
+ VALUE err = rb_funcall(eStreamRESPError, ID_new, 1, msg);
225
225
  RB_GC_GUARD(msg);
226
226
  return err;
227
227
  }
@@ -264,7 +264,7 @@ VALUE resp_decode(struct um_stream *stream, VALUE out_buffer) {
264
264
  case ':': // integer
265
265
  return resp_decode_integer(ptr);
266
266
  case '(': // big integer
267
- rb_raise(rb_eRuntimeError, "Big integers are not supported");
267
+ um_raise_internal_error("Big integers are not supported");
268
268
  case ',': // float
269
269
  return resp_decode_float(ptr);
270
270
 
@@ -274,7 +274,7 @@ VALUE resp_decode(struct um_stream *stream, VALUE out_buffer) {
274
274
  data_len = resp_parse_length_field(ptr, len);
275
275
  return resp_decode_error(stream, out_buffer, data_len);
276
276
  default:
277
- rb_raise(rb_eRuntimeError, "Invalid character encountered");
277
+ um_raise_internal_error("Invalid character encountered");
278
278
  }
279
279
 
280
280
  RB_GC_GUARD(msg);
@@ -390,6 +390,6 @@ void resp_encode(struct um_write_buffer *buf, VALUE obj) {
390
390
  return;
391
391
  }
392
392
  default:
393
- rb_raise(rb_eRuntimeError, "Can't encode object");
393
+ um_raise_internal_error("Can't encode object");
394
394
  }
395
395
  }
@@ -1,6 +1,7 @@
1
1
  #include "um.h"
2
2
 
3
3
  VALUE cStream;
4
+ VALUE eStreamRESPError;
4
5
 
5
6
  static void Stream_mark(void *ptr) {
6
7
  struct um_stream *stream = ptr;
@@ -94,4 +95,6 @@ void Init_Stream(void) {
94
95
 
95
96
  rb_define_method(cStream, "resp_decode", Stream_resp_decode, 0);
96
97
  rb_define_singleton_method(cStream, "resp_encode", Stream_resp_encode, 2);
98
+
99
+ eStreamRESPError = rb_define_class_under(cStream, "RESPError", rb_eStandardError);
97
100
  }
data/ext/um/um_sync.c CHANGED
@@ -56,44 +56,45 @@ void um_futex_wake_transient(struct um *machine, uint32_t *futex, uint32_t num_w
56
56
 
57
57
  void um_mutex_init(struct um_mutex *mutex) {
58
58
  mutex->state = MUTEX_UNLOCKED;
59
+ mutex->num_waiters = 0;
59
60
  }
60
61
 
61
- inline void um_mutex_lock(struct um *machine, uint32_t *state) {
62
- while (*state == MUTEX_LOCKED) {
63
- um_futex_wait(machine, state, MUTEX_LOCKED);
62
+ inline void um_mutex_lock(struct um *machine, struct um_mutex *mutex) {
63
+ mutex->num_waiters++;
64
+ while (mutex->state == MUTEX_LOCKED) {
65
+ um_futex_wait(machine, &mutex->state, MUTEX_LOCKED);
64
66
  }
65
- *state = MUTEX_LOCKED;
67
+ mutex->num_waiters--;
68
+ mutex->state = MUTEX_LOCKED;
66
69
  }
67
70
 
68
- inline void um_mutex_unlock(struct um *machine, uint32_t *state) {
69
- *state = MUTEX_UNLOCKED;
70
- // Wake up 1 waiting fiber
71
- um_futex_wake(machine, state, 1);
71
+ inline void um_mutex_unlock(struct um *machine, struct um_mutex *mutex) {
72
+ mutex->state = MUTEX_UNLOCKED;
73
+
74
+ if (mutex->num_waiters)
75
+ // Wake up 1 waiting fiber
76
+ um_futex_wake(machine, &mutex->state, 1);
72
77
  }
73
78
 
74
79
  struct sync_ctx {
75
80
  struct um *machine;
76
- VALUE mutex;
77
- uint32_t *state;
81
+ struct um_mutex *mutex;
78
82
  };
79
83
 
80
84
  VALUE synchronize_start(VALUE arg) {
81
85
  struct sync_ctx *ctx = (struct sync_ctx *)arg;
82
- um_mutex_lock(ctx->machine, ctx->state);
86
+ um_mutex_lock(ctx->machine, ctx->mutex);
83
87
  return rb_yield(Qnil);
84
88
  }
85
89
 
86
90
  VALUE synchronize_complete(VALUE arg) {
87
91
  struct sync_ctx *ctx = (struct sync_ctx *)arg;
88
- // Mutex is an embedded data class, so it might have moved while the operation
89
- // was ongoing. We need to update the pointer to the embedded state variable.
90
- ctx->state = &Mutex_data(ctx->mutex)->state;
91
- um_mutex_unlock(ctx->machine, ctx->state);
92
+ um_mutex_unlock(ctx->machine, ctx->mutex);
92
93
  return Qnil;
93
94
  }
94
95
 
95
- inline VALUE um_mutex_synchronize(struct um *machine, VALUE mutex, uint32_t *state) {
96
- struct sync_ctx ctx = { .machine = machine, .mutex = mutex, .state = state };
96
+ inline VALUE um_mutex_synchronize(struct um *machine, struct um_mutex *mutex) {
97
+ struct sync_ctx ctx = { .machine = machine, .mutex = mutex };
97
98
  return rb_ensure(synchronize_start, (VALUE)&ctx, synchronize_complete, (VALUE)&ctx);
98
99
  }
99
100
 
@@ -228,7 +229,6 @@ enum queue_op { QUEUE_POP, QUEUE_SHIFT };
228
229
 
229
230
  struct queue_wait_ctx {
230
231
  struct um *machine;
231
- VALUE queue_obj;
232
232
  struct um_queue *queue;
233
233
  enum queue_op op;
234
234
  };
@@ -242,9 +242,9 @@ VALUE um_queue_remove_start(VALUE arg) {
242
242
  }
243
243
 
244
244
  if (ctx->queue->state != QUEUE_READY)
245
- rb_raise(rb_eRuntimeError, "Internal error: queue should be in ready state!");
245
+ um_raise_internal_error("Internal error: queue should be in ready state!");
246
246
  if (!ctx->queue->tail)
247
- rb_raise(rb_eRuntimeError, "Internal error: queue should be in ready state!");
247
+ um_raise_internal_error("Internal error: queue should be in ready state!");
248
248
 
249
249
  ctx->queue->count--;
250
250
  return (ctx->op == QUEUE_POP ? queue_remove_tail : queue_remove_head)(ctx->queue);
@@ -252,11 +252,6 @@ VALUE um_queue_remove_start(VALUE arg) {
252
252
 
253
253
  VALUE um_queue_remove_complete(VALUE arg) {
254
254
  struct queue_wait_ctx *ctx = (struct queue_wait_ctx *)arg;
255
-
256
- // the um_queue struct is embedded, so it might have been moved while the op
257
- // was ongoing, so we need to get it again on op completion
258
- ctx->queue = Queue_data(ctx->queue_obj);
259
-
260
255
  ctx->queue->num_waiters--;
261
256
 
262
257
  if (ctx->queue->num_waiters && ctx->queue->tail) {
@@ -270,11 +265,11 @@ VALUE um_queue_remove_complete(VALUE arg) {
270
265
  }
271
266
 
272
267
  VALUE um_queue_pop(struct um *machine, struct um_queue *queue) {
273
- struct queue_wait_ctx ctx = { .machine = machine, .queue_obj = queue->self, .queue = queue, .op = QUEUE_POP };
268
+ struct queue_wait_ctx ctx = { .machine = machine, .queue = queue, .op = QUEUE_POP };
274
269
  return rb_ensure(um_queue_remove_start, (VALUE)&ctx, um_queue_remove_complete, (VALUE)&ctx);
275
270
  }
276
271
 
277
272
  VALUE um_queue_shift(struct um *machine, struct um_queue *queue) {
278
- struct queue_wait_ctx ctx = { .machine = machine, .queue_obj = queue->self, .queue = queue, .op = QUEUE_SHIFT };
273
+ struct queue_wait_ctx ctx = { .machine = machine, .queue = queue, .op = QUEUE_SHIFT };
279
274
  return rb_ensure(um_queue_remove_start, (VALUE)&ctx, um_queue_remove_complete, (VALUE)&ctx);
280
275
  }
data/ext/um/um_utils.c CHANGED
@@ -1,6 +1,7 @@
1
1
  #include "um.h"
2
2
  #include <sys/mman.h>
3
3
  #include <stdlib.h>
4
+ #include <ruby/io/buffer.h>
4
5
 
5
6
  inline struct __kernel_timespec um_double_to_timespec(double value) {
6
7
  double integral;
@@ -32,35 +33,70 @@ inline void um_raise_on_error_result(int result) {
32
33
  if (unlikely(result < 0)) rb_syserr_fail(-result, strerror(-result));
33
34
  }
34
35
 
35
- inline void * um_prepare_read_buffer(VALUE buffer, unsigned len, int ofs) {
36
- unsigned current_len = RSTRING_LEN(buffer);
37
- if (ofs < 0) ofs = current_len + ofs + 1;
38
- unsigned new_len = len + (unsigned)ofs;
39
-
40
- if (current_len < new_len)
41
- rb_str_modify_expand(buffer, new_len);
36
+ inline void * um_prepare_read_buffer(VALUE buffer, ssize_t len, ssize_t ofs) {
37
+ if (TYPE(buffer) == T_STRING) {
38
+ size_t current_len = RSTRING_LEN(buffer);
39
+ if (len == -1) len = current_len;
40
+ if (ofs < 0) ofs = current_len + ofs + 1;
41
+ size_t new_len = len + (size_t)ofs;
42
+
43
+ if (current_len < new_len)
44
+ rb_str_modify_expand(buffer, new_len);
45
+ else
46
+ rb_str_modify(buffer);
47
+ return RSTRING_PTR(buffer) + ofs;
48
+ }
49
+ else if (IO_BUFFER_P(buffer)) {
50
+ void *base;
51
+ size_t size;
52
+ rb_io_buffer_get_bytes_for_writing(buffer, &base, &size); // writing *to* buffer
53
+ if (len == -1) len = size;
54
+ if (ofs < 0) ofs = size + ofs + 1;
55
+ size_t new_size = len + (size_t)ofs;
56
+
57
+ if (size < new_size) {
58
+ rb_io_buffer_resize(buffer, new_size);
59
+ rb_io_buffer_get_bytes_for_writing(buffer, &base, &size);
60
+ }
61
+ return base + ofs;
62
+ }
42
63
  else
43
- rb_str_modify(buffer);
44
- return RSTRING_PTR(buffer) + ofs;
64
+ um_raise_internal_error("Invalid buffer provided");
45
65
  }
46
66
 
47
- static inline void adjust_read_buffer_len(VALUE buffer, int result, int ofs) {
48
- rb_str_modify(buffer);
49
- unsigned len = result > 0 ? (unsigned)result : 0;
50
- unsigned current_len = RSTRING_LEN(buffer);
51
- if (ofs < 0) ofs = current_len + ofs + 1;
52
- rb_str_set_len(buffer, len + (unsigned)ofs);
67
+ static inline void adjust_read_buffer_len(VALUE buffer, int result, ssize_t ofs) {
68
+ if (TYPE(buffer) == T_STRING) {
69
+ rb_str_modify(buffer);
70
+ unsigned len = result > 0 ? (unsigned)result : 0;
71
+ unsigned current_len = RSTRING_LEN(buffer);
72
+ if (ofs < 0) ofs = current_len + ofs + 1;
73
+ rb_str_set_len(buffer, len + (unsigned)ofs);
74
+ }
75
+ else if (IO_BUFFER_P(buffer)) {
76
+ // do nothing?
77
+ }
53
78
  }
54
79
 
55
- inline void um_update_read_buffer(struct um *machine, VALUE buffer, int buffer_offset, __s32 result, __u32 flags) {
80
+ inline void um_update_read_buffer(struct um *machine, VALUE buffer, ssize_t buffer_offset, __s32 result, __u32 flags) {
56
81
  if (!result) return;
57
82
 
58
83
  adjust_read_buffer_len(buffer, result, buffer_offset);
59
84
  }
60
85
 
86
+ inline void um_get_buffer_bytes_for_writing(VALUE buffer, const void **base, size_t *size) {
87
+ if (TYPE(buffer) == T_STRING) {
88
+ *base = RSTRING_PTR(buffer);
89
+ *size = RSTRING_LEN(buffer);
90
+ }
91
+ else if (IO_BUFFER_P(buffer))
92
+ rb_io_buffer_get_bytes_for_reading(buffer, base, size); // reading *from* buffer
93
+ else
94
+ um_raise_internal_error("Invalid buffer provided");
95
+ }
96
+
61
97
  int um_setup_buffer_ring(struct um *machine, unsigned size, unsigned count) {
62
98
  if (machine->buffer_ring_count == BUFFER_RING_MAX_COUNT)
63
- rb_raise(rb_eRuntimeError, "Cannot setup more than BUFFER_RING_MAX_COUNT buffer rings");
99
+ um_raise_internal_error("Cannot setup more than BUFFER_RING_MAX_COUNT buffer rings");
64
100
 
65
101
  struct buf_ring_descriptor *desc = machine->buffer_rings + machine->buffer_ring_count;
66
102
  desc->buf_count = count;
@@ -72,7 +108,7 @@ int um_setup_buffer_ring(struct um *machine, unsigned size, unsigned count) {
72
108
  NULL, desc->br_size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0
73
109
  );
74
110
  if (mapped == MAP_FAILED)
75
- rb_raise(rb_eRuntimeError, "Failed to allocate buffer ring");
111
+ um_raise_internal_error("Failed to allocate buffer ring");
76
112
 
77
113
  desc->br = (struct io_uring_buf_ring *)mapped;
78
114
  io_uring_buf_ring_init(desc->br);
@@ -88,7 +124,7 @@ int um_setup_buffer_ring(struct um *machine, unsigned size, unsigned count) {
88
124
  if (size > 0) {
89
125
  if (posix_memalign(&desc->buf_base, 4096, desc->buf_count * desc->buf_size)) {
90
126
  io_uring_free_buf_ring(&machine->ring, desc->br, desc->buf_count, bg_id);
91
- rb_raise(rb_eRuntimeError, "Failed to allocate buffers");
127
+ um_raise_internal_error("Failed to allocate buffers");
92
128
  }
93
129
 
94
130
  void *ptr = desc->buf_base;
@@ -143,3 +179,7 @@ inline void um_add_strings_to_buffer_ring(struct um *machine, int bgid, VALUE st
143
179
  RB_GC_GUARD(converted);
144
180
  io_uring_buf_ring_advance(desc->br, count);
145
181
  }
182
+
183
+ inline void um_raise_internal_error(const char *msg) {
184
+ rb_raise(eUMError, "UringMachine error: %s", msg);
185
+ }
@@ -0,0 +1,229 @@
1
+ # 2025-11-14
2
+
3
+ ## Call with Samuel
4
+
5
+ - I explained the tasks that I want to do:
6
+
7
+ 1. FiberScheduler implementation for UringMachine
8
+ 2. Async SSL I/O
9
+ 3. Extend UringMachine & FiberScheduler with new functionality
10
+
11
+ - Samuel talked about two aspects:
12
+
13
+ - Experimentation.
14
+ - Integrating and improving on existing ecosystem, publicly visible changes to
15
+ interfaces.
16
+
17
+ So, improve on FiberScheduler interface, and show UringMachine as implementation.
18
+
19
+ Suggestion for tasks around FiberScheduler from Samuel:
20
+
21
+ 1. Add Fiber::Scheduler#io_splice + IO-uring backing for IO.copy_stream
22
+
23
+ Summary:
24
+
25
+ Build an async-aware, zero-copy data-transfer path in Ruby by exposing Linux’s
26
+ splice(2) through the Fiber Scheduler, and wiring it up so IO.copy_stream can
27
+ take advantage of io_uring when available. Why it matters: Large file copies and
28
+ proxying workloads become dramatically faster and cheaper because the data never
29
+ touches user space. This gives Ruby a modern, high-performance primitive for
30
+ bulk I/O.
31
+
32
+ 2. Add support for registered IO-uring buffers via IO::Buffer
33
+
34
+ Summary:
35
+
36
+ Integrate io_uring’s “registered buffers” feature with Ruby’s IO::Buffer,
37
+ allowing pre-allocated, pinned buffers to be reused across operations.
38
+
39
+ Why it matters:
40
+
41
+ Drastically reduces syscalls and buffer management overhead. Enables fully
42
+ zero-copy, high-throughput network servers and a more direct path to competitive
43
+ I/O performance.
44
+
45
+ 3. Richer process APIs using pidfds (Fiber::Scheduler#process_open)
46
+
47
+ Summary:
48
+
49
+ Introduce pidfd-backed process primitives in Ruby so processes can be opened,
50
+ monitored, and waited on safely through the scheduler.
51
+
52
+ Why it matters:
53
+
54
+ Pidfds eliminate race conditions, improve cross-thread safety, and make process
55
+ management reliably asynchronous. This enables safer job-runners, supervisors,
56
+ and async orchestration patterns in Ruby.
57
+
58
+ 4. Proper fork support for Fiber Scheduler (Fiber::Scheduler#process_fork)
59
+
60
+ Summary:
61
+
62
+ Define how fiber schedulers behave across fork: the child should start in a
63
+ clean state, with hooks to reinitialize or discard scheduler data safely.
64
+
65
+ Why it matters:
66
+
67
+ fork + async currently work inconsistently. This project makes forking
68
+ predictable, allowing libraries and apps to do post-fork setup (e.g., reconnect
69
+ I/O, restart loops) correctly and safely.
70
+
71
+ 5. Async-aware IO#close via io_uring prep_close + scheduler hook
72
+
73
+ Summary:
74
+
75
+ Introduce a formal closing state in Ruby’s IO internals, add io_uring’s
76
+ prep_close support, and provide Fiber::Scheduler#io_close as an official hook.
77
+
78
+ Why it matters:
79
+
80
+ Today, IO#close can be slow or unsafe to call in async contexts because it must
81
+ run synchronously. This project allows deferred/batched closing, avoids races,
82
+ and modernizes Ruby’s internal I/O lifecycle.
83
+
84
+ GDB/LLDB extensions: https://github.com/socketry/toolbox
85
+
86
+ # 2025-11-17
87
+
88
+ ## Work on io-event Uring selector
89
+
90
+ I added an implementation of `process_wait` using `io_uring_prep_waitid`. This
91
+ necessitates being able to create instances of `Process::Status`. For this, I've
92
+ submitted a PR for exposing `rb_process_status_new`:
93
+ https://github.com/ruby/ruby/pull/15213. Hopefully, this PR will be merged
94
+ before the release of Ruby 4.0.
95
+
96
+ # 2025-11-21
97
+
98
+ ## Work on UringMachine Fiber Scheduler
99
+
100
+ I've finally made some progress on the UringMachine fiber scheduler. This was a
101
+ process of learning the mchanics of how the scheduler is integrated with the
102
+ Ruby I/O layer. Some interesting warts in the Ruby `IO` implementation:
103
+
104
+ - When you call `Kernel.puts`, the trailing newline character is actually
105
+ written separately, which can lead to unexpected output if for example you
106
+ have multiple fibers writing to STDOUT at the same time. To prevent this, Ruby
107
+ uses a mutex (per IO instance) to synchronize writes to the same IO.
108
+ - There are inconsistencies in how different kinds of IO objects are handled,
109
+ with regards to blocking/non-blocking operation
110
+ ([O_NONBLOCK](https://linux.die.net/man/2/fcntl)):
111
+
112
+ - Files and standard I/O are blocking.
113
+ - Pipes are non-blocking.
114
+ - Sockets are non-blocking.
115
+ - OpenSSL sockets are non-blocking.
116
+
117
+ The problem is that for io_uring to function properly, the fds passed to it
118
+ should always be in blocking mode. To rectify this, I've added code to the
119
+ fiber scheduler implementation that makes sure the IO instance is blocking:
120
+
121
+ ```ruby
122
+ def io_write(io, buffer, length, offset)
123
+ reset_nonblock(io)
124
+ @machine.write(io.fileno, buffer.get_string)
125
+ rescue Errno::EINTR
126
+ retry
127
+ end
128
+
129
+ def reset_nonblock(io)
130
+ return if @ios.key?(io)
131
+
132
+ @ios[io] = true
133
+ UM.io_set_nonblock(io, false)
134
+ end
135
+ ```
136
+
137
+ - A phenomenon I've observed is that in some situations of multiple fibers doing
138
+ I/O, some of those I/O operations would raise an `EINTR`, which should mean
139
+ the I/O operation was interrupted because of a signal sent to the process.
140
+ Weird!
141
+
142
+ - There's some interesting stuff going on when calling `IO#close`. Apparently
143
+ there's a mutex involved, and I noticed two scheduler hooks are being called:
144
+ `#blocking_operation_wait` which means a blocking operation that should be ran
145
+ on a separate thread, and `#block`, which means a mutex is being locked. I
146
+ still need to figure out what is going on there and why it is so complex.
147
+ FWIW, UringMachine has a `#close_async` method which, as its name suggests,
148
+ submits a close operation, but does not wait for it to complete.
149
+
150
+ - I've added some basic documentation to the `FiberScheduler` class, and started
151
+ writing some tests. Now that I have a working fiber scheduler implementation
152
+ and I'm beginning to understand the mechanics of it, I can start TDD'ing...
153
+
154
+ ## Work on io-event Uring selector
155
+
156
+ - I've submitted a [PR](https://github.com/socketry/io-event/pull/154) for using
157
+ `io_uring_prep_waitid` in the `process_wait` implementation. This relies on
158
+ having a recent Linux kernel (>=6.7) and the afore-mentioned Ruby
159
+ [PR](https://github.com/ruby/ruby/pull/15213) for exposing
160
+ `rb_process_status_new` being merged. Hopefully this will happen in time for
161
+ the Ruby 4.0 release.
162
+
163
+ # 2025-11-26
164
+
165
+ - Added some benchmarks for measuring mutex performance vs stock Ruby Mutex
166
+ class. It turns out the `UM#synchronize` was much slower than core Ruby
167
+ `Mutex#synchronize`. This was because the UM version was always performing a
168
+ futex wake before returning, even if no fiber was waiting to lock the mutex. I
169
+ rectified this by adding a `num_waiters` field to `struct um_mutex`, which
170
+ indicates the number of fibers currently waiting to lock the mutex, and
171
+ avoiding calling `um_futex_wake` if it's 0.
172
+
173
+ - I also noticed that the `UM::Mutex` and `UM::Queue` classes were marked as
174
+ `RUBY_TYPED_EMBEDDABLE`, which means the underlying `struct um_mutex` and
175
+ `struct um_queue` were subject to moving. Obviously, you cannot just move a
176
+ futex var while the kernel is potentially waiting on it to change. I fixed
177
+ this by removing the `RUBY_TYPED_EMBEDDABLE` flag. This is a possible
178
+ explanation for the occasional segfaults I've been seeing in Syntropy when
179
+ doing lots of cancelled `UM#shift` ops (watching for file changes). (commit 3b013407ff94f8849517b0fca19839d37e046915)
180
+
181
+ - Added support for `IO::Buffer` in all low-level I/O APIs, which also means the
182
+ fiber scheduler doesn't need to convert from `IO::Buffer` to strings in order
183
+ to invoke the UringMachine API. (commits
184
+ 620680d9f80b6b46cb6037a6833d9cde5a861bcd,
185
+ 16d2008dd052e9d73df0495c16d11f52bee4fd15,
186
+ 4b2634d018fdbc52d63eafe6b0a102c0e409ebca,
187
+ bc9939f25509c0432a3409efd67ff73f0b316c61,
188
+ a9f38d9320baac3eeaf2fcb2143294ab8d115fe9)
189
+
190
+ - Added a custom `UM::Error` exception class raised on bad arguments or other
191
+ API misuse. I've also added a `UM::Stream::RESPError` exception class to be
192
+ instantiated on RESP errors. (commit 72a597d9f47d36b42977efa0f6ceb2e73a072bdf)
193
+
194
+ - I explored the fiber scheduler behaviour after forking. A fork done from a
195
+ thread where a scheduler was set will result in a main thread with the same
196
+ scheduler instantance. For the scheduler to work correctly after a fork, its
197
+ state must be reset. This is because sharing the same io_uring instance
198
+ between parent and child processes is not possible
199
+ (https://github.com/axboe/liburing/issues/612), and also because the child
200
+ process keeps only the fiber from which the fork was made as its main fiber
201
+ (the other fibers are lost).
202
+
203
+ So, the right thing to do here would be to add a `Fiber::Scheduler` hook that
204
+ will be invoked automatically by Ruby after a fork, and together with Samuel
205
+ I'll see if I can prepare a PR for that to be merged for the Ruby 4.0 release.
206
+
207
+ For the time being, I've added a `#post_fork` method to the UM fiber scheduler
208
+ which should be manually called after a fork. (commit 2c7877385869c6acbdd8354e2b2909cff448651b)
209
+
210
+ - Added two new low-level APIs for waiting on processes, instead of
211
+ `UM#waitpid`, using the io_uring version of `waitid`. The vanilla version
212
+ `UM#waitid` returns an array containing the terminated process pid, exit
213
+ status and code. The `UM#waitid_status` method returns a `Process::Status`
214
+ with the pid and exit status. This method is present only if the
215
+ `rb_process_status_new` function is available (see above).
216
+
217
+ - Implemented `FiberScheduler#process_wait` hook using `#waitid_status`.
218
+
219
+ - For the sake of completeness, I also added `UM.pidfd_open` and
220
+ `UM.pidfd_send_signal` for working with PID. A simple example:
221
+
222
+ ```ruby
223
+ child_pid = fork { ... }
224
+ fd = UM.pidfd_open(child_pid)
225
+ ...
226
+ UM.pidfd_send_signal(fd, UM::SIGUSR1)
227
+ ...
228
+ pid2, status = machine.waitid(P_PIDFD, fd, UM::WEXITED)
229
+ ```
@@ -0,0 +1,66 @@
1
+ - [v] io-event
2
+
3
+ - [v] Make PR to use io_uring_prep_waitid for kernel version >= 6.7
4
+
5
+ - https://github.com/socketry/io-event/blob/44666dc92ac3e093ca6ce3ab47052b808a58a325/ext/io/event/selector/uring.c#L460
6
+ - https://github.com/digital-fabric/uringmachine/blob/d5505d7fd94b800c848d186e17585e03ad9af6f2/ext/um/um.c#L697-L713
7
+
8
+ - [ ] UringMachine
9
+ - [v] Add support for IO::Buffer in UM API. (How can we detect an IO::Buffer object?)
10
+ https://docs.ruby-lang.org/capi/en/master/d8/d36/group__object.html#gab1b70414d07e7de585f47ee50a64a86c
11
+
12
+ - [v] Add `UM::Error` class to be used instead of RuntimeError
13
+
14
+ - [ ] Do batch allocation for `struct um_op`, so they'll be adjacent
15
+ - [ ] Add optional buffer depth argument to `UM.new` (for example, a the
16
+ worker thread for the scheduler `blocking_operation_wait` hook does not need
17
+ a lot of depth, so you can basically do `UM.new(4)`)
18
+
19
+ - [ ] Add support for using IO::Buffer in association with io_uring registered buffers / buffer rings
20
+
21
+ - [ ] FiberScheduler implementation
22
+ 4
23
+ - [v] Check how scheduler interacts with `fork`.
24
+ - [v] Implement `process_wait` (with `rb_process_status_new`)
25
+ - [ ] Implement timeouts (how do timeouts interact with blocking ops?)
26
+ - [ ] Implement address resolution hook
27
+ - [ ] Add tests:
28
+ - [ ] Sockets
29
+ - [ ] Files
30
+ - [ ] Mutex / Queue
31
+ - [ ] Thread.join
32
+ - [ ] Process.wait
33
+ - [ ] fork
34
+ - [ ] system / exec / etc.
35
+ - [ ] popen
36
+
37
+ - [ ] Benchmarks
38
+ - [ ] UM queue / Ruby queue (threads) / Ruby queue with UM fiber scheduler
39
+ - [ ] UM mutex / Ruby mutex (threads) / Ruby mutex with UM fiber scheduler
40
+ - [ ] Pipe IO raw UM / Ruby threaded / Ruby with UM fiber scheduler
41
+ - [ ] Socket IO (with socketpair) raw UM / Ruby threaded / Ruby with UM fiber scheduler
42
+ - [ ] Measure CPU (thread) time usage for above examples
43
+
44
+ - run each version 1M times
45
+ - measure total real time, total CPU time
46
+
47
+ ```ruby
48
+ real_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+ cpu_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
50
+ ```
51
+
52
+ - my hunch is we'll be able to show with io_uring real_time is less,
53
+ while cpu_time is more. But it's just a hunch.
54
+
55
+
56
+ - https://github.com/ruby/ruby/blob/master/doc/fiber.md
57
+ - https://github.com/ruby/ruby/blob/master/test/fiber/scheduler.rb
58
+ - https://github.com/socketry/async/blob/main/context/getting-started.md
59
+ - https://github.com/socketry/async/blob/main/context/scheduler.md
60
+ - https://github.com/socketry/async/blob/main/lib/async/scheduler.rb#L28
61
+
62
+ - [ ] SSL
63
+ - [ ] openssl gem: custom BIO?
64
+
65
+ - curl: https://github.com/curl/curl/blob/5f4cd4c689c822ce957bb415076f0c78e5f474b5/lib/vtls/openssl.c#L786-L803
66
+