Architecture¶
go-crap-tracker is a two-process system coordinated through NATS JetStream and Postgres. The control plane (serve) accepts work and tracks state; workers (worker) do the scanning. They never talk to each other directly — everything flows through the message bus and the database.
Components¶
| Component | Role |
|---|---|
serve (control plane) |
HTTP API (REST + SSE + web UI), the reconciler, the worker registry, and the migration runner |
worker |
NATS JetStream consumer that claims tasks, clones, tests, scans, and saves results |
| NATS JetStream | Durable job queue + event bus (two streams) |
| Postgres | Durable store for tasks, results, locks, per-repo config, and repos |
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ serve │────▶│ NATS JetStream │────▶│ worker(s) │
│ (control plane)│◀────│ (jobs + events) │◀────│ (scan engine) │
│ API / UI / SSE │ └─────────────────────┘ └──────────────────┘
│ reconciler │ │
│ migrations │ │
└─────────────────┘ │
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Postgres │
│ scan_tasks │ repo_analyses │ repo_configs │ repo_locks │ repos │
└──────────────────────────────────────────────────────────────────┘
NATS JetStream¶
Two streams, created on demand by whichever process starts first.
Job stream — crap_jobs_scan¶
| Subject | Purpose |
|---|---|
crap.jobs (and crap.jobs.>) |
scheduled scan jobs |
A pull consumer crap-workers is the single shared durable consumer:
| Setting | Value |
|---|---|
| Durable | crap-workers |
| Filter subject | crap.jobs |
| Ack policy | explicit |
| Max deliver | 3 |
| Ack wait | 10m |
| Deliver policy | new |
Workers fetch from the shared consumer (PullMaxMessages(1)) in a reconnect loop, so each job is handed to whichever worker is free and not redelivered while legitimately in flight (10-minute AckWait is well above the 5-minute scan cap).
Event stream — crap_events¶
| Subject | Purpose |
|---|---|
crap.event (and crap.event.>) |
task status updates |
The control plane's crap-sse consumer (AckNone) reads the event stream and relays each event onto the SSE hub, so every browser subscriber sees queued → running → completed live.
Worker control subjects¶
| Subject | Purpose |
|---|---|
crap.worker.hb |
worker heartbeats (id, host, pid, cpu, status) |
crap.worker.cmd.<workerID> |
per-worker commands (pause / unpause) |
Each worker only subscribes to its own crap.worker.cmd.<workerID> subject, so pausing one worker leaves the rest consuming.
Postgres¶
| Table | Contents |
|---|---|
scan_tasks |
task state machine (status, attempt, worker, timestamps, error) |
repo_analyses |
one row per analysed commit — scores, metadata, full report-v1 JSON |
repo_configs |
per-repo overrides (ref, test args, crap args, submodules, timeouts, go flags) |
repo_locks |
distributed, lease-based repo locks (owner, host, expiry) |
repos |
canonical repo registry (id → url, display metadata) |
Analyses are upserted by a deterministic ID (sha256(repo_url + commit_sha)[:16]), so re-scanning the same commit replaces the record instead of duplicating it.
A scan, end to end¶
sequenceDiagram
participant C as Client / UI
participant S as serve
participant N as NATS JetStream
participant W as worker
participant P as Postgres
C->>S: POST /api/tasks
S->>P: insert scan_task (pending)
S->>N: publish crap.jobs
S-->>C: 201 {id, status: queued}
N-->>W: worker pulls job
W->>P: claim_if_queued (queued -> running)
W->>N: publish crap.event (running)
W->>P: acquire repo lock
W->>W: clone/fetch + go test + go-crap scan
W->>P: upsert repo_analyses
W->>P: complete (running -> completed)
W->>N: publish crap.event (completed)
W->>P: release lock
N-->>S: crap-sse consumer reads event
S-->>C: SSE push (live status)
Design notes¶
- Swappable infra. Core logic in
internal/coredepends only on port interfaces (internal/core/ports); the NATS, Postgres, cache, and runner adapters implement them. That keeps infrastructure replaceable without touching the domain. - Shared cache. Workers share one clone/coverage cache directory (typically a mounted PVC) plus Postgres locks, so repeat scans of the same commit skip both the clone and the
go testrun. See Repo Cache. - Bounded work. Job retries are capped at the task level (
max_attempts3) and the transport level (MaxDeliver3); the reconciler expires tasks stuck inrunning. See Task Lifecycle.