io-event 1.22.1 → 1.22.3
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/futex.md +208 -0
- data/context/getting-started.md +6 -0
- data/context/index.yaml +4 -0
- data/ext/extconf.rb +7 -2
- data/ext/io/event/event.c +5 -0
- data/ext/io/event/futex.c +489 -0
- data/ext/io/event/futex.h +42 -0
- data/ext/io/event/selector/uring.c +211 -0
- data/ext/io/event/worker_pool.c +30 -20
- data/ext/io/event/worker_pool_test.c +6 -0
- data/lib/io/event/debug/selector.rb +12 -0
- data/lib/io/event/version.rb +1 -1
- data/license.md +2 -0
- data/readme.md +15 -11
- data/releases.md +9 -0
- data.tar.gz.sig +0 -0
- metadata +27 -29
- metadata.gz.sig +0 -0
- data/agent.md +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01f54cee2259c78ebfd5c7c55614d8bc0a4d5509e410c0b5e1729838dba13a5c
|
|
4
|
+
data.tar.gz: 5879a047f144aeb781e3ee2f159b2bd53770ff6a6fa1f5eeb44e6800e598a9a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7eabcaf11cb0467cc87270cae4db7532473d0bf1ed0eee7b709d0792214f416d217af1c2ef1cfccefcaa856444f1ed50fcd2cbecdbe55725eb44f4765c283329
|
|
7
|
+
data.tar.gz: 904d807808513f5e6d36a70eef168ec1a972e0c31395529b50e014ca9a47e080eddd14494b3e06f485ed33682c94df19394b5000c217be63523879e9d19eedae
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/futex.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Futex Notifications
|
|
2
|
+
|
|
3
|
+
This guide explains how to use `IO::Event::Futex` to notify threads or processes when shared state changes, without continuously polling it.
|
|
4
|
+
|
|
5
|
+
Shared state describes what changed; the futex tells you to check it. A wake-up does not transfer a message, reserve a worker, or grant ownership of a permit.
|
|
6
|
+
|
|
7
|
+
## Availability
|
|
8
|
+
|
|
9
|
+
`IO::Event::Futex` is available on Linux with Ruby 4.1 or later and the required Linux build headers. It uses Ruby's counted buffer locks to retain an aligned, writable 32-bit word. The class itself does not require `io_uring`.
|
|
10
|
+
|
|
11
|
+
There are two ways to wait:
|
|
12
|
+
|
|
13
|
+
- Without an active fiber scheduler for the calling fiber, waits release the GVL and block the calling thread using Linux futex syscalls.
|
|
14
|
+
- With a scheduler, waits invoke its optional `futex_wait` or `futex_waitv` hook. A missing hook raises `NotImplementedError`; it does not silently block the event loop.
|
|
15
|
+
|
|
16
|
+
Only the URing selector provides asynchronous futex waits. EPoll, KQueue, and Select do not. URing exposes each method only when the build's liburing and the running kernel support the corresponding operation. Check the selected backend before choosing an IPC mechanism:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
require "io/event"
|
|
20
|
+
|
|
21
|
+
selector = IO::Event::Selector.new(Fiber.current)
|
|
22
|
+
begin
|
|
23
|
+
futex_available = IO::Event.const_defined?(:Futex, false)
|
|
24
|
+
puts "Single waits: #{futex_available && selector.respond_to?(:futex_wait)}"
|
|
25
|
+
puts "Vector waits: #{futex_available && selector.respond_to?(:futex_waitv)}"
|
|
26
|
+
ensure
|
|
27
|
+
selector.close
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Creating a selector does not install a fiber scheduler. A scheduler integration must forward the following hooks to its own selector, and should only expose hooks that selector supports:
|
|
32
|
+
|
|
33
|
+
| Scheduler hook | Selector call |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `futex_wait(futex, expected)` | `selector.futex_wait(Fiber.current, futex, expected)` |
|
|
36
|
+
| `futex_waitv(entries)` | `selector.futex_waitv(Fiber.current, entries)` |
|
|
37
|
+
|
|
38
|
+
For blocking vector waits, `IO::Event::Futex.respond_to?(:wait_any)` indicates build support, not running-kernel support; the syscall can still raise `Errno::ENOSYS`. Deployment restrictions can also prevent kernel operations. Negotiate capabilities before peers begin waiting, and choose a socket, pipe, or an IPC protocol such as `async-bus` when asynchronous futex waits are unavailable. There is no automatic IPC fallback in `Futex`.
|
|
39
|
+
|
|
40
|
+
## Binding Shared Memory
|
|
41
|
+
|
|
42
|
+
Each futex refers to four bytes at a byte offset within an `IO::Buffer`. The address must be aligned to four bytes, and the buffer must have at least four bytes remaining at that offset. The default offset is zero. Constructing a futex preserves the word's existing value.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
require "io/event"
|
|
46
|
+
|
|
47
|
+
buffer = IO::Buffer.new(8)
|
|
48
|
+
first = IO::Event::Futex.new(buffer)
|
|
49
|
+
second = IO::Event::Futex.new(buffer, offset: 4)
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
first.value = 0
|
|
53
|
+
second.value = 0
|
|
54
|
+
first.signal
|
|
55
|
+
puts first.value # => 1
|
|
56
|
+
ensure
|
|
57
|
+
first.close
|
|
58
|
+
second.close
|
|
59
|
+
buffer.free
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An ordinary `IO::Buffer.new` allocation is suitable for threads in one process. Forking does not make that allocation shared between processes. For IPC, map shared storage instead: for example, each process can use `IO::Buffer.map(file, size)` on the same pre-sized file opened for reading and writing, without `IO::Buffer::PRIVATE`. Bind futexes at matching offsets in that mapping; virtual addresses need not match between processes.
|
|
64
|
+
|
|
65
|
+
Initialize the shared words once, before peers attach. Attaching peers must not reset live state. Futexes do not discover peers or transport Ruby objects; the application supplies the shared-state layout and synchronization protocol.
|
|
66
|
+
|
|
67
|
+
## Atomic Operations and Notifications
|
|
68
|
+
|
|
69
|
+
| Operation | Effect and return value |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `value` | Atomically reads the word. |
|
|
72
|
+
| `value = n` | Atomically stores `n`. |
|
|
73
|
+
| `increment(amount = 1)` | Adds `amount` and returns the new value. |
|
|
74
|
+
| `decrement(amount = 1)` | Subtracts `amount` and returns the new value. |
|
|
75
|
+
| `compare_exchange(expected, desired)` | Stores `desired` only if the word equals `expected`; returns whether it succeeded. |
|
|
76
|
+
| `wake(count = 1)` | Wakes at most `count` waiters without changing the word; returns the number woken. |
|
|
77
|
+
| `signal(count = 1)` | For a positive count, increments the word by one, then wakes at most `count` waiters; returns the word value. |
|
|
78
|
+
|
|
79
|
+
Loads use acquire ordering, stores use release ordering, and read-modify-write operations use acquire-release ordering. A failed compare-and-exchange uses acquire ordering. Arithmetic wraps modulo `2**32`; a notification counter is not an indefinitely increasing event history.
|
|
80
|
+
|
|
81
|
+
Only `wake` and `signal` notify sleeping waiters. Assignment, increment, decrement, and compare-and-exchange do not wake them. For a positive count, `signal` performs an atomic increment followed by a wake syscall, not one indivisible increment-and-wake operation. Its argument is the waiter count, not the increment amount. Wake counts must be nonnegative integers that fit in a C `int`.
|
|
82
|
+
|
|
83
|
+
Both `wake(0)` and `signal(0)` are no-ops: they leave the word and waiters unchanged. `wake(0)` returns `0`; `signal(0)` returns the current word value. Closed or uninitialized futexes still raise `IOError`.
|
|
84
|
+
|
|
85
|
+
Publish application state before notifying consumers. A futex does not make other memory accesses atomic or provide a queue's synchronization. Use atomic operations or another suitable synchronization protocol for that state, and do not mix concurrent non-atomic buffer accesses with atomic accesses to the futex word.
|
|
86
|
+
|
|
87
|
+
## Waiting Without Missing Notifications
|
|
88
|
+
|
|
89
|
+
`futex.wait(expected)` requires an explicit expected value and asks the kernel to wait only if the word still equals it. Comparing the word and beginning the wait is atomic with respect to futex operations. If the value has already changed, the call returns `false` without sleeping; a successful wake returns `true`. Omitting `expected` raises `ArgumentError`.
|
|
90
|
+
|
|
91
|
+
Always recheck application state after a wait. Wake-ups can be spurious, another consumer may have taken the available work, and waking a waiter does not guarantee FIFO ordering or ownership. System errors raise exceptions. Neither wait API currently accepts a timeout argument.
|
|
92
|
+
|
|
93
|
+
When the word is a notification counter for separate application state, the consumer must:
|
|
94
|
+
|
|
95
|
+
1. Read the notification counter.
|
|
96
|
+
2. Check or attempt to consume the synchronized application state.
|
|
97
|
+
3. If no work is available, wait using the saved counter value.
|
|
98
|
+
4. Repeat after the wait returns.
|
|
99
|
+
|
|
100
|
+
The producer publishes work before incrementing the counter and waking consumers. If a producer publishes between steps 2 and 3, the changed counter prevents the consumer from sleeping.
|
|
101
|
+
|
|
102
|
+
### Why the Expected Value Is Required
|
|
103
|
+
|
|
104
|
+
Reading the counter *after* checking for work can miss a notification:
|
|
105
|
+
|
|
106
|
+
1. The consumer finds the queue empty. The notification counter is `0`.
|
|
107
|
+
2. The producer adds work and signals, changing the counter to `1`. Nobody is waiting yet.
|
|
108
|
+
3. The consumer reads the counter and gets `1`.
|
|
109
|
+
4. The consumer calls `wait(1)`. Since the counter is still `1`, it sleeps despite available work. Without another notification, it can remain asleep indefinitely.
|
|
110
|
+
|
|
111
|
+
An argument-free `wait` would hide step 3 inside the method. Requiring `expected` makes the snapshot explicit: the consumer should capture `0` before checking the queue, then call `wait(0)`. In the same sequence, the kernel sees that the counter is now `1` and returns immediately. If the consumer starts waiting before the producer signals, the signal wakes it instead.
|
|
112
|
+
|
|
113
|
+
The required argument does not enforce the ordering by itself. Calling `wait(futex.value)` after checking for work has the same race. The intended sequence is **snapshot, check state, then wait using that snapshot**, followed by another state check after the wait returns.
|
|
114
|
+
|
|
115
|
+
### Producer and Consumer
|
|
116
|
+
|
|
117
|
+
This example uses a thread-safe Ruby `Queue` for application state and a futex for notifications. `Queue` already supports blocking `pop`; the explicit wait here demonstrates the protocol you would use with your own shared state. This particular queue is local to one process, not an IPC queue.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
require "io/event"
|
|
121
|
+
|
|
122
|
+
buffer = IO::Buffer.new(4)
|
|
123
|
+
notification = IO::Event::Futex.new(buffer)
|
|
124
|
+
notification.value = 0
|
|
125
|
+
queue = Thread::Queue.new
|
|
126
|
+
|
|
127
|
+
producer = Thread.new do
|
|
128
|
+
["first", "second", "third", nil].each do |message|
|
|
129
|
+
queue << message
|
|
130
|
+
notification.signal
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
begin
|
|
135
|
+
loop do
|
|
136
|
+
# Snapshot before checking application state:
|
|
137
|
+
expected = notification.value
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
message = queue.pop(true)
|
|
141
|
+
rescue ThreadError
|
|
142
|
+
notification.wait(expected)
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# A nil message marks the end of this example:
|
|
147
|
+
break if message.nil?
|
|
148
|
+
puts message
|
|
149
|
+
end
|
|
150
|
+
ensure
|
|
151
|
+
producer.join
|
|
152
|
+
notification.close
|
|
153
|
+
buffer.free
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The queue retains work even when a notification wakes nobody. Notifications may be coalesced; consumers must inspect the state rather than count wake-ups. A 32-bit counter can wrap back to a saved value, so protocols must account for wraparound if a consumer could miss an entire counter cycle between its snapshot and wait.
|
|
158
|
+
|
|
159
|
+
## Waiting on Several Words
|
|
160
|
+
|
|
161
|
+
When supported, `IO::Event::Futex.wait_any(entries)` waits on a vector of `[futex, expected]` pairs. Supply between one and `IO::Event::Futex::WAITV_LIMIT` entries. It returns a zero-based index when woken, or `nil` if any word does not match its expected value. The index is a notification hint, not an exhaustive list of changes or a claim on the associated resource.
|
|
162
|
+
|
|
163
|
+
For example, wait until either of two initially-zero words is nonzero:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
require "io/event"
|
|
167
|
+
|
|
168
|
+
buffer = IO::Buffer.new(8)
|
|
169
|
+
first = IO::Event::Futex.new(buffer)
|
|
170
|
+
second = IO::Event::Futex.new(buffer, offset: 4)
|
|
171
|
+
first.value = second.value = 0
|
|
172
|
+
|
|
173
|
+
producer = Thread.new{second.signal}
|
|
174
|
+
|
|
175
|
+
begin
|
|
176
|
+
while first.value == 0 && second.value == 0
|
|
177
|
+
IO::Event::Futex.wait_any([[first, 0], [second, 0]])
|
|
178
|
+
end
|
|
179
|
+
puts "At least one word changed."
|
|
180
|
+
ensure
|
|
181
|
+
producer.join
|
|
182
|
+
first.close
|
|
183
|
+
second.close
|
|
184
|
+
buffer.free
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
For notification counters alongside separate state, snapshot all counters before checking that state, just as with a single wait. Vector waits retain a private snapshot of the supplied futex references; mutating the original entries does not change an already-pending wait.
|
|
189
|
+
|
|
190
|
+
## Lifetime and Cancellation
|
|
191
|
+
|
|
192
|
+
Each futex owns one counted allocation lock from construction until `close` or finalization. Slices lock their root allocation, and multiple words can independently retain the same allocation. The lock prevents freeing, resizing, or transferring that allocation; it does not prevent writing shared state.
|
|
193
|
+
|
|
194
|
+
`close` is idempotent, and `closed?` reports whether the futex has been released. Word operations and waits on closed or uninitialized instances raise `IOError`. Futexes cannot be copied or reinitialized. A finalizer releases the lock if a futex is collected, but explicit `close` makes release deterministic. Avoid application references from the retained buffer back to its futex, which would keep it reachable through the finalizer.
|
|
195
|
+
|
|
196
|
+
A futex with pending waits cannot be closed: `close` raises `IOError` rather than cancelling or waking those waits. For orderly shutdown:
|
|
197
|
+
|
|
198
|
+
1. Publish shutdown state and notify the necessary waiters, or cancel them through the scheduler.
|
|
199
|
+
2. Let the waits finish, including cancellation cleanup.
|
|
200
|
+
3. Close all futexes, then free or unmap the buffer.
|
|
201
|
+
|
|
202
|
+
URing cancellation drains the original kernel operation before releasing its retained references or wait vector. Keep the event loop running until that cleanup completes. Exceptions still propagate; if a wait is resumed out of band without an exception, it can instead return `false` for a single wait or `nil` for a vector wait. These returns still require rechecking application state.
|
|
203
|
+
|
|
204
|
+
Allocation locks cannot protect against an external owner releasing memory or another process truncating a mapped file. The application must preserve the underlying storage for every process that can still access it.
|
|
205
|
+
|
|
206
|
+
## Further Reading
|
|
207
|
+
|
|
208
|
+
The Linux [futex overview](https://man7.org/linux/man-pages/man2/futex.2.html), [FUTEX_WAIT](https://man7.org/linux/man-pages/man2/FUTEX_WAIT.2const.html), and [FUTEX_WAKE](https://man7.org/linux/man-pages/man2/FUTEX_WAKE.2const.html) documentation describe the underlying shared-memory and notification semantics.
|
data/context/getting-started.md
CHANGED
|
@@ -76,6 +76,12 @@ puts "[main] Done"
|
|
|
76
76
|
# [main] Done
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Shared-Memory Notifications
|
|
80
|
+
|
|
81
|
+
On Linux with Ruby 4.1 or later, `IO::Event::Futex` provides atomic operations and notifications on an aligned 32-bit word in an `IO::Buffer`. It supports blocking thread waits and optional asynchronous waits through the URing selector.
|
|
82
|
+
|
|
83
|
+
See the [Futex Notifications guide](../futex/index) for capability detection, shared-memory setup, wait/recheck examples, vector waits, and buffer lifetime management.
|
|
84
|
+
|
|
79
85
|
## Debugging
|
|
80
86
|
|
|
81
87
|
The {ruby IO::Event::Debug::Selector} class adds extra validations and checks at the expense of performance. It can also log all operations. You can use this by setting the following environment variables:
|
data/context/index.yaml
CHANGED
|
@@ -9,3 +9,7 @@ files:
|
|
|
9
9
|
- path: getting-started.md
|
|
10
10
|
title: Getting Started
|
|
11
11
|
description: This guide explains how to use `io-event` for non-blocking IO.
|
|
12
|
+
- path: futex.md
|
|
13
|
+
title: Futex Notifications
|
|
14
|
+
description: This guide explains how to use `IO::Event::Futex` to notify threads
|
|
15
|
+
or processes when shared state changes, without continuously polling it.
|
data/ext/extconf.rb
CHANGED
|
@@ -31,9 +31,16 @@ $VPATH << "$(srcdir)/io/event/selector"
|
|
|
31
31
|
|
|
32
32
|
have_func("rb_ext_ractor_safe")
|
|
33
33
|
have_func("&rb_fiber_transfer")
|
|
34
|
+
have_io_buffer = have_header("ruby/io/buffer.h")
|
|
35
|
+
|
|
36
|
+
if RUBY_PLATFORM.include?("linux") && have_io_buffer && have_header("linux/futex.h") && have_header("sys/syscall.h")
|
|
37
|
+
$srcs << "io/event/futex.c"
|
|
38
|
+
end
|
|
34
39
|
|
|
35
40
|
if have_library("uring") and have_header("liburing.h")
|
|
36
41
|
have_func("io_uring_prep_waitid", "liburing.h")
|
|
42
|
+
have_func("io_uring_prep_futex_wait", "liburing.h")
|
|
43
|
+
have_func("io_uring_prep_futex_waitv", "liburing.h")
|
|
37
44
|
$srcs << "io/event/selector/uring.c"
|
|
38
45
|
end
|
|
39
46
|
|
|
@@ -57,8 +64,6 @@ have_func("rb_fiber_current")
|
|
|
57
64
|
have_func("&rb_fiber_raise")
|
|
58
65
|
have_func("epoll_pwait2(0, 0, 0, 0, 0)", "sys/epoll.h") if enable_config("epoll_pwait2", true)
|
|
59
66
|
|
|
60
|
-
have_header("ruby/io/buffer.h")
|
|
61
|
-
|
|
62
67
|
# Feature detection for blocking operation support
|
|
63
68
|
if have_func("rb_fiber_scheduler_blocking_operation_extract")
|
|
64
69
|
# Feature detection for pthread support (needed for WorkerPool)
|
data/ext/io/event/event.c
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#include "event.h"
|
|
5
5
|
#include "fiber.h"
|
|
6
|
+
#include "futex.h"
|
|
6
7
|
#include "selector/selector.h"
|
|
7
8
|
|
|
8
9
|
void Init_IO_Event(void)
|
|
@@ -15,6 +16,10 @@ void Init_IO_Event(void)
|
|
|
15
16
|
|
|
16
17
|
Init_IO_Event_Fiber(IO_Event);
|
|
17
18
|
|
|
19
|
+
#ifdef IO_EVENT_FUTEX
|
|
20
|
+
Init_IO_Event_Futex(IO_Event);
|
|
21
|
+
#endif
|
|
22
|
+
|
|
18
23
|
#ifdef HAVE_IO_EVENT_WORKER_POOL
|
|
19
24
|
Init_IO_Event_WorkerPool(IO_Event);
|
|
20
25
|
#endif
|