Repository navigation
fix(purchase): claim the exact managed schedule root - #908
Conversation
|
Warning Review limit reached
This review includes 6 billable files and costs up to $1.50.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 1 minute for your next included review. View limit details
Comment |
|
@coderabbitai review |
|
7d9c807 to
03144c3
Compare
a808bb4 to
ae15eeb
Compare
cristim
left a comment
There was a problem hiding this comment.
Independent review of ae15eeb: no actionable findings
Base tip used: 03144c3, which is the current head of #907. The stack is consistent. Diff reviewed: 03144c39...ae15eeba (+396/-1; production +95, tests +301; plan_step_managed.go is 58 lines).
Nits (non-blocking):
internal/purchase/plan_step_managed.go:31root.Status == "scheduled"is redundant.managedRootClockAvailable(store_postgres_plan_schedule_clock.go) andadoptableRootRow(notifications.go:285) already refuse scheduled roots. Mutation probe: dropping the status check from the guard leaves the new tests green. Harmless defence in depth.- Skips are silent. A managed plan whose next root is missing, blocked or duplicated returns
errPlanStepSkipped, which is logged at debug only. For an auto-purchase plan that is a skipped purchase with no alert: the old code materialized a missing root, this code does not. This is deliberate (fail closed, no root creation in this leaf), but a Warn log or metric would make a stuck managed plan visible. Follow-up, not a blocker. - Dependency on #909:
CompleteTargetMetStepTx(store_postgres_ramp_target_met.go:73) still writes the root before locking the plan, which is the opposite order to the new plan-then-executions lock here. Worst case is a Postgres deadlock abort (40P01) on one transaction, not a double purchase. #909 is listed as the next PR in the stack; keep the merge order.
What I attacked:
- Concurrent claimers: a scratch test (not committed) ran 8
Managers throughgetOrCreateExecutionat once on one managed plan with an empty pending root, against native Postgres via testcontainers. Result: 1 execution row, resolver called once, all 8 callers got the same root ID. The provider advisory lock and plan row lock serialize them, andResolveManagedPlanRootTxaddsFOR UPDATEon the step's rows. This test passes at the parent too, because plan-lock serialization already existed. The new value is exact-root selection, not serialization. - Mutation probe 1: removing
FOR UPDATEfromResolveManagedPlanRootTxmakesTestManagedClaimWaitsForOutcomefail ("Condition never satisfied": the second client never waits on the execution row). Killed. - Mutation probe 2: the
scheduledguard, see nit 1. Survives. - Stale claim after a crash: the claim persists nothing on a skip. All locks are transaction-scoped and released on connection loss.
NextExecutionDateis only set on the local copy of the locked plan (plan_step_managed.go:36). Nothing inplan_step_*.gowrites it back. No stale claim state exists. - Missed root after a re-plan: roots with a canceled predecessor that took no action plus one healthy replacement are adopted (the
replacementcase). A canceled predecessor that acted, duplicates, children, retry lineage and incoming or outgoing retry links stop the claim. A stale or nil cached plan date no longer blocks the claim (nil clockandpast clockcases). The cost is nit 2. I found no path that skips a purchase that should happen except the deliberate fail-closed cases. - Eligibility:
managedPlanUnchangedkeeps Enabled, AutoPurchase, provider set and CurrentStep, and adds an UpdatedAt check. Dropping the cached-date check does not weaken any non-date predicate. A finished plan with an extra stored root is refused. - Fail-before/pass-after: I copied the PR's integration tests onto the parent (03144c3) and ran them. They fail there: Conservative roots (missing, duplicate, lineage, outgoing, canceled acted, executed, completed, purchased, skip, zero date, pending timer, incoming step, incoming foreign), Eligibility (finished, version, step, marker, nil clock, past clock), ManualDue, and WaitsForOutcome. At the PR head,
go test -tags integration -run TestManaged ./internal/purchase/ ./internal/config/passes in both packages. - Real-path scope: the evidence is fixtures against real Postgres and the real
Manager, with a recording resolver and sender. No provider purchases and no deploys were run.
Gate status against origin/main "Review gate":
- Independent review: this comment, at the SHA above.
- Findings resolved: none actionable; nits above.
- CI and CodeRabbit: not applicable until retarget. The PR targets fix/631-m2-allocation, and CodeRabbit skipped the review for that reason. The CI and CodeRabbit gate restarts on retarget to main.
- Local real-path verification: PR body states native PG17
Managerfixtures and a lock-wait regression, and I reproduced both. Real-account verification is not covered. - Labels: mirror the triage set (priority/p1, severity/high, urgency/this-sprint, impact/many, effort/xl, type/feat, triaged). The PR has no closing-issue reference (
Refs #631), so I could not compare against a closing issue. - Size: 396 additions, within the 400 cap; the 500-line file limit holds.
03144c3 to
cf12d41
Compare
ae15eeb to
06a3368
Compare
Resolve managed claims and manual due notifications from the locked next root instead of a cached plan date. Stop on missing or occupied roots and recheck outcomes after waiting for execution locks.
06a3368 to
67eed57
Compare
|
Rebased onto main after #907 merged (squash 2e7a535); retargeted to main. Pure rebase, Empty range-diff, so the independent verdict on ae15eeb (carried to 06a3368) carries to 67eed57. Gate still needs CI green on 67eed57. |
|
Gate record for 67eed57 (origin/main CLAUDE.md "Review gate"):
Merging at 67eed57 with --match-head-commit. #909 will be retargeted to main and rebased next. |
What
Resolve managed claims and manual due notifications from the locked next schedule root instead of a cached plan date. Stop on missing or occupied roots and recheck outcomes after waiting on execution locks.
Why
M2 managed-ramp chain for #631: a claim must bind to the exact managed root, otherwise it can adopt a wrong root or materialize a missing one.
Local verification
go build ./...andgo test -race ./internal/config/... ./internal/purchase/...on this tip (macOS).Managercaller; recommendation resolver and email are recordings; no real provider purchases. Includes a two-client lock-wait regression. Not a real-account scenario.Stack (M2 chain, merge in order)
Refs #631
🤖 Generated with Claude Code