Developer Guide
Implementation reference for the contracts-service. For its architectural role, see Contracts Service Architecture; for the domain model, see Telemetry Distribution.
The only Rust service in the platform. Built on Axum and Tokio, it runs an HTTP API and a background telemetry tunnel in a single process, holding per-building preferences in memory for a lock-free hot path.
The source is organised by role: a composition root and shared kernel at the crate root, the two inbound adapters (api/ for HTTP, tunnel.rs for the broker stream), and outbound integrations plus cross-cutting infrastructure grouped under infra/.
| Path | Responsibility |
|---|---|
src/main.rs | Composition root: connect Mongo, hydrate the in-memory map, start the tunnel, serve the Axum router. |
src/state.rs | AppState: the shared DashMap projection plus the typed Mongo collection handle. |
src/models.rs | PreferenceDocument, MetricContract, and the discovery response types. |
src/api/ (dashboard.rs, data.rs, init.rs) | Inbound HTTP handlers: the aggregated catalog (GET /), preferences CRUD, and defaults seeding. |
src/tunnel.rs | Inbound stream worker: routes telemetry:raw to each event’s own building, and records fan-out metrics. |
src/infra/db.rs | Mongo access: connect (with unique index), load_all (hydration), upsert_preference. |
src/infra/discovery.rs | Resolves downstream metric endpoints from *_METRICS_URL environment variables. |
src/infra/metrics.rs | Prometheus registry, telemetry counters and fan-out latency, and the GET /metrics handler. |
graph TD
MAIN["main.rs\ncomposition root"] --> API["api/\ndashboard.rs, data.rs, init.rs"]
MAIN --> TUNNEL["tunnel.rs\nbackground worker"]
MAIN --> DB["infra/db.rs\nconnect + hydrate"]
API --> STATE["state.rs\nAppState: DashMap + Mongo handle"]
TUNNEL --> STATE
API --> DISC["infra/discovery.rs"]
API --> METRICS["infra/metrics.rs"]
TUNNEL --> METRICS
STATE -.-> DB| Variable | Default | Purpose |
|---|---|---|
MONGO_URI | mongodb://localhost:27017 | Durable store for preferences (database crowdvision, collection preferences). |
REDIS_URL | redis://127.0.0.1/ | Broker for the telemetry tunnel. |
PORT | 3000 | HTTP listen port. |
*_METRICS_URL | — | Any variable with this suffix is treated as a service base URL for metric discovery. |
pub struct AppState {
pub building_preferences: Arc<DashMap<String, Vec<String>>>, // hot path
pub mongo_col: Collection<PreferenceDocument>, // durable
}
pub struct PreferenceDocument {
pub id: Option<ObjectId>,
pub building_id: String, // unique index
pub allowed_columns: Vec<String>,
}DashMap shards its internal locks across cores, so the tunnel’s millions of reads almost never contend with an occasional write. The Mongo collection enforces one document per building via a unique index on building_id.
1. connect(MONGO_URI) -> ensure unique index on building_id
2. load_all() -> seed the DashMap from persisted documents
3. start_telemetry_tunnel() -> spawn the background filter task
4. axum::serve(router) -> begin handling HTTPHydration at boot means a restart restores all preferences before the first request is served.
Reads and the tunnel operate purely in memory; durability is handled off the request path.
DashMap lookups; the tunnel never blocks on a write.update_preferences mutates the DashMap immediately, then spawns a fire-and-forget Tokio task to upsert_preference into Mongo. The HTTP response returns 200 without waiting for the database. The same write-through applies to init when it creates a new entry.Because the database write is fire-and-forget, a crash in the narrow window between the in-memory insert and the persisted upsert can lose the most recent change. This is an accepted trade-off for hot-path latency; the next successful save reconciles it.
All routes except /health and /metrics require the mesh-verified x-gateway-claims header (decoded by a GatewayClaims extractor that returns 401 on a missing or malformed header) — the same contract every other service consumes. The user-facing routes are gated at the edge (require_gateway_auth in the Caddyfile, the /contracts AuthorizationPolicy rule in Kubernetes); POST /preferences/init is an internal call and receives the building creator’s claims forwarded by twin-service. This enforces authentication only; per-building authorization is deferred to the Cedar phase.
| Method · Path | Description |
|---|---|
GET /preferences/{building_id} | Return { building_id, allowed_columns }. 404 if the building has no entry. Requires x-gateway-claims. |
POST /preferences/{building_id} | Replace the allowed-column list (body { allowed_columns }). Instant cache write + async persist. Returns { status, building_id, active_columns }. Requires x-gateway-claims. |
POST /preferences/init/{building_id} | Idempotently seed defaults ["roomName", "roomMaxOccupancy"] with an atomic or-insert, so a repeat call is a safe no-op. Called by the twin-service on building creation, forwarding the creator’s x-gateway-claims. |
GET / | The aggregated metric catalog (see Metric Discovery). Requires x-gateway-claims. |
GET /metrics | Prometheus exposition: telemetry events received/published counters and fan-out latency. Open (scraped in-cluster). |
Started at boot, the tunnel opens a dedicated Redis PubSub connection and subscribes to telemetry:raw.
sequenceDiagram
participant R as Redis (telemetry:raw)
participant T as Tunnel
participant D as DashMap
participant F as Redis (telemetry:filtered:{id})
R-->>T: { type, buildingId, roomId, value, ... }
Note over T: spawn a task per message (ingestion never blocks)
T->>D: is buildingId a known building?
alt known
T->>F: PUBLISH telemetry:filtered:{building_id} (full payload)
endbuildingId: a reading is published to that one building’s channel and no other, so it can never leak into another tenant’s feed. The payload is forwarded intact (no field stripping).allowed_columns only drive what the dashboard table displays (client-side). The DashMap is consulted just to confirm the building is known.task::spawn, so processing time never back-pressures the subscription stream.resolve_channel) so it is unit-tested in isolation; the worker increments received/published counters and observes fan-out latency, exported via GET /metrics.GET / builds the catalog the frontend uses to populate column pickers.
discover_services() collects every environment value whose key ends in _METRICS_URL.GET /contracts is fetched and deserialised (either a ServiceMetricsContract or a bare metric list).source_service and added via push_unique_metric, which deduplicates on (metric_key, interface_name, source_service).New metric types therefore appear automatically once a service advertises them; no change is needed here.
There is no DELETE /preferences/{building_id}. When a building is removed from the twin-service, its entry remains in both the DashMap and Mongo, and the tunnel keeps evaluating it. A deletion endpoint (called from the twin on removal) is the intended fix.