---
phase: "06-background-worker"
plan: "01"
subsystem: "background-worker"
tags: ["river", "jobs", "sqlc", "orphan-cleanup", "worker"]
dependency_graph:
  requires: []
  provides:
    - "river dependency in go.mod (v0.37.0 + riverpgxv5)"
    - "ListOrphanFiles sqlc query and ListOrphanFilesRow type"
    - "internal/jobs package: HeartbeatWorker, OrphanCleanupWorker, SlogErrorHandler"
  affects:
    - "backend/go.mod (river added)"
    - "backend/internal/db/queries/files.sql (ListOrphanFiles appended)"
    - "backend/internal/jobs/ (new package)"
tech_stack:
  added:
    - "github.com/riverqueue/river v0.37.0"
    - "github.com/riverqueue/river/riverdriver/riverpgxv5 v0.37.0"
  patterns:
    - "river.WorkerDefaults[T] embedding for job structs"
    - "fileQuerier interface for mock-based unit testing"
    - "S3-delete-before-DB-delete ordering in orphan cleanup"
key_files:
  created:
    - "backend/internal/jobs/heartbeat.go"
    - "backend/internal/jobs/orphan_cleanup.go"
    - "backend/internal/jobs/error_handler.go"
    - "backend/internal/jobs/heartbeat_test.go"
    - "backend/internal/jobs/orphan_cleanup_test.go"
    - "backend/internal/jobs/error_handler_test.go"
  modified:
    - "backend/go.mod"
    - "backend/go.sum"
    - "backend/internal/db/queries/files.sql"
decisions:
  - "fileQuerier interface added to OrphanCleanupWorker for mock injection (pool still present for production)"
  - "river deps show as // indirect until cmd/worker imports them (Plan 02 wires this)"
  - "sqlc-generated files not committed per project convention (.gitignore excludes internal/db/sqlc/*.go)"
metrics:
  duration: "~15min"
  completed: "2026-05-15"
  tasks: 2
  files: 9
---

# Phase 06 Plan 01: river Dependency + internal/jobs Package Summary

**One-liner:** River v0.37.0 added to go.mod, ListOrphanFiles sqlc query generated, and internal/jobs package implemented with HeartbeatWorker, OrphanCleanupWorker, and SlogErrorHandler — all unit-tested with pure mocks, no DB required.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Add river dependency + ListOrphanFiles sqlc query | 62e5e3e | go.mod, go.sum, files.sql |
| 2 | Create internal/jobs/ package + unit tests | a1c2828 | 6 new files under internal/jobs/ |

## Outcomes

### Task 1: river + sqlc query
- `github.com/riverqueue/river v0.37.0` and `riverpgxv5` added via `go get`
- `ListOrphanFiles :many` query appended to `files.sql` with NOT EXISTS subquery
- sqlc regenerated: `ListOrphanFilesRow{ID uuid.UUID, TabloID uuid.UUID, S3Key string}` and `ListOrphanFiles(ctx) ([]ListOrphanFilesRow, error)` exported
- `go build ./...` exits 0

### Task 2: internal/jobs package
- `HeartbeatArgs` (Kind="heartbeat") + `HeartbeatWorker` with `Work` logging slog.Info
- `OrphanCleanupArgs` (Kind="orphan_file_cleanup") + `OrphanCleanupWorker` with S3-first delete loop
- `NewOrphanCleanupWorker(pool, store)` constructor; `fileQuerier` interface enables test injection
- `SlogErrorHandler` implementing `river.ErrorHandler` (HandleError + HandlePanic return nil)
- 7 unit tests — all pass, no DB required

## Test Results

```
=== RUN   TestSlogErrorHandler_HandleError     PASS
=== RUN   TestSlogErrorHandler_HandlePanic     PASS
=== RUN   TestHeartbeatWorker                  PASS
=== RUN   TestHeartbeatArgs_Kind               PASS
=== RUN   TestOrphanCleanupWorker_NoOrphans    PASS
=== RUN   TestOrphanCleanupWorker_DeletesOrphan PASS
=== RUN   TestOrphanCleanupArgs_Kind           PASS
ok  backend/internal/jobs  0.452s
```

## Deviations from Plan

### Auto-added: fileQuerier interface

**Rule 2 — Missing critical functionality**
- **Found during:** Task 2
- **Issue:** Plan specified `pool *pgxpool.Pool` as the only DB field but also required pure mock-based unit tests where a `mockQuerier` replaces real DB calls. Without an injectable querier, the worker would call `sqlc.New(w.pool)` unconditionally and mock tests could not intercept DB operations.
- **Fix:** Added internal `fileQuerier` interface (ListOrphanFiles + DeleteTabloFile methods) as a struct field. `NewOrphanCleanupWorker` sets it from `sqlc.New(pool)` at construction. Tests inject `mockQuerier` directly. Pool field is retained for production; nil-fallback path in `Work` provides defense in depth.
- **Files modified:** `backend/internal/jobs/orphan_cleanup.go`
- **Commit:** a1c2828

### river // indirect in go.mod

**Observation (not a deviation):** river entries appear as `// indirect` because no main package imports `internal/jobs` yet. Plan 02 (`cmd/worker` wiring) will move them to direct. This is expected Go module behavior and does not affect compilation.

## Known Stubs

None. All behavior is implemented and tested.

## Threat Flags

None. No new network endpoints or trust-boundary crossings introduced in this plan.

## Self-Check: PASSED

- `backend/internal/jobs/heartbeat.go` — FOUND
- `backend/internal/jobs/orphan_cleanup.go` — FOUND
- `backend/internal/jobs/error_handler.go` — FOUND
- commit `62e5e3e` — FOUND
- commit `a1c2828` — FOUND
- `go build ./...` — PASSES
- `go test ./internal/jobs/... -v -count=1` — 7/7 PASS
