io-event 1.9.0 → 1.11.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.
@@ -0,0 +1,200 @@
1
+ // worker_pool_test.c - Test functions for WorkerPool cancellation
2
+ // Released under the MIT License.
3
+ // Copyright, 2025, by Samuel Williams.
4
+
5
+ #include "worker_pool_test.h"
6
+
7
+ #include <ruby/thread.h>
8
+ #include <stdlib.h>
9
+ #include <string.h>
10
+
11
+ #include <unistd.h>
12
+ #include <sys/select.h>
13
+ #include <errno.h>
14
+ #include <time.h>
15
+
16
+ static ID id_duration;
17
+
18
+ struct BusyOperationData {
19
+ int read_fd;
20
+ int write_fd;
21
+ volatile int cancelled;
22
+ double duration; // How long to wait (for testing)
23
+ clock_t start_time;
24
+ clock_t end_time;
25
+ int operation_result;
26
+ VALUE exception;
27
+ };
28
+
29
+ // The actual blocking operation that can be cancelled
30
+ static void* busy_blocking_operation(void *data) {
31
+ struct BusyOperationData *busy_data = (struct BusyOperationData*)data;
32
+
33
+ // Use select() to wait for the pipe to become readable
34
+ fd_set read_fds;
35
+ struct timeval timeout;
36
+
37
+ FD_ZERO(&read_fds);
38
+ FD_SET(busy_data->read_fd, &read_fds);
39
+
40
+ // Set timeout based on duration
41
+ timeout.tv_sec = (long)busy_data->duration;
42
+ timeout.tv_usec = ((busy_data->duration - timeout.tv_sec) * 1000000);
43
+
44
+ // This will block until:
45
+ // 1. The pipe becomes readable (cancellation)
46
+ // 2. The timeout expires
47
+ // 3. An error occurs
48
+ int result = select(busy_data->read_fd + 1, &read_fds, NULL, NULL, &timeout);
49
+
50
+ if (result > 0 && FD_ISSET(busy_data->read_fd, &read_fds)) {
51
+ // Pipe became readable - we were cancelled
52
+ char buffer;
53
+ read(busy_data->read_fd, &buffer, 1); // Consume the byte
54
+ busy_data->cancelled = 1;
55
+ return (void*)-1; // Indicate cancellation
56
+ } else if (result == 0) {
57
+ // Timeout - operation completed normally
58
+ return (void*)0; // Indicate success
59
+ } else {
60
+ // Error occurred
61
+ return (void*)-2; // Indicate error
62
+ }
63
+ }
64
+
65
+ // Unblock function that writes to the pipe to cancel the operation
66
+ static void busy_unblock_function(void *data) {
67
+ struct BusyOperationData *busy_data = (struct BusyOperationData*)data;
68
+
69
+ busy_data->cancelled = 1;
70
+
71
+ // Write a byte to the pipe to wake up the select()
72
+ char wake_byte = 1;
73
+ write(busy_data->write_fd, &wake_byte, 1);
74
+ }
75
+
76
+ // Function for the main operation execution (for rb_rescue)
77
+ static VALUE busy_operation_execute(VALUE data_value) {
78
+ struct BusyOperationData *busy_data = (struct BusyOperationData*)data_value;
79
+
80
+ // Record start time
81
+ busy_data->start_time = clock();
82
+
83
+ // Execute the blocking operation
84
+ void *block_result = rb_nogvl(
85
+ busy_blocking_operation,
86
+ busy_data,
87
+ busy_unblock_function,
88
+ busy_data,
89
+ RB_NOGVL_UBF_ASYNC_SAFE | RB_NOGVL_OFFLOAD_SAFE
90
+ );
91
+
92
+ // Record end time
93
+ busy_data->end_time = clock();
94
+
95
+ // Store the operation result
96
+ busy_data->operation_result = (int)(intptr_t)block_result;
97
+
98
+ return Qnil;
99
+ }
100
+
101
+ // Function for exception handling (for rb_rescue)
102
+ static VALUE busy_operation_rescue(VALUE data_value, VALUE exception) {
103
+ struct BusyOperationData *busy_data = (struct BusyOperationData*)data_value;
104
+
105
+ // Record end time even in case of exception
106
+ busy_data->end_time = clock();
107
+
108
+ // Mark that an exception was caught
109
+ busy_data->exception = exception;
110
+
111
+ return exception;
112
+ }
113
+
114
+ // Ruby method: IO::Event::WorkerPool.busy(duration: 1.0)
115
+ // This creates a cancellable blocking operation for testing
116
+ static VALUE worker_pool_test_busy(int argc, VALUE *argv, VALUE self) {
117
+ double duration = 1.0; // Default 1 second
118
+
119
+ // Extract keyword arguments
120
+ VALUE kwargs = Qnil;
121
+ VALUE rb_duration = Qnil;
122
+
123
+ rb_scan_args(argc, argv, "0:", &kwargs);
124
+
125
+ if (!NIL_P(kwargs)) {
126
+ VALUE kwvals[1];
127
+ ID kwkeys[1] = {id_duration};
128
+ rb_get_kwargs(kwargs, kwkeys, 0, 1, kwvals);
129
+ rb_duration = kwvals[0];
130
+ }
131
+
132
+ if (!NIL_P(rb_duration)) {
133
+ duration = NUM2DBL(rb_duration);
134
+ }
135
+
136
+ // Create pipe for cancellation
137
+ int pipe_fds[2];
138
+ if (pipe(pipe_fds) != 0) {
139
+ rb_sys_fail("pipe creation failed");
140
+ }
141
+
142
+ // Stack allocate operation data
143
+ struct BusyOperationData busy_data = {
144
+ .read_fd = pipe_fds[0],
145
+ .write_fd = pipe_fds[1],
146
+ .cancelled = 0,
147
+ .duration = duration,
148
+ .start_time = 0,
149
+ .end_time = 0,
150
+ .operation_result = 0,
151
+ .exception = Qnil
152
+ };
153
+
154
+ // Execute the blocking operation with exception handling using function pointers
155
+ rb_rescue(
156
+ busy_operation_execute,
157
+ (VALUE)&busy_data,
158
+ busy_operation_rescue,
159
+ (VALUE)&busy_data
160
+ );
161
+
162
+ // Calculate elapsed time from the state stored in busy_data
163
+ double elapsed = ((double)(busy_data.end_time - busy_data.start_time)) / CLOCKS_PER_SEC;
164
+
165
+ // Create result hash using the state from busy_data
166
+ VALUE result = rb_hash_new();
167
+ rb_hash_aset(result, ID2SYM(rb_intern("duration")), DBL2NUM(duration));
168
+ rb_hash_aset(result, ID2SYM(rb_intern("elapsed")), DBL2NUM(elapsed));
169
+
170
+ // Determine result based on operation outcome
171
+ if (busy_data.exception != Qnil) {
172
+ rb_hash_aset(result, ID2SYM(rb_intern("result")), ID2SYM(rb_intern("exception")));
173
+ rb_hash_aset(result, ID2SYM(rb_intern("cancelled")), Qtrue);
174
+ rb_hash_aset(result, ID2SYM(rb_intern("exception")), busy_data.exception);
175
+ } else if (busy_data.operation_result == -1) {
176
+ rb_hash_aset(result, ID2SYM(rb_intern("result")), ID2SYM(rb_intern("cancelled")));
177
+ rb_hash_aset(result, ID2SYM(rb_intern("cancelled")), Qtrue);
178
+ } else if (busy_data.operation_result == 0) {
179
+ rb_hash_aset(result, ID2SYM(rb_intern("result")), ID2SYM(rb_intern("completed")));
180
+ rb_hash_aset(result, ID2SYM(rb_intern("cancelled")), Qfalse);
181
+ } else {
182
+ rb_hash_aset(result, ID2SYM(rb_intern("result")), ID2SYM(rb_intern("error")));
183
+ rb_hash_aset(result, ID2SYM(rb_intern("cancelled")), Qfalse);
184
+ }
185
+
186
+ // Clean up pipe file descriptors
187
+ close(pipe_fds[0]);
188
+ close(pipe_fds[1]);
189
+
190
+ return result;
191
+ }
192
+
193
+ // Initialize the test functions
194
+ void Init_IO_Event_WorkerPool_Test(VALUE IO_Event_WorkerPool) {
195
+ // Initialize symbols
196
+ id_duration = rb_intern("duration");
197
+
198
+ // Add test methods to IO::Event::WorkerPool class
199
+ rb_define_singleton_method(IO_Event_WorkerPool, "busy", worker_pool_test_busy, -1);
200
+ }
@@ -0,0 +1,9 @@
1
+ // worker_pool_test.h - Header for WorkerPool test functions
2
+ // Released under the MIT License.
3
+ // Copyright, 2025, by Samuel Williams.
4
+
5
+ #pragma once
6
+
7
+ #include <ruby.h>
8
+
9
+ void Init_IO_Event_WorkerPool_Test(VALUE IO_Event_WorkerPool);
@@ -421,19 +421,25 @@ module IO::Event
421
421
  writable = Array.new
422
422
  priority = Array.new
423
423
 
424
- @waiting.each do |io, waiter|
425
- waiter.each do |fiber, events|
426
- if (events & IO::READABLE) > 0
427
- readable << io
428
- end
429
-
430
- if (events & IO::WRITABLE) > 0
431
- writable << io
432
- end
433
-
434
- if (events & IO::PRIORITY) > 0
435
- priority << io
424
+ @waiting.delete_if do |io, waiter|
425
+ if io.closed?
426
+ true
427
+ else
428
+ waiter.each do |fiber, events|
429
+ if (events & IO::READABLE) > 0
430
+ readable << io
431
+ end
432
+
433
+ if (events & IO::WRITABLE) > 0
434
+ writable << io
435
+ end
436
+
437
+ if (events & IO::PRIORITY) > 0
438
+ priority << io
439
+ end
436
440
  end
441
+
442
+ false
437
443
  end
438
444
  end
439
445
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "priority_heap"
7
7
 
@@ -91,7 +91,6 @@ class IO
91
91
  schedule(self.now + offset.to_f, block)
92
92
  end
93
93
 
94
-
95
94
  # Compute the time interval until the next timer fires.
96
95
  #
97
96
  # @parameter now [Float] The current time.
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Event
10
- VERSION = "1.9.0"
10
+ VERSION = "1.11.0"
11
11
  end
12
12
  end
data/license.md CHANGED
@@ -11,6 +11,7 @@ Copyright, 2024, by Pavel Rosický.
11
11
  Copyright, 2024, by Anthony Ross.
12
12
  Copyright, 2024, by Shizuo Fujita.
13
13
  Copyright, 2024, by Jean Boussier.
14
+ Copyright, 2025, by Stanislav (Stas) Katkov.
14
15
 
15
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
16
17
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- # ![Event](logo.svg)
1
+ # ![IO::Event](logo.svg)
2
2
 
3
3
  Provides low level cross-platform primitives for constructing event loops, with support for `select`, `kqueue`, `epoll` and `io_uring`.
4
4
 
@@ -18,13 +18,26 @@ 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.11.0
22
+
23
+ - [Introduce `IO::Event::WorkerPool` for off-loading blocking operations.](https://socketry.github.io/io-event/releases/index#introduce-io::event::workerpool-for-off-loading-blocking-operations.)
24
+
25
+ ### v1.10.2
26
+
27
+ - Improved consistency of handling closed IO when invoking `#select`.
28
+
29
+ ### v1.10.0
30
+
31
+ - `IO::Event::Profiler` is moved to dedicated gem: [fiber-profiler](https://github.com/socketry/fiber-profiler).
32
+ - Perform runtime checks for native selectors to ensure they are supported in the current environment. While compile-time checks determine availability, restrictions like seccomp and SELinux may still prevent them from working.
33
+
21
34
  ### v1.9.0
22
35
 
23
- - [Improved `IO::Event::Profiler` for detecting stalls.](https://socketry.github.io/io-event/releases/index#improved-io::event::profiler-for-detecting-stalls.)
36
+ - Improved `IO::Event::Profiler` for detecting stalls.
24
37
 
25
38
  ### v1.8.0
26
39
 
27
- - [Detecting fibers that are stalling the event loop.](https://socketry.github.io/io-event/releases/index#detecting-fibers-that-are-stalling-the-event-loop.)
40
+ - Detecting fibers that are stalling the event loop.
28
41
 
29
42
  ### v1.7.5
30
43
 
data/releases.md CHANGED
@@ -1,56 +1,47 @@
1
1
  # Releases
2
2
 
3
- ## v1.9.0
3
+ ## v1.11.0
4
4
 
5
- ### Improved `IO::Event::Profiler` for detecting stalls.
5
+ ### Introduce `IO::Event::WorkerPool` for off-loading blocking operations.
6
6
 
7
- A new `IO::Event::Profiler` class has been added to help detect stalls in the event loop. The previous approach was insufficient to detect all possible stalls. This new approach uses the `RUBY_EVENT_FIBER_SWITCH` event to track context switching by the scheduler, and can detect stalls no matter how they occur.
7
+ The {ruby IO::Event::WorkerPool} provides a mechanism for executing blocking operations on separate OS threads while properly integrating with Ruby's fiber scheduler and GVL (Global VM Lock) management. This enables true parallelism for CPU-intensive or blocking operations that would otherwise block the event loop.
8
8
 
9
9
  ``` ruby
10
- profiler = IO::Event::Profiler.new
10
+ # Fiber scheduler integration via blocking_operation_wait hook
11
+ class MyScheduler
12
+ def initialize
13
+ @worker_pool = IO::Event::WorkerPool.new
14
+ end
15
+
16
+ def blocking_operation_wait(operation)
17
+ @worker_pool.call(operation)
18
+ end
19
+ end
20
+
21
+ # Usage with automatic offloading
22
+ Fiber.set_scheduler(MyScheduler.new)
23
+ # Automatically offload `rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` to a background thread:
24
+ result = some_blocking_operation()
25
+ ```
11
26
 
12
- profiler.start
13
-
14
- Fiber.new do
15
- sleep 1.0
16
- end.transfer
27
+ The implementation uses one or more background threads and a list of pending blocking operations. Those operations either execute through to completion or may be cancelled, which executes the "unblock function" provided to `rb_nogvl`.
17
28
 
18
- profiler.stop
19
- ```
29
+ ## v1.10.2
20
30
 
21
- A default profiler is exposed using `IO::Event::Profiler.default` which is controlled by the following environment variables:
31
+ - Improved consistency of handling closed IO when invoking `#select`.
22
32
 
23
- - `IO_EVENT_PROFILER=true` - Enable the profiler, otherwise `IO::Event::Profiler.default` will return `nil`.
24
- - `IO_EVENT_PROFILER_LOG_THRESHOLD` - Specify the threshold in seconds for logging a stall. Defaults to `0.01`.
25
- - `IO_EVENT_PROFILER_TRACK_CALLS` - Track the method call for each event, in order to log specifically which method is causing the stall. Defaults to `true`.
33
+ ## v1.10.0
34
+
35
+ - `IO::Event::Profiler` is moved to dedicated gem: [fiber-profiler](https://github.com/socketry/fiber-profiler).
36
+ - Perform runtime checks for native selectors to ensure they are supported in the current environment. While compile-time checks determine availability, restrictions like seccomp and SELinux may still prevent them from working.
37
+
38
+ ## v1.9.0
26
39
 
27
- The previous environment variables `IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD` and `IO_EVENT_SELECTOR_STALL_LOG` no longer have any effect.
40
+ - Improved `IO::Event::Profiler` for detecting stalls.
28
41
 
29
42
  ## v1.8.0
30
43
 
31
- ### Detecting fibers that are stalling the event loop.
32
-
33
- A new (experimental) feature for detecting fiber stalls has been added. This feature is disabled by default and can be enabled by setting the `IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD` to `true` or a floating point number representing the threshold in seconds.
34
-
35
- When enabled, the event loop will measure and profile user code when resuming a fiber. If the fiber takes too long to return back to the event loop, the event loop will log a warning message with a profile of the fiber's execution.
36
-
37
- > cat test.rb
38
- #!/usr/bin/env ruby
39
-
40
- require_relative "lib/async"
41
-
42
- Async do
43
- Fiber.blocking do
44
- sleep 1
45
- end
46
- end
47
-
48
- > IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD=true bundle exec ./test.rb
49
- Fiber stalled for 1.003 seconds
50
- /home/samuel/Developer/socketry/async/test.rb:6 in '#<Class:Fiber>#blocking' (1s)
51
- /home/samuel/Developer/socketry/async/test.rb:7 in 'Kernel#sleep' (1s)
52
-
53
- There is a performance overhead to this feature, so it is recommended to only enable it when debugging performance issues.
44
+ - Detecting fibers that are stalling the event loop.
54
45
 
55
46
  ## v1.7.5
56
47
 
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.9.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -15,6 +15,7 @@ authors:
15
15
  - Delton Ding
16
16
  - Pavel Rosický
17
17
  - Shizuo Fujita
18
+ - Stanislav (Stas) Katkov
18
19
  bindir: bin
19
20
  cert_chain:
20
21
  - |
@@ -46,7 +47,7 @@ cert_chain:
46
47
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
47
48
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
48
49
  -----END CERTIFICATE-----
49
- date: 2025-02-10 00:00:00.000000000 Z
50
+ date: 1980-01-02 00:00:00.000000000 Z
50
51
  dependencies: []
51
52
  executables: []
52
53
  extensions:
@@ -63,8 +64,6 @@ files:
63
64
  - ext/io/event/interrupt.c
64
65
  - ext/io/event/interrupt.h
65
66
  - ext/io/event/list.h
66
- - ext/io/event/profiler.c
67
- - ext/io/event/profiler.h
68
67
  - ext/io/event/selector/epoll.c
69
68
  - ext/io/event/selector/epoll.h
70
69
  - ext/io/event/selector/kqueue.c
@@ -76,12 +75,15 @@ files:
76
75
  - ext/io/event/selector/uring.h
77
76
  - ext/io/event/time.c
78
77
  - ext/io/event/time.h
78
+ - ext/io/event/worker_pool.c
79
+ - ext/io/event/worker_pool.h
80
+ - ext/io/event/worker_pool_test.c
81
+ - ext/io/event/worker_pool_test.h
79
82
  - lib/io/event.rb
80
83
  - lib/io/event/debug/selector.rb
81
84
  - lib/io/event/interrupt.rb
82
85
  - lib/io/event/native.rb
83
86
  - lib/io/event/priority_heap.rb
84
- - lib/io/event/profiler.rb
85
87
  - lib/io/event/selector.rb
86
88
  - lib/io/event/selector/nonblock.rb
87
89
  - lib/io/event/selector/select.rb
@@ -111,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
113
  - !ruby/object:Gem::Version
112
114
  version: '0'
113
115
  requirements: []
114
- rubygems_version: 3.6.2
116
+ rubygems_version: 3.6.7
115
117
  specification_version: 4
116
118
  summary: An event loop.
117
119
  test_files: []
metadata.gz.sig CHANGED
Binary file