Class: Hyperion::Server
- Inherits:
-
Object
- Object
- Hyperion::Server
- Defined in:
- lib/hyperion/server.rb
Overview
Phase 2a server: bind a TCPServer, accept connections, schedule each on its own fiber via Async. Multiple in-flight requests run concurrently on a single OS thread. Keep-alive is still off — connection closes after one request (Phase 2b will add keep-alive).
Phase 7 (scoped): when ‘tls:` is supplied, wrap the listener in an OpenSSL::SSL::SSLServer with ALPN advertising `h2` + `http/1.1`. After the handshake, dispatch on the negotiated protocol — http/1.1 goes through Connection (real path); h2 goes to Http2Handler (505 stub until Phase 8).
Constant Summary collapse
- DEFAULT_READ_TIMEOUT_SECONDS =
30- DEFAULT_THREAD_COUNT =
5- REJECT_503 =
Pre-built minimal 503 response for the backpressure path. We bypass ResponseWriter / Rack entirely — no env build, no app dispatch, no access-log line. The bytes are frozen and reused across every rejection so the overload path stays allocation-free. Body is JSON so JSON-only API consumers don’t have to special-case the format.
lambda { body = +%({"error":"server_busy","retry_after_seconds":1}\n) body.force_encoding(Encoding::ASCII_8BIT) head = +"HTTP/1.1 503 Service Unavailable\r\n" \ "content-type: application/json\r\n" \ "content-length: #{body.bytesize}\r\n" \ "retry-after: 1\r\n" \ "connection: close\r\n" \ "\r\n" head.force_encoding(Encoding::ASCII_8BIT) (head + body).freeze }.call
Instance Attribute Summary collapse
-
#host ⇒ Object
readonly
Returns the value of attribute host.
-
#port ⇒ Object
readonly
Returns the value of attribute port.
Instance Method Summary collapse
-
#adopt_listener(sock) ⇒ Object
Phase 3: workers pass in a pre-bound, SO_REUSEPORT-set socket built by Hyperion::Worker.
-
#initialize(app:, host: '127.0.0.1', port: 9292, read_timeout: DEFAULT_READ_TIMEOUT_SECONDS, tls: nil, thread_count: DEFAULT_THREAD_COUNT, max_pending: nil, max_request_read_seconds: 60, h2_settings: nil, async_io: nil) ⇒ Server
constructor
A new instance of Server.
- #listen ⇒ Object
- #run_one ⇒ Object
- #start ⇒ Object
- #stop ⇒ Object
Constructor Details
#initialize(app:, host: '127.0.0.1', port: 9292, read_timeout: DEFAULT_READ_TIMEOUT_SECONDS, tls: nil, thread_count: DEFAULT_THREAD_COUNT, max_pending: nil, max_request_read_seconds: 60, h2_settings: nil, async_io: nil) ⇒ Server
Returns a new instance of Server.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/hyperion/server.rb', line 43 def initialize(app:, host: '127.0.0.1', port: 9292, read_timeout: DEFAULT_READ_TIMEOUT_SECONDS, tls: nil, thread_count: DEFAULT_THREAD_COUNT, max_pending: nil, max_request_read_seconds: 60, h2_settings: nil, async_io: nil) @host = host @port = port @app = app @read_timeout = read_timeout @tls = tls @thread_count = thread_count @max_pending = max_pending @max_request_read_seconds = max_request_read_seconds @h2_settings = h2_settings @async_io = async_io @thread_pool = nil @stopped = false end |
Instance Attribute Details
#host ⇒ Object (readonly)
Returns the value of attribute host.
41 42 43 |
# File 'lib/hyperion/server.rb', line 41 def host @host end |
#port ⇒ Object (readonly)
Returns the value of attribute port.
41 42 43 |
# File 'lib/hyperion/server.rb', line 41 def port @port end |
Instance Method Details
#adopt_listener(sock) ⇒ Object
Phase 3: workers pass in a pre-bound, SO_REUSEPORT-set socket built by Hyperion::Worker. Bypasses #listen but keeps the rest of the accept loop intact since Socket and TCPServer both quack #accept_nonblock.
Phase 8: when ‘tls:` was supplied to the constructor, also build the SSL context here so the accept loop can wrap incoming connections. Each worker builds its own context — they don’t share state.
84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/hyperion/server.rb', line 84 def adopt_listener(sock) @server = sock @tcp_server = sock @port = case sock when ::TCPServer sock.addr[1] else sock.local_address.ip_port end @ssl_ctx = TLS.context(cert: @tls[:cert], key: @tls[:key], chain: @tls[:chain]) if @tls self end |
#listen ⇒ Object
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/hyperion/server.rb', line 60 def listen tcp = ::TCPServer.new(@host, @port) @port = tcp.addr[1] if @tls @ssl_ctx = TLS.context(cert: @tls[:cert], key: @tls[:key], chain: @tls[:chain]) ssl_server = ::OpenSSL::SSL::SSLServer.new(tcp, @ssl_ctx) ssl_server.start_immediately = false @server = ssl_server @tcp_server = tcp else @server = tcp @tcp_server = tcp end self end |
#run_one ⇒ Object
97 98 99 100 101 102 103 104 105 |
# File 'lib/hyperion/server.rb', line 97 def run_one Async do socket = blocking_accept next unless socket apply_timeout(socket) dispatch(socket) end.wait end |
#start ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/hyperion/server.rb', line 107 def start listen unless @server @thread_pool = ThreadPool.new(size: @thread_count, max_pending: @max_pending) if @thread_count.positive? if @tls || @async_io # TLS path: ALPN may pick `h2`, and h2 spawns one fiber per stream # inside Http2Handler. Keep the Async wrapper so the scheduler is # available for those fibers and for handshake yields. Plain # HTTP/1.1-over-TLS dispatch is also handled inline on the calling # fiber by default in 1.4.0+ (see #dispatch) — fiber-cooperative # libraries (async-pg, async-redis) work without --async-io. # # async_io: true: operator opt-in for plain HTTP/1.1. The Async wrap # is required when callers want fiber cooperative I/O — e.g. # `hyperion-async-pg` yielding while a Postgres query is in flight. # Pays ~5% throughput vs the raw-loop fast path; in exchange one # OS thread can serve N concurrent in-flight DB queries instead of 1. start_async_loop else # Plain HTTP/1.1, async_io: nil (default with no TLS) or # async_io: false (explicit opt-out): the worker thread owns each # connection for its lifetime, so the Async wrapper adds zero value # (no fibers ever run on this loop's task). Skip it — pure # IO.select + accept_nonblock shaves measurable overhead off the # accept hot path. start_raw_loop end ensure @thread_pool&.shutdown end |
#stop ⇒ Object
138 139 140 141 142 143 |
# File 'lib/hyperion/server.rb', line 138 def stop @stopped = true @server&.close @server = nil @tcp_server = nil end |