uringmachine 0.19.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/TODO.md +0 -1
- data/examples/bm_fileno.rb +33 -0
- data/examples/bm_mutex.rb +85 -0
- data/examples/bm_mutex_single.rb +33 -0
- data/examples/bm_queue.rb +27 -28
- data/examples/bm_send.rb +2 -5
- data/examples/bm_snooze.rb +20 -42
- data/examples/fiber_scheduler_demo.rb +15 -51
- data/examples/fiber_scheduler_fork.rb +24 -0
- data/examples/nc_ssl.rb +71 -0
- data/ext/um/extconf.rb +5 -15
- data/ext/um/um.c +57 -41
- data/ext/um/um.h +21 -11
- data/ext/um/um_async_op_class.c +2 -2
- data/ext/um/um_buffer.c +1 -1
- data/ext/um/um_class.c +94 -23
- data/ext/um/um_const.c +51 -3
- data/ext/um/um_mutex_class.c +1 -1
- data/ext/um/um_queue_class.c +1 -1
- data/ext/um/um_stream.c +5 -5
- data/ext/um/um_stream_class.c +3 -0
- data/ext/um/um_sync.c +22 -27
- data/ext/um/um_utils.c +59 -19
- data/grant-2025/journal.md +229 -0
- data/grant-2025/tasks.md +66 -0
- data/lib/uringmachine/fiber_scheduler.rb +180 -48
- data/lib/uringmachine/version.rb +1 -1
- data/lib/uringmachine.rb +6 -0
- data/test/test_fiber_scheduler.rb +138 -0
- data/test/test_stream.rb +2 -2
- data/test/test_um.rb +427 -34
- data/vendor/liburing/.github/workflows/ci.yml +94 -1
- data/vendor/liburing/.github/workflows/test_build.c +9 -0
- data/vendor/liburing/configure +27 -0
- data/vendor/liburing/examples/Makefile +6 -0
- data/vendor/liburing/examples/helpers.c +8 -0
- data/vendor/liburing/examples/helpers.h +5 -0
- data/vendor/liburing/liburing.spec +1 -1
- data/vendor/liburing/src/Makefile +9 -3
- data/vendor/liburing/src/include/liburing/barrier.h +11 -5
- data/vendor/liburing/src/include/liburing/io_uring/query.h +41 -0
- data/vendor/liburing/src/include/liburing/io_uring.h +50 -0
- data/vendor/liburing/src/include/liburing/sanitize.h +16 -4
- data/vendor/liburing/src/include/liburing.h +445 -121
- data/vendor/liburing/src/liburing-ffi.map +15 -0
- data/vendor/liburing/src/liburing.map +8 -0
- data/vendor/liburing/src/sanitize.c +4 -1
- data/vendor/liburing/src/setup.c +7 -4
- data/vendor/liburing/test/232c93d07b74.c +4 -16
- data/vendor/liburing/test/Makefile +15 -1
- data/vendor/liburing/test/accept.c +2 -13
- data/vendor/liburing/test/conn-unreach.c +132 -0
- data/vendor/liburing/test/fd-pass.c +32 -7
- data/vendor/liburing/test/fdinfo.c +39 -12
- data/vendor/liburing/test/fifo-futex-poll.c +114 -0
- data/vendor/liburing/test/fifo-nonblock-read.c +1 -12
- data/vendor/liburing/test/futex.c +1 -1
- data/vendor/liburing/test/helpers.c +99 -2
- data/vendor/liburing/test/helpers.h +9 -0
- data/vendor/liburing/test/io_uring_passthrough.c +6 -12
- data/vendor/liburing/test/mock_file.c +379 -0
- data/vendor/liburing/test/mock_file.h +47 -0
- data/vendor/liburing/test/nop.c +2 -2
- data/vendor/liburing/test/nop32-overflow.c +150 -0
- data/vendor/liburing/test/nop32.c +126 -0
- data/vendor/liburing/test/pipe.c +166 -0
- data/vendor/liburing/test/poll-race-mshot.c +13 -1
- data/vendor/liburing/test/recv-mshot-fair.c +81 -34
- data/vendor/liburing/test/recvsend_bundle.c +1 -1
- data/vendor/liburing/test/resize-rings.c +2 -0
- data/vendor/liburing/test/ring-query.c +322 -0
- data/vendor/liburing/test/ringbuf-loop.c +87 -0
- data/vendor/liburing/test/runtests.sh +2 -2
- data/vendor/liburing/test/send-zerocopy.c +43 -5
- data/vendor/liburing/test/send_recv.c +102 -32
- data/vendor/liburing/test/shutdown.c +2 -12
- data/vendor/liburing/test/socket-nb.c +3 -14
- data/vendor/liburing/test/socket-rw-eagain.c +2 -12
- data/vendor/liburing/test/socket-rw-offset.c +2 -12
- data/vendor/liburing/test/socket-rw.c +2 -12
- data/vendor/liburing/test/sqe-mixed-bad-wrap.c +87 -0
- data/vendor/liburing/test/sqe-mixed-nop.c +82 -0
- data/vendor/liburing/test/sqe-mixed-uring_cmd.c +153 -0
- data/vendor/liburing/test/timestamp.c +56 -19
- data/vendor/liburing/test/vec-regbuf.c +2 -4
- data/vendor/liburing/test/wq-aff.c +7 -0
- metadata +24 -2
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
+
um_raise_internal_error("Can't encode object");
|
|
394
394
|
}
|
|
395
395
|
}
|
data/ext/um/um_stream_class.c
CHANGED
|
@@ -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,
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
+
mutex->num_waiters--;
|
|
68
|
+
mutex->state = MUTEX_LOCKED;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
inline void um_mutex_unlock(struct um *machine,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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->
|
|
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
|
-
|
|
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,
|
|
96
|
-
struct sync_ctx ctx = { .machine = machine, .mutex = mutex
|
|
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
|
-
|
|
245
|
+
um_raise_internal_error("Internal error: queue should be in ready state!");
|
|
246
246
|
if (!ctx->queue->tail)
|
|
247
|
-
|
|
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, .
|
|
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, .
|
|
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,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
```
|
data/grant-2025/tasks.md
ADDED
|
@@ -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
|
+
|