io-event 1.19.2 → 1.19.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce297a0d80038a28c3814c5a1bb227453ad27b30cdf253cd20c9abfaed90dd59
4
- data.tar.gz: 7017435d4b51d770524b289dedad72f39a4f166290af2767ea7a32123396f471
3
+ metadata.gz: 9d72eba7de108f1051424997dec0f1d83a38a7a4d927089e1ca7ba0478922ef5
4
+ data.tar.gz: 0a423df73be6c4aec9efc892014b867807794989b45500700a5bbca423ff8915
5
5
  SHA512:
6
- metadata.gz: d214fc9c5cd13f627e1f1be6676e1b86daa013ee27d360323df85b4f7f33d267f2f61bd2db7f8dcbbe46f8b3e448d0aca995a1b3f23cd8275048066f3c03ef05
7
- data.tar.gz: 6b7f4d739d1b5bb13648575fcb0d57b2dc5965b29ae136f91cd03a200cfceb08a191476f528a487807f37b178b8e1a25021dba9b00929cc68ca7166cf081c034
6
+ metadata.gz: cc15fd5deeaf2aeb9fcad4d58da2e06daf6e9ec6933020ccc2bfb1ad3705e52f52958b5086e459cc4a85c0a5ee22c16482b7cd9d3a0e0ff254f725df72bcc02e
7
+ data.tar.gz: 7fcab614752bb5db345e31e60a34d52e9b06d1386ac6531b301063ae77a1dd01cc1f5b2beeb5e482a89f4898ca9c85ce4cc56faf4db37219e3fd9c2478266118
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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_INTR_FAIL
851
- // On Rubies without `RB_NOGVL_PENDING_INTR_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
 
@@ -862,6 +847,7 @@ struct select_arguments {
862
847
 
863
848
  int count;
864
849
  int result;
850
+ int error;
865
851
  struct epoll_event events[EPOLL_MAX_EVENTS];
866
852
 
867
853
  struct timespec * timeout;
@@ -883,9 +869,9 @@ static int make_timeout_ms(struct timespec * timeout) {
883
869
  }
884
870
 
885
871
  static
886
- int enosys_error(int result) {
872
+ int enosys_error(int result, int error) {
887
873
  if (result == -1) {
888
- return errno == ENOSYS;
874
+ return error == ENOSYS;
889
875
  }
890
876
 
891
877
  return 0;
@@ -897,12 +883,13 @@ void * select_internal(void *_arguments) {
897
883
 
898
884
  #if defined(HAVE_EPOLL_PWAIT2)
899
885
  arguments->result = epoll_pwait2(arguments->selector->descriptor, arguments->events, arguments->count, arguments->timeout, NULL);
886
+ arguments->error = errno;
900
887
 
901
888
  // Comment out the above line and enable the below lines to test ENOSYS code path.
902
889
  // arguments->result = -1;
903
- // errno = ENOSYS;
890
+ // arguments->error = ENOSYS;
904
891
 
905
- if (!enosys_error(arguments->result)) {
892
+ if (!enosys_error(arguments->result, arguments->error)) {
906
893
  return NULL;
907
894
  }
908
895
  else {
@@ -911,6 +898,7 @@ void * select_internal(void *_arguments) {
911
898
  #endif
912
899
 
913
900
  arguments->result = epoll_wait(arguments->selector->descriptor, arguments->events, arguments->count, make_timeout_ms(arguments->timeout));
901
+ arguments->error = errno;
914
902
 
915
903
  return NULL;
916
904
  }
@@ -918,18 +906,12 @@ void * select_internal(void *_arguments) {
918
906
  static
919
907
  int select_internal_without_gvl(struct select_arguments *arguments) {
920
908
  arguments->result = -1;
921
- arguments->selector->blocked = 1;
922
- #ifdef RB_NOGVL_PENDING_INTR_FAIL
923
- rb_nogvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0, RB_NOGVL_INTR_FAIL | RB_NOGVL_PENDING_INTR_FAIL);
924
- #else
925
- rb_thread_call_without_gvl2(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
926
- #endif
927
- arguments->selector->blocked = 0;
909
+ arguments->error = EINTR;
910
+ IO_Event_Selector_blocking_operation(&arguments->selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0);
928
911
 
929
912
  if (arguments->result == -1) {
930
- // 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.
931
- if (errno != EINTR && errno != 0) {
932
- rb_sys_fail("select_internal_without_gvl:epoll_wait");
913
+ if (arguments->error != EINTR) {
914
+ rb_syserr_fail(arguments->error, "select_internal_without_gvl:epoll_wait");
933
915
  } else {
934
916
  return 0;
935
917
  }
@@ -943,8 +925,8 @@ int select_internal_with_gvl(struct select_arguments *arguments) {
943
925
  select_internal((void *)arguments);
944
926
 
945
927
  if (arguments->result == -1) {
946
- if (errno != EINTR) {
947
- rb_sys_fail("select_internal_with_gvl:epoll_wait");
928
+ if (arguments->error != EINTR) {
929
+ rb_syserr_fail(arguments->error, "select_internal_with_gvl:epoll_wait");
948
930
  } else {
949
931
  return 0;
950
932
  }
@@ -1089,7 +1071,7 @@ VALUE IO_Event_Selector_EPoll_wakeup(VALUE self) {
1089
1071
  TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector);
1090
1072
 
1091
1073
  // If we are blocking, we can schedule a nop event to wake up the selector:
1092
- if (selector->blocked) {
1074
+ if (selector->backend.blocked) {
1093
1075
  IO_Event_Interrupt_signal(&selector->interrupt);
1094
1076
 
1095
1077
  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_INTR_FAIL
855
- // On Rubies without `RB_NOGVL_PENDING_INTR_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
 
@@ -866,6 +851,7 @@ struct select_arguments {
866
851
 
867
852
  int count;
868
853
  int result;
854
+ int error;
869
855
  struct kevent events[KQUEUE_MAX_EVENTS];
870
856
 
871
857
  struct timespec storage;
@@ -879,6 +865,7 @@ void * select_internal(void *_arguments) {
879
865
  struct select_arguments * arguments = (struct select_arguments *)_arguments;
880
866
 
881
867
  arguments->result = kevent(arguments->selector->descriptor, NULL, 0, arguments->events, arguments->count, arguments->timeout);
868
+ arguments->error = errno;
882
869
 
883
870
  return NULL;
884
871
  }
@@ -886,19 +873,12 @@ void * select_internal(void *_arguments) {
886
873
  static
887
874
  int select_internal_without_gvl(struct select_arguments *arguments) {
888
875
  arguments->result = -1;
889
- arguments->selector->blocked = 1;
890
-
891
- #ifdef RB_NOGVL_PENDING_INTR_FAIL
892
- rb_nogvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0, RB_NOGVL_INTR_FAIL | RB_NOGVL_PENDING_INTR_FAIL);
893
- #else
894
- rb_thread_call_without_gvl2(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
895
- #endif
896
- arguments->selector->blocked = 0;
876
+ arguments->error = EINTR;
877
+ IO_Event_Selector_blocking_operation(&arguments->selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0);
897
878
 
898
879
  if (arguments->result == -1) {
899
- // 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.
900
- if (errno != EINTR && errno != 0) {
901
- rb_sys_fail("select_internal_without_gvl:kevent");
880
+ if (arguments->error != EINTR) {
881
+ rb_syserr_fail(arguments->error, "select_internal_without_gvl:kevent");
902
882
  } else {
903
883
  return 0;
904
884
  }
@@ -912,8 +892,8 @@ int select_internal_with_gvl(struct select_arguments *arguments) {
912
892
  select_internal((void *)arguments);
913
893
 
914
894
  if (arguments->result == -1) {
915
- if (errno != EINTR) {
916
- rb_sys_fail("select_internal_with_gvl:kevent");
895
+ if (arguments->error != EINTR) {
896
+ rb_syserr_fail(arguments->error, "select_internal_with_gvl:kevent");
917
897
  } else {
918
898
  return 0;
919
899
  }
@@ -1069,7 +1049,7 @@ VALUE IO_Event_Selector_KQueue_wakeup(VALUE self) {
1069
1049
  struct IO_Event_Selector_KQueue *selector = NULL;
1070
1050
  TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector);
1071
1051
 
1072
- if (selector->blocked) {
1052
+ if (selector->backend.blocked) {
1073
1053
  #ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT
1074
1054
  IO_Event_Interrupt_signal(&selector->interrupt);
1075
1055
  #else
@@ -8,7 +8,59 @@
8
8
 
9
9
  static const int DEBUG = 0;
10
10
 
11
- ID IO_Event_Selector_pending_interrupt_p_id;
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
- IO_Event_Selector_pending_interrupt_p_id = rb_intern("pending_interrupt?");
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
- extern ID IO_Event_Selector_pending_interrupt_p_id;
44
-
45
- static inline int IO_Event_Selector_pending_interrupt(void) {
46
- return RTEST(rb_funcall(rb_cThread, IO_Event_Selector_pending_interrupt_p_id, 0));
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
 
@@ -1247,14 +1242,6 @@ int select_blocking_allowed(struct __kernel_timespec *timespec) {
1247
1242
  if (timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0) {
1248
1243
  return 0;
1249
1244
  }
1250
-
1251
- #ifndef RB_NOGVL_PENDING_INTR_FAIL
1252
- // On Rubies without `RB_NOGVL_PENDING_INTR_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.
1253
- if (IO_Event_Selector_pending_interrupt()) {
1254
- return 0;
1255
- }
1256
- #endif
1257
-
1258
1245
  return 1;
1259
1246
  }
1260
1247
 
@@ -1295,13 +1282,7 @@ int select_internal_without_gvl(struct select_arguments *arguments) {
1295
1282
  io_uring_submit_flush(selector);
1296
1283
 
1297
1284
  arguments->result = -EINTR;
1298
- selector->blocked = 1;
1299
- #ifdef RB_NOGVL_PENDING_INTR_FAIL
1300
- rb_nogvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0, RB_NOGVL_INTR_FAIL | RB_NOGVL_PENDING_INTR_FAIL);
1301
- #else
1302
- rb_thread_call_without_gvl2(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
1303
- #endif
1304
- selector->blocked = 0;
1285
+ IO_Event_Selector_blocking_operation(&selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0);
1305
1286
 
1306
1287
  if (arguments->result == -ETIME) {
1307
1288
  return 0;
@@ -1455,7 +1436,7 @@ VALUE IO_Event_Selector_URing_wakeup(VALUE self) {
1455
1436
 
1456
1437
  // Wake the selector by signalling the interrupt. This is safe from any thread
1457
1438
  // and never touches the ring's SQ, which is required for IORING_SETUP_SINGLE_ISSUER.
1458
- if (selector->blocked) {
1439
+ if (selector->backend.blocked) {
1459
1440
  IO_Event_Interrupt_signal(&selector->interrupt);
1460
1441
  return Qtrue;
1461
1442
  }
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Event
10
- VERSION = "1.19.2"
10
+ VERSION = "1.19.4"
11
11
  end
12
12
  end
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.4
22
+
23
+ - Capture `errno` immediately after `epoll_wait` / `kevent`, preventing stale or subsequently clobbered values from raising a spurious `Errno::*` when a native selector wait is interrupted or skipped.
24
+
25
+ ### v1.19.3
26
+
27
+ - 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.
28
+
21
29
  ### v1.19.2
22
30
 
23
31
  - 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`.
@@ -51,20 +59,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
51
59
 
52
60
  - Improve timer heap performance by batching scheduled timer insertion, compacting cancelled timers during flush, and avoiding unnecessary heap rebuilds for small incremental inserts.
53
61
 
54
- ### v1.16.1
55
-
56
- - Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
57
-
58
- ### v1.16.0
59
-
60
- - 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.
61
- - 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.
62
- - Improve `WorkerPool` GC compaction support and add proper write barriers, fixing potential use-after-free under compacting GC.
63
- - 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.
64
- - 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.
65
- - 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.
66
- - 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.
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.4
4
+
5
+ - Capture `errno` immediately after `epoll_wait` / `kevent`, preventing stale or subsequently clobbered values from raising a spurious `Errno::*` when a native selector wait is interrupted or skipped.
6
+
7
+ ## v1.19.3
8
+
9
+ - 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.
10
+
3
11
  ## v1.19.2
4
12
 
5
13
  - 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`.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-event
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.2
4
+ version: 1.19.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file