hyperion-rb 1.6.2 → 2.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
- data/CHANGELOG.md +4768 -0
- data/README.md +222 -13
- data/ext/hyperion_h2_codec/Cargo.lock +7 -0
- data/ext/hyperion_h2_codec/Cargo.toml +33 -0
- data/ext/hyperion_h2_codec/extconf.rb +73 -0
- data/ext/hyperion_h2_codec/src/frames.rs +140 -0
- data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
- data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
- data/ext/hyperion_h2_codec/src/lib.rs +296 -0
- data/ext/hyperion_http/extconf.rb +28 -0
- data/ext/hyperion_http/h2_codec_glue.c +408 -0
- data/ext/hyperion_http/page_cache.c +1125 -0
- data/ext/hyperion_http/parser.c +473 -38
- data/ext/hyperion_http/sendfile.c +982 -0
- data/ext/hyperion_http/websocket.c +493 -0
- data/ext/hyperion_io_uring/Cargo.lock +33 -0
- data/ext/hyperion_io_uring/Cargo.toml +34 -0
- data/ext/hyperion_io_uring/extconf.rb +74 -0
- data/ext/hyperion_io_uring/src/lib.rs +316 -0
- data/lib/hyperion/adapter/rack.rb +370 -42
- data/lib/hyperion/admin_listener.rb +207 -0
- data/lib/hyperion/admin_middleware.rb +36 -7
- data/lib/hyperion/cli.rb +310 -11
- data/lib/hyperion/config.rb +440 -14
- data/lib/hyperion/connection.rb +679 -22
- data/lib/hyperion/deprecations.rb +81 -0
- data/lib/hyperion/dispatch_mode.rb +165 -0
- data/lib/hyperion/fiber_local.rb +75 -13
- data/lib/hyperion/h2_admission.rb +77 -0
- data/lib/hyperion/h2_codec.rb +499 -0
- data/lib/hyperion/http/page_cache.rb +122 -0
- data/lib/hyperion/http/sendfile.rb +696 -0
- data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
- data/lib/hyperion/http2_handler.rb +618 -19
- data/lib/hyperion/io_uring.rb +317 -0
- data/lib/hyperion/lint_wrapper_pool.rb +126 -0
- data/lib/hyperion/master.rb +96 -9
- data/lib/hyperion/metrics/path_templater.rb +68 -0
- data/lib/hyperion/metrics.rb +256 -0
- data/lib/hyperion/prometheus_exporter.rb +150 -0
- data/lib/hyperion/request.rb +13 -0
- data/lib/hyperion/response_writer.rb +477 -16
- data/lib/hyperion/runtime.rb +195 -0
- data/lib/hyperion/server/route_table.rb +179 -0
- data/lib/hyperion/server.rb +519 -55
- data/lib/hyperion/static_preload.rb +133 -0
- data/lib/hyperion/thread_pool.rb +61 -7
- data/lib/hyperion/tls.rb +343 -1
- data/lib/hyperion/version.rb +1 -1
- data/lib/hyperion/websocket/close_codes.rb +71 -0
- data/lib/hyperion/websocket/connection.rb +876 -0
- data/lib/hyperion/websocket/frame.rb +356 -0
- data/lib/hyperion/websocket/handshake.rb +525 -0
- data/lib/hyperion/worker.rb +111 -9
- data/lib/hyperion.rb +137 -3
- metadata +50 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
//! Hyperion 2.3-A — io_uring accept primitives.
|
|
2
|
+
//!
|
|
3
|
+
//! Surface (extern "C", consumed by Ruby via Fiddle):
|
|
4
|
+
//!
|
|
5
|
+
//! * `hyperion_io_uring_abi_version()` — bumped on any breaking
|
|
6
|
+
//! ABI change. Ruby refuses to load a mismatched binary.
|
|
7
|
+
//! * `hyperion_io_uring_probe()` — runtime feature probe. Returns
|
|
8
|
+
//! 0 on success, negative errno-ish on failure. Ruby calls this
|
|
9
|
+
//! once at boot to decide whether the kernel actually exposes
|
|
10
|
+
//! io_uring (handles -ENOSYS sandboxes).
|
|
11
|
+
//! * `hyperion_io_uring_ring_new(queue_depth)` — allocate a per-
|
|
12
|
+
//! fiber ring. Returns an opaque pointer (NULL on failure).
|
|
13
|
+
//! * `hyperion_io_uring_ring_free(ptr)` — close + free the ring.
|
|
14
|
+
//! MUST be called from the same fiber that opened it. Workers
|
|
15
|
+
//! never share rings across fork — each child opens its own.
|
|
16
|
+
//! * `hyperion_io_uring_accept(ptr, listener_fd, out_errno_ptr)` —
|
|
17
|
+
//! submit one accept SQE, wait for the matching CQE, return the
|
|
18
|
+
//! accepted client fd. Returns:
|
|
19
|
+
//! >= 0 : accepted client fd
|
|
20
|
+
//! -1 : EAGAIN (caller treats as :wouldblock — should not
|
|
21
|
+
//! normally happen with a blocking accept submission,
|
|
22
|
+
//! but defensive against IORING_FEAT_NODROP races)
|
|
23
|
+
//! -2 : ring submission failed; out_errno_ptr written with errno
|
|
24
|
+
//! -3 : CQE wait failed
|
|
25
|
+
//! * `hyperion_io_uring_read(ptr, fd, buf, max, out_errno_ptr)` —
|
|
26
|
+
//! submit one read SQE, wait for the CQE, return bytes read.
|
|
27
|
+
//! Returns the byte count, 0 on EOF, or -1/-2/-3 mirroring the
|
|
28
|
+
//! accept error scheme.
|
|
29
|
+
//!
|
|
30
|
+
//! Memory model: rings are owned `Box`es; Ruby holds an opaque
|
|
31
|
+
//! `void*` and explicitly frees via `ring_free`. The ring's
|
|
32
|
+
//! submission queue + completion queue memory is mmap'd by the
|
|
33
|
+
//! kernel and reclaimed on free.
|
|
34
|
+
//!
|
|
35
|
+
//! Fork story: io_uring under fork has sharp edges — the parent's
|
|
36
|
+
//! outstanding SQEs leak into the child's CQ, and IORING_SETUP_SQPOLL
|
|
37
|
+
//! kernel threads don't survive fork. Hyperion's worker model (master
|
|
38
|
+
//! never opens a ring; each worker opens its own per-fiber rings
|
|
39
|
+
//! lazily on first use) sidesteps both. We do NOT use SQPOLL —
|
|
40
|
+
//! single-issuer per fiber is enough for the accept path.
|
|
41
|
+
//!
|
|
42
|
+
//! Darwin / non-Linux: the io-uring crate is Linux-only via Cargo
|
|
43
|
+
//! `target.'cfg(target_os = "linux")'` gating, so the symbols below
|
|
44
|
+
//! compile down to stubs on macOS that always return -ENOSYS-ish
|
|
45
|
+
//! sentinels. Ruby's `IOUring.supported?` checks the OS first and
|
|
46
|
+
//! never reaches these stubs in practice.
|
|
47
|
+
|
|
48
|
+
#![allow(clippy::missing_safety_doc)]
|
|
49
|
+
|
|
50
|
+
use std::os::raw::{c_int, c_uchar, c_uint};
|
|
51
|
+
|
|
52
|
+
const ABI_VERSION: u32 = 1;
|
|
53
|
+
|
|
54
|
+
// ---------- ABI version + probe ----------
|
|
55
|
+
|
|
56
|
+
#[no_mangle]
|
|
57
|
+
pub extern "C" fn hyperion_io_uring_abi_version() -> u32 {
|
|
58
|
+
ABI_VERSION
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ===== Linux implementation =====
|
|
62
|
+
|
|
63
|
+
#[cfg(target_os = "linux")]
|
|
64
|
+
mod linux_impl {
|
|
65
|
+
use super::*;
|
|
66
|
+
use io_uring::{opcode, squeue, types, IoUring};
|
|
67
|
+
use std::os::unix::io::RawFd;
|
|
68
|
+
|
|
69
|
+
/// Owned per-fiber ring. Holds the IoUring + a tiny scratch
|
|
70
|
+
/// `libc::sockaddr_storage` so accept SQEs can hand the kernel a
|
|
71
|
+
/// pointer to write the accepted peer addr into. We don't expose
|
|
72
|
+
/// the peer addr to Ruby (the connection layer already pulls it
|
|
73
|
+
/// off `accept` if needed via getpeername), but the kernel
|
|
74
|
+
/// requires the pointer to be non-NULL on the accept opcode.
|
|
75
|
+
pub struct Ring {
|
|
76
|
+
// Explicit Entry/Entry generics: io-uring 0.6's IoUring is
|
|
77
|
+
// generic over squeue::EntryMarker / cqueue::EntryMarker; the
|
|
78
|
+
// type-inference can't solve `IoUring::builder().build(_)`
|
|
79
|
+
// without help. Pinning to the default Entry pair (32-byte
|
|
80
|
+
// SQEs / 16-byte CQEs) here keeps the rest of the impl clean.
|
|
81
|
+
ring: IoUring<squeue::Entry, io_uring::cqueue::Entry>,
|
|
82
|
+
addr_storage: libc::sockaddr_storage,
|
|
83
|
+
addr_len: libc::socklen_t,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
impl Ring {
|
|
87
|
+
pub fn new(queue_depth: u32) -> std::io::Result<Self> {
|
|
88
|
+
let ring: IoUring<squeue::Entry, io_uring::cqueue::Entry> =
|
|
89
|
+
IoUring::builder().build(queue_depth)?;
|
|
90
|
+
let addr_storage: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
|
|
91
|
+
Ok(Ring {
|
|
92
|
+
ring,
|
|
93
|
+
addr_storage,
|
|
94
|
+
addr_len: std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Submit an accept SQE for `listener_fd` and wait for its
|
|
99
|
+
/// CQE. Returns Ok(client_fd) or Err(errno).
|
|
100
|
+
pub fn accept(&mut self, listener_fd: RawFd) -> Result<RawFd, i32> {
|
|
101
|
+
// Reset addr_len each call — the kernel writes the actual
|
|
102
|
+
// sockaddr length into it on success and we don't want a
|
|
103
|
+
// narrowed value from a previous call to confuse it.
|
|
104
|
+
self.addr_len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
|
105
|
+
|
|
106
|
+
let accept_e = opcode::Accept::new(
|
|
107
|
+
types::Fd(listener_fd),
|
|
108
|
+
&mut self.addr_storage as *mut _ as *mut libc::sockaddr,
|
|
109
|
+
&mut self.addr_len,
|
|
110
|
+
)
|
|
111
|
+
.build()
|
|
112
|
+
.user_data(0xacce_0000);
|
|
113
|
+
|
|
114
|
+
unsafe {
|
|
115
|
+
if self.ring.submission().push(&accept_e).is_err() {
|
|
116
|
+
return Err(libc::EAGAIN);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// submit_and_wait drives io_uring_enter with min_complete=1.
|
|
121
|
+
// On success the kernel wakes us when the accept completes
|
|
122
|
+
// (or errors out). We do NOT block the OS thread under
|
|
123
|
+
// Async — the fiber is parked here, but io_uring_enter
|
|
124
|
+
// itself is a blocking syscall. For the accept fiber that's
|
|
125
|
+
// fine: the worker's ONE accept fiber owning ONE ring is
|
|
126
|
+
// exactly the design. For per-connection read rings (a
|
|
127
|
+
// future 2.3-A.x deliverable) we'll need IORING_ENTER_GETEVENTS
|
|
128
|
+
// with a non-blocking submission instead, paired with
|
|
129
|
+
// scheduler integration.
|
|
130
|
+
self.ring.submit_and_wait(1).map_err(|_| libc::EIO)?;
|
|
131
|
+
|
|
132
|
+
let cqe = self.ring.completion().next().ok_or(libc::EIO)?;
|
|
133
|
+
let res = cqe.result();
|
|
134
|
+
if res < 0 {
|
|
135
|
+
Err(-res)
|
|
136
|
+
} else {
|
|
137
|
+
Ok(res)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Submit a read SQE for `fd` into `buf` and wait for the
|
|
142
|
+
/// CQE. Returns Ok(bytes) (0 on EOF) or Err(errno).
|
|
143
|
+
pub fn read(&mut self, fd: RawFd, buf: *mut u8, len: u32) -> Result<i32, i32> {
|
|
144
|
+
let read_e = opcode::Read::new(types::Fd(fd), buf, len)
|
|
145
|
+
.build()
|
|
146
|
+
.user_data(0xeead_0001);
|
|
147
|
+
|
|
148
|
+
unsafe {
|
|
149
|
+
if self.ring.submission().push(&read_e).is_err() {
|
|
150
|
+
return Err(libc::EAGAIN);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
self.ring.submit_and_wait(1).map_err(|_| libc::EIO)?;
|
|
155
|
+
let cqe = self.ring.completion().next().ok_or(libc::EIO)?;
|
|
156
|
+
let res = cqe.result();
|
|
157
|
+
if res < 0 {
|
|
158
|
+
Err(-res)
|
|
159
|
+
} else {
|
|
160
|
+
Ok(res)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Probe: try to set up a tiny ring. Returns 0 on success or the
|
|
166
|
+
/// negative errno (e.g. -ENOSYS in a sandbox where io_uring_setup
|
|
167
|
+
/// is blocked, or -EPERM in a seccomp-filtered container).
|
|
168
|
+
pub fn probe() -> c_int {
|
|
169
|
+
let result: std::io::Result<IoUring<squeue::Entry, io_uring::cqueue::Entry>> =
|
|
170
|
+
IoUring::builder().build(8);
|
|
171
|
+
match result {
|
|
172
|
+
Ok(_) => 0,
|
|
173
|
+
Err(e) => -(e.raw_os_error().unwrap_or(libc::ENOSYS)),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ===== Non-Linux stubs =====
|
|
179
|
+
//
|
|
180
|
+
// On Darwin / BSD we still compile the crate so the `cargo build`
|
|
181
|
+
// step in the gem's extconf succeeds and the cdylib drops into the
|
|
182
|
+
// expected place — but every entry point returns the canonical
|
|
183
|
+
// "kernel feature missing" sentinel so the Ruby probe falls back
|
|
184
|
+
// cleanly.
|
|
185
|
+
|
|
186
|
+
#[cfg(not(target_os = "linux"))]
|
|
187
|
+
mod stub_impl {
|
|
188
|
+
use super::*;
|
|
189
|
+
|
|
190
|
+
pub struct Ring;
|
|
191
|
+
|
|
192
|
+
impl Ring {
|
|
193
|
+
pub fn new(_queue_depth: u32) -> std::io::Result<Self> {
|
|
194
|
+
Err(std::io::Error::from_raw_os_error(38)) // ENOSYS on Linux; reuse code on Darwin
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pub fn probe() -> c_int {
|
|
199
|
+
-38 // -ENOSYS
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[cfg(target_os = "linux")]
|
|
204
|
+
use linux_impl::Ring;
|
|
205
|
+
#[cfg(not(target_os = "linux"))]
|
|
206
|
+
use stub_impl::Ring;
|
|
207
|
+
|
|
208
|
+
// ---------- C ABI ----------
|
|
209
|
+
|
|
210
|
+
#[no_mangle]
|
|
211
|
+
pub extern "C" fn hyperion_io_uring_probe() -> c_int {
|
|
212
|
+
#[cfg(target_os = "linux")]
|
|
213
|
+
{
|
|
214
|
+
linux_impl::probe()
|
|
215
|
+
}
|
|
216
|
+
#[cfg(not(target_os = "linux"))]
|
|
217
|
+
{
|
|
218
|
+
stub_impl::probe()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[no_mangle]
|
|
223
|
+
pub extern "C" fn hyperion_io_uring_ring_new(queue_depth: c_uint) -> *mut Ring {
|
|
224
|
+
let depth = if queue_depth == 0 { 256 } else { queue_depth };
|
|
225
|
+
match Ring::new(depth) {
|
|
226
|
+
Ok(r) => Box::into_raw(Box::new(r)),
|
|
227
|
+
Err(_) => std::ptr::null_mut(),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[no_mangle]
|
|
232
|
+
pub unsafe extern "C" fn hyperion_io_uring_ring_free(ptr: *mut Ring) {
|
|
233
|
+
if !ptr.is_null() {
|
|
234
|
+
drop(Box::from_raw(ptr));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[no_mangle]
|
|
239
|
+
pub unsafe extern "C" fn hyperion_io_uring_accept(
|
|
240
|
+
ptr: *mut Ring,
|
|
241
|
+
listener_fd: c_int,
|
|
242
|
+
out_errno: *mut c_int,
|
|
243
|
+
) -> c_int {
|
|
244
|
+
if ptr.is_null() {
|
|
245
|
+
if !out_errno.is_null() {
|
|
246
|
+
*out_errno = 22; // EINVAL
|
|
247
|
+
}
|
|
248
|
+
return -2;
|
|
249
|
+
}
|
|
250
|
+
#[cfg(target_os = "linux")]
|
|
251
|
+
{
|
|
252
|
+
let ring = &mut *ptr;
|
|
253
|
+
match ring.accept(listener_fd) {
|
|
254
|
+
Ok(fd) => fd,
|
|
255
|
+
Err(errno) => {
|
|
256
|
+
if !out_errno.is_null() {
|
|
257
|
+
*out_errno = errno;
|
|
258
|
+
}
|
|
259
|
+
if errno == libc::EAGAIN {
|
|
260
|
+
-1
|
|
261
|
+
} else {
|
|
262
|
+
-2
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
#[cfg(not(target_os = "linux"))]
|
|
268
|
+
{
|
|
269
|
+
let _ = (ptr, listener_fd);
|
|
270
|
+
if !out_errno.is_null() {
|
|
271
|
+
*out_errno = 38; // ENOSYS
|
|
272
|
+
}
|
|
273
|
+
-2
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[no_mangle]
|
|
278
|
+
pub unsafe extern "C" fn hyperion_io_uring_read(
|
|
279
|
+
ptr: *mut Ring,
|
|
280
|
+
fd: c_int,
|
|
281
|
+
buf: *mut c_uchar,
|
|
282
|
+
max: c_uint,
|
|
283
|
+
out_errno: *mut c_int,
|
|
284
|
+
) -> c_int {
|
|
285
|
+
if ptr.is_null() || buf.is_null() {
|
|
286
|
+
if !out_errno.is_null() {
|
|
287
|
+
*out_errno = 22; // EINVAL
|
|
288
|
+
}
|
|
289
|
+
return -2;
|
|
290
|
+
}
|
|
291
|
+
#[cfg(target_os = "linux")]
|
|
292
|
+
{
|
|
293
|
+
let ring = &mut *ptr;
|
|
294
|
+
match ring.read(fd, buf, max) {
|
|
295
|
+
Ok(n) => n,
|
|
296
|
+
Err(errno) => {
|
|
297
|
+
if !out_errno.is_null() {
|
|
298
|
+
*out_errno = errno;
|
|
299
|
+
}
|
|
300
|
+
if errno == libc::EAGAIN {
|
|
301
|
+
-1
|
|
302
|
+
} else {
|
|
303
|
+
-2
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
#[cfg(not(target_os = "linux"))]
|
|
309
|
+
{
|
|
310
|
+
let _ = (ptr, fd, buf, max);
|
|
311
|
+
if !out_errno.is_null() {
|
|
312
|
+
*out_errno = 38;
|
|
313
|
+
}
|
|
314
|
+
-2
|
|
315
|
+
}
|
|
316
|
+
}
|