Repository navigation
fix(storage): retain unfinished ramp execution history - #904
Conversation
Protect held execution rows through every database DELETE path. Track monotonic ledger provenance for runtime enforcement in a follow-up change.
|
Warning Review limit reached
This review includes 3 billable files and costs up to $0.75.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 54 minutes for your next included review. View limit details
Comment |
|
@coderabbitai review |
|
|
@coderabbitai review |
|
cristim
left a comment
There was a problem hiding this comment.
Independent review of 17a3377: no actionable findings
Base is main (not stacked below this PR); #905 is stacked on it. origin/main is now d6ffd5d (#892, #899 merged). git merge-tree of origin/main with this SHA is clean, and neither merge touched migrations, so the verdict does not depend on a rebase.
Notes (all NON-BLOCKING)
- Merge order vs #896 (nit, but must be settled before merge).
000124_ramp_ledger_retentionis the next free number on main (max 000122). #896 (open, draft) claims 000123. golang-migrateUpstarts after the recorded version, so a database that reaches 124 first will never apply a later-merged 123. Merge #896 first, or renumber whichever lands second. Main has no gap guard. - Deploy note is imprecise (nit,
PR body). The DELETE guard and the legacy-default marker take effect for every writer the moment the migration runs, old Lambdas included: an old cleanup DELETE on a held row is silently skipped (fewer rows affected, no error), old INSERTs normalize tolegacy, and an old baseline write degradesnewtolegacy. Nothing in this PR reads the marker, so no drain is needed for this PR alone. The drain window matters for #905 (runtime refuses untrusted legacy plans; old attachers ignore the marker), and the note should say that. The reverse direction is the real hazard: #905 code against a pre-migration schema fails on the missing column, and a rollbackdowndrops the column while #905 is deployed. Down then up is lossy but fail-safe (everything returns tolegacy; covered byTestRampRetention_DownUpKeepsExistingLineageUntrusted). guard_ramp_ledger_stateruns on everypurchase_planswrite and casts JSON fields (::INT,::BOOLEAN) with no WHEN clause. Probed with array/scalar services, stringterm, floattotal_steps, stringenabled, null schedule: all inserts and updates succeed on legacy rows (the legacy branch short-circuits). Malformed JSON could only error onnew/trustedrows, which only the Go runtime writes. No action.
Money-path check: can retention delete history still needed to avoid a double purchase?
- Held rows of any plan whose
total_steps > 1 AND current_step < total_stepsare skipped by the BEFORE DELETE trigger (up.sqlretain_ramp_ledger_execution), for every writer:CleanupOldExecutions, the cleanup-lambda statement, raw DELETE. Status classification mirrorsholdsCapacity(unknown statuses count as held); parity test passes for canceled/cancelled/expired/failed with and withoutno_submission/future status. Empty,{},nulland[]recommendation payloads are retained. - Plan delete detaches rows via the existing
ON DELETE SET NULL(migration 33), so it still succeeds and the plan's own ledger is moot. Account delete is already RESTRICT (migration 53). Detached, immediate and finished-ramp history still expires. - Ledger trust cannot be re-armed through SQL: legacy never promotes, trusted never returns to new, baseline replace/clear, progress rewind, leaving the unfinished schedule, and shape changes degrade to legacy, except the narrow fresh-edit exception (new, step 0, baseline NULL, no held execution). Forged
ramp_ledger_state='trusted'on a legacy row is ignored. - Concurrency: the DELETE takes no parent lock (no lock-order reversal); an overlapping protected-exit sees the committed shape and over-retains.
TestRampRetention_DeleteOverlapsProtectedExitWithoutParentLockcovers it. - Residual, by design and outside this PR: old attachers still ignore the marker until #905 lands; this PR only guarantees the history is not lost meanwhile.
What I attacked
- Ran
go test -tags integration -run TestRampRetention ./internal/config/on the exact SHA against Postgres containers: 22 passed. - Mutation probes on
000124_ramp_ledger_retention.up.sql(each reverted):<to<=inramp_ledger_protected; dropped thefailed AND no_submissionexclusion inramp_ledger_holds; triggerRETURN NULLtoRETURN OLD; dropped the baseline-replacement degradation. All four made the suite fail. - Fail-before: without the migration the tests cannot run (functions and column absent); the original loss (completed row deleted at 31 days, ledger drops to 0) is the recorded RED in the plan, not re-run here.
- Probed trigger robustness against malformed JSON (note 3). Down/up cycle exercised by the test itself.
- Size: +305/-0, 3 files, test file 212 lines.
Gate status (origin/main Review gate)
- Independent review at this SHA: this comment.
- Findings resolved: none open. No unresolved threads. CodeRabbit shows "review rate limited" on two pings and has not reviewed; it is optional when exact-revision verification plus independent review cover the SHA.
- CI on 17a3377:
gh pr checksandgh run listshow every check success, including Unit, Integration, E2E, Lint, pre-commit, Security, and CI Success. - Labels: match #631 (priority/p1, severity/high, urgency/this-sprint, impact/many, effort/xl, type/feat, triaged).
- Local real-path evidence: PR body claims a local run against a real Postgres with all migrations applied; I reproduced it above. The body does not record the fail-before result or the mutation probes; this comment adds the probes.
- Verdict and reviewed SHA: recorded here. A new commit or a non-empty range-diff on rebase restarts the gate.
|
Gate record for 17a3377 (origin/main CLAUDE.md "Review gate"):
Merging at 17a3377 with --match-head-commit. #905 stays open with changes requested. |
What and why
Adds migration
000124_ramp_ledger_retentionso database DELETE paths (including cascades) cannot remove ramp execution rows that are still held by an unfinished ramp. It also tracks monotonic ledger provenance so the runtime can later tell whether a ramp history is verifiable. Without it, deleting an account or plan can silently erase the history that ramp sizing depends on.The runtime enforcement is in the stacked follow-up PR.
Stack position
1 of 2. Base:
main. Next: the runtime PR (basefix/631-retention-ledger-db).Verification (local, macOS)
go build ./...andgo test -race ./internal/config/... ./internal/purchase/...pass.store_postgres_retention_integration_test.goruns against a real local Postgres with all migrations applied (real DB, not mocks).Deploy note
Retention needs an old-Lambda drain window: Lambda versions that predate the runtime PR do not write the new provenance, so let them drain before relying on retention.
Refs #631
🤖 Generated with Claude Code