io-event 1.18.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: 7ad9ab9326ef0bb4d637833e5d9705940a7d150c0e42154ee4642fbf6f4b4b49
4
- data.tar.gz: bb1274afaa893df427b3eb422aa5c198da08493126e8b5f3ee2daac375f1ebda
3
+ metadata.gz: f4e37070c965d89037b3fac95c4b5de378273050ee28eb397a954c5fce84a6ae
4
+ data.tar.gz: 7571ee0edd25008cafe5cc9a81488969d5d269ec1e53793fa7cca393bbfd754c
5
5
  SHA512:
6
- metadata.gz: 543bdb171056e37849b7042c0d822e7421908bbd006227be92b1b2c5c12d7988c8201ce02316c9f0aa32df9746f4a6d88345c8eb9f38f9c5018f4adbaeca27b9
7
- data.tar.gz: 65b3f2b61cf38b3c66cb2d0e9299ece198e546d0e30676f8bbd616fdccb043b47f032f0e8fd00bb5bf14bdef471f5ebb1c4769e8de86654900916fc8952c2be7
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;
@@ -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");
@@ -24,7 +24,7 @@ static VALUE rb_Process_Status = Qnil;
24
24
 
25
25
  VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags)
26
26
  {
27
- 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));
28
28
  }
29
29
  #endif
30
30
 
@@ -80,9 +80,21 @@ static VALUE IO_Event_Selector_nonblock(VALUE class, VALUE io)
80
80
  return rb_ensure(rb_yield, io, IO_Event_Selector_nonblock_ensure, (VALUE)&arguments);
81
81
  }
82
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
+
83
91
  void Init_IO_Event_Selector(VALUE IO_Event_Selector) {
84
92
  IO_Event_Selector_pending_interrupt_p_id = rb_intern("pending_interrupt?");
85
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
+
86
98
  #ifndef HAVE_RB_IO_DESCRIPTOR
87
99
  id_fileno = rb_intern("fileno");
88
100
  #endif
@@ -52,13 +52,21 @@ static inline int IO_Event_Selector_pending_interrupt(void) {
52
52
  int IO_Event_Selector_io_descriptor(VALUE io);
53
53
  #endif
54
54
 
55
- // 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`.
56
56
  #ifdef HAVE_RB_PROCESS_STATUS_WAIT
57
- #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)
58
58
  #else
59
59
  VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags);
60
60
  #endif
61
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
+
62
70
  int IO_Event_Selector_nonblock_set(int file_descriptor);
63
71
  void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags);
64
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
 
@@ -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.18.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,11 @@ 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
+
21
26
  ### v1.18.0
22
27
 
23
28
  - **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
@@ -60,10 +65,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
60
65
 
61
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.
62
67
 
63
- ### v1.14.4
64
-
65
- - Allow `epoll_pwait2` to be disabled via `--disable-epoll_pwait2`.
66
-
67
68
  ## Contributing
68
69
 
69
70
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## v1.18.0
4
9
 
5
10
  - **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
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.18.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