Class: Hyperion::Server

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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

#hostObject (readonly)

Returns the value of attribute host.



41
42
43
# File 'lib/hyperion/server.rb', line 41

def host
  @host
end

#portObject (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

#listenObject



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_oneObject



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

#startObject



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

#stopObject



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