Module: Hyperion::Server::ConnectionLoop

Defined in:
lib/hyperion/server/connection_loop.rb

Overview

2.12-C — Connection lifecycle in C.

Engaged by ‘Server#start_raw_loop` when ALL of the following hold:

* The listener is plain TCP (no TLS, no h2 ALPN dance).
* The route table has at least one `RouteTable::StaticEntry`
  registration (i.e. `Server.handle_static` was called).
* The route table has NO non-StaticEntry registrations
  (any `Server.handle(:GET, '/api', dynamic_handler)` disables
  the C path; the C loop only knows how to write prebuilt
  responses).
* The `HYPERION_C_ACCEPT_LOOP` env knob is not set to `"0"` /
  `"off"` (operator escape hatch for debug).

On engage, the Ruby accept loop is not run for this listener; ‘Hyperion::Http::PageCache.run_static_accept_loop` drives the accept-and-serve loop entirely in C and only re-enters Ruby for:

1. Per-request lifecycle hooks
   (`Runtime#fire_request_start` / `fire_request_end`), gated
   by a single C-side integer flag so the no-hook hot path
   stays one syscall.
2. Connection handoff: requests that don't match a `StaticEntry`
   (or are malformed, h2/upgrade, or carry a body) are passed
   back as `(fd, partial_buffer)` — Ruby resumes ownership of
   the fd and dispatches via the regular `Connection` path.

The wiring lives in this module so the conditional logic stays out of the Server hot-path entry methods.

Class Method Summary collapse

Class Method Details

.available?Boolean

Whether the C accept loop is available and the env didn’t disable it.

Returns:

  • (Boolean)


39
40
41
42
43
44
45
# File 'lib/hyperion/server/connection_loop.rb', line 39

def available?
  return false unless defined?(::Hyperion::Http::PageCache)
  return false unless ::Hyperion::Http::PageCache.respond_to?(:run_static_accept_loop)

  env = ENV['HYPERION_C_ACCEPT_LOOP']
  env.nil? || !%w[0 off false no].include?(env.downcase)
end

.build_handoff_callback(server) ⇒ Object

Build the handoff callback the C loop invokes when a connection’s first request can’t be served from the static cache. Receives ‘(fd, partial_buffer_or_nil)` — Ruby owns the fd from that point on. We wrap the fd in a `Socket` (so `apply_timeout` and the rest of the Connection path see the same surface they always see) and dispatch through the server’s existing ‘dispatch_handed_off` helper.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/hyperion/server/connection_loop.rb', line 140

def build_handoff_callback(server)
  lambda do |fd, partial|
    server.send(:dispatch_handed_off, fd, partial)
  rescue StandardError => e
    server.send(:runtime_logger).warn do
      { message: 'C loop handoff dispatch failed',
        error: e.message, error_class: e.class.name }
    end
    # Always close the fd if dispatch raised — Ruby owns it.
    begin
      require 'socket'
      ::Socket.for_fd(fd).close
    rescue StandardError
      nil
    end
  end
end

.build_lifecycle_callback(runtime) ⇒ Object

Build a lifecycle callback that, when invoked from the C loop with ‘(method_str, path_str)`, fires the runtime’s ‘fire_request_start` / `fire_request_end` hooks against a minimal `Hyperion::Request` value object. `env=nil` and the response slot carries the `:c_static` symbol (just a marker —the wire write already happened in C and we have no `[status, headers, body]` tuple to hand back).

The proc captures ‘runtime` so multi-tenant deployments with per-Server runtimes route hooks to the right observer registry. Allocation cost: one Request per request when hooks are active. The C loop only invokes this callback when `lifecycle_active?` is true; the no-hook path pays nothing.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/hyperion/server/connection_loop.rb', line 110

def build_lifecycle_callback(runtime)
  lambda do |method_str, path_str|
    request = ::Hyperion::Request.new(
      method: method_str,
      path: path_str,
      query_string: nil,
      http_version: 'HTTP/1.1',
      headers: {},
      body: nil
    )
    if runtime.has_request_hooks?
      runtime.fire_request_start(request, nil)
      runtime.fire_request_end(request, nil, :c_static, nil)
    end
    nil
  rescue StandardError
    # Hook errors are already swallowed inside `Runtime#fire_*`;
    # this rescue catches Request allocation oddities so a
    # misbehaving observer can't take down the C loop.
    nil
  end
end

.eligible_route_table?(route_table) ⇒ Boolean

Whether the route table is C-loop eligible: only ‘StaticEntry` handlers, at least one of them, no dynamic handlers anywhere.

Returns:

  • (Boolean)


83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/hyperion/server/connection_loop.rb', line 83

def eligible_route_table?(route_table)
  return false unless route_table

  any_static = false
  route_table.instance_variable_get(:@routes).each_value do |path_table|
    path_table.each_value do |handler|
      return false unless handler.is_a?(::Hyperion::Server::RouteTable::StaticEntry)

      any_static = true
    end
  end
  any_static
end

.io_uring_eligible?Boolean

2.12-D — whether to engage the io_uring accept loop variant over the 2.12-C ‘accept4` loop. All four conditions must hold:

1. Operator opted in via `HYPERION_IO_URING_ACCEPT=1`. This
   is OFF by default for 2.12.0 — flipping the default to ON
   is a 2.13 decision after production-soak.
2. The C ext was compiled with `HAVE_LIBURING` (probed at
   gem-install time via `extconf.rb` — needs `liburing-dev`
   headers). Builds without it ship the stub method that
   returns `:unavailable` regardless of the env var.
3. `Hyperion::Http::PageCache.run_static_io_uring_loop` is
   defined (paranoia: the symbol always exists on builds
   that loaded the C ext, but the check keeps us from
   NameError'ing on partial installs).
4. A liburing runtime probe — opening a tiny ring with
   `io_uring_queue_init`. The probe lives inside the C
   method itself (`run_static_io_uring_loop` returns
   `:unavailable` if `io_uring_queue_init` fails); we
   don't pre-probe here because that would require holding
   a ring open across the eligibility check, and the
   penalty for "engaged but probe-fail at run time" is
   one cheap fall-through to the `accept4` path.

Returns:

  • (Boolean)


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

def io_uring_eligible?
  return false unless available?
  return false unless ::Hyperion::Http::PageCache.respond_to?(:run_static_io_uring_loop)
  return false unless ::Hyperion::Http::PageCache.respond_to?(:io_uring_loop_compiled?) &&
                      ::Hyperion::Http::PageCache.io_uring_loop_compiled?

  env = ENV['HYPERION_IO_URING_ACCEPT']
  return false unless env

  %w[1 on true yes].include?(env.downcase)
end