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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4768 -0
- data/README.md +222 -13
- data/ext/hyperion_h2_codec/Cargo.lock +7 -0
- data/ext/hyperion_h2_codec/Cargo.toml +33 -0
- data/ext/hyperion_h2_codec/extconf.rb +73 -0
- data/ext/hyperion_h2_codec/src/frames.rs +140 -0
- data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
- data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
- data/ext/hyperion_h2_codec/src/lib.rs +296 -0
- data/ext/hyperion_http/extconf.rb +28 -0
- data/ext/hyperion_http/h2_codec_glue.c +408 -0
- data/ext/hyperion_http/page_cache.c +1125 -0
- data/ext/hyperion_http/parser.c +473 -38
- data/ext/hyperion_http/sendfile.c +982 -0
- data/ext/hyperion_http/websocket.c +493 -0
- data/ext/hyperion_io_uring/Cargo.lock +33 -0
- data/ext/hyperion_io_uring/Cargo.toml +34 -0
- data/ext/hyperion_io_uring/extconf.rb +74 -0
- data/ext/hyperion_io_uring/src/lib.rs +316 -0
- data/lib/hyperion/adapter/rack.rb +370 -42
- data/lib/hyperion/admin_listener.rb +207 -0
- data/lib/hyperion/admin_middleware.rb +36 -7
- data/lib/hyperion/cli.rb +310 -11
- data/lib/hyperion/config.rb +440 -14
- data/lib/hyperion/connection.rb +679 -22
- data/lib/hyperion/deprecations.rb +81 -0
- data/lib/hyperion/dispatch_mode.rb +165 -0
- data/lib/hyperion/fiber_local.rb +75 -13
- data/lib/hyperion/h2_admission.rb +77 -0
- data/lib/hyperion/h2_codec.rb +499 -0
- data/lib/hyperion/http/page_cache.rb +122 -0
- data/lib/hyperion/http/sendfile.rb +696 -0
- data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
- data/lib/hyperion/http2_handler.rb +618 -19
- data/lib/hyperion/io_uring.rb +317 -0
- data/lib/hyperion/lint_wrapper_pool.rb +126 -0
- data/lib/hyperion/master.rb +96 -9
- data/lib/hyperion/metrics/path_templater.rb +68 -0
- data/lib/hyperion/metrics.rb +256 -0
- data/lib/hyperion/prometheus_exporter.rb +150 -0
- data/lib/hyperion/request.rb +13 -0
- data/lib/hyperion/response_writer.rb +477 -16
- data/lib/hyperion/runtime.rb +195 -0
- data/lib/hyperion/server/route_table.rb +179 -0
- data/lib/hyperion/server.rb +519 -55
- data/lib/hyperion/static_preload.rb +133 -0
- data/lib/hyperion/thread_pool.rb +61 -7
- data/lib/hyperion/tls.rb +343 -1
- data/lib/hyperion/version.rb +1 -1
- data/lib/hyperion/websocket/close_codes.rb +71 -0
- data/lib/hyperion/websocket/connection.rb +876 -0
- data/lib/hyperion/websocket/frame.rb +356 -0
- data/lib/hyperion/websocket/handshake.rb +525 -0
- data/lib/hyperion/worker.rb +111 -9
- data/lib/hyperion.rb +137 -3
- metadata +50 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hyperion
|
|
4
|
+
# Runtime services container — holds per-process or per-server services
|
|
5
|
+
# (metrics sink, logger, clock) that used to live as module-level
|
|
6
|
+
# singletons on `Hyperion`.
|
|
7
|
+
#
|
|
8
|
+
# Pre-1.7 every Connection / Http2Handler / ResponseWriter reached for
|
|
9
|
+
# `Hyperion.metrics` / `Hyperion.logger` directly. That made:
|
|
10
|
+
#
|
|
11
|
+
# 1. Long-lived keep-alive connections impossible to swap services on
|
|
12
|
+
# mid-flight — `Connection#initialize` cached the singletons in ivars
|
|
13
|
+
# and never re-read them.
|
|
14
|
+
# 2. Multi-tenant apps unable to give each `Hyperion::Server` its own
|
|
15
|
+
# metrics sink — the module-level singleton is process-global.
|
|
16
|
+
# 3. Tests messy: stubbing `Hyperion.metrics` is global state mutation
|
|
17
|
+
# that bleeds across examples unless every spec resets explicitly.
|
|
18
|
+
#
|
|
19
|
+
# 1.7.0 introduces this Runtime and adds a `runtime:` kwarg to `Server`
|
|
20
|
+
# and `Connection`. The default is `Runtime.default`, a process-wide
|
|
21
|
+
# singleton — back-compat with the 1.6.x behaviour. Tests and library
|
|
22
|
+
# users construct their own `Runtime.new(metrics: …, logger: …)` and
|
|
23
|
+
# pass it explicitly; that runtime is then used exclusively by the
|
|
24
|
+
# connection/server it was given to.
|
|
25
|
+
#
|
|
26
|
+
# `Runtime.default` is intentionally NOT frozen after first read.
|
|
27
|
+
# RFC §5 Q4: tests need to swap metrics/logger on the default runtime,
|
|
28
|
+
# and freezing for no real safety benefit just adds ceremony.
|
|
29
|
+
#
|
|
30
|
+
# Module-level `Hyperion.metrics` / `Hyperion.logger` (and their
|
|
31
|
+
# writers) keep working — they delegate to `Runtime.default`. They're
|
|
32
|
+
# marked for deprecation in 1.8.0 and removal in 2.0.
|
|
33
|
+
class Runtime
|
|
34
|
+
attr_reader :clock
|
|
35
|
+
attr_writer :metrics, :logger
|
|
36
|
+
|
|
37
|
+
# The default Runtime's metrics / logger readers honour module-level
|
|
38
|
+
# ivar overrides on `Hyperion` itself. This preserves a back-compat
|
|
39
|
+
# seam for 1.6.x specs that swap by reaching into private internals
|
|
40
|
+
# via `Hyperion.instance_variable_set(:@metrics, …)` — the new
|
|
41
|
+
# Runtime-routed code paths (Server / Connection / Http2Handler) all
|
|
42
|
+
# read `runtime.metrics`, so without this the override would only
|
|
43
|
+
# affect the legacy `Hyperion.metrics` reader and the new code path
|
|
44
|
+
# would still write to the original Runtime-owned object.
|
|
45
|
+
#
|
|
46
|
+
# Custom Runtimes (`Hyperion::Runtime.new(...)`) ignore the override
|
|
47
|
+
# entirely — they're per-Server isolated by design.
|
|
48
|
+
def metrics
|
|
49
|
+
override = Hyperion.instance_variable_get(:@metrics) if default?
|
|
50
|
+
override || @metrics
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def logger
|
|
54
|
+
override = Hyperion.instance_variable_get(:@logger) if default?
|
|
55
|
+
override || @logger
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# True when this runtime is `Runtime.default`. The default runtime
|
|
59
|
+
# is the one consulted by legacy module-level accessors — see the
|
|
60
|
+
# `metrics` / `logger` readers above.
|
|
61
|
+
def default?
|
|
62
|
+
Runtime.instance_variable_get(:@default).equal?(self)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Process-wide default Runtime. Lazily initialized on first read.
|
|
66
|
+
# Module-level `Hyperion.metrics` / `Hyperion.logger` accessors and
|
|
67
|
+
# writers all delegate to this instance, so legacy callers in 1.6.x
|
|
68
|
+
# shape (`Hyperion.metrics = MyAdapter.new`) keep working without
|
|
69
|
+
# any source change.
|
|
70
|
+
#
|
|
71
|
+
# Tests can mutate `Runtime.default.metrics = …` directly or replace
|
|
72
|
+
# the whole default with `Runtime.default = Runtime.new(...)` (writer
|
|
73
|
+
# below). Resetting between examples is on the test author — there's
|
|
74
|
+
# no auto-reset because the singleton is part of the public surface.
|
|
75
|
+
def self.default
|
|
76
|
+
@default ||= new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Test seam: replace the process-wide default. Used in specs that
|
|
80
|
+
# need to inject a known-state Runtime without reaching into
|
|
81
|
+
# `@default` directly.
|
|
82
|
+
def self.default=(runtime)
|
|
83
|
+
raise ArgumentError, 'expected a Hyperion::Runtime' unless runtime.is_a?(Runtime)
|
|
84
|
+
|
|
85
|
+
@default = runtime
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Test seam: clear the memoized default so the next `default` call
|
|
89
|
+
# builds a fresh one. Equivalent to `default = Runtime.new` but
|
|
90
|
+
# without forcing the caller to allocate.
|
|
91
|
+
def self.reset_default!
|
|
92
|
+
@default = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def initialize(metrics: nil, logger: nil, clock: Process)
|
|
96
|
+
@metrics = metrics || Hyperion::Metrics.new
|
|
97
|
+
@logger = logger || Hyperion::Logger.new
|
|
98
|
+
@clock = clock
|
|
99
|
+
# 2.5-C: per-request lifecycle hooks. Pre-allocated empty Arrays so
|
|
100
|
+
# `has_request_hooks?` can be a single `any?` check on each side
|
|
101
|
+
# — no nil-guard, no lazy-init branch on the hot path. Hooks are
|
|
102
|
+
# appended in registration order; FIFO dispatch.
|
|
103
|
+
@before_request_hooks = []
|
|
104
|
+
@after_request_hooks = []
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 2.5-C — register a Proc to fire AFTER env is built but BEFORE
|
|
108
|
+
# `app.call`. Receives `(request, env)` where `request` is the
|
|
109
|
+
# parsed `Hyperion::Request` and `env` is the mutable Rack env Hash
|
|
110
|
+
# — callbacks may stash trace context (NewRelic transactions,
|
|
111
|
+
# OpenTelemetry spans, AppSignal/DataDog handles) into the env so
|
|
112
|
+
# the corresponding after-hook can finish them.
|
|
113
|
+
#
|
|
114
|
+
# Hook errors are caught and logged; they DO NOT abort dispatch.
|
|
115
|
+
# Multiple hooks fire in registration order (FIFO).
|
|
116
|
+
def on_request_start(&block)
|
|
117
|
+
raise ArgumentError, 'block required' unless block
|
|
118
|
+
|
|
119
|
+
@before_request_hooks << block
|
|
120
|
+
block
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# 2.5-C — register a Proc to fire AFTER `app.call` returns or
|
|
124
|
+
# raises. Receives `(request, env, response, error)`:
|
|
125
|
+
#
|
|
126
|
+
# * `response` is the `[status, headers, body]` tuple when the
|
|
127
|
+
# app returned normally, or `nil` when the app raised.
|
|
128
|
+
# * `error` is the `StandardError` the app raised, or `nil` on
|
|
129
|
+
# success.
|
|
130
|
+
#
|
|
131
|
+
# Use this to finish trace spans, attach response codes to the
|
|
132
|
+
# active transaction, increment per-route counters, etc. Hook
|
|
133
|
+
# errors are caught and logged — they never break dispatch.
|
|
134
|
+
def on_request_end(&block)
|
|
135
|
+
raise ArgumentError, 'block required' unless block
|
|
136
|
+
|
|
137
|
+
@after_request_hooks << block
|
|
138
|
+
block
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# 2.5-C — zero-cost guard used by Adapter::Rack#call. When both
|
|
142
|
+
# arrays are empty (the default — no hooks registered), the
|
|
143
|
+
# adapter skips the dispatch entirely: no Array iteration, no
|
|
144
|
+
# Proc invocation, no allocation. The audit harness
|
|
145
|
+
# (`yjit_alloc_audit_spec`) verifies the per-request alloc count
|
|
146
|
+
# is unchanged from 2.5-B.
|
|
147
|
+
def has_request_hooks?
|
|
148
|
+
!@before_request_hooks.empty? || !@after_request_hooks.empty?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# 2.5-C — invoked by Adapter::Rack#call after env is built. Wraps
|
|
152
|
+
# each hook in a rescue so a misbehaving observer can't break the
|
|
153
|
+
# dispatch chain — failures are logged with the block's source
|
|
154
|
+
# location so operators can identify which hook went wrong.
|
|
155
|
+
def fire_request_start(request, env)
|
|
156
|
+
@before_request_hooks.each do |hook|
|
|
157
|
+
hook.call(request, env)
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
log_hook_failure(:before_request, hook, e)
|
|
160
|
+
end
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# 2.5-C — invoked by Adapter::Rack#call after `app.call` returns
|
|
165
|
+
# (or raises). `response` is the [status, headers, body] tuple on
|
|
166
|
+
# success, `nil` on error; `error` is the raised exception or nil.
|
|
167
|
+
# Same rescue contract as `fire_request_start`: each hook runs
|
|
168
|
+
# independently; one failure does not prevent later hooks from
|
|
169
|
+
# firing or the response from being written.
|
|
170
|
+
def fire_request_end(request, env, response, error)
|
|
171
|
+
@after_request_hooks.each do |hook|
|
|
172
|
+
hook.call(request, env, response, error)
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
log_hook_failure(:after_request, hook, e)
|
|
175
|
+
end
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def log_hook_failure(phase, hook, error)
|
|
182
|
+
file, line = hook.source_location
|
|
183
|
+
logger.error do
|
|
184
|
+
{
|
|
185
|
+
message: 'request lifecycle hook raised',
|
|
186
|
+
phase: phase,
|
|
187
|
+
hook_source: file ? "#{file}:#{line}" : 'unknown',
|
|
188
|
+
error: error.message,
|
|
189
|
+
error_class: error.class.name,
|
|
190
|
+
backtrace: (error.backtrace || []).first(5).join(' | ')
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hyperion
|
|
4
|
+
class Server
|
|
5
|
+
# 2.10-D — direct-dispatch route registry. Mirrors agoo's
|
|
6
|
+
# `Agoo::Server.handle(:GET, "/hello", handler)` design: a lookup
|
|
7
|
+
# table indexed by HTTP method then exact-match path. When a
|
|
8
|
+
# match hits inside `Connection#serve` (after parse, before the
|
|
9
|
+
# Rack adapter), the dispatcher skips the env-hash build, the
|
|
10
|
+
# middleware chain, and the body-iteration overhead — the
|
|
11
|
+
# handler is called directly with a `Hyperion::Request` value
|
|
12
|
+
# object and returns either a `[status, headers, body]` Rack
|
|
13
|
+
# tuple or a sentinel that points at a pre-built static
|
|
14
|
+
# response buffer (the `handle_static` path, where the response
|
|
15
|
+
# bytes are baked at registration time so the hot path is one
|
|
16
|
+
# `socket.write` syscall and zero Ruby allocation past the
|
|
17
|
+
# Connection ivars).
|
|
18
|
+
#
|
|
19
|
+
# The table is per-process: forked workers each inherit a copy
|
|
20
|
+
# of the parent's table at fork time (no IPC, no shared memory)
|
|
21
|
+
# so registrations made before `Server.start` propagate to every
|
|
22
|
+
# worker via copy-on-write. Registrations made AFTER fork (e.g.
|
|
23
|
+
# from `on_worker_boot`) only affect the calling worker — by
|
|
24
|
+
# design, this is the operator's escape hatch for per-worker
|
|
25
|
+
# routing. The hot-path lookup is a two-Hash-key access (O(1));
|
|
26
|
+
# write paths are guarded by a Mutex so a registration racing
|
|
27
|
+
# with an in-flight request lookup is safe.
|
|
28
|
+
#
|
|
29
|
+
# Mutability invariant: registrations replace any prior entry
|
|
30
|
+
# for the same `(method, path)` tuple — last writer wins. No
|
|
31
|
+
# delete API for now (operator restarts to clear), keeps the
|
|
32
|
+
# public surface minimal.
|
|
33
|
+
class RouteTable
|
|
34
|
+
# The seven HTTP methods agoo's `Server.handle` accepts. We
|
|
35
|
+
# match agoo's surface verbatim so apps porting their
|
|
36
|
+
# registrations across servers don't have to relearn the
|
|
37
|
+
# matrix. `OPTIONS` is included for CORS preflight handlers
|
|
38
|
+
# (commonly registered as direct routes since they have no
|
|
39
|
+
# business model).
|
|
40
|
+
KNOWN_METHODS = %i[GET POST PUT DELETE HEAD PATCH OPTIONS].freeze
|
|
41
|
+
|
|
42
|
+
# 2.10-D — sentinel result returned by `Server.handle_static`'s
|
|
43
|
+
# internal handler. When `Connection#serve` sees it, the writer
|
|
44
|
+
# short-circuits to a single `socket.write(buf)` of the
|
|
45
|
+
# pre-built response buffer — no header build, no body
|
|
46
|
+
# iteration. Wrapping the buffer in a small struct (rather
|
|
47
|
+
# than returning the raw String from `handle`) keeps the
|
|
48
|
+
# `[status, headers, body]` shape contract visible while
|
|
49
|
+
# giving the dispatcher a single `is_a?` branch to engage the
|
|
50
|
+
# one-syscall fast path.
|
|
51
|
+
# 2.10-F adds `headers_len` so the C fast path
|
|
52
|
+
# (`PageCache.serve_request`) can write the headers-only prefix
|
|
53
|
+
# for HEAD requests without reparsing the buffer. Defaults to
|
|
54
|
+
# `buffer.bytesize` for back-compat with callers that
|
|
55
|
+
# constructed StaticEntry the 2.10-D way (3 args, no body
|
|
56
|
+
# split) — those entries fall back to writing the whole buffer
|
|
57
|
+
# on HEAD too, which is RFC-correct (HEAD MAY include the body
|
|
58
|
+
# so long as Content-Length matches; the spec only forbids the
|
|
59
|
+
# SERVER from sending body bytes the client didn't ask for).
|
|
60
|
+
StaticEntry = Struct.new(:method, :path, :buffer, :headers_len) do
|
|
61
|
+
# Returns the pre-built response bytes ready for one
|
|
62
|
+
# `socket.write` call. Always frozen.
|
|
63
|
+
def response_bytes
|
|
64
|
+
buffer
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 2.10-F — StaticEntry responds to `#call` so it can be
|
|
68
|
+
# registered directly in the route table (instead of via a
|
|
69
|
+
# closure wrapping it). Returning `self` keeps the
|
|
70
|
+
# `[status, headers, body]` contract: `dispatch_direct!`'s
|
|
71
|
+
# is_a?(StaticEntry) branch handles the wire write. Pre-
|
|
72
|
+
# 2.10-F callers that registered via
|
|
73
|
+
# `Server.handle_static` still work — that registration
|
|
74
|
+
# path now stores the entry directly and the route table's
|
|
75
|
+
# `respond_to?(:call)` invariant is preserved.
|
|
76
|
+
def call(_request)
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 2.10-F — bytes-count of the headers-only prefix. Used by
|
|
81
|
+
# callers that reach the StaticEntry directly (specs, custom
|
|
82
|
+
# writers); the C fast path reads the C-side `headers_len`
|
|
83
|
+
# mirror that `PageCache.register_prebuilt` records.
|
|
84
|
+
def headers_bytesize
|
|
85
|
+
headers_len || buffer.bytesize
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def initialize
|
|
90
|
+
# Per-method Hash so the lookup is `@routes[:GET][path]`
|
|
91
|
+
# — two integer-keyed-Hash hits. Pre-allocate the seven
|
|
92
|
+
# slots so the request hot path never lazily creates an
|
|
93
|
+
# entry under a misspelled method (we just miss).
|
|
94
|
+
@routes = KNOWN_METHODS.each_with_object({}) { |m, h| h[m] = {} }
|
|
95
|
+
@mutex = Mutex.new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Register a direct-dispatch handler for the given method +
|
|
99
|
+
# path. `handler` must respond to `#call(request)` and return
|
|
100
|
+
# either:
|
|
101
|
+
#
|
|
102
|
+
# * a `[status, headers, body]` Rack tuple — the dispatcher
|
|
103
|
+
# writes it via the standard ResponseWriter (no env hash,
|
|
104
|
+
# no middleware), or
|
|
105
|
+
# * a `StaticEntry` (built only via `Server.handle_static`)
|
|
106
|
+
# — the dispatcher emits the pre-built bytes in one
|
|
107
|
+
# write syscall.
|
|
108
|
+
#
|
|
109
|
+
# `method_sym` is upper-cased before lookup so callers may pass
|
|
110
|
+
# `:get` or `'get'` interchangeably with `:GET`.
|
|
111
|
+
def register(method_sym, path, handler)
|
|
112
|
+
method_key = normalize_method(method_sym)
|
|
113
|
+
raise ArgumentError, "unknown method #{method_sym.inspect}" unless KNOWN_METHODS.include?(method_key)
|
|
114
|
+
raise ArgumentError, 'path must be a String' unless path.is_a?(String)
|
|
115
|
+
raise ArgumentError, 'handler must respond to #call' unless handler.respond_to?(:call)
|
|
116
|
+
|
|
117
|
+
@mutex.synchronize { @routes[method_key][path.dup.freeze] = handler }
|
|
118
|
+
handler
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Hot-path lookup. `method_str` is the request method as the
|
|
122
|
+
# parser produced it (a String like `'GET'`); `path` is the
|
|
123
|
+
# request path String. Returns the registered handler or nil.
|
|
124
|
+
#
|
|
125
|
+
# No mutex on the read side — Ruby Hash reads under MRI are
|
|
126
|
+
# safe against a concurrent write that's mutex-guarded
|
|
127
|
+
# (the GVL pins the writer during the bucket update), and
|
|
128
|
+
# the cost of a Mutex acquire on every request would defeat
|
|
129
|
+
# the whole point of the fast path.
|
|
130
|
+
def lookup(method_str, path)
|
|
131
|
+
method_key = METHOD_LOOKUP[method_str] || normalize_method(method_str)
|
|
132
|
+
table = @routes[method_key]
|
|
133
|
+
return nil unless table
|
|
134
|
+
|
|
135
|
+
table[path]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Inspection helper — returns the count of registered routes
|
|
139
|
+
# across all methods. Used by specs and the bench harness
|
|
140
|
+
# to assert registrations took effect.
|
|
141
|
+
def size
|
|
142
|
+
@routes.values.sum(&:size)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Clear all registrations. Test / spec seam — production
|
|
146
|
+
# code restarts the process to drop routes.
|
|
147
|
+
def clear
|
|
148
|
+
@mutex.synchronize { @routes.each_value(&:clear) }
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# Pre-built lookup for the seven canonical method strings the
|
|
155
|
+
# parser emits. Skips the Symbol allocation + upcase that
|
|
156
|
+
# `normalize_method` does for unrecognised inputs. Frozen so
|
|
157
|
+
# the table itself is shared across all RouteTable instances
|
|
158
|
+
# (one allocation, process-wide).
|
|
159
|
+
METHOD_LOOKUP = {
|
|
160
|
+
'GET' => :GET,
|
|
161
|
+
'POST' => :POST,
|
|
162
|
+
'PUT' => :PUT,
|
|
163
|
+
'DELETE' => :DELETE,
|
|
164
|
+
'HEAD' => :HEAD,
|
|
165
|
+
'PATCH' => :PATCH,
|
|
166
|
+
'OPTIONS' => :OPTIONS
|
|
167
|
+
}.freeze
|
|
168
|
+
|
|
169
|
+
def normalize_method(value)
|
|
170
|
+
case value
|
|
171
|
+
when Symbol
|
|
172
|
+
KNOWN_METHODS.include?(value) ? value : value.to_s.upcase.to_sym
|
|
173
|
+
else
|
|
174
|
+
value.to_s.upcase.to_sym
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|