Skip to content

feat(#184): laurel.serve, a standalone host with built-in providers - #188

Merged
octalide merged 5 commits into
devfrom
feat/184-serve
Sep 27, 2026
Merged

octalide merged 5 commits into
devfrom
feat/184-serve

Conversation

@octalide

@octalide octalide commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #184

The serve half of #184, on top of the provider interfaces from #185. Part of #183.

Plan

  • laurel.serve: serve.Host is created first and owns the built-in providers, which serve.providers(host) hands the application as one provider.Provider entry. serve.run(host, app, config) serves an assembled application on http.server.server until a drain finishes, then serve.destroy(host).
  • Request bridge. Each exchange's scratch holds laurel's request state and the request arena. serve routes, binds (app.bind_context), executes, and resumes a suspended step when the server re-enters it. A step's handler deadline is handed to the server as Call.wake_at, and a step past it is timed out through the exchange scope. abandon runs the owed middleware exit halves, and the server's settle releases the context (app.release_context) once the response is written, so the terminal event carries the final transfer and a drain waits for responses still on the wire. A request the application cannot admit is answered 503.
  • Lifecycle through the server's hooks: start is app.start, ready is app.poll_ready then Config.ready, drain(deadline) is app.drain plus task.drain on the application's task provider with the same deadline, and stop is app.stop then task.stop. A start the server abandons never reaches its stop hook, so run stops the application and tasks itself within stop_timeout_ns.
  • Signals. With Config.signals, run owns the process's std.process.events source for its duration, and SIGTERM or SIGINT (console close or service stop on windows) calls hs.shutdown, the bounded drain to drain_timeout_ns.
  • Built-in providers, each replaceable by listing the application's own provider for the key first:
    • task runner: laurel.testing.tasks becomes laurel.task.runner, one runner for both hosts. It is now thread-safe: every call and poll take its lock, and a step runs with it released. laurel.testing steps it by hand, and laurel.serve drives it from its own thread, woken by on_wake and next_due.
    • configuration and secrets from the environment under configurable prefixes. A secret is copied into welded stack scratch for the borrow and wiped after it. The resolver sits in a bounded module-private table, since it cannot erase.
    • telemetry: one line per signal on stderr, written with one call.
  • Example: demo/standalone is one application, hosted by laurel.serve in bin/main.mach and tested in process under laurel.testing with the same assemble. CI builds it and runs its test on the primary leg.
  • Docs: doc/serve.md (the contract and the current limits), doc/providers.md (runner and built-ins), and the README's two hosting modes.
  • Re-pin: http ^0.23 at v0.23.0 via mach dep update.

Needed from mach-http, released as 0.22.0 and 0.23.0

Current limits, stated in doc/serve.md and the README

Not in this PR

The hedge-hosted demo, and the bench server hosted the same way, stay here until graft takes them (briar-systems/graft#1). hedge 0.12.0 pins http =0.21.0, so both override it by ref = "tag/v0.23.0". 0.22 and 0.23 only add the runner and fix the h1 engine compatibly, and the demo builds that way.

Local runs

mach 6.3.0: mach test . for the library, mach test demo/standalone, and mach build demo. I ran the example binary with curl (keep-alive, framing, config, task snapshot, echo) and sent it SIGTERM. The SIGTERM test also ran against a counterfactual with the signal thread's shutdown removed, and it fails.

laurel.testing.tasks becomes laurel.task.runner, the runner both hosts use.
Every provider call and poll take the runner's lock and a step runs with it
released, so a step can publish or trigger through the provider and handlers
can read snapshots from any thread. on_wake tells a driver of new work, and
next_due and busy say when to poll again. laurel.testing still drives it by
hand with a fixed clock.
laurel.serve needs http 0.23's server runner, its settle call and wake_at,
and its framing of unframed responses. The hedge-hosted demo and bench server
override hedge 0.12.0's =0.21.0 pin by ref until they move to graft, since
0.22 and 0.23 only add the runner and fix the h1 engine compatibly.
serve.Host owns the built-in providers (the task runner on its own thread,
configuration and secrets from the environment, telemetry on stderr), handed
to the application as one provider entry it can override key by key.
serve.run serves the application on http.server.server: each exchange is
routed, bound, executed and resumed through app, woken at its handler deadline
through wake_at, and released when the server settles it. The lifecycle runs
through the server's start, ready, drain and stop hooks, the task provider
drains beside it, and SIGTERM or SIGINT begins the bounded drain.

Tests: the same assemble under laurel.testing, and a real socket served,
signalled with SIGTERM while a request is in flight, and drained.
demo/standalone is one application written against its host's providers: a
text route, a configuration value, a periodic task's snapshot and telemetry,
and a body echo that suspends on the host. bin/main.mach hosts it with
laurel.serve, and its test runs the same assemble in process under
laurel.testing. CI builds it and runs the test on the primary leg.
doc/serve.md states the laurel.serve contract: running, the lifecycle
mapping, signals and drain, request handling, and the current limits, no
WebSockets until mach-http#191 and long streams bounded by the runner's
whole-exchange deadline until mach-http#193. doc/providers.md describes the
shared task runner and the built-in providers, and the README shows both
hosting modes.
@octalide
octalide marked this pull request as ready for review September 27, 2026 07:51
@octalide
octalide merged commit 0818155 into dev Sep 27, 2026
10 checks passed
@octalide
octalide deleted the feat/184-serve branch September 27, 2026 07:52
@octalide octalide mentioned this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

laurel.serve and provider interfaces: run a laurel app standalone

1 participant