flare_mlake · API · testing

flare.testing.fork_server

Fork-and-serve helpers for cookbook examples and integration tests.

Most flare examples want to demo a real end-to-end flow: bind a port, accept connections, drive them with the real client. HttpServer.serve(handler) blocks the calling thread, so a single-process example can't both serve and connect to itself.

The standard Unix workaround is fork(2): the parent reads the bound port, forks, the child calls serve() (blocks forever), the parent uses the port for client work, then the parent SIGKILLs the child on cleanup. The mechanics aren't interesting -- five FFI thunks plus a brief startup sleep -- but they're 50 LoC of noise at the top of every multi-process example.

Usage:

from flare.testing import fork_server, kill_forked_server


def my_handler(req: Request) raises -> Response:
    return ok("hi")


def main() raises:
    var srv = HttpServer.bind(SocketAddr.localhost(0))
    var port = srv.local_addr().port  # parent reads before fork

    var pid = fork_server(srv^, my_handler)  # forks, parent waits 150 ms
    # ... parent uses ``port`` to drive HttpClient, etc. ...
    kill_forked_server(pid)                  # SIGKILL + waitpid

The bare-function and Handler-struct overloads both forward to HttpServer.serve, so anything that HttpServer.serve accepts is acceptable here. The startup sleep (startup_us=150_000 by default) covers the kernel-side listen/accept ramp on the slowest CI runners; tune it down for tighter integration tests.

Functions

fork_server

def fork_server(var srv: HttpServer, handler: def(Request) raises thin -> Response, startup_us: Int = Int(150000)) -> Int

Fork a child, run srv.serve(handler) in it, return the child PID to the parent after a brief startup wait.

The bare-function overload. For Handler-struct shapes use the parametric overload below.

Args
srv: A bound HttpServer (the parent reads srv.local_addr() before the call so it knows the port). Moved into the child via the standard fork(2) semantics; the parent's reference is consumed.
handler: The same def(Request) raises -> Response shape HttpServer.serve accepts.
startup_us: Microseconds the parent sleeps after the fork so the child has time to enter serve(). Default 150 ms covers the slowest CI runners.

Raises Error: If fork(2) fails. Failures inside the child's serve() are swallowed (the child _exits); the parent only ever sees the PID.

def fork_server[H: Handler](var srv: HttpServer, var handler: H, startup_us: Int = Int(150000)) -> Int

Fork-and-serve overload for Handler-struct shapes.

Identical semantics to the bare-function overload; this one exists so Router, middleware wrappers, and user-defined Handler structs can flow through the helper without an FnHandler shim. Copyable is not required -- the child process runs the single-worker HttpServer.serve path, which only needs Handler.

Args
srv: A bound HttpServer. Moved into the child.
handler: Any Handler struct.
startup_us: Parent-side startup sleep (microseconds).

Raises Error: If fork(2) fails.

kill_forked_server

def kill_forked_server(pid: Int)

SIGKILL a forked-server child and waitpid for it.

Args
pid: The PID returned by :func:fork_server.