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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4768 -0
  3. data/README.md +222 -13
  4. data/ext/hyperion_h2_codec/Cargo.lock +7 -0
  5. data/ext/hyperion_h2_codec/Cargo.toml +33 -0
  6. data/ext/hyperion_h2_codec/extconf.rb +73 -0
  7. data/ext/hyperion_h2_codec/src/frames.rs +140 -0
  8. data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
  9. data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
  10. data/ext/hyperion_h2_codec/src/lib.rs +296 -0
  11. data/ext/hyperion_http/extconf.rb +28 -0
  12. data/ext/hyperion_http/h2_codec_glue.c +408 -0
  13. data/ext/hyperion_http/page_cache.c +1125 -0
  14. data/ext/hyperion_http/parser.c +473 -38
  15. data/ext/hyperion_http/sendfile.c +982 -0
  16. data/ext/hyperion_http/websocket.c +493 -0
  17. data/ext/hyperion_io_uring/Cargo.lock +33 -0
  18. data/ext/hyperion_io_uring/Cargo.toml +34 -0
  19. data/ext/hyperion_io_uring/extconf.rb +74 -0
  20. data/ext/hyperion_io_uring/src/lib.rs +316 -0
  21. data/lib/hyperion/adapter/rack.rb +370 -42
  22. data/lib/hyperion/admin_listener.rb +207 -0
  23. data/lib/hyperion/admin_middleware.rb +36 -7
  24. data/lib/hyperion/cli.rb +310 -11
  25. data/lib/hyperion/config.rb +440 -14
  26. data/lib/hyperion/connection.rb +679 -22
  27. data/lib/hyperion/deprecations.rb +81 -0
  28. data/lib/hyperion/dispatch_mode.rb +165 -0
  29. data/lib/hyperion/fiber_local.rb +75 -13
  30. data/lib/hyperion/h2_admission.rb +77 -0
  31. data/lib/hyperion/h2_codec.rb +499 -0
  32. data/lib/hyperion/http/page_cache.rb +122 -0
  33. data/lib/hyperion/http/sendfile.rb +696 -0
  34. data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
  35. data/lib/hyperion/http2_handler.rb +618 -19
  36. data/lib/hyperion/io_uring.rb +317 -0
  37. data/lib/hyperion/lint_wrapper_pool.rb +126 -0
  38. data/lib/hyperion/master.rb +96 -9
  39. data/lib/hyperion/metrics/path_templater.rb +68 -0
  40. data/lib/hyperion/metrics.rb +256 -0
  41. data/lib/hyperion/prometheus_exporter.rb +150 -0
  42. data/lib/hyperion/request.rb +13 -0
  43. data/lib/hyperion/response_writer.rb +477 -16
  44. data/lib/hyperion/runtime.rb +195 -0
  45. data/lib/hyperion/server/route_table.rb +179 -0
  46. data/lib/hyperion/server.rb +519 -55
  47. data/lib/hyperion/static_preload.rb +133 -0
  48. data/lib/hyperion/thread_pool.rb +61 -7
  49. data/lib/hyperion/tls.rb +343 -1
  50. data/lib/hyperion/version.rb +1 -1
  51. data/lib/hyperion/websocket/close_codes.rb +71 -0
  52. data/lib/hyperion/websocket/connection.rb +876 -0
  53. data/lib/hyperion/websocket/frame.rb +356 -0
  54. data/lib/hyperion/websocket/handshake.rb +525 -0
  55. data/lib/hyperion/worker.rb +111 -9
  56. data/lib/hyperion.rb +137 -3
  57. 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
+ }