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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/ext/extconf.rb +18 -7
- data/ext/io/event/event.c +5 -4
- data/ext/io/event/event.h +4 -0
- data/ext/io/event/fiber.c +1 -1
- data/ext/io/event/selector/epoll.c +18 -0
- data/ext/io/event/selector/kqueue.c +19 -0
- data/ext/io/event/selector/uring.c +25 -3
- data/ext/io/event/worker_pool.c +464 -0
- data/ext/io/event/{profiler.h → worker_pool.h} +1 -1
- data/ext/io/event/worker_pool_test.c +200 -0
- data/ext/io/event/worker_pool_test.h +9 -0
- data/lib/io/event/selector/select.rb +18 -12
- data/lib/io/event/timers.rb +1 -2
- data/lib/io/event/version.rb +1 -1
- data/license.md +1 -0
- data/readme.md +16 -3
- data/releases.md +30 -39
- data.tar.gz.sig +0 -0
- metadata +8 -6
- metadata.gz.sig +0 -0
- data/ext/io/event/profiler.c +0 -505
- data/lib/io/event/profiler.rb +0 -18
@@ -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
|
+
}
|
@@ -421,19 +421,25 @@ module IO::Event
|
|
421
421
|
writable = Array.new
|
422
422
|
priority = Array.new
|
423
423
|
|
424
|
-
@waiting.
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
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
|
|
data/lib/io/event/timers.rb
CHANGED
@@ -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.
|
data/lib/io/event/version.rb
CHANGED
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
|
-
# 
|
1
|
+
# 
|
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
|
-
-
|
36
|
+
- Improved `IO::Event::Profiler` for detecting stalls.
|
24
37
|
|
25
38
|
### v1.8.0
|
26
39
|
|
27
|
-
-
|
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.
|
3
|
+
## v1.11.0
|
4
4
|
|
5
|
-
###
|
5
|
+
### Introduce `IO::Event::WorkerPool` for off-loading blocking operations.
|
6
6
|
|
7
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
19
|
-
```
|
29
|
+
## v1.10.2
|
20
30
|
|
21
|
-
|
31
|
+
- Improved consistency of handling closed IO when invoking `#select`.
|
22
32
|
|
23
|
-
|
24
|
-
|
25
|
-
- `
|
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
|
-
|
40
|
+
- Improved `IO::Event::Profiler` for detecting stalls.
|
28
41
|
|
29
42
|
## v1.8.0
|
30
43
|
|
31
|
-
|
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.
|
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:
|
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.
|
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
|