Repository navigation
lori_http_server — Phased Implementation Plan #2
Replies: 2 comments 2 replies
|
Phase 4 note:
Phase 4 should add a mechanism for the application to be notified of listen failures (callback, promise, or similar). |
|
Note: Both |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Context
We're building an HTTP/1.1 server library on top of lori (v0.8.1). Lori provides raw TCP I/O with a connection-actor model: data arrives as
Array[U8] isochunks via_on_received(), and we send viaTCPConnection.send(data): (SendToken | SendError). Lori also provides backpressure notifications and SSL support.The existing
ponylang/http_serveris built on the stdlibnetpackage and has actor-interaction issues we want to avoid. We may borrow internal logic (e.g., parsing techniques) but will design the architecture fresh around lori's model.This plan covers the phases of building the server. API details are deferred to each phase's design work — this is about what components we need, in what order, and how we verify each phase.
Cross-cutting: Trait-based state machines
State machines in this project should follow the pattern used in
ponylang/postgres: states are classes implementing trait hierarchies. The trait hierarchy provides categorized default behaviors — traits for state categories (e.g., "not yet connected", "authenticated") supply default implementations that trap on invalid operations, so new states get correct behavior for their category without manually implementing every method. Each state class owns its per-state data (buffers, accumulators), which is automatically cleaned up when the state transitions out. This applies "make illegal states unrepresentable" to per-state data — a state that doesn't need a buffer doesn't have one — while invalid operations are enforced at runtime through the trait defaults.The postgres pattern uses a wide interface (~40 methods) because the Postgres protocol has many message types. Our state machines will be narrower — the HTTP parser and connection lifecycle have fewer distinct operations — so the interface width should be adapted to each use case rather than copying postgres's scale.
This pattern applies in two places:
Phase 1: HTTP Data Types and Response SerializationWhat we build: The foundational value types that the rest of the library operates on. Plain classes and primitives — no actors, no lori dependency.
Components:
GET,POST, etc., plus a parse function)HTTP10,HTTP11)Array[U8] valin HTTP wire format)Testing:
addvssetsemantics, orderingmake testWhy first: Everything downstream depends on these types. Testing them in isolation means the rest of the library operates on trusted data.
Phase 2: HTTP Request ParserWhat we build: A plain
classimplementing an HTTP/1.1 request parser as a state machine. Data is fed in as chunks (matching what lori delivers), and parsed requests are delivered via a callback interface.Components:
reftrait withfun refmethods (nottag/be). This is a key difference from the existing http_server: since the parser runs inside the connection actor, callbacks can be synchronousrefcalls, avoiding unnecessary actor messagingTesting:
make testWhy second: Parsing depends on Phase 1 types. Parsing must exist before we can do anything useful with TCP data. Testing independently of networking gives us confidence without flaky network tests.
Phase 3: Lori Integration — Connection Actor and Minimal ServerWhat we build: The actor layer connecting lori's TCP primitives to the parser and delivering parsed requests to application code. This produces a minimal functional HTTP server.
Components:
TCPConnectionActor & ServerLifecycleEventReceiver. Owns aTCPConnection, a parser, and a handler. In_on_received, feeds data to the parser. Parser callbacks forward to the handler. Response sending usesTCPConnection.send(). The connection lifecycle uses the trait-based state pattern (see cross-cutting section) to track where the connection is in the request/response cycle — states like "awaiting request", "handler processing", "sending response" enforce what operations are valid at each pointTCPListenerActor. On_on_accept, creates a new connection actor. Holds a handler factory and passes it to each connectionref(run synchronously inside the connection actor). The handler receives parsed requests and has access to a response-sending capabilityvalinterface for creating per-connection handlersKey architectural decision — single-actor connection model:
Unlike the existing http_server (which uses two actors per connection with message-passing between them), our connection actor owns everything: TCP I/O, parsing, handler dispatch, and response sending. No unnecessary actor boundaries. If an application needs async processing, the handler can delegate to its own actors and respond later, but the simple synchronous path has zero extra actor hops.
Testing:
127.0.0.2on Linux /localhoston macOS (WSL2 workaround)examples/basic/main.ponyto be a working "Hello, World!" HTTP servermake testWhy third: Needs both data types and parser. This phase produces the minimum viable HTTP server — accepts connections, parses requests, dispatches to handler, sends responses.
Phase 4: Connection ManagementWhat we build: Correct HTTP/1.1 connection semantics: persistent connections, backpressure propagation, error responses, and timeouts.
Components:
Connection: close, HTTP/1.0 withoutConnection: keep-alive, or server decision_on_throttled()/_on_unthrottled()through to the handler so it knows when to stop generating response data. Usemute()/unmute()for read-side pressure when handler processing falls behindvalbyte arraysvalconfig type with host, port, timeouts, max concurrent connections (maps to lori'sTCPListenerlimit), parser size limitsTesting:
Connection: close, verify connection closes after responseConnectionheaders and HTTP versions, verify open/close behavior matches protocol specmake testWhy fourth: The Phase 3 server works but handles only single requests per connection and ignores backpressure. This phase makes it correct for real HTTP/1.1 usage.
Phase 5: Pipelining and Streaming ResponsesWhat we build: Full HTTP pipelining support and chunked/streaming response capability.
Components:
Testing:
make testWhy last: Pipelining adds significant complexity (response queue, request ID tracking) and most real-world clients rarely pipeline. A server with keep-alive (Phase 4) is useful without pipelining. Deferring this also lets us refine the handler API based on experience from earlier phases.
What's NOT in this plan
ssl_server()instead ofserver(). Adding HTTPS is a configuration concern, not architectural. Follow-up work.All reactions