io-event 1.16.2 → 1.21.1

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.
@@ -306,15 +306,54 @@ static VALUE worker_pool_initialize(int argc, VALUE *argv, VALUE self) {
306
306
  return self;
307
307
  }
308
308
 
309
- static VALUE worker_pool_work_begin(VALUE _work) {
309
+ static VALUE worker_pool_work_block(VALUE _work) {
310
310
  struct IO_Event_WorkerPool_Work *work = (void*)_work;
311
311
 
312
- if (DEBUG) fprintf(stderr, "worker_pool_work_begin:rb_fiber_scheduler_block work=%p\n", work);
312
+ if (DEBUG) fprintf(stderr, "worker_pool_work_block:rb_fiber_scheduler_block work=%p\n", work);
313
313
  rb_fiber_scheduler_block(work->scheduler, work->blocker, Qnil);
314
314
 
315
315
  return Qnil;
316
316
  }
317
317
 
318
+ static VALUE worker_pool_work_wait(VALUE _work) {
319
+ struct IO_Event_WorkerPool_Work *work = (void*)_work;
320
+
321
+ while (true) {
322
+ worker_pool_work_block(_work);
323
+ if (DEBUG) fprintf(stderr, "-- worker_pool_work_wait:work completed=%d\n", work->completed);
324
+
325
+ if (work->completed) {
326
+ break;
327
+ }
328
+
329
+ if (DEBUG) fprintf(stderr, "worker_pool_work_wait:rb_fiber_scheduler_blocking_operation_cancel\n");
330
+ rb_fiber_scheduler_blocking_operation_cancel(work->blocking_operation);
331
+ }
332
+
333
+ return Qtrue;
334
+ }
335
+
336
+ static VALUE worker_pool_work_ensure(VALUE _work) {
337
+ struct IO_Event_WorkerPool_Work *work = (void*)_work;
338
+
339
+ while (!work->completed) {
340
+ if (DEBUG) fprintf(stderr, "worker_pool_work_ensure:rb_fiber_scheduler_blocking_operation_cancel\n");
341
+ rb_fiber_scheduler_blocking_operation_cancel(work->blocking_operation);
342
+
343
+ int state = 0;
344
+ rb_protect(worker_pool_work_block, _work, &state);
345
+ if (DEBUG) fprintf(stderr, "-- worker_pool_work_ensure:work completed=%d, state=%d\n", work->completed, state);
346
+
347
+ if (state) {
348
+ // Ignore errors raised while waiting for cancellation to complete. rb_ensure
349
+ // will restore and rethrow the original control-flow state after this returns.
350
+ rb_set_errinfo(Qnil);
351
+ }
352
+ }
353
+
354
+ return Qnil;
355
+ }
356
+
318
357
  // Ruby method to submit work and wait for completion
319
358
  static VALUE worker_pool_call(VALUE self, VALUE _blocking_operation) {
320
359
  struct IO_Event_WorkerPool *pool;
@@ -357,38 +396,10 @@ static VALUE worker_pool_call(VALUE self, VALUE _blocking_operation) {
357
396
  pthread_cond_signal(&pool->work_available);
358
397
  pthread_mutex_unlock(&pool->mutex);
359
398
 
360
- // Block the current fiber until work is completed:
361
- int state = 0;
362
- while (true) {
363
- int current_state = 0;
364
- rb_protect(worker_pool_work_begin, (VALUE)&work, &current_state);
365
- if (DEBUG) fprintf(stderr, "-- worker_pool_call:work completed=%d, current_state=%d, state=%d\n", work.completed, current_state, state);
366
-
367
- // Store the first exception state:
368
- if (!state) {
369
- state = current_state;
370
- }
371
-
372
- // If the work is still in the queue, we must wait for a worker to complete it (even if cancelled):
373
- if (work.completed) {
374
- // The work was completed, we can exit the loop:
375
- break;
376
- } else {
377
- if (DEBUG) fprintf(stderr, "worker_pool_call:rb_fiber_scheduler_blocking_operation_cancel\n");
378
- // Ensure the blocking operation is cancelled:
379
- rb_fiber_scheduler_blocking_operation_cancel(blocking_operation);
380
-
381
- // The work was not completed, we need to wait for it to be completed, so we go around the loop again.
382
- }
383
- }
384
-
385
- if (DEBUG) fprintf(stderr, "<- worker_pool_call:work completed=%d, state=%d\n", work.completed, state);
386
-
387
- if (state) {
388
- rb_jump_tag(state);
389
- } else {
390
- return Qtrue;
391
- }
399
+ // Block the current fiber until work is completed. If blocking exits via an
400
+ // exception or another non-local jump, ensure the work is cancelled and fully
401
+ // drained before Ruby restores and rethrows the original control-flow state.
402
+ return rb_ensure(worker_pool_work_wait, (VALUE)&work, worker_pool_work_ensure, (VALUE)&work);
392
403
  }
393
404
 
394
405
  static VALUE worker_pool_allocate(VALUE klass) {
@@ -107,6 +107,13 @@ module IO::Event
107
107
 
108
108
  @selector.close
109
109
  @selector = nil
110
+
111
+ @log&.flush
112
+ end
113
+
114
+ # @returns [Boolean] Whether the wrapped selector is closed.
115
+ def closed?
116
+ @selector.nil? || @selector.closed?
110
117
  end
111
118
 
112
119
  # Transfer from the calling fiber to the selector.
@@ -170,15 +177,15 @@ module IO::Event
170
177
  end
171
178
 
172
179
  # Read from the given IO, forwarded to the underlying selector.
173
- def io_read(fiber, io, buffer, length, offset = 0)
174
- log("Reading from IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}")
175
- @selector.io_read(fiber, io, buffer, length, offset)
180
+ def io_read(fiber, io, buffer, *arguments)
181
+ log("Reading from IO #{io.inspect} with buffer #{buffer}; arguments #{arguments.inspect}")
182
+ @selector.io_read(fiber, io, buffer, *arguments)
176
183
  end
177
184
 
178
185
  # Write to the given IO, forwarded to the underlying selector.
179
- def io_write(fiber, io, buffer, length, offset = 0)
180
- log("Writing to IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}")
181
- @selector.io_write(fiber, io, buffer, length, offset)
186
+ def io_write(fiber, io, buffer, *arguments)
187
+ log("Writing to IO #{io.inspect} with buffer #{buffer}; arguments #{arguments.inspect}")
188
+ @selector.io_write(fiber, io, buffer, *arguments)
182
189
  end
183
190
 
184
191
  # Forward the given method to the underlying selector.
@@ -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.
@@ -14,29 +14,34 @@ module IO::Event
14
14
  @selector = selector
15
15
  @input, @output = ::IO.pipe
16
16
 
17
+ @output.sync = true
18
+
17
19
  @fiber = Fiber.new do
18
20
  while true
19
21
  if @selector.io_wait(@fiber, @input, IO::READABLE)
20
22
  @input.read_nonblock(1)
21
23
  end
22
24
  end
25
+ rescue IOError
26
+ # This is expected on shutdown.
23
27
  end
24
28
 
25
29
  @fiber.transfer
26
30
  end
27
31
 
28
- # Send a sigle byte interrupt.
32
+ # Send a single byte interrupt.
29
33
  def signal
30
- @output.write(".")
31
- @output.flush
32
- rescue IOError
33
- # Ignore.
34
+ # This must not block or enter blocking operations or raise an exception.
35
+ # Note that `Scheduler#unblock` defers exceptions, so `IOError` will not be raised by `@output.close` until later.
36
+ @output.write_nonblock(".", exception: false) rescue nil
34
37
  end
35
38
 
36
39
  def close
40
+ # In principle, this should cause the fiber to exit:
37
41
  @input.close
42
+
43
+ # This may cause `signal` to raise an exception:
38
44
  @output.close
39
- # @fiber.raise(::Interrupt)
40
45
  end
41
46
  end
42
47
 
@@ -13,6 +13,7 @@ module IO::Event
13
13
  # Initialize the selector with the given event loop fiber.
14
14
  def initialize(loop)
15
15
  @loop = loop
16
+ @owner = Process.pid
16
17
 
17
18
  @waiting = Hash.new.compare_by_identity
18
19
 
@@ -52,15 +53,24 @@ module IO::Event
52
53
  @waiting = nil
53
54
  end
54
55
 
56
+ # @returns [Boolean] Whether the selector is closed or belongs to a different process.
57
+ def closed?
58
+ @loop.nil? || @owner != Process.pid
59
+ end
60
+
61
+ # An optional reference to a fiber which can be cleared before it is resumed.
55
62
  Optional = Struct.new(:fiber) do
63
+ # Transfer control to the fiber if it is still available.
56
64
  def transfer(*arguments)
57
65
  fiber&.transfer(*arguments)
58
66
  end
59
67
 
68
+ # @returns [Boolean | Nil] Whether the referenced fiber is still alive.
60
69
  def alive?
61
70
  fiber&.alive?
62
71
  end
63
72
 
73
+ # Clear the referenced fiber.
64
74
  def nullify
65
75
  self.fiber = nil
66
76
  end
@@ -111,7 +121,9 @@ module IO::Event
111
121
  !@ready.empty?
112
122
  end
113
123
 
124
+ # A linked list node used to track fibers waiting for IO events.
114
125
  Waiter = Struct.new(:fiber, :events, :tail) do
126
+ # @returns [Boolean | Nil] Whether the waiting fiber is still alive.
115
127
  def alive?
116
128
  self.fiber&.alive?
117
129
  end
@@ -138,10 +150,12 @@ module IO::Event
138
150
  tail&.dispatch(events, &reactivate)
139
151
  end
140
152
 
153
+ # Clear the waiting fiber so it will not be resumed.
141
154
  def invalidate
142
155
  self.fiber = nil
143
156
  end
144
157
 
158
+ # Iterate over each active waiting fiber and its requested events.
145
159
  def each(&block)
146
160
  if fiber = self.fiber
147
161
  yield fiber, self.events
@@ -183,74 +197,112 @@ module IO::Event
183
197
  errno == EAGAIN or errno == EWOULDBLOCK
184
198
  end
185
199
 
186
- # Read from the given IO to the buffer.
187
- #
188
- # @parameter length [Integer] The minimum number of bytes to read.
189
- # @parameter offset [Integer] The offset into the buffer to read to.
190
- def io_read(fiber, io, buffer, length, offset = 0)
191
- # Ensure offset is within the bounds of the buffer to avoid ArgumentError
192
- if offset > buffer.size
193
- return -Errno::EINVAL::Errno
200
+ if defined?(IO::Buffer::VERSION) && IO::Buffer::VERSION >= 3
201
+ # Read at most `length` bytes from the given IO to the buffer in one operation.
202
+ #
203
+ # @parameter offset [Integer] The offset into the buffer to read to.
204
+ # @parameter length [Integer] The maximum number of bytes to read.
205
+ def io_read(fiber, io, buffer, offset, length)
206
+ if offset > buffer.size || length > buffer.size - offset
207
+ return -Errno::EINVAL::Errno
208
+ elsif length == 0
209
+ return 0
210
+ end
211
+
212
+ Selector.nonblock(io) do
213
+ return Fiber.blocking{buffer.read(io, offset, length)}
214
+ end
194
215
  end
195
216
 
196
- total = 0
197
-
198
- Selector.nonblock(io) do
199
- while true
200
- result = Fiber.blocking{buffer.read(io, 0, offset)}
201
-
202
- if result < 0
203
- if length > 0 and again?(result)
204
- self.io_wait(fiber, io, IO::READABLE)
217
+ # Write at most `length` bytes to the given IO from the buffer in one operation.
218
+ #
219
+ # @parameter offset [Integer] The offset into the buffer to write from.
220
+ # @parameter length [Integer] The maximum number of bytes to write.
221
+ def io_write(fiber, io, buffer, offset, length)
222
+ if offset > buffer.size || length > buffer.size - offset
223
+ return -Errno::EINVAL::Errno
224
+ elsif length == 0
225
+ return 0
226
+ end
227
+
228
+ Selector.nonblock(io) do
229
+ return Fiber.blocking{buffer.write(io, offset, length)}
230
+ end
231
+ end
232
+ else
233
+ # Read from the given IO to the buffer.
234
+ #
235
+ # @parameter length [Integer] The minimum number of bytes to read.
236
+ # @parameter offset [Integer] The offset into the buffer to read to.
237
+ def io_read(fiber, io, buffer, length, offset = 0)
238
+ # Ensure offset is within the bounds of the buffer to avoid `ArgumentError`:
239
+ if offset > buffer.size
240
+ return -Errno::EINVAL::Errno
241
+ elsif offset == buffer.size
242
+ return 0
243
+ end
244
+
245
+ total = 0
246
+
247
+ Selector.nonblock(io) do
248
+ while true
249
+ result = Fiber.blocking{buffer.read(io, 0, offset)}
250
+
251
+ if result < 0
252
+ if length > 0 and again?(result)
253
+ self.io_wait(fiber, io, IO::READABLE)
254
+ else
255
+ return result
256
+ end
257
+ elsif result == 0
258
+ break
205
259
  else
206
- return result
260
+ total += result
261
+ break if total >= length
262
+ offset += result
207
263
  end
208
- elsif result == 0
209
- break
210
- else
211
- total += result
212
- break if total >= length
213
- offset += result
214
264
  end
215
265
  end
266
+
267
+ return total
216
268
  end
217
269
 
218
- return total
219
- end
220
-
221
- # Write to the given IO from the buffer.
222
- #
223
- # @parameter length [Integer] The minimum number of bytes to write.
224
- # @parameter offset [Integer] The offset into the buffer to write from.
225
- def io_write(fiber, io, buffer, length, offset = 0)
226
- # Ensure offset is within the bounds of the buffer to avoid ArgumentError
227
- if offset > buffer.size
228
- return -Errno::EINVAL::Errno
229
- end
230
-
231
- total = 0
232
-
233
- Selector.nonblock(io) do
234
- while true
235
- result = Fiber.blocking{buffer.write(io, 0, offset)}
236
-
237
- if result < 0
238
- if length > 0 and again?(result)
239
- self.io_wait(fiber, io, IO::WRITABLE)
270
+ # Write to the given IO from the buffer.
271
+ #
272
+ # @parameter length [Integer] The minimum number of bytes to write.
273
+ # @parameter offset [Integer] The offset into the buffer to write from.
274
+ def io_write(fiber, io, buffer, length, offset = 0)
275
+ # Ensure offset is within the bounds of the buffer to avoid `ArgumentError`:
276
+ if offset > buffer.size
277
+ return -Errno::EINVAL::Errno
278
+ elsif offset == buffer.size
279
+ return 0
280
+ end
281
+
282
+ total = 0
283
+
284
+ Selector.nonblock(io) do
285
+ while true
286
+ result = Fiber.blocking{buffer.write(io, 0, offset)}
287
+
288
+ if result < 0
289
+ if length > 0 and again?(result)
290
+ self.io_wait(fiber, io, IO::WRITABLE)
291
+ else
292
+ return result
293
+ end
294
+ elsif result == 0
295
+ break result
240
296
  else
241
- return result
297
+ total += result
298
+ break if total >= length
299
+ offset += result
242
300
  end
243
- elsif result == 0
244
- break result
245
- else
246
- total += result
247
- break if total >= length
248
- offset += result
249
301
  end
250
302
  end
303
+
304
+ return total
251
305
  end
252
-
253
- return total
254
306
  end
255
307
 
256
308
  # Wait for a process to change state.
@@ -260,9 +312,7 @@ module IO::Event
260
312
  # @parameter flags [Integer] Flags to pass to Process::Status.wait.
261
313
  # @returns [Process::Status] The status of the waited process.
262
314
  def process_wait(fiber, pid, flags)
263
- Thread.new do
264
- Process::Status.wait(pid, flags)
265
- end.value
315
+ Selector.process_wait(pid, flags)
266
316
  end
267
317
 
268
318
  private def pop_ready
@@ -3,12 +3,17 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2026, by Samuel Williams.
5
5
 
6
+ require_relative "native"
6
7
  require_relative "selector/select"
7
8
  require_relative "debug/selector"
8
9
 
9
10
  module IO::Event
10
11
  # @namespace
11
12
  module Selector
13
+ selectors = [:URing, :EPoll, :KQueue, :Select]
14
+ BEST = const_get(selectors.find{|name| const_defined?(name)})
15
+ private_constant :BEST
16
+
12
17
  # The default selector implementation, which is chosen based on the environment and available implementations.
13
18
  #
14
19
  # @parameter env [Hash] The environment to read configuration from.
@@ -16,16 +21,8 @@ module IO::Event
16
21
  def self.default(env = ENV)
17
22
  if name = env["IO_EVENT_SELECTOR"]&.to_sym
18
23
  return const_get(name)
19
- end
20
-
21
- if self.const_defined?(:URing)
22
- URing
23
- elsif self.const_defined?(:EPoll)
24
- EPoll
25
- elsif self.const_defined?(:KQueue)
26
- KQueue
27
24
  else
28
- Select
25
+ BEST
29
26
  end
30
27
  end
31
28
 
@@ -43,5 +40,23 @@ module IO::Event
43
40
 
44
41
  return selector
45
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
46
61
  end
47
62
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "priority_heap"
7
7
 
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Event
10
- VERSION = "1.16.2"
10
+ VERSION = "1.21.1"
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,51 +18,47 @@ 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.16.2
21
+ ### v1.21.1
22
22
 
23
- - Improve timer heap performance by batching scheduled timer insertion, compacting cancelled timers during flush, and avoiding unnecessary heap rebuilds for small incremental inserts.
23
+ - Fix the `URing` completion free-list empty check so its sole entry can be reused instead of unnecessarily allocating a new completion.
24
+ - Keep cancelled `URing` completion records allocated until both the operation and cancellation completions have been processed.
24
25
 
25
- ### v1.16.1
26
+ ### v1.20.0
26
27
 
27
- - Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
28
+ - Add compatibility with Ruby 4.1's fiber scheduler interface version 4. Buffered IO operations now use `(offset, length)`, perform a single transfer of at most `length` bytes, return short transfers directly, and report `-EAGAIN` without waiting. Earlier Ruby versions retain the existing minimum-progress behavior.
28
29
 
29
- ### v1.16.0
30
+ ### v1.19.5
30
31
 
31
- - 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.
32
- - 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.
33
- - Improve `WorkerPool` GC compaction support and add proper write barriers, fixing potential use-after-free under compacting GC.
34
- - 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.
35
- - 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.
36
- - 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.
37
- - 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.
32
+ - Preserve the original exception or non-local control flow when `IO::Event::WorkerPool` cancellation interrupts a blocked fiber, while still cancelling and draining the in-flight blocking operation before returning control to Ruby.
38
33
 
39
- ### v1.15.1
34
+ ### v1.19.4
40
35
 
41
- - 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.
36
+ - 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.
42
37
 
43
- ### v1.15.0
38
+ ### v1.19.3
44
39
 
45
- - 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.
40
+ - 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.
46
41
 
47
- ### v1.14.4
42
+ ### v1.19.2
48
43
 
49
- - Allow `epoll_pwait2` to be disabled via `--disable-epoll_pwait2`.
44
+ - 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`.
50
45
 
51
- ### v1.14.3
46
+ ### v1.19.1
52
47
 
53
- - Fix several implementation bugs that could cause deadlocks on blocking writes.
48
+ - 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.
54
49
 
55
- ### v1.14.0
50
+ ### v1.19.0
56
51
 
57
- - [Enhanced `IO::Event::PriorityHeap` with deletion and bulk insertion methods](https://socketry.github.io/io-event/releases/index#enhanced-io::event::priorityheap-with-deletion-and-bulk-insertion-methods)
52
+ - 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.
53
+ - 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.
58
54
 
59
- ### v1.11.2
55
+ ### v1.18.0
60
56
 
61
- - Fix Windows build.
57
+ - **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
62
58
 
63
- ### v1.11.1
59
+ ### v1.17.0
64
60
 
65
- - Fix `read_nonblock` when using the `URing` selector, which was not handling zero-length reads correctly. This allows reading available data without blocking.
61
+ - Report inherited selector objects as closed after fork, and avoid closing descriptors they no longer own.
66
62
 
67
63
  ## Contributing
68
64
 
data/releases.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Releases
2
2
 
3
+ ## v1.21.1
4
+
5
+ - Fix the `URing` completion free-list empty check so its sole entry can be reused instead of unnecessarily allocating a new completion.
6
+ - Keep cancelled `URing` completion records allocated until both the operation and cancellation completions have been processed.
7
+
8
+ ## v1.20.0
9
+
10
+ - Add compatibility with Ruby 4.1's fiber scheduler interface version 4. Buffered IO operations now use `(offset, length)`, perform a single transfer of at most `length` bytes, return short transfers directly, and report `-EAGAIN` without waiting. Earlier Ruby versions retain the existing minimum-progress behavior.
11
+
12
+ ## v1.19.5
13
+
14
+ - Preserve the original exception or non-local control flow when `IO::Event::WorkerPool` cancellation interrupts a blocked fiber, while still cancelling and draining the in-flight blocking operation before returning control to Ruby.
15
+
16
+ ## v1.19.4
17
+
18
+ - 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.
19
+
20
+ ## v1.19.3
21
+
22
+ - 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.
23
+
24
+ ## v1.19.2
25
+
26
+ - 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`.
27
+
28
+ ## v1.19.1
29
+
30
+ - 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.
31
+
32
+ ## v1.19.0
33
+
34
+ - 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.
35
+ - 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.
36
+
37
+ ## v1.18.0
38
+
39
+ - **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
40
+
41
+ ## v1.17.0
42
+
43
+ - Report inherited selector objects as closed after fork, and avoid closing descriptors they no longer own.
44
+
45
+ ## v1.16.4
46
+
47
+ - Correctly implement `Interrupt#signal` so that it is robust enough to be called by `Scheduler#unblock`.
48
+
49
+ ## v1.16.3
50
+
51
+ - Handle `IOError` raised while shutting down the pure Ruby interrupt pipe, so `IO::Event::Interrupt#close` does not leak expected shutdown errors from the interrupt fiber.
52
+
3
53
  ## v1.16.2
4
54
 
5
55
  - Improve timer heap performance by batching scheduled timer insertion, compacting cancelled timers during flush, and avoiding unnecessary heap rebuilds for small incremental inserts.
data.tar.gz.sig CHANGED
Binary file