Class: Stipa::Server

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

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

#startObject

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