Files
steamcache2/plans/P2.md
T

176 lines
10 KiB
Markdown

# P2: Performance, Quality & Maintainability Improvements
**Priority**: P2 — Performance, refactoring, and long-term health
**Theme**: Make the codebase faster, smaller, easier to reason about, and production-operable at scale.
**Status**: Not started
**Depends on**: P0 strongly recommended; P1 nice-to-have for some verification steps
## Goal
Turn a clever but monolithic prototype into a high-quality, maintainable Go project that is pleasant to work on and easy to operate.
## Overview
After the critical stability (P0) and safety (P1) work, the project still carries technical debt that affects:
- Runtime performance under sustained load (lock contention, unnecessary copies)
- Developer velocity (huge source file, magic numbers, copy-paste)
- Operational visibility (weak metrics, no benchmarks, incomplete CI)
- Correctness confidence (very low test coverage on the storage layer)
These items are important for long-term success but are not immediate crash or security risks.
## Tasks
### P2-01: Refactor the monolithic `ServeHTTP` and split `steamcache/steamcache.go`
- **Description**: The core request handler is a single ~500+ line function with many responsibilities (authz, rate limiting, coalescing, upstream fetch, cache write, metrics, adaptive recording). The file itself is 1724 lines.
- **Impact**:
- Extremely hard to test individual behaviors in isolation.
- High risk of regression when touching any part of request handling.
- New contributors are intimidated.
- **Affected Files**:
- `steamcache/steamcache.go` (primary)
- Potentially new files: `steamcache/handler.go`, `steamcache/coalescing.go`, `steamcache/upstream.go`, `steamcache/response.go`, etc.
- **Approach**:
1. Extract clear types for the request context (e.g. `requestContext` holding clientIP, cacheKey, service, timing, etc.).
2. Break `ServeHTTP` into smaller focused methods: `handleCacheHit`, `handleCoalesced`, `fetchAndCache`, `writeCacheEntry`, etc.
3. Move pure helper logic (range parsing, response reconstruction, hash generation) into separate small files if they aren't already.
4. Keep the `SteamCache` struct as the coordinator but reduce its god-object nature over time.
- **Acceptance Criteria**:
- No single function in the package > 150 lines.
- `ServeHTTP` itself becomes a thin dispatcher (< 80 lines).
- All existing behavior (including edge cases around coalescing + errors + Ranges) still passes the test suite.
- New unit tests become feasible for the extracted pieces.
- **Dependencies**: None (can be done in parallel with other P2 work)
- **Effort**: Large (8-16 hours). Best done as a series of small, reviewable refactors rather than one giant PR.
### P2-02: Reduce lock contention during eviction
- **Description**: `EvictLRU`, `EvictBySize`, etc. take the global `mu.Lock()` on the entire `MemoryFS`/`DiskFS` for the duration of the scan + deletion loop.
- **Impact**: Under cache pressure (very common when the disk cache fills), all other operations (Open, Stat, Create) serialize behind the eviction. This can cause request latency spikes even for hot memory-tier hits.
- **Affected Files**:
- `vfs/memory/memory.go` (eviction methods)
- `vfs/disk/disk.go` (eviction methods)
- **Approach** options:
1. Collect candidates under read lock, then do the actual deletes and size updates in a second phase or in small batches while briefly acquiring write locks.
2. Move eviction into a dedicated background goroutine that the GC layer signals, using finer-grained coordination.
3. Use a "generation" or "watermark" approach so readers can proceed while eviction cleans up.
- **Acceptance Criteria**:
- Benchmark or load test shows improved tail latencies for `Open`/`Stat` while eviction is running.
- No data races introduced (race detector clean).
- Total size and LRU invariants remain correct after concurrent eviction.
- **Dependencies**: Good test coverage on the VFS layer (see P2-04)
- **Effort**: Medium-Large (4-8 hours + measurement)
### P2-03: Dramatically improve test coverage on the VFS and storage layer
- **Description**: Most `vfs/*` packages currently have 0% coverage. The critical storage, eviction, GC, and tiering logic is almost untested in isolation.
- **Impact**:
- Very low confidence when changing eviction, promotion, or GC behavior.
- Hard to catch regressions in size accounting, LRU ordering, or sharded locking.
- Blocks safe execution of P2-02 and future performance work.
- **Affected Areas** (need new or expanded tests):
- `vfs/memory/*`
- `vfs/disk/*`
- `vfs/gc/*`
- `vfs/cache/*`
- `vfs/eviction/*`
- `vfs/locks/*` and `vfs/lru/*` (at least basic)
- **Approach**:
1. Write focused unit tests for each VFS implementation using `t.TempDir` for disk.
2. Add property-style or table-driven tests that verify size never exceeds capacity after many Create + Evict cycles.
3. Test concurrent Create/Open/Delete/Delete under load (with `-race`).
4. Test promotion, tier fallback, and lazy discovery paths.
5. Add benchmarks (`BenchmarkMemoryFS_CreateOpen`, `BenchmarkEvictionUnderPressure`, etc.).
- **Acceptance Criteria**:
- Combined coverage for all `vfs/*` packages ≥ 70% (statement).
- At least one benchmark per major component that can be run in CI or locally.
- New tests catch at least one real bug during development (celebrated in commit message).
- **Dependencies**: None
- **Effort**: Large (12-20 hours spread over multiple sessions). High leverage.
### P2-04: Clean up build, CI, linting, and repository hygiene
- **Description**:
- Makefile `run` / `run-debug` targets are hardcoded to a Windows binary path.
- `dist/` artifacts are committed even though `.gitignore` lists `/dist/`.
- No golangci-lint, no `go vet` in CI, no vulnerability scanning.
- Test target exists but coverage reporting and gates are missing.
- **Impact**: Painful local development on non-Windows machines. Risk of shipping known-bad artifacts. Harder to maintain code quality over time.
- **Affected Files**:
- `Makefile`
- `.gitea/workflows/test-pr.yaml` (and release workflow)
- `.gitignore` (verify dist is truly ignored)
- Possibly add `.golangci.yml`
- **Approach**:
1. Fix Makefile to use `go run .` or build a platform-appropriate binary.
2. Add a `make lint` target and wire golangci-lint (with reasonable defaults + errcheck, gosec, etc.).
3. Update Gitea workflows to run `go vet`, lint, and (optionally) `govulncheck`.
4. Remove any committed files under `dist/` (or add them to `.gitignore` more aggressively and git-rm them).
5. Consider adding a coverage report step (even if not enforcing a hard gate yet).
- **Acceptance Criteria**:
- `make test` and `make run` work cleanly on Linux and macOS.
- CI runs lint + vet and fails the PR on new issues.
- Repository no longer contains built binaries in its tree.
- `go mod tidy` + build is reproducible.
- **Dependencies**: None
- **Effort**: Small-Medium (3-5 hours)
### P2-05: Use the existing rich error types consistently and improve observability
- **Description**: A nice `steamcache/errors` package with context, unwrap, retry classification, and client/server error helpers exists but is almost unused. Metrics are still very basic.
- **Impact**:
- Lost opportunity for better structured logging and error handling.
- Harder to write generic retry / circuit-breaker logic later.
- `/metrics` and logs give limited insight into what actually failed and why.
- **Approach**:
1. Audit the top 10-15 error sites in `ServeHTTP` and VFS layers.
2. Convert the most important ones to use `NewSteamCacheError*` helpers.
3. Wire more structured fields into zerolog calls using the error types.
4. Expand the metrics package (per-service error counts, upstream error breakdown, cache write failures, etc.).
5. Consider exporting Prometheus-style metrics in addition to the current text format (optional).
- **Acceptance Criteria**:
- At least the major error categories (upstream fetch, cache corruption, rate limit, validation) use the custom error types.
- `/metrics` surface becomes more useful (new counters for categories of errors).
- No behavior change for clients (still get the same HTTP status codes).
- **Dependencies**: P0-04 (basic error counting)
- **Effort**: Medium (4-6 hours)
## Definition of Done (P2 Milestone)
- [ ] Major refactoring (P2-01) landed in reviewable chunks; `ServeHTTP` is no longer a monster function.
- [ ] Eviction lock contention measurably reduced (P2-02).
- [ ] VFS/storage layer has ≥70% test coverage + benchmarks (P2-03).
- [ ] Build/CI hygiene is excellent: cross-platform make targets, lint in CI, clean repo (P2-04).
- [ ] Error handling and metrics are noticeably better (P2-05).
- [ ] `go test -race -shuffle=on ./...` + `go vet` + linter are all green.
- [ ] A new contributor can run `make help`, `make test`, `make run` on Linux/macOS without friction.
- [ ] At least one performance or quality regression has been prevented by the new tests/benchmarks during the work.
## Suggested Order of Execution (within P2)
1. P2-04 (CI hygiene) — cheap wins, improves confidence for everything else.
2. P2-03 (test coverage on VFS) — unblocks safe work on P2-02.
3. P2-02 (eviction locking).
4. P2-01 (big refactor) — do this when the test safety net is stronger.
5. P2-05 (errors + observability) — can run in parallel with others.
## Notes for Implementers
- P2 work has the highest risk of "refactoring for its own sake." Every change should be justified by either a concrete performance win, a maintainability win that reduces future bug rate, or enabling future features.
- Keep changes reviewable. Large refactors should be broken into multiple PRs with clear "no behavior change" invariants.
- The custom LRU list and sharded locking are clever — make sure any refactoring preserves their performance characteristics.
## References
- `steamcache/steamcache.go:1724` (file size)
- `vfs/disk/disk.go` and `vfs/memory/memory.go` eviction methods (global lock held)
- `.gitea/workflows/`
- `Makefile`
- `steamcache/errors/errors.go` (under-used)
---
**After P2**: The project should feel like a mature, professional Go service rather than a sophisticated prototype.