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) ⇒ Server

Returns a new instance of Server.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 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)
  @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
  @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.



83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/hyperion/server.rb', line 83

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



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/hyperion/server.rb', line 59

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



96
97
98
99
100
101
102
103
104
# File 'lib/hyperion/server.rb', line 96

def run_one
  Async do
    socket = blocking_accept
    next unless socket

    apply_timeout(socket)
    dispatch(socket)
  end.wait
end

#startObject



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/hyperion/server.rb', line 106

def start
  listen unless @server
  @thread_pool = ThreadPool.new(size: @thread_count, max_pending: @max_pending) if @thread_count.positive?

  if @tls
    # 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.
    start_async_loop
  else
    # Plain HTTP/1.1: 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



126
127
128
129
130
131
# File 'lib/hyperion/server.rb', line 126

def stop
  @stopped = true
  @server&.close
  @server = nil
  @tcp_server = nil
end