io-event 1.19.3 → 1.19.5

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: f9b4e34ef8c8e421b9fe40c1e0556a1317b3233f0746f3c8c613c26bc962a2db
4
- data.tar.gz: 8c4839836a933969612713d34031d2a88e8bac969061b64ad1d188db4f56e4db
3
+ metadata.gz: f7b5b3a617c2f1ef060c072fc860927172b31a8e7e574cc3607f8107326b57bf
4
+ data.tar.gz: '078687c1ee12579d38d9c5d85f8ad4a49827872567ed48f6835098a8081a6929'
5
5
  SHA512:
6
- metadata.gz: 75d55f360c232dbbb88c9794db98cd3c4e685652f7e407b1f411c2296991a6a31b947f5448e91a44daed431d562859e3f3d45640ecc99ae0936b65edbcb48183
7
- data.tar.gz: 8f47bb334168fc983d765e39c53ccc6a19501204c15ae68d72bbeb3d11f0459b6a255c2ea944061ac494c8696c28d992bb3b241c6685ba3c6aaa2fc8ef5c98a1
6
+ metadata.gz: '063501649153028126c8b1087d60c8c089aeb03f536ca86e3f5be2a494d891ab1bfca0b08fc796f5d050a8d28dab68c1ffda6fcf2f6e3ea4b20c4ea0310ae8c3'
7
+ data.tar.gz: cd0df567da5d936491da4c454106ae0c80e4b5a0368bc2ce2ddf02642dd5e313aec14e34d74d3b700be46d9f15f9e76413dae7a8397716afcc08e24a8280b1d9
checksums.yaml.gz.sig CHANGED
Binary file
@@ -25,29 +25,55 @@ This example shows how to perform a blocking operation
25
25
  require "fiber"
26
26
  require "io/event"
27
27
 
28
+ # Create an I/O event selector controlled by the main Fiber.
28
29
  selector = IO::Event::Selector.new(Fiber.current)
30
+
31
+ # input: read end, output: write end.
29
32
  input, output = IO.pipe
30
33
 
31
34
  writer = Fiber.new do
35
+ puts "[writer] Writing data"
36
+
32
37
  output.write("Hello World")
33
38
  output.close
34
39
  end
35
40
 
36
41
  reader = Fiber.new do
37
- selector.io_wait(Fiber.current, input, IO::READABLE)
38
- pp read: input.read
42
+ puts "[reader] No data available, waiting for input"
43
+
44
+ # Suspend this Fiber until the input becomes readable.
45
+ selector.io_wait(
46
+ Fiber.current,
47
+ input,
48
+ IO::READABLE
49
+ )
50
+
51
+ # Execution resumes here once the selector detects the event.
52
+ puts "[reader] Received: #{input.read.inspect}"
39
53
  end
40
54
 
41
- # The reader will be blocked until the IO has data available:
55
+ puts "[main] Transferring to reader fiber"
42
56
  reader.transfer
43
57
 
44
- # Write some data to the pipe and close the writing end:
58
+ puts "[main] Reader is waiting, but main can keep running"
59
+
60
+ puts "[main] Transferring to writer fiber"
45
61
  writer.transfer
46
62
 
63
+ puts "[main] Checking for I/O events"
47
64
  selector.select(1)
48
65
 
66
+ puts "[main] Done"
67
+
49
68
  # Results in:
50
- # {:read=>"Hello World"}
69
+ # [main] Transferring to reader fiber
70
+ # [reader] No data available, waiting for input
71
+ # [main] Reader is waiting, but main can keep running
72
+ # [main] Transferring to writer fiber
73
+ # [writer] Writing data
74
+ # [main] Checking for I/O events
75
+ # [reader] Received: "Hello World"
76
+ # [main] Done
51
77
  ```
52
78
 
53
79
  ## Debugging
@@ -847,6 +847,7 @@ struct select_arguments {
847
847
 
848
848
  int count;
849
849
  int result;
850
+ int error;
850
851
  struct epoll_event events[EPOLL_MAX_EVENTS];
851
852
 
852
853
  struct timespec * timeout;
@@ -868,9 +869,9 @@ static int make_timeout_ms(struct timespec * timeout) {
868
869
  }
869
870
 
870
871
  static
871
- int enosys_error(int result) {
872
+ int enosys_error(int result, int error) {
872
873
  if (result == -1) {
873
- return errno == ENOSYS;
874
+ return error == ENOSYS;
874
875
  }
875
876
 
876
877
  return 0;
@@ -882,12 +883,13 @@ void * select_internal(void *_arguments) {
882
883
 
883
884
  #if defined(HAVE_EPOLL_PWAIT2)
884
885
  arguments->result = epoll_pwait2(arguments->selector->descriptor, arguments->events, arguments->count, arguments->timeout, NULL);
886
+ arguments->error = errno;
885
887
 
886
888
  // Comment out the above line and enable the below lines to test ENOSYS code path.
887
889
  // arguments->result = -1;
888
- // errno = ENOSYS;
890
+ // arguments->error = ENOSYS;
889
891
 
890
- if (!enosys_error(arguments->result)) {
892
+ if (!enosys_error(arguments->result, arguments->error)) {
891
893
  return NULL;
892
894
  }
893
895
  else {
@@ -896,6 +898,7 @@ void * select_internal(void *_arguments) {
896
898
  #endif
897
899
 
898
900
  arguments->result = epoll_wait(arguments->selector->descriptor, arguments->events, arguments->count, make_timeout_ms(arguments->timeout));
901
+ arguments->error = errno;
899
902
 
900
903
  return NULL;
901
904
  }
@@ -903,12 +906,12 @@ void * select_internal(void *_arguments) {
903
906
  static
904
907
  int select_internal_without_gvl(struct select_arguments *arguments) {
905
908
  arguments->result = -1;
909
+ arguments->error = EINTR;
906
910
  IO_Event_Selector_blocking_operation(&arguments->selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0);
907
911
 
908
912
  if (arguments->result == -1) {
909
- // 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.
910
- if (errno != EINTR && errno != 0) {
911
- rb_sys_fail("select_internal_without_gvl:epoll_wait");
913
+ if (arguments->error != EINTR) {
914
+ rb_syserr_fail(arguments->error, "select_internal_without_gvl:epoll_wait");
912
915
  } else {
913
916
  return 0;
914
917
  }
@@ -922,8 +925,8 @@ int select_internal_with_gvl(struct select_arguments *arguments) {
922
925
  select_internal((void *)arguments);
923
926
 
924
927
  if (arguments->result == -1) {
925
- if (errno != EINTR) {
926
- rb_sys_fail("select_internal_with_gvl:epoll_wait");
928
+ if (arguments->error != EINTR) {
929
+ rb_syserr_fail(arguments->error, "select_internal_with_gvl:epoll_wait");
927
930
  } else {
928
931
  return 0;
929
932
  }
@@ -851,6 +851,7 @@ struct select_arguments {
851
851
 
852
852
  int count;
853
853
  int result;
854
+ int error;
854
855
  struct kevent events[KQUEUE_MAX_EVENTS];
855
856
 
856
857
  struct timespec storage;
@@ -864,6 +865,7 @@ void * select_internal(void *_arguments) {
864
865
  struct select_arguments * arguments = (struct select_arguments *)_arguments;
865
866
 
866
867
  arguments->result = kevent(arguments->selector->descriptor, NULL, 0, arguments->events, arguments->count, arguments->timeout);
868
+ arguments->error = errno;
867
869
 
868
870
  return NULL;
869
871
  }
@@ -871,12 +873,12 @@ void * select_internal(void *_arguments) {
871
873
  static
872
874
  int select_internal_without_gvl(struct select_arguments *arguments) {
873
875
  arguments->result = -1;
876
+ arguments->error = EINTR;
874
877
  IO_Event_Selector_blocking_operation(&arguments->selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0);
875
878
 
876
879
  if (arguments->result == -1) {
877
- // 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.
878
- if (errno != EINTR && errno != 0) {
879
- rb_sys_fail("select_internal_without_gvl:kevent");
880
+ if (arguments->error != EINTR) {
881
+ rb_syserr_fail(arguments->error, "select_internal_without_gvl:kevent");
880
882
  } else {
881
883
  return 0;
882
884
  }
@@ -890,8 +892,8 @@ int select_internal_with_gvl(struct select_arguments *arguments) {
890
892
  select_internal((void *)arguments);
891
893
 
892
894
  if (arguments->result == -1) {
893
- if (errno != EINTR) {
894
- rb_sys_fail("select_internal_with_gvl:kevent");
895
+ if (arguments->error != EINTR) {
896
+ rb_syserr_fail(arguments->error, "select_internal_with_gvl:kevent");
895
897
  } else {
896
898
  return 0;
897
899
  }
@@ -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) {
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Event
10
- VERSION = "1.19.3"
10
+ VERSION = "1.19.5"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -18,6 +18,14 @@ Please see the [project documentation](https://socketry.github.io/io-event/) for
18
18
 
19
19
  Please see the [project releases](https://socketry.github.io/io-event/releases/index) for all releases.
20
20
 
21
+ ### v1.19.5
22
+
23
+ - 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.
24
+
25
+ ### v1.19.4
26
+
27
+ - 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.
28
+
21
29
  ### v1.19.3
22
30
 
23
31
  - 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.
@@ -51,14 +59,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
51
59
 
52
60
  - 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.
53
61
 
54
- ### v1.16.2
55
-
56
- - Improve timer heap performance by batching scheduled timer insertion, compacting cancelled timers during flush, and avoiding unnecessary heap rebuilds for small incremental inserts.
57
-
58
- ### v1.16.1
59
-
60
- - Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
61
-
62
62
  ## Contributing
63
63
 
64
64
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Releases
2
2
 
3
+ ## v1.19.5
4
+
5
+ - 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.
6
+
7
+ ## v1.19.4
8
+
9
+ - 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.
10
+
3
11
  ## v1.19.3
4
12
 
5
13
  - 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.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-event
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.3
4
+ version: 1.19.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file