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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +31 -5
- data/ext/extconf.rb +4 -4
- data/ext/io/event/list.h +1 -1
- data/ext/io/event/selector/epoll.c +196 -97
- data/ext/io/event/selector/kqueue.c +195 -101
- data/ext/io/event/selector/selector.c +77 -1
- data/ext/io/event/selector/selector.h +24 -2
- data/ext/io/event/selector/uring.c +508 -88
- data/ext/io/event/worker_pool.c +45 -34
- data/lib/io/event/debug/selector.rb +13 -6
- data/lib/io/event/interrupt.rb +12 -7
- data/lib/io/event/selector/select.rb +108 -58
- data/lib/io/event/selector.rb +24 -9
- data/lib/io/event/timers.rb +1 -1
- data/lib/io/event/version.rb +1 -1
- data/license.md +1 -0
- data/readme.md +22 -26
- data/releases.md +50 -0
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
data/ext/io/event/worker_pool.c
CHANGED
|
@@ -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
|
|
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, "
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
rb_protect(worker_pool_work_begin, (VALUE)&work, ¤t_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,
|
|
174
|
-
log("Reading from IO #{io.inspect} with buffer #{buffer};
|
|
175
|
-
@selector.io_read(fiber, io, buffer,
|
|
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,
|
|
180
|
-
log("Writing to IO #{io.inspect} with buffer #{buffer};
|
|
181
|
-
@selector.io_write(fiber, io, buffer,
|
|
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.
|
data/lib/io/event/interrupt.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2021-
|
|
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
|
|
32
|
+
# Send a single byte interrupt.
|
|
29
33
|
def signal
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/io/event/selector.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/io/event/timers.rb
CHANGED
data/lib/io/event/version.rb
CHANGED
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.
|
|
21
|
+
### v1.21.1
|
|
22
22
|
|
|
23
|
-
-
|
|
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.
|
|
26
|
+
### v1.20.0
|
|
26
27
|
|
|
27
|
-
-
|
|
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.
|
|
30
|
+
### v1.19.5
|
|
30
31
|
|
|
31
|
-
-
|
|
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.
|
|
34
|
+
### v1.19.4
|
|
40
35
|
|
|
41
|
-
-
|
|
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.
|
|
38
|
+
### v1.19.3
|
|
44
39
|
|
|
45
|
-
-
|
|
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.
|
|
42
|
+
### v1.19.2
|
|
48
43
|
|
|
49
|
-
-
|
|
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.
|
|
46
|
+
### v1.19.1
|
|
52
47
|
|
|
53
|
-
- Fix
|
|
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.
|
|
50
|
+
### v1.19.0
|
|
56
51
|
|
|
57
|
-
-
|
|
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.
|
|
55
|
+
### v1.18.0
|
|
60
56
|
|
|
61
|
-
-
|
|
57
|
+
- **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
|
|
62
58
|
|
|
63
|
-
### v1.
|
|
59
|
+
### v1.17.0
|
|
64
60
|
|
|
65
|
-
-
|
|
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
|