io-event 1.19.1 → 1.19.3
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
- checksums.yaml.gz.sig +0 -0
- data/ext/extconf.rb +1 -0
- data/ext/io/event/selector/epoll.c +2 -23
- data/ext/io/event/selector/kqueue.c +2 -24
- data/ext/io/event/selector/selector.c +62 -2
- data/ext/io/event/selector/selector.h +9 -5
- data/ext/io/event/selector/uring.c +39 -21
- data/lib/io/event/version.rb +1 -1
- data/readme.md +8 -14
- data/releases.md +8 -0
- data.tar.gz.sig +0 -0
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9b4e34ef8c8e421b9fe40c1e0556a1317b3233f0746f3c8c613c26bc962a2db
|
|
4
|
+
data.tar.gz: 8c4839836a933969612713d34031d2a88e8bac969061b64ad1d188db4f56e4db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75d55f360c232dbbb88c9794db98cd3c4e685652f7e407b1f411c2296991a6a31b947f5448e91a44daed431d562859e3f3d45640ecc99ae0936b65edbcb48183
|
|
7
|
+
data.tar.gz: 8f47bb334168fc983d765e39c53ccc6a19501204c15ae68d72bbeb3d11f0459b6a255c2ea944061ac494c8696c28d992bb3b241c6685ba3c6aaa2fc8ef5c98a1
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/ext/extconf.rb
CHANGED
|
@@ -41,11 +41,6 @@ struct IO_Event_Selector_EPoll
|
|
|
41
41
|
int descriptor;
|
|
42
42
|
pid_t owner;
|
|
43
43
|
|
|
44
|
-
// Flag indicating whether the selector is currently blocked in a system call.
|
|
45
|
-
// Set to 1 when blocked in epoll_wait() without GVL, 0 otherwise.
|
|
46
|
-
// Used by wakeup() to determine if an interrupt signal is needed.
|
|
47
|
-
int blocked;
|
|
48
|
-
|
|
49
44
|
struct timespec idle_duration;
|
|
50
45
|
|
|
51
46
|
struct IO_Event_Interrupt interrupt;
|
|
@@ -320,8 +315,6 @@ VALUE IO_Event_Selector_EPoll_allocate(VALUE self) {
|
|
|
320
315
|
IO_Event_Selector_initialize(&selector->backend, self, Qnil);
|
|
321
316
|
selector->descriptor = -1;
|
|
322
317
|
selector->owner = 0;
|
|
323
|
-
selector->blocked = 0;
|
|
324
|
-
|
|
325
318
|
selector->descriptors.element_initialize = IO_Event_Selector_EPoll_Descriptor_initialize;
|
|
326
319
|
selector->descriptors.element_free = IO_Event_Selector_EPoll_Descriptor_free;
|
|
327
320
|
IO_Event_Array_initialize(&selector->descriptors, IO_EVENT_ARRAY_DEFAULT_COUNT, sizeof(struct IO_Event_Selector_EPoll_Descriptor));
|
|
@@ -846,14 +839,6 @@ int select_blocking_allowed(struct timespec * timespec) {
|
|
|
846
839
|
if (timeout_is_nonblocking(timespec)) {
|
|
847
840
|
return 0;
|
|
848
841
|
}
|
|
849
|
-
|
|
850
|
-
#ifndef RB_NOGVL_PENDING_INTERRUPT_FAIL
|
|
851
|
-
// On Rubies without `RB_NOGVL_PENDING_INTERRUPT_FAIL`, `rb_thread_call_without_gvl2` can enter an indefinite native wait even if a masked interrupt is already pending for this thread. This is the last safe point to avoid that wait: we still hold the GVL, so the pending interrupt queue cannot change concurrently before we decide whether to enter the blocking path.
|
|
852
|
-
if (IO_Event_Selector_pending_interrupt()) {
|
|
853
|
-
return 0;
|
|
854
|
-
}
|
|
855
|
-
#endif
|
|
856
|
-
|
|
857
842
|
return 1;
|
|
858
843
|
}
|
|
859
844
|
|
|
@@ -918,13 +903,7 @@ void * select_internal(void *_arguments) {
|
|
|
918
903
|
static
|
|
919
904
|
int select_internal_without_gvl(struct select_arguments *arguments) {
|
|
920
905
|
arguments->result = -1;
|
|
921
|
-
arguments->selector->
|
|
922
|
-
#ifdef RB_NOGVL_PENDING_INTERRUPT_FAIL
|
|
923
|
-
rb_nogvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0, RB_NOGVL_INTR_FAIL | RB_NOGVL_PENDING_INTERRUPT_FAIL);
|
|
924
|
-
#else
|
|
925
|
-
rb_thread_call_without_gvl2(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
|
|
926
|
-
#endif
|
|
927
|
-
arguments->selector->blocked = 0;
|
|
906
|
+
IO_Event_Selector_blocking_operation(&arguments->selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0);
|
|
928
907
|
|
|
929
908
|
if (arguments->result == -1) {
|
|
930
909
|
// If Ruby skips the native callback, the result sentinel can remain `-1`; `errno` may be `0` or `EINTR` depending on the Ruby implementation. Both cases mean the blocking wait did not produce any events.
|
|
@@ -1089,7 +1068,7 @@ VALUE IO_Event_Selector_EPoll_wakeup(VALUE self) {
|
|
|
1089
1068
|
TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector);
|
|
1090
1069
|
|
|
1091
1070
|
// If we are blocking, we can schedule a nop event to wake up the selector:
|
|
1092
|
-
if (selector->blocked) {
|
|
1071
|
+
if (selector->backend.blocked) {
|
|
1093
1072
|
IO_Event_Interrupt_signal(&selector->interrupt);
|
|
1094
1073
|
|
|
1095
1074
|
return Qtrue;
|
|
@@ -50,11 +50,6 @@ struct IO_Event_Selector_KQueue
|
|
|
50
50
|
int descriptor;
|
|
51
51
|
pid_t owner;
|
|
52
52
|
|
|
53
|
-
// Flag indicating whether the selector is currently blocked in a system call.
|
|
54
|
-
// Set to 1 when blocked in kevent() without GVL, 0 otherwise.
|
|
55
|
-
// Used by wakeup() to determine if an interrupt signal is needed.
|
|
56
|
-
int blocked;
|
|
57
|
-
|
|
58
53
|
struct timespec idle_duration;
|
|
59
54
|
|
|
60
55
|
#ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT
|
|
@@ -299,8 +294,6 @@ VALUE IO_Event_Selector_KQueue_allocate(VALUE self) {
|
|
|
299
294
|
IO_Event_Selector_initialize(&selector->backend, self, Qnil);
|
|
300
295
|
selector->descriptor = -1;
|
|
301
296
|
selector->owner = 0;
|
|
302
|
-
selector->blocked = 0;
|
|
303
|
-
|
|
304
297
|
selector->descriptors.element_initialize = IO_Event_Selector_KQueue_Descriptor_initialize;
|
|
305
298
|
selector->descriptors.element_free = IO_Event_Selector_KQueue_Descriptor_free;
|
|
306
299
|
|
|
@@ -850,14 +843,6 @@ int select_blocking_allowed(struct timespec * timespec) {
|
|
|
850
843
|
if (timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0) {
|
|
851
844
|
return 0;
|
|
852
845
|
}
|
|
853
|
-
|
|
854
|
-
#ifndef RB_NOGVL_PENDING_INTERRUPT_FAIL
|
|
855
|
-
// On Rubies without `RB_NOGVL_PENDING_INTERRUPT_FAIL`, `rb_thread_call_without_gvl2` can enter an indefinite native wait even if a masked interrupt is already pending for this thread. This is the last safe point to avoid that wait: we still hold the GVL, so the pending interrupt queue cannot change concurrently before we decide whether to enter the blocking path.
|
|
856
|
-
if (IO_Event_Selector_pending_interrupt()) {
|
|
857
|
-
return 0;
|
|
858
|
-
}
|
|
859
|
-
#endif
|
|
860
|
-
|
|
861
846
|
return 1;
|
|
862
847
|
}
|
|
863
848
|
|
|
@@ -886,14 +871,7 @@ void * select_internal(void *_arguments) {
|
|
|
886
871
|
static
|
|
887
872
|
int select_internal_without_gvl(struct select_arguments *arguments) {
|
|
888
873
|
arguments->result = -1;
|
|
889
|
-
arguments->selector->
|
|
890
|
-
|
|
891
|
-
#ifdef RB_NOGVL_PENDING_INTERRUPT_FAIL
|
|
892
|
-
rb_nogvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0, RB_NOGVL_INTR_FAIL | RB_NOGVL_PENDING_INTERRUPT_FAIL);
|
|
893
|
-
#else
|
|
894
|
-
rb_thread_call_without_gvl2(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
|
|
895
|
-
#endif
|
|
896
|
-
arguments->selector->blocked = 0;
|
|
874
|
+
IO_Event_Selector_blocking_operation(&arguments->selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0);
|
|
897
875
|
|
|
898
876
|
if (arguments->result == -1) {
|
|
899
877
|
// If Ruby skips the native callback, the result sentinel can remain `-1`; `errno` may be `0` or `EINTR` depending on the Ruby implementation. Both cases mean the blocking wait did not produce any events.
|
|
@@ -1069,7 +1047,7 @@ VALUE IO_Event_Selector_KQueue_wakeup(VALUE self) {
|
|
|
1069
1047
|
struct IO_Event_Selector_KQueue *selector = NULL;
|
|
1070
1048
|
TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector);
|
|
1071
1049
|
|
|
1072
|
-
if (selector->blocked) {
|
|
1050
|
+
if (selector->backend.blocked) {
|
|
1073
1051
|
#ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT
|
|
1074
1052
|
IO_Event_Interrupt_signal(&selector->interrupt);
|
|
1075
1053
|
#else
|
|
@@ -8,7 +8,59 @@
|
|
|
8
8
|
|
|
9
9
|
static const int DEBUG = 0;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
#ifndef RB_NOGVL_PENDING_INTR_FAIL
|
|
12
|
+
static ID handle_interrupt_id;
|
|
13
|
+
static VALUE signal_exception_never = Qnil;
|
|
14
|
+
|
|
15
|
+
struct IO_Event_Selector_blocking_operation_arguments {
|
|
16
|
+
void *(*function)(void *);
|
|
17
|
+
void *data;
|
|
18
|
+
rb_unblock_function_t *unblock_function;
|
|
19
|
+
void *unblock_data;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
static VALUE IO_Event_Selector_blocking_operation_yield(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_argument, callback_argument)) {
|
|
23
|
+
struct IO_Event_Selector_blocking_operation_arguments *arguments = (struct IO_Event_Selector_blocking_operation_arguments *)callback_argument;
|
|
24
|
+
|
|
25
|
+
rb_thread_call_without_gvl2(arguments->function, arguments->data, arguments->unblock_function, arguments->unblock_data);
|
|
26
|
+
|
|
27
|
+
return Qnil;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static VALUE IO_Event_Selector_blocking_operation_fallback(VALUE callback_argument) {
|
|
31
|
+
return rb_block_call(rb_cThread, handle_interrupt_id, 1, &signal_exception_never, IO_Event_Selector_blocking_operation_yield, callback_argument);
|
|
32
|
+
}
|
|
33
|
+
#endif
|
|
34
|
+
|
|
35
|
+
void IO_Event_Selector_blocking_operation(struct IO_Event_Selector *selector, void *(*function)(void *), void *data, rb_unblock_function_t *unblock_function, void *unblock_data) {
|
|
36
|
+
int state = 0;
|
|
37
|
+
selector->blocked = 1;
|
|
38
|
+
|
|
39
|
+
#ifdef RB_NOGVL_PENDING_INTR_FAIL
|
|
40
|
+
rb_nogvl(function, data, unblock_function, unblock_data, RB_NOGVL_INTR_FAIL | RB_NOGVL_PENDING_INTR_FAIL);
|
|
41
|
+
#else
|
|
42
|
+
// `Thread.handle_interrupt` resets `pending_interrupt_queue_checked` and
|
|
43
|
+
// raises the VM interrupt flag when its mask is pushed with a non-empty
|
|
44
|
+
// pending queue. Its C block calls `rb_thread_call_without_gvl2` directly,
|
|
45
|
+
// without a Ruby safepoint between refreshing the flag and entering
|
|
46
|
+
// `unblock_function_set`. That function either observes the interrupt or
|
|
47
|
+
// installs the UBF while holding Ruby's interrupt lock.
|
|
48
|
+
//
|
|
49
|
+
// Keep signal exceptions deferred here: this fallback should prevent a
|
|
50
|
+
// stranded native wait without changing the caller's delivery timing.
|
|
51
|
+
struct IO_Event_Selector_blocking_operation_arguments arguments = {
|
|
52
|
+
.function = function,
|
|
53
|
+
.data = data,
|
|
54
|
+
.unblock_function = unblock_function,
|
|
55
|
+
.unblock_data = unblock_data,
|
|
56
|
+
};
|
|
57
|
+
rb_protect(IO_Event_Selector_blocking_operation_fallback, (VALUE)&arguments, &state);
|
|
58
|
+
#endif
|
|
59
|
+
|
|
60
|
+
selector->blocked = 0;
|
|
61
|
+
|
|
62
|
+
if (state) rb_jump_tag(state);
|
|
63
|
+
}
|
|
12
64
|
|
|
13
65
|
#ifndef HAVE_RB_IO_DESCRIPTOR
|
|
14
66
|
static ID id_fileno;
|
|
@@ -89,7 +141,14 @@ VALUE IO_Event_Selector_process_wait(rb_pid_t pid, int flags) {
|
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
void Init_IO_Event_Selector(VALUE IO_Event_Selector) {
|
|
92
|
-
|
|
144
|
+
#ifndef RB_NOGVL_PENDING_INTR_FAIL
|
|
145
|
+
handle_interrupt_id = rb_intern("handle_interrupt");
|
|
146
|
+
signal_exception_never = rb_hash_new();
|
|
147
|
+
rb_hash_aset(signal_exception_never, rb_eSignal, ID2SYM(rb_intern("never")));
|
|
148
|
+
rb_funcall(signal_exception_never, rb_intern("compare_by_identity"), 0);
|
|
149
|
+
rb_obj_freeze(signal_exception_never);
|
|
150
|
+
rb_gc_register_mark_object(signal_exception_never);
|
|
151
|
+
#endif
|
|
93
152
|
|
|
94
153
|
rb_IO_Event_Selector = IO_Event_Selector;
|
|
95
154
|
rb_gc_register_mark_object(rb_IO_Event_Selector);
|
|
@@ -114,6 +173,7 @@ void IO_Event_Selector_initialize(struct IO_Event_Selector *backend, VALUE self,
|
|
|
114
173
|
|
|
115
174
|
backend->waiting = NULL;
|
|
116
175
|
backend->ready = NULL;
|
|
176
|
+
backend->blocked = 0;
|
|
117
177
|
}
|
|
118
178
|
|
|
119
179
|
VALUE IO_Event_Selector_loop_resume(struct IO_Event_Selector *backend, VALUE fiber, int argc, VALUE *argv) {
|
|
@@ -40,11 +40,11 @@ static inline int IO_Event_try_again(int error) {
|
|
|
40
40
|
return error == EAGAIN || error == EWOULDBLOCK;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
// Enter a blocking region without the GVL. On Rubies without
|
|
44
|
+
// `RB_NOGVL_PENDING_INTR_FAIL`, refresh pending-interrupt state immediately
|
|
45
|
+
// before releasing the GVL so a signal cannot be stranded in the queue.
|
|
46
|
+
struct IO_Event_Selector;
|
|
47
|
+
void IO_Event_Selector_blocking_operation(struct IO_Event_Selector *selector, void *(*function)(void *), void *data, rb_unblock_function_t *unblock_function, void *unblock_data);
|
|
48
48
|
|
|
49
49
|
#ifdef HAVE_RB_IO_DESCRIPTOR
|
|
50
50
|
#define IO_Event_Selector_io_descriptor(io) rb_io_descriptor(io)
|
|
@@ -90,6 +90,10 @@ struct IO_Event_Selector {
|
|
|
90
90
|
VALUE self;
|
|
91
91
|
VALUE loop;
|
|
92
92
|
|
|
93
|
+
// Whether the selector is currently blocked in a system call without the GVL.
|
|
94
|
+
// Used by wakeup() to determine if an interrupt signal is needed.
|
|
95
|
+
int blocked;
|
|
96
|
+
|
|
93
97
|
// The ready queue is a list of fibers that are ready to be resumed from the event loop fiber.
|
|
94
98
|
// Append to waiting (front/head of queue).
|
|
95
99
|
struct IO_Event_Selector_Queue *waiting;
|
|
@@ -42,10 +42,6 @@ struct IO_Event_Selector_URing
|
|
|
42
42
|
struct io_uring ring;
|
|
43
43
|
pid_t owner;
|
|
44
44
|
|
|
45
|
-
// Flag indicating whether the selector is currently blocked in a system call.
|
|
46
|
-
// Set to 1 when blocked in io_uring_wait_cqe_timeout() without GVL, 0 otherwise.
|
|
47
|
-
int blocked;
|
|
48
|
-
|
|
49
45
|
// Interrupt used to wake the selector from another thread without touching the ring's SQ.
|
|
50
46
|
// This allows IORING_SETUP_SINGLE_ISSUER: only the owner thread ever submits SQEs.
|
|
51
47
|
// Uses eventfd on Linux, pipe fallback elsewhere.
|
|
@@ -254,7 +250,6 @@ VALUE IO_Event_Selector_URing_allocate(VALUE self) {
|
|
|
254
250
|
selector->ring.ring_fd = -1;
|
|
255
251
|
selector->owner = 0;
|
|
256
252
|
|
|
257
|
-
selector->blocked = 0;
|
|
258
253
|
selector->interrupt.descriptor = -1;
|
|
259
254
|
selector->wakeup_registered = 0;
|
|
260
255
|
|
|
@@ -546,6 +541,29 @@ struct process_wait_arguments {
|
|
|
546
541
|
#endif
|
|
547
542
|
};
|
|
548
543
|
|
|
544
|
+
#if defined(IO_EVENT_SELECTOR_URING_USE_WAITID) && defined(HAVE_RB_PROCESS_STATUS_FOR)
|
|
545
|
+
static inline int process_wait_status_exited(int exit_status) {
|
|
546
|
+
return (exit_status & 0xff) << 8;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
static inline int process_wait_status_signaled(int term_signal) {
|
|
550
|
+
return term_signal & 0x7f;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
static inline int process_wait_status_from_siginfo(const siginfo_t *siginfo) {
|
|
554
|
+
switch (siginfo->si_code) {
|
|
555
|
+
case CLD_EXITED:
|
|
556
|
+
return process_wait_status_exited(siginfo->si_status);
|
|
557
|
+
case CLD_KILLED:
|
|
558
|
+
return process_wait_status_signaled(siginfo->si_status);
|
|
559
|
+
case CLD_DUMPED:
|
|
560
|
+
return process_wait_status_signaled(siginfo->si_status) | 0x80;
|
|
561
|
+
default:
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
#endif
|
|
566
|
+
|
|
549
567
|
static
|
|
550
568
|
VALUE process_wait_transfer(VALUE _arguments) {
|
|
551
569
|
struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments;
|
|
@@ -559,11 +577,20 @@ VALUE process_wait_transfer(VALUE _arguments) {
|
|
|
559
577
|
|
|
560
578
|
if (result < 0) {
|
|
561
579
|
// The `waitid` failed (e.g. `ECHILD` when there are no children). Reproduce the failure as a `Process::Status` carrying the error, rather than raising, so callers like `Process.waitall` / `Process.detach` (which expect `waitpid` to report the error, not raise) behave correctly:
|
|
580
|
+
#ifdef HAVE_RB_PROCESS_STATUS_FOR
|
|
581
|
+
return rb_process_status_for(-1, 0, -result);
|
|
582
|
+
#else
|
|
562
583
|
return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
|
|
584
|
+
#endif
|
|
563
585
|
}
|
|
564
586
|
|
|
587
|
+
#ifdef HAVE_RB_PROCESS_STATUS_FOR
|
|
588
|
+
// The `waitid` operation already reaped the child. Convert the `siginfo_t` result directly into the Ruby process status value:
|
|
589
|
+
return rb_process_status_for(arguments->siginfo.si_pid, process_wait_status_from_siginfo(&arguments->siginfo), 0);
|
|
590
|
+
#else
|
|
565
591
|
// We waited with `WNOWAIT`, so the child has not been reaped yet. `si_pid` tells us exactly which child changed state (important when waiting for any child, e.g. pid -1). Reap it to obtain a correct `Process::Status`:
|
|
566
592
|
return IO_Event_Selector_process_status_reap(arguments->siginfo.si_pid, arguments->flags);
|
|
593
|
+
#endif
|
|
567
594
|
#else
|
|
568
595
|
if (arguments->waiting->result) {
|
|
569
596
|
return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
|
|
@@ -632,8 +659,13 @@ VALUE IO_Event_Selector_URing_process_wait(VALUE self, VALUE fiber, VALUE _pid,
|
|
|
632
659
|
id_t id;
|
|
633
660
|
idtype_t idtype = process_waitid_type(pid, &id);
|
|
634
661
|
if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_process_wait:io_uring_prep_waitid(fiber=%p, idtype=%d, id=%d, flags=%d)\n", (void*)fiber, idtype, (int)id, flags);
|
|
662
|
+
#ifdef HAVE_RB_PROCESS_STATUS_FOR
|
|
663
|
+
// Reap the child directly; the completion contains enough information to construct the Ruby process status value:
|
|
664
|
+
io_uring_prep_waitid(sqe, idtype, id, &process_wait_arguments.siginfo, WEXITED, 0);
|
|
665
|
+
#else
|
|
635
666
|
// `WNOWAIT` leaves the child in a waitable state so we can reap it with `rb_process_status_wait` afterwards and build a correct `Process::Status`:
|
|
636
667
|
io_uring_prep_waitid(sqe, idtype, id, &process_wait_arguments.siginfo, WEXITED | WNOWAIT, 0);
|
|
668
|
+
#endif
|
|
637
669
|
#else
|
|
638
670
|
if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_process_wait:io_uring_prep_poll_add(%p)\n", (void*)fiber);
|
|
639
671
|
io_uring_prep_poll_add(sqe, descriptor, POLLIN|POLLHUP|POLLERR);
|
|
@@ -1210,14 +1242,6 @@ int select_blocking_allowed(struct __kernel_timespec *timespec) {
|
|
|
1210
1242
|
if (timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0) {
|
|
1211
1243
|
return 0;
|
|
1212
1244
|
}
|
|
1213
|
-
|
|
1214
|
-
#ifndef RB_NOGVL_PENDING_INTERRUPT_FAIL
|
|
1215
|
-
// On Rubies without `RB_NOGVL_PENDING_INTERRUPT_FAIL`, `rb_thread_call_without_gvl2` can enter an indefinite native wait even if a masked interrupt is already pending for this thread. This is the last safe point to avoid that wait: we still hold the GVL, so the pending interrupt queue cannot change concurrently before we decide whether to enter the blocking path.
|
|
1216
|
-
if (IO_Event_Selector_pending_interrupt()) {
|
|
1217
|
-
return 0;
|
|
1218
|
-
}
|
|
1219
|
-
#endif
|
|
1220
|
-
|
|
1221
1245
|
return 1;
|
|
1222
1246
|
}
|
|
1223
1247
|
|
|
@@ -1258,13 +1282,7 @@ int select_internal_without_gvl(struct select_arguments *arguments) {
|
|
|
1258
1282
|
io_uring_submit_flush(selector);
|
|
1259
1283
|
|
|
1260
1284
|
arguments->result = -EINTR;
|
|
1261
|
-
selector->
|
|
1262
|
-
#ifdef RB_NOGVL_PENDING_INTERRUPT_FAIL
|
|
1263
|
-
rb_nogvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0, RB_NOGVL_INTR_FAIL | RB_NOGVL_PENDING_INTERRUPT_FAIL);
|
|
1264
|
-
#else
|
|
1265
|
-
rb_thread_call_without_gvl2(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
|
|
1266
|
-
#endif
|
|
1267
|
-
selector->blocked = 0;
|
|
1285
|
+
IO_Event_Selector_blocking_operation(&selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0);
|
|
1268
1286
|
|
|
1269
1287
|
if (arguments->result == -ETIME) {
|
|
1270
1288
|
return 0;
|
|
@@ -1418,7 +1436,7 @@ VALUE IO_Event_Selector_URing_wakeup(VALUE self) {
|
|
|
1418
1436
|
|
|
1419
1437
|
// Wake the selector by signalling the interrupt. This is safe from any thread
|
|
1420
1438
|
// and never touches the ring's SQ, which is required for IORING_SETUP_SINGLE_ISSUER.
|
|
1421
|
-
if (selector->blocked) {
|
|
1439
|
+
if (selector->backend.blocked) {
|
|
1422
1440
|
IO_Event_Interrupt_signal(&selector->interrupt);
|
|
1423
1441
|
return Qtrue;
|
|
1424
1442
|
}
|
data/lib/io/event/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -18,6 +18,14 @@ Please see the [project documentation](https://socketry.github.io/io-event/) for
|
|
|
18
18
|
|
|
19
19
|
Please see the [project releases](https://socketry.github.io/io-event/releases/index) for all releases.
|
|
20
20
|
|
|
21
|
+
### v1.19.3
|
|
22
|
+
|
|
23
|
+
- Prevent `URing`, `EPoll`, and `KQueue` from entering a native wait when a signal exception becomes pending during the GVL transition. On Ruby versions without `RB_NOGVL_PENDING_INTR_FAIL`, the fallback now refreshes pending-interrupt state immediately before releasing the GVL while preserving the caller's exception-delivery timing.
|
|
24
|
+
|
|
25
|
+
### v1.19.2
|
|
26
|
+
|
|
27
|
+
- Use `rb_process_status_for` when available to construct `URing` `process_wait` results directly from `waitid`, avoiding the extra reap syscall previously needed to build a `Process::Status`.
|
|
28
|
+
|
|
21
29
|
### v1.19.1
|
|
22
30
|
|
|
23
31
|
- Fix `Process.waitall` / `Process.detach` under the `URing` selector: when `io_uring`'s `waitid` reported an error (e.g. `ECHILD` when there are no more children), the `process_wait` hook raised instead of returning the error as a `Process::Status`, so callers that expect `waitpid` to *report* "no more children" rather than raise would fail.
|
|
@@ -51,20 +59,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
|
|
|
51
59
|
|
|
52
60
|
- Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
|
|
53
61
|
|
|
54
|
-
### v1.16.0
|
|
55
|
-
|
|
56
|
-
- Use `eventfd` for `URing` cross-thread wakeup, and enable `IORING_SETUP_SINGLE_ISSUER`, `IORING_SETUP_DEFER_TASKRUN`, and `IORING_SETUP_TASKRUN_FLAG`. The waking thread now signals via `eventfd` rather than submitting a `NOP` SQE, which unlocks the single-issuer optimisation, defers task work to the application thread, and lets `select()` skip the `io_uring_get_events()` syscall when no task work is pending.
|
|
57
|
-
- 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.
|
|
58
|
-
- Improve `WorkerPool` GC compaction support and add proper write barriers, fixing potential use-after-free under compacting GC.
|
|
59
|
-
- 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.
|
|
60
|
-
- Use Ruby's `xmalloc` / `xcalloc` / `xrealloc2` / `xfree` for all internal selector allocations (the per-fiber ready-queue entries in `IO_Event_Selector_ready_push`, and both the backing array and per-element allocations in `IO_Event_Array`). Previously a raw `malloc` paired with a debug-build-only `assert(...)` would silently dereference `NULL` and crash in release builds under memory pressure; the Ruby allocators trigger a GC sweep on pressure and raise `NoMemoryError` / `RangeError` on real failure, so the `-1` return-code paths through `IO_Event_Array_initialize` / `_resize` / `_lookup` and their callers in `epoll.c` / `kqueue.c` / `uring.c` are removed in favour of straight exception propagation.
|
|
61
|
-
- 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.
|
|
62
|
-
- Enable `IORING_SETUP_SUBMIT_ALL` (kernel 5.18+) on the `URing` selector so the kernel keeps processing the rest of an SQE batch past individual errors, reducing the frequency of short submits in practice.
|
|
63
|
-
|
|
64
|
-
### v1.15.1
|
|
65
|
-
|
|
66
|
-
- Simplify closed-IO handling in the `Select` selector: rely on Ruby 4's `rb_thread_io_close_interrupt` to wake fibers waiting on a descriptor that's been closed, removing a custom error-recovery path that could mis-attribute `IOError` / `Errno::EBADF` to the wrong waiter.
|
|
67
|
-
|
|
68
62
|
## Contributing
|
|
69
63
|
|
|
70
64
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v1.19.3
|
|
4
|
+
|
|
5
|
+
- Prevent `URing`, `EPoll`, and `KQueue` from entering a native wait when a signal exception becomes pending during the GVL transition. On Ruby versions without `RB_NOGVL_PENDING_INTR_FAIL`, the fallback now refreshes pending-interrupt state immediately before releasing the GVL while preserving the caller's exception-delivery timing.
|
|
6
|
+
|
|
7
|
+
## v1.19.2
|
|
8
|
+
|
|
9
|
+
- Use `rb_process_status_for` when available to construct `URing` `process_wait` results directly from `waitid`, avoiding the extra reap syscall previously needed to build a `Process::Status`.
|
|
10
|
+
|
|
3
11
|
## v1.19.1
|
|
4
12
|
|
|
5
13
|
- Fix `Process.waitall` / `Process.detach` under the `URing` selector: when `io_uring`'s `waitid` reported an error (e.g. `ECHILD` when there are no more children), the `process_wait` hook raised instead of returning the error as a `Process::Status`, so callers that expect `waitpid` to *report* "no more children" rather than raise would fail.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|