Module: Hyperion::IOUring
- Defined in:
- lib/hyperion/io_uring.rb
Overview
2.3-A — io_uring accept on Linux 5.6+ (opt-in).
The biggest unmovable bottleneck below the GVL on the plaintext h1 path is the kernel accept loop: every accept costs accept_nonblock + IO.select on the EAGAIN edge (two syscalls per accepted connection under burst). io_uring lets us submit accept SQEs and reap CQEs in one syscall, with the kernel batching multiple accepts in a single CQE drain when connections arrive faster than the fiber can consume them.
## Surface
Hyperion::IOUring.supported? # bool — Linux ≥ 5.6 + cdylib loaded
# + runtime probe succeeds
Hyperion::IOUring::Ring.new(queue_depth: 256)
# per-fiber ring; #accept(fd) → fd
# or :wouldblock; #close releases
# the ring's SQ/CQ memory.
## Per-fiber, NEVER per-process or per-thread
io_uring under fork+threads has known sharp edges:
* Submission queue is process-shared by default — under fork, the
parent's outstanding SQEs leak into the child's CQ.
* IORING_SETUP_SQPOLL kernel thread does not survive fork.
* Threads sharing a ring need IORING_SETUP_SINGLE_ISSUER + careful
submission discipline.
Hyperion’s safe pattern, matching the fiber-per-conn architecture:
* One ring per fiber that needs it (the accept fiber, optionally
per-connection read fibers in a future phase).
* Ring is opened lazily on first use:
Fiber.current[:hyperion_io_uring] ||=
Hyperion::IOUring::Ring.new(queue_depth: 256)
* Ring is closed when the fiber exits.
* Workers don't share rings across fork — each child opens its own.
## Default off in 2.3.0
Mirrors the 2.2.0 fix-B HYPERION_H2_NATIVE_HPACK pattern: ship the plumbing in 2.3.0 with the default OFF, give operators an env-var to A/B (HYPERION_IO_URING=on,auto), flip the default to :auto in 2.4 only after 6 months of soak. io_uring code in production has too many sharp edges to default-on without field validation.
Defined Under Namespace
Classes: Ring, Unsupported
Constant Summary collapse
- EXPECTED_ABI =
1- MIN_LINUX_KERNEL =
Linux 5.6 stabilized IORING_OP_ACCEPT (commit 17f2fe35d080, mainlined Mar 2020). 5.5 had a buggy precursor that the io-uring crate refuses to use. We gate on 5.6 to match the crate’s stance.
[5, 6].freeze
Class Method Summary collapse
- .candidate_paths ⇒ Object
-
.compute_supported ⇒ Object
—- Internal: feature gate —-.
- .kernel_supports_io_uring? ⇒ Boolean
- .linux? ⇒ Boolean
-
.load! ⇒ Object
—- Internal: Fiddle loader —-.
-
.parse_kernel_release ⇒ Object
‘Etc.uname` is the canonical source.
-
.reset! ⇒ Object
Test seam: clear cached probe so ‘supported?` re-runs.
-
.resolve_policy!(policy) ⇒ Object
Resolve the operator’s ‘io_uring` policy + the runtime gate into a boolean “use io_uring on this server”.
- .ring_accept(ptr, fd, errno_buf) ⇒ Object
- .ring_free(ptr) ⇒ Object
-
.ring_new(depth) ⇒ Object
—- FFI wrappers —-.
- .ring_read(ptr, fd, buf, max, errno_buf) ⇒ Object
-
.supported? ⇒ Boolean
Cached three-state result: nil = not-yet-probed, true/false = result.
Class Method Details
.candidate_paths ⇒ Object
259 260 261 262 263 264 265 |
# File 'lib/hyperion/io_uring.rb', line 259 def candidate_paths gem_lib = File.('../hyperion_io_uring', __dir__) ext_target = File.('../../ext/hyperion_io_uring/target/release', __dir__) %w[libhyperion_io_uring.dylib libhyperion_io_uring.so].flat_map do |name| [File.join(gem_lib, name), File.join(ext_target, name)] end end |
.compute_supported ⇒ Object
—- Internal: feature gate —-
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/hyperion/io_uring.rb', line 163 def compute_supported # Gate 1: Linux only. macOS/BSD don't have io_uring. return false unless linux? # Gate 2: Kernel ≥ 5.6. return false unless kernel_supports_io_uring? # Gate 3: cdylib loaded. load! return false unless @lib # Gate 4: runtime probe — try to set up a tiny ring. Catches # sandboxed containers (seccomp blocking io_uring_setup, # locked-down environments returning -EPERM, kernels with # io_uring disabled via /proc/sys/kernel/io_uring_disabled). rc = @probe_fn.call rc.zero? rescue StandardError false end |
.kernel_supports_io_uring? ⇒ Boolean
190 191 192 193 194 195 196 197 198 199 |
# File 'lib/hyperion/io_uring.rb', line 190 def kernel_supports_io_uring? return false unless linux? release = parse_kernel_release return false unless release major, minor = release min_major, min_minor = MIN_LINUX_KERNEL major > min_major || (major == min_major && minor >= min_minor) end |
.linux? ⇒ Boolean
184 185 186 187 188 |
# File 'lib/hyperion/io_uring.rb', line 184 def linux? Etc.uname[:sysname] == 'Linux' rescue StandardError false end |
.load! ⇒ Object
—- Internal: Fiddle loader —-
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/hyperion/io_uring.rb', line 219 def load! return @lib if defined?(@lib) && !@lib.nil? path = candidate_paths.find { |p| File.exist?(p) } unless path @lib = nil return nil end @lib = Fiddle.dlopen(path) @abi_fn = Fiddle::Function.new(@lib['hyperion_io_uring_abi_version'], [], Fiddle::TYPE_INT) abi = @abi_fn.call if abi != EXPECTED_ABI warn "[hyperion] IOUring ABI mismatch (got #{abi}, expected #{EXPECTED_ABI}); falling back" @lib = nil return nil end @probe_fn = Fiddle::Function.new(@lib['hyperion_io_uring_probe'], [], Fiddle::TYPE_INT) @ring_new_fn = Fiddle::Function.new(@lib['hyperion_io_uring_ring_new'], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP) @ring_free_fn = Fiddle::Function.new(@lib['hyperion_io_uring_ring_free'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID) @accept_fn = Fiddle::Function.new(@lib['hyperion_io_uring_accept'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT) @read_fn = Fiddle::Function.new(@lib['hyperion_io_uring_read'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT) @lib rescue Fiddle::DLError, StandardError => e warn "[hyperion] IOUring failed to load (#{e.class}: #{e.}); falling back to epoll" @lib = nil nil end |
.parse_kernel_release ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/hyperion/io_uring.rb', line 204 def parse_kernel_release release = Etc.uname[:release].to_s if release.empty? && File.exist?('/proc/sys/kernel/osrelease') release = File.read('/proc/sys/kernel/osrelease').strip end m = release.match(/\A(\d+)\.(\d+)/) return nil unless m [m[1].to_i, m[2].to_i] rescue StandardError nil end |
.reset! ⇒ Object
Test seam: clear cached probe so ‘supported?` re-runs. Used by specs that stub Etc.uname or RbConfig.
156 157 158 159 |
# File 'lib/hyperion/io_uring.rb', line 156 def reset! @supported = nil @lib = nil end |
.resolve_policy!(policy) ⇒ Object
Resolve the operator’s ‘io_uring` policy + the runtime gate into a boolean “use io_uring on this server”. Called by Server at boot.
Policy values:
:off → never. Returns false. Used for the 2.3.0 default.
:auto → use it when supported; quietly fall back otherwise.
:on → demand it. Raise UnsupportedError if not available
so the operator's misconfig surfaces at boot, not as
a slow-fallback mystery hours later.
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/hyperion/io_uring.rb', line 299 def self.resolve_policy!(policy) case policy when :off, nil, false false when :auto supported? when :on, true unless supported? raise Unsupported, 'io_uring required (io_uring: :on) but not supported on this host ' \ "(linux=#{linux?}, kernel_ok=#{kernel_supports_io_uring?}, lib_loaded=#{!@lib.nil?})" end true else raise ArgumentError, "io_uring must be :off, :auto, or :on (got #{policy.inspect})" end end |
.ring_accept(ptr, fd, errno_buf) ⇒ Object
278 279 280 |
# File 'lib/hyperion/io_uring.rb', line 278 def ring_accept(ptr, fd, errno_buf) @accept_fn.call(ptr, fd, errno_buf) end |
.ring_free(ptr) ⇒ Object
274 275 276 |
# File 'lib/hyperion/io_uring.rb', line 274 def ring_free(ptr) @ring_free_fn.call(ptr) end |
.ring_new(depth) ⇒ Object
—- FFI wrappers —-
269 270 271 272 |
# File 'lib/hyperion/io_uring.rb', line 269 def ring_new(depth) ptr = @ring_new_fn.call(depth) ptr.null? ? nil : ptr end |
.ring_read(ptr, fd, buf, max, errno_buf) ⇒ Object
282 283 284 |
# File 'lib/hyperion/io_uring.rb', line 282 def ring_read(ptr, fd, buf, max, errno_buf) @read_fn.call(ptr, fd, buf, max, errno_buf) end |
.supported? ⇒ Boolean
Cached three-state result: nil = not-yet-probed, true/false = result.
The probe is intentionally process-local (not Fiber-local) — the answer is the same for every fiber in this process, and probing once at boot avoids per-request syscall overhead.
148 149 150 151 152 |
# File 'lib/hyperion/io_uring.rb', line 148 def supported? return @supported unless @supported.nil? @supported = compute_supported end |