Server P0: one composition root, application-path test, Fly showcase container

Merged
SU
11d ago
+1297-207

Summary

This is the first phase of the server application-path plan. Goal: one consistent path from instrument create/import → market inputs → pricing → publication.

  • One composition root. convex_server::compose(config) builds storage, engine and server without starting or binding anything. main.rs now only loads config and calls it, and the tests use it too, so there is no second wiring. A failure reports the step that failed (StartupError::Composition { stage, .. }).

  • Acceptance test (tests/application_path.rs). It drives the real composition over HTTP using the showcase seed, which keeps the container fixture under CI. The test:

    • commits reference data,
    • prices the stored bond by ID from the same repository,
    • saves curve recipes (Unix only),
    • restarts and reads the same versions back.

    Book ingest (P6) and live quotes (P2) are #[ignore] tests for now; they get switched on when those phases land.

  • Fly showcase (deploy/showcase, Dockerfile, fly.toml). The image built from current main exits at startup: it binds 0.0.0.0 under the Production profile, and validate_startup refuses both. This PR fixes that:

    • Caddy listens publicly on :8080 and proxies to the server on loopback :8081.
    • Caddy uses a fail-closed allowlist: reads, /ws and stateless calculator POSTs pass; every other write gets 403.
    • tini plus an sh entrypoint reseeds redb from seed.json on every boot. The seed is one fictional bond and six demo curve recipes, all taken from existing fixtures.
    • CORS allows exactly https://convex-demo.pages.dev.
    • fly.toml adds a /health check and request concurrency limits.

Deferred from the plan

  • Demo quote feed → P2. It needs the P2 quote ingress route.
  • Demo profile as the real runtime composition → P2. Every tools/core_*_workflow.py CI step starts in demo mode without a plan, so demo startup is unchanged here.
  • Windows CI job → P4. Curve recipes go through CoreWorkflow, which is Unix-only until the redb snapshot adapter lands.

Testing

  • Windows: cargo test -p convex-server --test application_path passes, including the restart.
  • Linux (WSL):
    • cargo test --all-features -p convex-server passes (254 tests).
    • tools/core_workflow.py passes, so demo startup is unchanged.
    • cargo clippy -D warnings, cargo doc and tools/assurance/check.py (including --base main) all pass.
  • Entrypoint run under dash with Caddy 2.8.4, without Docker:
    • The seed imports.
    • Reads return 200, calculator POSTs reach the server, and blocked writes return 403.
    • CORS echoes the Pages origin, and /ws returns 101 through Caddy.
    • A reboot on a reused data directory works.
    • SIGTERM to the process group leaves nothing running.
  • Not tested: docker build (no Docker daemon was available).

Before fly deploy

  • Build the image once with docker build -t convex-showcase ..
  • Clear any leftover CONVEX_* Fly secrets or env (fly secrets list -a convex-pricing-api). The server rejects unknown CONVEX_* variables, and environment values override server.toml.

Known risks

  • Caddy is the only write guard. The server runs local_development access on loopback, and the access code forbids proxy mode together with local_development.
  • The curve calibration page is unavailable on the showcase until P1. POST /api/v1/curves and /curves/bootstrap store into the unbounded in-memory CurveBuilder, so Caddy blocks them. P1 makes bootstrap stateless and re-allows it.
  • Some pages were already broken and still are. The streaming-simulation and market-data pages call separate demo provider services that the Pages build never points to. P2's demo feed replaces them.

Review follow-up (d80804ed)

  • The entrypoint now supervises both processes and exits non-zero if either dies, because a failed Fly health check only removes the machine from routing. SIGTERM drains both and exits 0. Verified under dash: after a server kill the entrypoint exits 1, after SIGTERM it exits 0, and no processes are left either way.
  • The curve routes are blocked publicly (they return 403 through Caddy).
  • make_seed.py now materializes the packed fixtures first. Regenerating the seed produces identical bytes.
  • The docs no longer overstate the restart behaviour, the OPTIONS policy or the compose() error contract.
  • Not changed: the seed curve recipes are a dated capture. Nothing public consumes them in P0; P1 handles freshness.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a containerized showcase deployment with a preloaded pricing dataset and local startup seeding.
    • Added a read-only public edge for supported calculator requests, with JSON responses for blocked operations.
    • Added support for pricing workflows including bond retrieval, identifier-based pricing, curve recipes, reference versions, and ETags.
    • Added persistent behavior across server restarts within the configured storage.
  • Deployment

    • Added service health checks, request concurrency limits, and documented local Docker/Fly.io execution.