Class: Stipa::Server
- Inherits:
-
Object
- Object
- Stipa::Server
- Defined in:
- lib/stipa/server.rb
Overview
TCP accept loop and connection lifecycle manager.
Architecture:
Main thread (accept loop) Worker threads (pool)
───────────────────────── ──────────────────────────────────
TCPServer.accept_nonblock Connection.new(socket, ...).run
└─> pool.submit(job) ─────> └─> Request.parse
(drop → 503) └─> app.call(req, res)
(accept continues) └─> write_response
Socket options:
SO_REUSEADDR — always set; allows rebinding immediately after SIGTERM
without waiting for the TIME_WAIT timeout (~60 s).
SO_REUSEPORT — set on Linux ≥ 3.9 when available; multiple processes
can bind the same port simultaneously, enabling zero-
downtime rolling restarts via a process supervisor.
TCP_NODELAY — disables Nagle's algorithm; reduces latency for small
responses (JSON APIs) by sending immediately rather than
waiting to coalesce small writes.
listen(1024) — kernel-level SYN backlog; OSes cap at net.core.somaxconn.
Backpressure (when all workers are busy and the queue is full):
We write a 503 directly on the accept thread without involving a worker.
This keeps the accept loop free to continue processing new connections
and avoids wasting a worker thread on a connection we'll immediately reject.
Graceful shutdown (SIGTERM / SIGINT):
1. @running = false → accept loop exits after the current poll returns
2. pool.shutdown(drain_timeout:) → waits for in-flight requests to finish
3. server socket is closed in the ensure block of start
Constant Summary collapse
- DEFAULT_CONFIG =
{ host: '127.0.0.1', port: 3710, pool_size: 32, queue_depth: 64, drain_timeout: 30, header_read_timeout: 10, body_read_timeout: 30, write_timeout: 10, keepalive_timeout: 5, max_requests: 100, max_header_size: 8 * 1024, max_body_size: 1 * 1024 * 1024, backpressure: :drop, # :drop (503) or :block (wait briefly) log_level: :info, reload: ENV['STIPA_RELOAD'] == '1', }.freeze
Instance Method Summary collapse
-
#initialize(app:, **overrides) ⇒ Server
constructor
A new instance of Server.
-
#start ⇒ Object
Start the server.
Constructor Details
#initialize(app:, **overrides) ⇒ Server
Returns a new instance of Server.
59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/stipa/server.rb', line 59 def initialize(app:, **overrides) @app = app # compiled middleware+router callable @config = DEFAULT_CONFIG.merge(overrides) @logger = Logger.new(level: @config[:log_level]) @pool = ThreadPool.new( size: @config[:pool_size], queue_depth: @config[:queue_depth], on_error: method(:pool_error), ) @running = false @reloader = @config[:reload] ? Reloader.new(logger: @logger) : nil end |
Instance Method Details
#start ⇒ Object
Start the server. Blocks until SIGTERM/SIGINT.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/stipa/server.rb', line 73 def start @server = build_server_socket @running = true register_signals @reloader&.start @logger.info( req: nil, res: nil, msg: 'Stīpa listening', host: @config[:host], port: @config[:port], workers: @config[:pool_size], queue: @config[:queue_depth], ) accept_loop ensure @reloader&.stop @server&.close rescue nil @logger.info(req: nil, res: nil, msg: 'Stīpa stopped') end |