Class: Hyperion::Server::RouteTable

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

Overview

2.10-D — direct-dispatch route registry. Mirrors agoo’s ‘Agoo::Server.handle(:GET, “/hello”, handler)` design: a lookup table indexed by HTTP method then exact-match path. When a match hits inside `Connection#serve` (after parse, before the Rack adapter), the dispatcher skips the env-hash build, the middleware chain, and the body-iteration overhead — the handler is called directly with a `Hyperion::Request` value object and returns either a `[status, headers, body]` Rack tuple or a sentinel that points at a pre-built static response buffer (the `handle_static` path, where the response bytes are baked at registration time so the hot path is one `socket.write` syscall and zero Ruby allocation past the Connection ivars).

The table is per-process: forked workers each inherit a copy of the parent’s table at fork time (no IPC, no shared memory) so registrations made before ‘Server.start` propagate to every worker via copy-on-write. Registrations made AFTER fork (e.g. from `on_worker_boot`) only affect the calling worker — by design, this is the operator’s escape hatch for per-worker routing. The hot-path lookup is a two-Hash-key access (O(1)); write paths are guarded by a Mutex so a registration racing with an in-flight request lookup is safe.

Mutability invariant: registrations replace any prior entry for the same ‘(method, path)` tuple — last writer wins. No delete API for now (operator restarts to clear), keeps the public surface minimal.

Defined Under Namespace

Classes: StaticEntry

Constant Summary collapse

KNOWN_METHODS =

The seven HTTP methods agoo’s ‘Server.handle` accepts. We match agoo’s surface verbatim so apps porting their registrations across servers don’t have to relearn the matrix. ‘OPTIONS` is included for CORS preflight handlers (commonly registered as direct routes since they have no business model).

%i[GET POST PUT DELETE HEAD PATCH OPTIONS].freeze

Instance Method Summary collapse

Constructor Details

#initializeRouteTable

Returns a new instance of RouteTable.



89
90
91
92
93
94
95
96
# File 'lib/hyperion/server/route_table.rb', line 89

def initialize
  # Per-method Hash so the lookup is `@routes[:GET][path]`
  # — two integer-keyed-Hash hits.  Pre-allocate the seven
  # slots so the request hot path never lazily creates an
  # entry under a misspelled method (we just miss).
  @routes = KNOWN_METHODS.each_with_object({}) { |m, h| h[m] = {} }
  @mutex  = Mutex.new
end

Instance Method Details

#clearObject

Clear all registrations. Test / spec seam — production code restarts the process to drop routes.



147
148
149
150
# File 'lib/hyperion/server/route_table.rb', line 147

def clear
  @mutex.synchronize { @routes.each_value(&:clear) }
  nil
end

#lookup(method_str, path) ⇒ Object

Hot-path lookup. ‘method_str` is the request method as the parser produced it (a String like `’GET’‘); `path` is the request path String. Returns the registered handler or nil.

No mutex on the read side — Ruby Hash reads under MRI are safe against a concurrent write that’s mutex-guarded (the GVL pins the writer during the bucket update), and the cost of a Mutex acquire on every request would defeat the whole point of the fast path.



130
131
132
133
134
135
136
# File 'lib/hyperion/server/route_table.rb', line 130

def lookup(method_str, path)
  method_key = METHOD_LOOKUP[method_str] || normalize_method(method_str)
  table = @routes[method_key]
  return nil unless table

  table[path]
end

#register(method_sym, path, handler) ⇒ Object

Register a direct-dispatch handler for the given method + path. ‘handler` must respond to `#call(request)` and return either:

* a `[status, headers, body]` Rack tuple — the dispatcher
  writes it via the standard ResponseWriter (no env hash,
  no middleware), or
* a `StaticEntry` (built only via `Server.handle_static`)
  — the dispatcher emits the pre-built bytes in one
  write syscall.

‘method_sym` is upper-cased before lookup so callers may pass `:get` or `’get’‘ interchangeably with `:GET`.

Raises:

  • (ArgumentError)


111
112
113
114
115
116
117
118
119
# File 'lib/hyperion/server/route_table.rb', line 111

def register(method_sym, path, handler)
  method_key = normalize_method(method_sym)
  raise ArgumentError, "unknown method #{method_sym.inspect}" unless KNOWN_METHODS.include?(method_key)
  raise ArgumentError, 'path must be a String' unless path.is_a?(String)
  raise ArgumentError, 'handler must respond to #call' unless handler.respond_to?(:call)

  @mutex.synchronize { @routes[method_key][path.dup.freeze] = handler }
  handler
end

#sizeObject

Inspection helper — returns the count of registered routes across all methods. Used by specs and the bench harness to assert registrations took effect.



141
142
143
# File 'lib/hyperion/server/route_table.rb', line 141

def size
  @routes.values.sum(&:size)
end