Skip to content

v2.0.0 — All-Rust rewrite

Choose a tag to compare

@chiam-ck chiam-ck released this 21 Jul 02:57
· 22 commits to main since this release

BSS-CLI is now all-Rust. The complete Python → Rust rewrite is done and mainline. This was a behaviour-frozen port (strangler-fig): the motto, scope, write-policy, TMF surfaces, domain events, DB schema, and the LLM tool surface are unchanged — only the implementation stack changed. Existing deployments upgrade in place.

Highlights

  • Complete rewrite, same behaviour. Wire contracts, event routing keys, payloads, the Postgres schema, and every LLM tool are byte-compatible with v1.8. Your scenarios, integrations, and data keep working.
  • Dramatically lighter. The whole 11-service app plane now idles under 100 MiB — a headline motto-#6 win.
  • Faster cold start and lower tail latency (numbers below).
  • Alembic retired in favour of the sqlx migrator — no Python/greenlet in the runtime story.
  • Beyond the port: a new Open Order / MSISDN-reservation signup funnel, a signup confirmation screen (real eSIM QR), and async-plane self-healing — see "Beyond the rewrite" below.

Performance (motto #6 — re-measured, idle-vs-idle)

Budget v1.8 (Python) v2.0 (Rust) Δ
Full-stack RAM (11 services) < 4 GB 1204 MiB ~91 MiB idle (≤~160 under load) ~13× smaller — 2.2% of budget
Cold start (all 11) < 30 s 6.36 s 3.08 s ~2.1× faster
p99 /health < 50 ms 12.8 ms 6.16 ms ~2× lower tail
p99 real DB read < 50 ms — 8.47 ms under budget

Beyond the rewrite

v2.0 isn't only a port — it also shipped a new signup feature and hardened the async plane.

Open Order & MSISDN reservation (new feature)

Numbers no longer collide during a live signup, and an interrupted signup becomes a first-class, resumable record. (Roadmap feature — new behaviour, not in the prior Python line.)

  • Reserve-at-pick. Picking a number places a soft hold (distinct from the hard provisioning reserve) via a single atomic, self-healing UPDATE — concurrent pickers can't both win the same MSISDN.
  • Persisted, resumable order. One open order per customer (keyed on the verified email). Starting a second is blocked (open_order.already_open); returning resumes the same plan + held number.
  • "My open order" account screen. Customers view / resume / cancel their own pending order — ownership-bound (the id comes from the session, never the request) — with a resume link on the dashboard. Clearing a stuck order no longer needs API access.
  • 24h expiry sweep. A worker (FOR UPDATE SKIP LOCKED, multi-replica safe, bss_clock-driven — same discipline as renewal) auto-releases abandoned holds, so a stuck order never permanently strands a number.

Signup confirmation

  • A post-signup confirmation screen with a real PNG eSIM QR code + activation poll, plus the MSISDN picker.
  • An explicit "Review and confirm" step before the card is charged — previously a returning customer adding a line was charged + activated instantly with no pause. Signup block errors (taken number / existing open order) that used to render silently now show a banner with a "View your open order" link.

Async-plane self-healing & idempotency

The message plane now recovers from a broker blip on its own (lapin, unlike Python's aio-pika robust connection, doesn't auto-recover) — the reliability the soak gated on:

  • Publish path self-heals: the channel/connection sit behind a mutex and are recreated on demand (re-declaring the durable exchange), so a transient broker blip no longer wedges the outbox relay.
  • Consume path auto-resubscribes after a drop with a shared exponential backoff (1 s → 30 s) — before, a dropped consumer silently stopped consuming until a restart.
  • SOM task.completed is idempotent (already-activated → ack-and-skip), breaking a duplicate-delivery "park storm" that could starve fresh orders from ever completing.

Also fixed during the cutover

Stripe add-card Checkout flow (blank page → working), several Jinja2 → MiniJinja template-flip artifacts, cockpit transcript rehydration (a stale-turn bug in long sessions), and the REPL .env auto-load + branding/banner polish.

Stack (Python → Rust)

  • FastAPI → axum; SQLAlchemy/asyncpg → sqlx; Alembic → sqlx migrator (bss admin migrate)
  • Pydantic → serde; LangGraph → a hand-rolled ReAct loop (bss-orchestrator)
  • Typer/Rich → clap + reedline + ASCII renderers; aio-pika → lapin
  • FastAPI portals → axum + MiniJinja + HTMX (same templates, no npm/bundler)
  • OpenTelemetry hand-stitched at 4 seams (no auto-instrumentors); tracing JSON logs
  • Multi-stage cargo-chef Dockerfiles, non-root distroless final stage

Upgrading

  • Fresh install: bss admin migrate then make seed.
  • Existing (Alembic-created) DB: bss admin migrate --baseline once to stamp the baseline without re-running it, then you're on the sqlx migrator going forward. Runbook: docs/runbooks/rust-schema-baseline.md.
  • Build the CLI: cargo build --release -p bss-cli (or cargo install --path cli). make up / make up-all bring up the all-Rust stack.

Removals / changes

  • The original Python implementation was archived, not deleted — recoverable via git tag python-oracle-final + tarball. See docs/PYTHON-ORACLE.md.
  • Retired Python-only dev tooling: the py-* / python-check / check-clock / lint-types make targets and the Python Playwright make e2e* suite (the Rust ship gate is make scenarios-hero).
  • The demo seed was ported to bss admin seed-demo (+ --reset) — 3 demo customers + 2 promotions + VIP assignment, BSS + loyalty in lockstep. Its Python-only loyalty-wipe mode (and the demo-restore one-button) did not carry over — loyalty-cli owns its own DB reset.

Quality gate at release

fmt · clippy -D warnings · cargo test (120 suites, 0 failed) · 14 doctrine guards · hero suite 19/19.

The nominal 14-day soak was cut early by operator decision; all gates were green at cut. See DECISIONS.md (2026-07-21) and phases/2.0/07-SOAK.md. If a latent regression surfaces, the golden-diff oracle is one git worktree add … python-oracle-final away.