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: false) ⇒ 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: false) ⇒ 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: false) @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 |
# 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. # # 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: false (default): 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
134 135 136 137 138 139 |
# File 'lib/hyperion/server.rb', line 134 def stop @stopped = true @server&.close @server = nil @tcp_server = nil end |