Class: Hyperion::IOUring::Ring

Inherits:
Object
  • Object
show all
Defined in:
lib/hyperion/io_uring.rb

Overview

Per-Ring instance. Wraps the opaque pointer returned by ‘hyperion_io_uring_ring_new` and exposes the accept / read primitives over Fiddle.

Constant Summary collapse

DEFAULT_QUEUE_DEPTH =
256

Instance Method Summary collapse

Constructor Details

#initialize(queue_depth: DEFAULT_QUEUE_DEPTH) ⇒ Ring

Returns a new instance of Ring.

Raises:



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/hyperion/io_uring.rb', line 69

def initialize(queue_depth: DEFAULT_QUEUE_DEPTH)
  raise Unsupported, 'io_uring not supported on this platform' unless IOUring.supported?

  @ptr = IOUring.ring_new(queue_depth.to_i)
  raise Unsupported, 'io_uring_setup failed at ring allocation' if @ptr.nil? || @ptr.null?

  # `errno` scratch — reused across calls. Fiddle::Pointer to a
  # 4-byte buffer that the C side writes into on error. Saves
  # one Pointer allocation per accept.
  @errno_buf = Fiddle::Pointer.malloc(4, Fiddle::RUBY_FREE)
  @closed = false
end

Instance Method Details

#accept(listener_fd) ⇒ Object

Accept one connection on ‘listener_fd`. Returns the integer client fd, or `:wouldblock` on EAGAIN. Raises on hard errors.

The ring’s submit_and_wait drives io_uring_enter with min_complete=1, so this fiber parks here until the kernel delivers the matching CQE. Under Async, the Ruby side calls this from a Fiber — the fiber is logically blocked but the OS thread keeps running other fibers via the scheduler ONLY if ‘submit_and_wait` itself yields. It does not yield (it’s a syscall under FFI), so the accept fiber must be the only fiber with work-pending on its OS thread. In Hyperion’s default 1-accept-fiber-per-worker shape that’s always true.

Raises:

  • (IOError)


94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/hyperion/io_uring.rb', line 94

def accept(listener_fd)
  raise IOError, 'ring closed' if @closed

  rc = IOUring.ring_accept(@ptr, listener_fd.to_i, @errno_buf)
  return rc if rc.positive? || rc.zero?
  return :wouldblock if rc == -1

  errno = @errno_buf.to_str(4).unpack1('l<')
  # ECANCELED / EBADF / EINTR → caller treats as wouldblock and
  # loops. Anything else is a hard error.
  return :wouldblock if [4, 9, 103, 125].include?(errno) # EINTR / EBADF / ECONNABORTED / ECANCELED

  raise SystemCallError.new('io_uring accept failed', errno)
end

#closeObject

Close the ring + free its SQ/CQ memory. Idempotent — calling twice is a no-op (we null-out @ptr after the first free). Must be called from the same fiber that opened the ring.



129
130
131
132
133
134
135
# File 'lib/hyperion/io_uring.rb', line 129

def close
  return if @closed

  @closed = true
  IOUring.ring_free(@ptr) if @ptr && !@ptr.null?
  @ptr = nil
end

#closed?Boolean

Returns:

  • (Boolean)


137
138
139
# File 'lib/hyperion/io_uring.rb', line 137

def closed?
  @closed
end

#read(fd, max: 4096) ⇒ Object

Read up to ‘max` bytes from `fd` into a fresh ASCII-8BIT String. 2.3-A ships this for the accept-only path’s sibling use (per-connection short reads); the connection layer keeps using regular ‘read_nonblock` until a future 2.3-x round wires io_uring reads into the request-line + header parse.

Raises:

  • (IOError)


114
115
116
117
118
119
120
121
122
123
124
# File 'lib/hyperion/io_uring.rb', line 114

def read(fd, max: 4096)
  raise IOError, 'ring closed' if @closed

  buf = Fiddle::Pointer.malloc(max, Fiddle::RUBY_FREE)
  rc = IOUring.ring_read(@ptr, fd.to_i, buf, max.to_i, @errno_buf)
  return buf.to_str(rc) if rc >= 0
  return :wouldblock if rc == -1

  errno = @errno_buf.to_str(4).unpack1('l<')
  raise SystemCallError.new('io_uring read failed', errno)
end