---
phase: "06-background-worker"
plan: "02"
subsystem: "background-worker"
tags: ["river", "rivermigrate", "periodic-jobs", "worker", "justfile", "readme"]
dependency_graph:
  requires:
    - "06-01: river dependency in go.mod, internal/jobs package (HeartbeatWorker, OrphanCleanupWorker, SlogErrorHandler)"
  provides:
    - "cmd/worker/main.go: full river wiring — rivermigrate + river.Client + periodic jobs + graceful shutdown"
    - "just worker: local dev target with MinIO defaults"
    - "README.md: worker local dev instructions and single-worker constraint documentation"
  affects:
    - "backend/cmd/worker/main.go (replaced skeleton with full implementation)"
    - "backend/justfile (worker target added)"
    - "backend/README.md (worker section replaced)"
tech_stack:
  added: []
  patterns:
    - "context.Background() for startup I/O, signal.NotifyContext AFTER all startup completes (PATTERNS.md critical ordering)"
    - "rivermigrate.New(riverpgxv5.New(pool), nil).Migrate() for idempotent schema migration at startup"
    - "river.Client with slog.Default() Logger, SlogErrorHandler, PeriodicJobs, MaxWorkers:10"
    - "StopAndCancel(10s timeout context) + explicit pool.Close() after StopAndCancel returns"
key_files:
  created: []
  modified:
    - "backend/cmd/worker/main.go"
    - "backend/justfile"
    - "backend/README.md"
decisions:
  - "signal.NotifyContext created AFTER rivermigrate and S3 init (not before) — RESEARCH Pitfall 2 + PATTERNS.md critical ordering constraint"
  - "pool.Close() called explicitly after StopAndCancel returns, not via defer — PATTERNS.md pool close ordering"
  - "S3 init is non-optional (exits on error) — worker cannot run orphan cleanup without S3 credentials"
  - "OrphanCleanupArgs PeriodicJobOpts is nil (no RunOnStart) — first cleanup at 1 hour, not on startup"
metrics:
  duration: "~10min"
  completed: "2026-05-15"
  tasks: 2
  files: 3
---

# Phase 06 Plan 02: cmd/worker Full River Wiring Summary

**One-liner:** cmd/worker skeleton replaced with full river runtime — rivermigrate at startup, S3 init, signal context after I/O, worker registration, river.Client with two periodic jobs (heartbeat 1min RunOnStart:true, orphan cleanup 1hr), StopAndCancel graceful shutdown; `just worker` target and README docs added.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Replace cmd/worker/main.go with full river wiring | 6e70478 | backend/cmd/worker/main.go |
| 2 | Add just worker target + document worker in README.md | e202ad3 | backend/justfile, backend/README.md |

## Outcomes

### Task 1: Full river wiring in cmd/worker/main.go

- `rivermigrate.New(riverpgxv5.New(pool), nil).Migrate(ctx, rivermigrate.DirectionUp, nil)` at startup before client construction
- S3 env var loading: S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, S3_REGION (default "us-east-1"), S3_USE_PATH_STYLE
- `signal.NotifyContext` created AFTER pool connect + rivermigrate + S3 init (critical ordering constraint)
- `river.AddWorker(workers, &jobs.HeartbeatWorker{})` and `river.AddWorker(workers, jobs.NewOrphanCleanupWorker(pool, fileStore))`
- `river.NewClient` with `Logger: slog.Default()`, `MaxWorkers: 10`, `ErrorHandler: &jobs.SlogErrorHandler{}`
- Two `PeriodicJob` entries: heartbeat every 1 min (RunOnStart:true), orphan cleanup every 1 hr (nil opts)
- `riverClient.Start(sigCtx)` then `<-sigCtx.Done()` then `riverClient.StopAndCancel(10s timeout)`
- `pool.Close()` after StopAndCancel returns (not via defer)
- `go build ./cmd/worker/...` and `go build ./...` both exit 0

### Task 2: justfile + README

- `worker: db-up` target with all S3 MinIO dev defaults on separate continuation lines
- README "Running the Worker" section replaces the outdated Phase 1 skeleton section
- Single-worker constraint documented (D-08)
- Expected log sequence documented (worker ready, worker heartbeat, graceful shutdown messages)
- Failed job retry observation documented (SlogErrorHandler fields, 25-attempt max, attempts^4 backoff)

## Verification Results

```
go build ./...                               → exit 0
go build ./cmd/worker/...                    → exit 0
grep "rivermigrate.New" cmd/worker/main.go   → line found
grep "^worker:" justfile                     → 1 match
just --list | grep worker                    → target registered
grep "just worker" README.md                 → 1+ matches
```

## Deviations from Plan

None — plan executed exactly as written.

The research code example in 06-RESEARCH.md used `defer pool.Close()` in the sample, but the plan's task action explicitly specified explicit `pool.Close()` after StopAndCancel per PATTERNS.md. The implementation follows the plan (explicit close, not defer).

## Known Stubs

None. All river wiring is fully implemented. The worker binary will connect to Postgres, run rivermigrate, initialize S3, register workers, and process jobs when run with valid env vars.

## Threat Flags

None. No new network endpoints or trust-boundary crossings beyond what the plan's threat model covers (T-06-05 through T-06-08 are all addressed by the implementation).

- T-06-05 (rivermigrate concurrent execution): README single-worker constraint documented
- T-06-06 (Start with cancelled context): signal.NotifyContext created after startup I/O
- T-06-07 (S3 credentials in justfile): dev-only MinIO defaults; production uses real env vars
- T-06-08 (Orphan cleanup wrong S3 key): uses s3_key from tablo_files row directly

## Self-Check: PASSED

- `backend/cmd/worker/main.go` — FOUND
- `backend/justfile` worker target — FOUND (`grep "^worker:" justfile` returns line)
- `backend/README.md` worker section — FOUND (`grep "just worker" README.md` returns line)
- commit `6e70478` — FOUND
- commit `e202ad3` — FOUND
- `go build ./...` — PASSES
- `go build ./cmd/worker/...` — PASSES
