io-event 1.17.0 → 1.19.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be6f12aa408dcf76d86fbac8ae598d4a6313b65275357d611eeae9dc2fd1fe75
4
- data.tar.gz: facad52118ca469a78fa08187f14265fc98b577b1e703d76e66c83fc733a3b21
3
+ metadata.gz: f4e37070c965d89037b3fac95c4b5de378273050ee28eb397a954c5fce84a6ae
4
+ data.tar.gz: 7571ee0edd25008cafe5cc9a81488969d5d269ec1e53793fa7cca393bbfd754c
5
5
  SHA512:
6
- metadata.gz: a32f4bf15f0377d88912e799f87898787ee817e24c2b3638dca609d54ebc9ef3c97258732545be36fb90ef0a26455c210047872f686736d5fae2d6fbf8495aef
7
- data.tar.gz: 931c9b8ee443cc311a8d8823dde3128f109e8cdc739fdc421bf959c5ebfb8fea9a17a17fdc2020a65b0b970eafa91d4cba7fa6ff483165b2c4fca862061ca93c
6
+ metadata.gz: 22cb0db38d55f3710fc36469c2f2e1162fd69c69c228a9be2c4310f83dbc5304c8cb4194fc7fe2ca4b4316f906ff17341bcceeb24ed1cf9ebe70cd076bb05ca2
7
+ data.tar.gz: d7e4a07d6acf309898b3df66e8e6b2b21f09af99a7f21ac03f30c68ad32024a1766320159f350c450df18aa46c911951c87c6f38abd06c55c9635f7eb1c86831
checksums.yaml.gz.sig CHANGED
Binary file
data/ext/extconf.rb CHANGED
@@ -6,6 +6,7 @@
6
6
  # Copyright, 2023, by Math Ieu.
7
7
  # Copyright, 2025, by Stanislav (Stas) Katkov.
8
8
  # Copyright, 2026, by Stan Hu.
9
+ # Copyright, 2026, by Sharon Rosner.
9
10
 
10
11
  return if RUBY_DESCRIPTION =~ /jruby/
11
12
 
@@ -32,9 +33,7 @@ have_func("rb_ext_ractor_safe")
32
33
  have_func("&rb_fiber_transfer")
33
34
 
34
35
  if have_library("uring") and have_header("liburing.h")
35
- # We might want to consider using this in the future:
36
- # have_func("io_uring_submit_and_wait_timeout", "liburing.h")
37
-
36
+ have_func("io_uring_prep_waitid", "liburing.h")
38
37
  $srcs << "io/event/selector/uring.c"
39
38
  end
40
39
 
@@ -462,7 +462,7 @@ VALUE process_wait_transfer(VALUE _arguments) {
462
462
  IO_Event_Selector_loop_yield(&arguments->selector->backend);
463
463
 
464
464
  if (arguments->waiting->ready) {
465
- return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags);
465
+ return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
466
466
  } else {
467
467
  return Qfalse;
468
468
  }
@@ -488,6 +488,11 @@ VALUE IO_Event_Selector_EPoll_process_wait(VALUE self, VALUE fiber, VALUE _pid,
488
488
  pid_t pid = NUM2PIDT(_pid);
489
489
  int flags = NUM2INT(_flags);
490
490
 
491
+ // `pidfd_open` can only refer to a specific process, so waiting for any child or a process group (pid <= 0) is delegated to the threaded fallback:
492
+ if (pid <= 0) {
493
+ return IO_Event_Selector_process_wait(pid, flags);
494
+ }
495
+
491
496
  int descriptor = pidfd_open(pid, 0);
492
497
 
493
498
  if (descriptor == -1) {
@@ -497,7 +502,7 @@ VALUE IO_Event_Selector_EPoll_process_wait(VALUE self, VALUE fiber, VALUE _pid,
497
502
  rb_update_max_fd(descriptor);
498
503
 
499
504
  // `pidfd_open` (above) may be edge triggered, so we need to check if the process is already exited, and if so, return immediately, otherwise we will block indefinitely.
500
- VALUE status = IO_Event_Selector_process_status_wait(pid, flags);
505
+ VALUE status = IO_Event_Selector_process_status_reap(pid, flags);
501
506
  if (status != Qnil) {
502
507
  close(descriptor);
503
508
  return status;
@@ -830,14 +835,33 @@ struct timespec * make_timeout(VALUE duration, struct timespec * storage) {
830
835
  }
831
836
 
832
837
  static
833
- int timeout_nonblocking(struct timespec * timespec) {
838
+ int timeout_is_nonblocking(struct timespec * timespec) {
834
839
  return timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0;
835
840
  }
836
841
 
842
+ // Return true when it is safe and useful to enter the blocking selector wait.
843
+ static
844
+ int select_blocking_allowed(struct timespec * timespec) {
845
+ // A `0` timeout is already a poll. The selector has already performed the immediate poll previously, so there is no useful blocking wait to enter here.
846
+ if (timeout_is_nonblocking(timespec)) {
847
+ return 0;
848
+ }
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
+ return 1;
858
+ }
859
+
837
860
  struct select_arguments {
838
861
  struct IO_Event_Selector_EPoll *selector;
839
862
 
840
863
  int count;
864
+ int result;
841
865
  struct epoll_event events[EPOLL_MAX_EVENTS];
842
866
 
843
867
  struct timespec * timeout;
@@ -851,7 +875,7 @@ static int make_timeout_ms(struct timespec * timeout) {
851
875
  return -1;
852
876
  }
853
877
 
854
- if (timeout_nonblocking(timeout)) {
878
+ if (timeout_is_nonblocking(timeout)) {
855
879
  return 0;
856
880
  }
857
881
 
@@ -872,13 +896,13 @@ void * select_internal(void *_arguments) {
872
896
  struct select_arguments * arguments = (struct select_arguments *)_arguments;
873
897
 
874
898
  #if defined(HAVE_EPOLL_PWAIT2)
875
- arguments->count = epoll_pwait2(arguments->selector->descriptor, arguments->events, EPOLL_MAX_EVENTS, arguments->timeout, NULL);
899
+ arguments->result = epoll_pwait2(arguments->selector->descriptor, arguments->events, arguments->count, arguments->timeout, NULL);
876
900
 
877
901
  // Comment out the above line and enable the below lines to test ENOSYS code path.
878
- // arguments->count = -1;
902
+ // arguments->result = -1;
879
903
  // errno = ENOSYS;
880
904
 
881
- if (!enosys_error(arguments->count)) {
905
+ if (!enosys_error(arguments->result)) {
882
906
  return NULL;
883
907
  }
884
908
  else {
@@ -886,37 +910,47 @@ void * select_internal(void *_arguments) {
886
910
  }
887
911
  #endif
888
912
 
889
- arguments->count = epoll_wait(arguments->selector->descriptor, arguments->events, EPOLL_MAX_EVENTS, make_timeout_ms(arguments->timeout));
913
+ arguments->result = epoll_wait(arguments->selector->descriptor, arguments->events, arguments->count, make_timeout_ms(arguments->timeout));
890
914
 
891
915
  return NULL;
892
916
  }
893
917
 
894
918
  static
895
- void select_internal_without_gvl(struct select_arguments *arguments) {
919
+ int select_internal_without_gvl(struct select_arguments *arguments) {
920
+ arguments->result = -1;
896
921
  arguments->selector->blocked = 1;
897
- rb_thread_call_without_gvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
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
898
927
  arguments->selector->blocked = 0;
899
928
 
900
- if (arguments->count == -1) {
901
- if (errno != EINTR) {
929
+ 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) {
902
932
  rb_sys_fail("select_internal_without_gvl:epoll_wait");
903
933
  } else {
904
- arguments->count = 0;
934
+ return 0;
905
935
  }
906
936
  }
937
+
938
+ return arguments->result;
907
939
  }
908
940
 
909
941
  static
910
- void select_internal_with_gvl(struct select_arguments *arguments) {
942
+ int select_internal_with_gvl(struct select_arguments *arguments) {
911
943
  select_internal((void *)arguments);
912
944
 
913
- if (arguments->count == -1) {
945
+ if (arguments->result == -1) {
914
946
  if (errno != EINTR) {
915
947
  rb_sys_fail("select_internal_with_gvl:epoll_wait");
916
948
  } else {
917
- arguments->count = 0;
949
+ return 0;
918
950
  }
919
951
  }
952
+
953
+ return arguments->result;
920
954
  }
921
955
 
922
956
  static
@@ -972,7 +1006,7 @@ VALUE select_handle_events(VALUE _arguments)
972
1006
  struct select_arguments *arguments = (struct select_arguments *)_arguments;
973
1007
  struct IO_Event_Selector_EPoll *selector = arguments->selector;
974
1008
 
975
- for (int i = 0; i < arguments->count; i += 1) {
1009
+ for (int i = 0; i < arguments->result; i += 1) {
976
1010
  const struct epoll_event *event = &arguments->events[i];
977
1011
  if (DEBUG) fprintf(stderr, "-> fd=%d events=%d\n", event->data.fd, event->events);
978
1012
 
@@ -983,7 +1017,7 @@ VALUE select_handle_events(VALUE _arguments)
983
1017
  }
984
1018
  }
985
1019
 
986
- return INT2NUM(arguments->count);
1020
+ return INT2NUM(arguments->result);
987
1021
  }
988
1022
 
989
1023
  static
@@ -1008,6 +1042,8 @@ VALUE IO_Event_Selector_EPoll_select(VALUE self, VALUE duration) {
1008
1042
 
1009
1043
  struct select_arguments arguments = {
1010
1044
  .selector = selector,
1045
+ .count = EPOLL_MAX_EVENTS,
1046
+ .result = 0,
1011
1047
  .storage = {
1012
1048
  .tv_sec = 0,
1013
1049
  .tv_nsec = 0
@@ -1018,22 +1054,22 @@ VALUE IO_Event_Selector_EPoll_select(VALUE self, VALUE duration) {
1018
1054
  arguments.timeout = &arguments.storage;
1019
1055
 
1020
1056
  // Process any currently pending events:
1021
- select_internal_with_gvl(&arguments);
1057
+ int result = select_internal_with_gvl(&arguments);
1022
1058
 
1023
1059
  // If we:
1024
1060
  // 1. Didn't process any ready fibers, and
1025
1061
  // 2. Didn't process any events from non-blocking select (above), and
1026
1062
  // 3. There are no items in the ready list,
1027
1063
  // then we can perform a blocking select.
1028
- if (!ready && !arguments.count && !selector->backend.ready) {
1064
+ if (!ready && !result && !selector->backend.ready) {
1029
1065
  arguments.timeout = make_timeout(duration, &arguments.storage);
1030
1066
 
1031
- if (!timeout_nonblocking(arguments.timeout)) {
1067
+ if (select_blocking_allowed(arguments.timeout)) {
1032
1068
  struct timespec start_time;
1033
1069
  IO_Event_Time_current(&start_time);
1034
1070
 
1035
1071
  // Wait for events to occur:
1036
- select_internal_without_gvl(&arguments);
1072
+ result = select_internal_without_gvl(&arguments);
1037
1073
 
1038
1074
  struct timespec end_time;
1039
1075
  IO_Event_Time_current(&end_time);
@@ -1041,7 +1077,7 @@ VALUE IO_Event_Selector_EPoll_select(VALUE self, VALUE duration) {
1041
1077
  }
1042
1078
  }
1043
1079
 
1044
- if (arguments.count) {
1080
+ if (result) {
1045
1081
  return rb_ensure(select_handle_events, (VALUE)&arguments, select_handle_events_ensure, (VALUE)&arguments);
1046
1082
  } else {
1047
1083
  return RB_INT2NUM(0);
@@ -469,7 +469,7 @@ VALUE process_wait_transfer(VALUE _arguments) {
469
469
 
470
470
  if (arguments->waiting->ready) {
471
471
  process_prewait(arguments->pid);
472
- return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags);
472
+ return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
473
473
  } else {
474
474
  return Qfalse;
475
475
  }
@@ -493,6 +493,11 @@ VALUE IO_Event_Selector_KQueue_process_wait(VALUE self, VALUE fiber, VALUE _pid,
493
493
  pid_t pid = NUM2PIDT(_pid);
494
494
  int flags = NUM2INT(_flags);
495
495
 
496
+ // `EVFILT_PROC` can only refer to a specific process, so waiting for any child or a process group (pid <= 0) is delegated to the threaded fallback:
497
+ if (pid <= 0) {
498
+ return IO_Event_Selector_process_wait(pid, flags);
499
+ }
500
+
496
501
  struct IO_Event_Selector_KQueue_Waiting waiting = {
497
502
  .list = {.type = &IO_Event_Selector_KQueue_process_wait_list_type},
498
503
  .fiber = fiber,
@@ -514,7 +519,7 @@ VALUE IO_Event_Selector_KQueue_process_wait(VALUE self, VALUE fiber, VALUE _pid,
514
519
  if (errno == ESRCH) {
515
520
  process_prewait(pid);
516
521
 
517
- return IO_Event_Selector_process_status_wait(pid, flags);
522
+ return IO_Event_Selector_process_status_reap(pid, flags);
518
523
  }
519
524
 
520
525
  rb_sys_fail("IO_Event_Selector_KQueue_process_wait:IO_Event_Selector_KQueue_Waiting_register");
@@ -838,15 +843,29 @@ struct timespec * make_timeout(VALUE duration, struct timespec * storage) {
838
843
  return storage;
839
844
  }
840
845
 
846
+ // Return true when it is safe and useful to enter the blocking selector wait.
841
847
  static
842
- int timeout_nonblocking(struct timespec * timespec) {
843
- return timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0;
848
+ int select_blocking_allowed(struct timespec * timespec) {
849
+ // A `0` timeout is already a poll. The selector has already performed the immediate poll previously, so there is no useful blocking wait to enter here.
850
+ if (timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0) {
851
+ return 0;
852
+ }
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
+ return 1;
844
862
  }
845
863
 
846
864
  struct select_arguments {
847
865
  struct IO_Event_Selector_KQueue *selector;
848
866
 
849
867
  int count;
868
+ int result;
850
869
  struct kevent events[KQUEUE_MAX_EVENTS];
851
870
 
852
871
  struct timespec storage;
@@ -859,38 +878,48 @@ static
859
878
  void * select_internal(void *_arguments) {
860
879
  struct select_arguments * arguments = (struct select_arguments *)_arguments;
861
880
 
862
- arguments->count = kevent(arguments->selector->descriptor, NULL, 0, arguments->events, arguments->count, arguments->timeout);
881
+ arguments->result = kevent(arguments->selector->descriptor, NULL, 0, arguments->events, arguments->count, arguments->timeout);
863
882
 
864
883
  return NULL;
865
884
  }
866
885
 
867
886
  static
868
- void select_internal_without_gvl(struct select_arguments *arguments) {
887
+ int select_internal_without_gvl(struct select_arguments *arguments) {
888
+ arguments->result = -1;
869
889
  arguments->selector->blocked = 1;
870
890
 
871
- rb_thread_call_without_gvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
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
872
896
  arguments->selector->blocked = 0;
873
897
 
874
- if (arguments->count == -1) {
875
- if (errno != EINTR) {
898
+ 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) {
876
901
  rb_sys_fail("select_internal_without_gvl:kevent");
877
902
  } else {
878
- arguments->count = 0;
903
+ return 0;
879
904
  }
880
905
  }
906
+
907
+ return arguments->result;
881
908
  }
882
909
 
883
910
  static
884
- void select_internal_with_gvl(struct select_arguments *arguments) {
911
+ int select_internal_with_gvl(struct select_arguments *arguments) {
885
912
  select_internal((void *)arguments);
886
913
 
887
- if (arguments->count == -1) {
914
+ if (arguments->result == -1) {
888
915
  if (errno != EINTR) {
889
916
  rb_sys_fail("select_internal_with_gvl:kevent");
890
917
  } else {
891
- arguments->count = 0;
918
+ return 0;
892
919
  }
893
920
  }
921
+
922
+ return arguments->result;
894
923
  }
895
924
 
896
925
  static
@@ -944,14 +973,14 @@ VALUE select_handle_events(VALUE _arguments)
944
973
  struct select_arguments *arguments = (struct select_arguments *)_arguments;
945
974
  struct IO_Event_Selector_KQueue *selector = arguments->selector;
946
975
 
947
- for (int i = 0; i < arguments->count; i += 1) {
976
+ for (int i = 0; i < arguments->result; i += 1) {
948
977
  if (arguments->events[i].udata) {
949
978
  struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor = arguments->events[i].udata;
950
979
  kqueue_descriptor->ready_events |= events_from_kevent_filter(arguments->events[i].filter);
951
980
  }
952
981
  }
953
982
 
954
- for (int i = 0; i < arguments->count; i += 1) {
983
+ for (int i = 0; i < arguments->result; i += 1) {
955
984
  if (arguments->events[i].udata) {
956
985
  struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor = arguments->events[i].udata;
957
986
  IO_Event_Selector_KQueue_handle(selector, arguments->events[i].ident, kqueue_descriptor, &arguments->saved);
@@ -962,7 +991,7 @@ VALUE select_handle_events(VALUE _arguments)
962
991
  }
963
992
  }
964
993
 
965
- return RB_INT2NUM(arguments->count);
994
+ return RB_INT2NUM(arguments->result);
966
995
  }
967
996
 
968
997
  static
@@ -987,6 +1016,7 @@ VALUE IO_Event_Selector_KQueue_select(VALUE self, VALUE duration) {
987
1016
  struct select_arguments arguments = {
988
1017
  .selector = selector,
989
1018
  .count = KQUEUE_MAX_EVENTS,
1019
+ .result = 0,
990
1020
  .storage = {
991
1021
  .tv_sec = 0,
992
1022
  .tv_nsec = 0
@@ -997,14 +1027,14 @@ VALUE IO_Event_Selector_KQueue_select(VALUE self, VALUE duration) {
997
1027
  arguments.timeout = &arguments.storage;
998
1028
 
999
1029
  // We break this implementation into two parts.
1000
- // (1) count = kevent(..., timeout = 0)
1001
- // (2) without gvl: kevent(..., timeout = 0) if count == 0 and timeout != 0
1030
+ // (1) result = kevent(..., timeout = 0)
1031
+ // (2) without gvl: kevent(..., timeout = 0) if result == 0 and timeout != 0
1002
1032
  // This allows us to avoid releasing and reacquiring the GVL.
1003
1033
  // Non-comprehensive testing shows this gives a 1.5x speedup.
1004
1034
 
1005
1035
  // First do the syscall with no timeout to get any immediately available events:
1006
1036
  if (DEBUG) fprintf(stderr, "\r\nselect_internal_with_gvl timeout=" IO_EVENT_TIME_PRINTF_TIMESPEC "\r\n", IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(arguments.storage));
1007
- select_internal_with_gvl(&arguments);
1037
+ int result = select_internal_with_gvl(&arguments);
1008
1038
  if (DEBUG) fprintf(stderr, "\r\nselect_internal_with_gvl done\r\n");
1009
1039
 
1010
1040
  // If we:
@@ -1012,17 +1042,15 @@ VALUE IO_Event_Selector_KQueue_select(VALUE self, VALUE duration) {
1012
1042
  // 2. Didn't process any events from non-blocking select (above), and
1013
1043
  // 3. There are no items in the ready list,
1014
1044
  // then we can perform a blocking select.
1015
- if (!ready && !arguments.count && !selector->backend.ready) {
1045
+ if (!ready && !result && !selector->backend.ready) {
1016
1046
  arguments.timeout = make_timeout(duration, &arguments.storage);
1017
1047
 
1018
- if (!timeout_nonblocking(arguments.timeout)) {
1019
- arguments.count = KQUEUE_MAX_EVENTS;
1020
-
1048
+ if (select_blocking_allowed(arguments.timeout)) {
1021
1049
  struct timespec start_time;
1022
1050
  IO_Event_Time_current(&start_time);
1023
1051
 
1024
1052
  if (DEBUG) fprintf(stderr, "IO_Event_Selector_KQueue_select timeout=" IO_EVENT_TIME_PRINTF_TIMESPEC "\n", IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(arguments.storage));
1025
- select_internal_without_gvl(&arguments);
1053
+ result = select_internal_without_gvl(&arguments);
1026
1054
 
1027
1055
  struct timespec end_time;
1028
1056
  IO_Event_Time_current(&end_time);
@@ -1030,7 +1058,7 @@ VALUE IO_Event_Selector_KQueue_select(VALUE self, VALUE duration) {
1030
1058
  }
1031
1059
  }
1032
1060
 
1033
- if (arguments.count) {
1061
+ if (result) {
1034
1062
  return rb_ensure(select_handle_events, (VALUE)&arguments, select_handle_events_ensure, (VALUE)&arguments);
1035
1063
  } else {
1036
1064
  return RB_INT2NUM(0);
@@ -8,6 +8,8 @@
8
8
 
9
9
  static const int DEBUG = 0;
10
10
 
11
+ ID IO_Event_Selector_pending_interrupt_p_id;
12
+
11
13
  #ifndef HAVE_RB_IO_DESCRIPTOR
12
14
  static ID id_fileno;
13
15
 
@@ -22,7 +24,7 @@ static VALUE rb_Process_Status = Qnil;
22
24
 
23
25
  VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags)
24
26
  {
25
- return rb_funcall(rb_Process_Status, id_wait, 2, PIDT2NUM(pid), INT2NUM(flags | WNOHANG));
27
+ return rb_funcall(rb_Process_Status, id_wait, 2, PIDT2NUM(pid), INT2NUM(flags));
26
28
  }
27
29
  #endif
28
30
 
@@ -78,7 +80,21 @@ static VALUE IO_Event_Selector_nonblock(VALUE class, VALUE io)
78
80
  return rb_ensure(rb_yield, io, IO_Event_Selector_nonblock_ensure, (VALUE)&arguments);
79
81
  }
80
82
 
83
+ static VALUE rb_IO_Event_Selector = Qnil;
84
+ static ID id_process_wait;
85
+
86
+ // Wait for a process when the selector cannot do so natively (e.g. `pid <= 0`: any child, or a process group). Delegates to the pure-Ruby `IO::Event::Selector.process_wait`, which performs a blocking wait on a separate thread; joining it is fiber-scheduler aware, so the reactor keeps running.
87
+ VALUE IO_Event_Selector_process_wait(rb_pid_t pid, int flags) {
88
+ return rb_funcall(rb_IO_Event_Selector, id_process_wait, 2, PIDT2NUM(pid), INT2NUM(flags));
89
+ }
90
+
81
91
  void Init_IO_Event_Selector(VALUE IO_Event_Selector) {
92
+ IO_Event_Selector_pending_interrupt_p_id = rb_intern("pending_interrupt?");
93
+
94
+ rb_IO_Event_Selector = IO_Event_Selector;
95
+ rb_gc_register_mark_object(rb_IO_Event_Selector);
96
+ id_process_wait = rb_intern("process_wait");
97
+
82
98
  #ifndef HAVE_RB_IO_DESCRIPTOR
83
99
  id_fileno = rb_intern("fileno");
84
100
  #endif
@@ -40,19 +40,33 @@ 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
+ }
48
+
43
49
  #ifdef HAVE_RB_IO_DESCRIPTOR
44
50
  #define IO_Event_Selector_io_descriptor(io) rb_io_descriptor(io)
45
51
  #else
46
52
  int IO_Event_Selector_io_descriptor(VALUE io);
47
53
  #endif
48
54
 
49
- // Reap a process without hanging.
55
+ // Wait for a process to change state. This blocks until the process changes state, unless `WNOHANG` is given in `flags`.
50
56
  #ifdef HAVE_RB_PROCESS_STATUS_WAIT
51
- #define IO_Event_Selector_process_status_wait(pid, flags) rb_process_status_wait(pid, flags | WNOHANG)
57
+ #define IO_Event_Selector_process_status_wait(pid, flags) rb_process_status_wait(pid, flags)
52
58
  #else
53
59
  VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags);
54
60
  #endif
55
61
 
62
+ // Reap a process that is known to have changed state (e.g. after a readiness event), without blocking.
63
+ static inline VALUE IO_Event_Selector_process_status_reap(rb_pid_t pid, int flags) {
64
+ return IO_Event_Selector_process_status_wait(pid, flags | WNOHANG);
65
+ }
66
+
67
+ // Wait for a process the selector cannot represent natively (e.g. `pid <= 0`: any child, or a process group), using a fiber-scheduler aware blocking wait on a separate thread.
68
+ VALUE IO_Event_Selector_process_wait(rb_pid_t pid, int flags);
69
+
56
70
  int IO_Event_Selector_nonblock_set(int file_descriptor);
57
71
  void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags);
58
72
 
@@ -15,10 +15,17 @@
15
15
 
16
16
  #include "../interrupt.h"
17
17
 
18
- #include "pidfd.c"
19
-
20
18
  #include <linux/version.h>
21
19
 
20
+ // `io_uring` support for `IORING_OP_WAITID` was introduced in Linux 6.7. When available, we use it to wait for process exit directly in the ring, instead of polling on a pidfd.
21
+ #if defined(HAVE_IO_URING_PREP_WAITID) && (LINUX_VERSION_CODE >= KERNEL_VERSION(6,7,0))
22
+ #define IO_EVENT_SELECTOR_URING_USE_WAITID
23
+ #endif
24
+
25
+ #ifndef IO_EVENT_SELECTOR_URING_USE_WAITID
26
+ #include "pidfd.c"
27
+ #endif
28
+
22
29
  enum {
23
30
  DEBUG = 0,
24
31
  DEBUG_COMPLETION = 0,
@@ -500,13 +507,43 @@ struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) {
500
507
 
501
508
  #pragma mark - Process.wait
502
509
 
510
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
511
+ // Translate a Ruby/`waitpid`-style pid into the `waitid(2)` idtype and id, mirroring the semantics of `waitpid(2)`:
512
+ //
513
+ // pid == -1 -> any child (P_ALL)
514
+ // pid == 0 -> any child in the caller's process group (P_PGID, id 0; Linux >= 5.4)
515
+ // pid < -1 -> any child in process group |pid| (P_PGID)
516
+ // pid > 0 -> the specific child (P_PID)
517
+ //
518
+ static inline idtype_t process_waitid_type(pid_t pid, id_t *id) {
519
+ if (pid == -1) {
520
+ *id = 0;
521
+ return P_ALL;
522
+ } else if (pid == 0) {
523
+ *id = 0;
524
+ return P_PGID;
525
+ } else if (pid < -1) {
526
+ *id = (id_t)(-pid);
527
+ return P_PGID;
528
+ } else {
529
+ *id = (id_t)pid;
530
+ return P_PID;
531
+ }
532
+ }
533
+ #endif
534
+
503
535
  struct process_wait_arguments {
504
536
  struct IO_Event_Selector_URing *selector;
505
537
  struct IO_Event_Selector_URing_Waiting *waiting;
506
538
 
507
539
  pid_t pid;
508
540
  int flags;
541
+
542
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
543
+ siginfo_t siginfo;
544
+ #else
509
545
  int descriptor;
546
+ #endif
510
547
  };
511
548
 
512
549
  static
@@ -515,18 +552,32 @@ VALUE process_wait_transfer(VALUE _arguments) {
515
552
 
516
553
  IO_Event_Selector_loop_yield(&arguments->selector->backend);
517
554
 
555
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
556
+ int32_t result = arguments->waiting->result;
557
+ if (result < 0) {
558
+ rb_syserr_fail(-result, "IO_Event_Selector_URing_process_wait:io_uring_prep_waitid");
559
+ }
560
+
561
+ if (DEBUG) fprintf(stderr, "waitid result=%d pid=%d code=%d status=%d\n", result, arguments->siginfo.si_pid, arguments->siginfo.si_code, arguments->siginfo.si_status);
562
+
563
+ // 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`:
564
+ return IO_Event_Selector_process_status_reap(arguments->siginfo.si_pid, arguments->flags);
565
+ #else
518
566
  if (arguments->waiting->result) {
519
- return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags);
567
+ return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
520
568
  } else {
521
569
  return Qfalse;
522
570
  }
571
+ #endif
523
572
  }
524
573
 
525
574
  static
526
575
  VALUE process_wait_ensure(VALUE _arguments) {
527
576
  struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments;
528
577
 
578
+ #ifndef IO_EVENT_SELECTOR_URING_USE_WAITID
529
579
  close(arguments->descriptor);
580
+ #endif
530
581
 
531
582
  IO_Event_Selector_URing_Waiting_cancel(arguments->waiting);
532
583
 
@@ -540,11 +591,18 @@ VALUE IO_Event_Selector_URing_process_wait(VALUE self, VALUE fiber, VALUE _pid,
540
591
  pid_t pid = NUM2PIDT(_pid);
541
592
  int flags = NUM2INT(_flags);
542
593
 
594
+ #ifndef IO_EVENT_SELECTOR_URING_USE_WAITID
595
+ // `pidfd_open` can only refer to a specific process, so waiting for any child or a process group (pid <= 0) is delegated to the threaded fallback:
596
+ if (pid <= 0) {
597
+ return IO_Event_Selector_process_wait(pid, flags);
598
+ }
599
+
543
600
  int descriptor = pidfd_open(pid, 0);
544
601
  if (descriptor < 0) {
545
602
  rb_syserr_fail(errno, "IO_Event_Selector_URing_process_wait:pidfd_open");
546
603
  }
547
604
  rb_update_max_fd(descriptor);
605
+ #endif
548
606
 
549
607
  struct IO_Event_Selector_URing_Waiting waiting = {
550
608
  .fiber = fiber,
@@ -559,12 +617,25 @@ VALUE IO_Event_Selector_URing_process_wait(VALUE self, VALUE fiber, VALUE _pid,
559
617
  .waiting = &waiting,
560
618
  .pid = pid,
561
619
  .flags = flags,
620
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
621
+ .siginfo = {0},
622
+ #else
562
623
  .descriptor = descriptor,
624
+ #endif
563
625
  };
564
626
 
565
- if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_process_wait:io_uring_prep_poll_add(%p)\n", (void*)fiber);
566
627
  struct io_uring_sqe *sqe = io_get_sqe(selector);
628
+
629
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
630
+ id_t id;
631
+ idtype_t idtype = process_waitid_type(pid, &id);
632
+ 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);
633
+ // `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`:
634
+ io_uring_prep_waitid(sqe, idtype, id, &process_wait_arguments.siginfo, WEXITED | WNOWAIT, 0);
635
+ #else
636
+ if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_process_wait:io_uring_prep_poll_add(%p)\n", (void*)fiber);
567
637
  io_uring_prep_poll_add(sqe, descriptor, POLLIN|POLLHUP|POLLERR);
638
+ #endif
568
639
  io_uring_sqe_set_data(sqe, completion);
569
640
  io_uring_submit_pending(selector);
570
641
 
@@ -1130,9 +1201,22 @@ struct __kernel_timespec * make_timeout(VALUE duration, struct __kernel_timespec
1130
1201
  return storage;
1131
1202
  }
1132
1203
 
1204
+ // Return true when it is safe and useful to enter the blocking selector wait.
1133
1205
  static
1134
- int timeout_nonblocking(struct __kernel_timespec *timespec) {
1135
- return timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0;
1206
+ int select_blocking_allowed(struct __kernel_timespec *timespec) {
1207
+ // A `0` timeout is already a poll. The selector has already performed the immediate poll previously, so there is no useful blocking wait to enter here.
1208
+ if (timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0) {
1209
+ return 0;
1210
+ }
1211
+
1212
+ #ifndef RB_NOGVL_PENDING_INTERRUPT_FAIL
1213
+ // 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.
1214
+ if (IO_Event_Selector_pending_interrupt()) {
1215
+ return 0;
1216
+ }
1217
+ #endif
1218
+
1219
+ return 1;
1136
1220
  }
1137
1221
 
1138
1222
  struct select_arguments {
@@ -1171,22 +1255,27 @@ int select_internal_without_gvl(struct select_arguments *arguments) {
1171
1255
 
1172
1256
  io_uring_submit_flush(selector);
1173
1257
 
1258
+ arguments->result = -EINTR;
1174
1259
  selector->blocked = 1;
1175
- rb_thread_call_without_gvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
1260
+ #ifdef RB_NOGVL_PENDING_INTERRUPT_FAIL
1261
+ rb_nogvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0, RB_NOGVL_INTR_FAIL | RB_NOGVL_PENDING_INTERRUPT_FAIL);
1262
+ #else
1263
+ rb_thread_call_without_gvl2(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
1264
+ #endif
1176
1265
  selector->blocked = 0;
1177
1266
 
1178
1267
  if (arguments->result == -ETIME) {
1179
- arguments->result = 0;
1268
+ return 0;
1180
1269
  } else if (arguments->result == -EINTR) {
1181
- arguments->result = 0;
1270
+ return 0;
1182
1271
  } else if (arguments->result < 0) {
1183
1272
  rb_syserr_fail(-arguments->result, "select_internal_without_gvl:io_uring_wait_cqe_timeout");
1184
1273
  } else {
1185
1274
  // At least 1 event is waiting:
1186
- arguments->result = 1;
1275
+ return 1;
1187
1276
  }
1188
1277
 
1189
- return arguments->result;
1278
+ return 0;
1190
1279
  }
1191
1280
 
1192
1281
  static inline
@@ -1283,28 +1372,29 @@ VALUE IO_Event_Selector_URing_select(VALUE self, VALUE duration) {
1283
1372
 
1284
1373
  int ready = IO_Event_Selector_ready_flush(&selector->backend);
1285
1374
 
1286
- int result = select_process_completions(selector);
1375
+ int completed = select_process_completions(selector);
1287
1376
 
1288
1377
  // If we:
1289
1378
  // 1. Didn't process any ready fibers, and
1290
1379
  // 2. Didn't process any events from non-blocking select (above), and
1291
1380
  // 3. There are no items in the ready list,
1292
1381
  // then we can perform a blocking select.
1293
- if (!ready && !result && !selector->backend.ready) {
1382
+ if (!ready && !completed && !selector->backend.ready) {
1294
1383
  // We might need to wait for events:
1295
1384
  struct select_arguments arguments = {
1296
1385
  .selector = selector,
1386
+ .result = 0,
1297
1387
  .timeout = NULL,
1298
1388
  };
1299
1389
 
1300
1390
  arguments.timeout = make_timeout(duration, &arguments.storage);
1301
1391
 
1302
- if (!selector->backend.ready && !timeout_nonblocking(arguments.timeout)) {
1392
+ if (!selector->backend.ready && select_blocking_allowed(arguments.timeout)) {
1303
1393
  struct timespec start_time;
1304
1394
  IO_Event_Time_current(&start_time);
1305
1395
 
1306
1396
  // This is a blocking operation, we wait for events:
1307
- result = select_internal_without_gvl(&arguments);
1397
+ int result = select_internal_without_gvl(&arguments);
1308
1398
 
1309
1399
  struct timespec end_time;
1310
1400
  IO_Event_Time_current(&end_time);
@@ -1312,12 +1402,12 @@ VALUE IO_Event_Selector_URing_select(VALUE self, VALUE duration) {
1312
1402
 
1313
1403
  // After waiting/flushing the SQ, check if there are any completions:
1314
1404
  if (result > 0) {
1315
- result = select_process_completions(selector);
1405
+ completed = select_process_completions(selector);
1316
1406
  }
1317
1407
  }
1318
1408
  }
1319
1409
 
1320
- return RB_INT2NUM(result);
1410
+ return RB_INT2NUM(completed);
1321
1411
  }
1322
1412
 
1323
1413
  VALUE IO_Event_Selector_URing_wakeup(VALUE self) {
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2026, by Samuel Williams.
5
5
 
6
6
  module IO::Event
7
7
  # A thread safe synchronisation primative.
@@ -278,9 +278,7 @@ module IO::Event
278
278
  # @parameter flags [Integer] Flags to pass to Process::Status.wait.
279
279
  # @returns [Process::Status] The status of the waited process.
280
280
  def process_wait(fiber, pid, flags)
281
- Thread.new do
282
- Process::Status.wait(pid, flags)
283
- end.value
281
+ Selector.process_wait(pid, flags)
284
282
  end
285
283
 
286
284
  private def pop_ready
@@ -40,5 +40,23 @@ module IO::Event
40
40
 
41
41
  return selector
42
42
  end
43
+
44
+ # Wait for a process to change state, for the cases a selector cannot represent natively (e.g. `pid <= 0`: any child, or a process group). The native selectors integrate process waiting with the event loop using per-process primitives (`pidfd_open`, `EVFILT_PROC`) which can only refer to a single, specific process, and delegate here otherwise.
45
+ #
46
+ # The wait is performed on a separate thread, which has no fiber scheduler and therefore blocks. Joining it via `Thread#value` is fiber-scheduler aware, so the calling fiber yields to the event loop and the reactor keeps running other fibers.
47
+ #
48
+ # @parameter pid [Integer] The process ID (or process group) to wait for.
49
+ # @parameter flags [Integer] Flags to pass to `Process::Status.wait`.
50
+ # @returns [Process::Status] The status of the waited process.
51
+ def self.process_wait(pid, flags)
52
+ thread = ::Thread.new do
53
+ ::Process::Status.wait(pid, flags)
54
+ end
55
+
56
+ thread.value
57
+ ensure
58
+ # If the calling fiber was interrupted before the wait completed, don't leave the thread running:
59
+ thread&.kill
60
+ end
43
61
  end
44
62
  end
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Event
10
- VERSION = "1.17.0"
10
+ VERSION = "1.19.0"
11
11
  end
12
12
  end
data/license.md CHANGED
@@ -19,6 +19,7 @@ Copyright, 2026, by John Hawthorn.
19
19
  Copyright, 2026, by Italo Brandão.
20
20
  Copyright, 2026, by Fletcher Dares.
21
21
  Copyright, 2026, by Tavian Barnes.
22
+ Copyright, 2026, by Sharon Rosner.
22
23
 
23
24
  Permission is hereby granted, free of charge, to any person obtaining a copy
24
25
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -18,6 +18,15 @@ 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.0
22
+
23
+ - Use `io_uring_prep_waitid` for `process_wait` in the `URing` selector (Linux 6.7+), waiting for child exit directly in the ring instead of polling on a `pidfd`. The child is reaped via `rb_process_status_wait` (using `WEXITED | WNOWAIT`) to construct a correct `Process::Status`, and `process_wait(-1, ...)` / `process_wait(0, ...)` are now supported.
24
+ - Support waiting for any child or a process group (`pid <= 0`) on all selectors. The `EPoll` (`pidfd_open`) and `KQueue` (`EVFILT_PROC`) selectors can only watch a specific process, so these cases now fall back to a blocking wait on a dedicated thread; joining it is fiber-scheduler aware, so the reactor keeps running.
25
+
26
+ ### v1.18.0
27
+
28
+ - **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
29
+
21
30
  ### v1.17.0
22
31
 
23
32
  - Report inherited selector objects as closed after fork, and avoid closing descriptors they no longer own.
@@ -56,14 +65,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
56
65
 
57
66
  - Add bounds checks, in the unlikely event of a user providing an invalid offset that exceeds the buffer size. This prevents potential memory corruption and ensures safe operation when using buffered IO methods.
58
67
 
59
- ### v1.14.4
60
-
61
- - Allow `epoll_pwait2` to be disabled via `--disable-epoll_pwait2`.
62
-
63
- ### v1.14.3
64
-
65
- - Fix several implementation bugs that could cause deadlocks on blocking writes.
66
-
67
68
  ## Contributing
68
69
 
69
70
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Releases
2
2
 
3
+ ## v1.19.0
4
+
5
+ - Use `io_uring_prep_waitid` for `process_wait` in the `URing` selector (Linux 6.7+), waiting for child exit directly in the ring instead of polling on a `pidfd`. The child is reaped via `rb_process_status_wait` (using `WEXITED | WNOWAIT`) to construct a correct `Process::Status`, and `process_wait(-1, ...)` / `process_wait(0, ...)` are now supported.
6
+ - Support waiting for any child or a process group (`pid <= 0`) on all selectors. The `EPoll` (`pidfd_open`) and `KQueue` (`EVFILT_PROC`) selectors can only watch a specific process, so these cases now fall back to a blocking wait on a dedicated thread; joining it is fiber-scheduler aware, so the reactor keeps running.
7
+
8
+ ## v1.18.0
9
+
10
+ - **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
11
+
3
12
  ## v1.17.0
4
13
 
5
14
  - Report inherited selector objects as closed after fork, and avoid closing descriptors they no longer own.
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.17.0
4
+ version: 1.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -20,6 +20,7 @@ authors:
20
20
  - John Hawthorn
21
21
  - Luke Gruber
22
22
  - Pavel Rosický
23
+ - Sharon Rosner
23
24
  - Stan Hu
24
25
  - Stanislav (Stas) Katkov
25
26
  - William T. Nelson
metadata.gz.sig CHANGED
Binary file