Skip to content

Commit 426a88c

Browse files
tavianatorcursoragent
authored andcommitted
Handle short io_uring submissions
io_uring does not necessarily submit all the pending SQEs when you call io_uring_submit(). In particular, in the default configuration, an error while processing an SQE will cause the rest of the batch to be aborted. Even with IORING_SETUP_SUBMIT_ALL, some errors (like ENOMEM) will still lead to a short submit. Fix the accounting to keep selector->pending equal to the number of unsubmitted SQEs at all times, so that the entire queue can be flushed reliably. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e1303a4 commit 426a88c

2 files changed

Lines changed: 25 additions & 36 deletions

File tree

ext/io/event/selector/uring.c

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include <liburing.h>
1010
#include <poll.h>
11+
#include <stdbool.h>
1112
#include <stdint.h>
1213
#include <time.h>
1314

@@ -406,59 +407,46 @@ void IO_Event_Selector_URing_dump_completion_queue(struct IO_Event_Selector_URin
406407
}
407408
}
408409

409-
// Flush the submission queue if pending operations are present.
410+
// Flush the submission queue, optionally yielding if unsuccessful.
410411
static
411-
int io_uring_submit_flush(struct IO_Event_Selector_URing *selector) {
412-
if (selector->pending) {
413-
if (DEBUG) fprintf(stderr, "io_uring_submit_flush(pending=%ld)\n", selector->pending);
414-
415-
// Try to submit:
412+
int io_uring_submit_all(struct IO_Event_Selector_URing *selector, bool yield) {
413+
while (selector->pending > 0) {
416414
int result = io_uring_submit(&selector->ring);
417415

418416
if (result >= 0) {
419-
// If it was submitted, reset pending count:
420-
selector->pending = 0;
421-
} else if (result != -EBUSY && result != -EAGAIN) {
422-
rb_syserr_fail(-result, "io_uring_submit_flush:io_uring_submit");
417+
// io_uring_submit() returns the number of submitted SQEs
418+
selector->pending -= result;
419+
} else if (result == -EBUSY || result == -EAGAIN) {
420+
if (yield) IO_Event_Selector_yield(&selector->backend);
421+
} else {
422+
rb_syserr_fail(-result, "io_uring_submit_all:io_uring_submit");
423+
return result;
423424
}
424-
425-
return result;
426425
}
427-
428-
if (DEBUG) {
429-
IO_Event_Selector_URing_dump_completion_queue(selector);
430-
}
431-
426+
427+
if (DEBUG) IO_Event_Selector_URing_dump_completion_queue(selector);
432428
return 0;
433429
}
434430

431+
// Flush the submission queue if pending operations are present.
432+
static
433+
int io_uring_submit_flush(struct IO_Event_Selector_URing *selector) {
434+
if (DEBUG) fprintf(stderr, "io_uring_submit_now(pending=%ld)\n", selector->pending);
435+
436+
return io_uring_submit_all(selector, false);
437+
}
438+
435439
// Immediately flush the submission queue, yielding to the event loop if it was not successful.
436440
static
437441
int io_uring_submit_now(struct IO_Event_Selector_URing *selector) {
438442
if (DEBUG) fprintf(stderr, "io_uring_submit_now(pending=%ld)\n", selector->pending);
439443

440-
while (true) {
441-
int result = io_uring_submit(&selector->ring);
442-
443-
if (result >= 0) {
444-
selector->pending = 0;
445-
if (DEBUG) IO_Event_Selector_URing_dump_completion_queue(selector);
446-
return result;
447-
}
448-
449-
if (result == -EBUSY || result == -EAGAIN) {
450-
IO_Event_Selector_yield(&selector->backend);
451-
} else {
452-
rb_syserr_fail(-result, "io_uring_submit_now:io_uring_submit");
453-
}
454-
}
444+
return io_uring_submit_all(selector, true);
455445
}
456446

457447
// Submit a pending operation. This does not submit the operation immediately, but instead defers it to the next call to `io_uring_submit_flush` or `io_uring_submit_now`. This is useful for operations that are not urgent, but should be used with care as it can lead to a deadlock if the submission queue is not flushed.
458448
static
459449
void io_uring_submit_pending(struct IO_Event_Selector_URing *selector) {
460-
selector->pending += 1;
461-
462450
if (DEBUG) fprintf(stderr, "io_uring_submit_pending(ring=%p, pending=%ld)\n", &selector->ring, selector->pending);
463451
}
464452

@@ -471,7 +459,8 @@ struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) {
471459

472460
sqe = io_uring_get_sqe(&selector->ring);
473461
}
474-
462+
463+
selector->pending += 1;
475464
return sqe;
476465
}
477466

@@ -1136,7 +1125,6 @@ int select_internal_without_gvl(struct select_arguments *arguments) {
11361125
io_uring_prep_read(sqe, IO_Event_Interrupt_descriptor(&selector->interrupt), &selector->wakeup_value, sizeof(selector->wakeup_value), 0);
11371126
io_uring_sqe_set_data(sqe, &selector->interrupt);
11381127
selector->wakeup_registered = 1;
1139-
selector->pending += 1;
11401128
}
11411129

11421130
io_uring_submit_flush(selector);

releases.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Add support for the `io_close` fiber-scheduler hook (Ruby 4.0+). The `URing` selector performs the close asynchronously via the ring; the `Debug::Selector` and `TestScheduler` wrappers forward to the underlying selector when supported.
77
- Improve `WorkerPool` GC compaction support and add proper write barriers, fixing potential use-after-free under compacting GC.
88
- Keep blocked scheduler fibers alive during GC by registering them as roots in `TestScheduler#block`, preventing premature collection and the resulting use-after-free crash on resume.
9+
- Correctly handle short `io_uring_submit()` results in the `URing` selector. `io_uring_submit()` returns the number of SQEs actually accepted by the kernel and can be short (SQE prep errors, `ENOMEM`, transient `EAGAIN`); the old accounting reset `pending = 0` on any success and silently lost track of unsubmitted SQEs.
910

1011
## v1.15.1
1112

0 commit comments

Comments
 (0)