Skip to content

fix(purchase): claim the exact managed schedule root - #908

Merged
cristim merged 1 commit into
mainfrom
fix/631-m2-claim
Oct 16, 2026
Merged

cristim merged 1 commit into
mainfrom
fix/631-m2-claim

Conversation

@cristim

@cristim cristim commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

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 ./... and go test -race ./internal/config/... ./internal/purchase/... on this tip (macOS).
  • Fixture-backed: native PostgreSQL 17 store with the real Manager caller; recommendation resolver and email are recordings; no real provider purchases. Includes a two-client lock-wait regression. Not a real-account scenario.
  • Out of scope: ordinary GET/List clock derivation.

Stack (M2 chain, merge in order)

  1. scheduling PR: fix(plans): preserve managed next-root dates on completion #906
  2. allocation PR: fix(plans): allocate marked ramps by exact step count #907
  3. this PR (fix(purchase): claim the exact managed schedule root #908, base allocation branch)
  4. target-met PR: fix(plans): lock the plan before completing a target-met step #909

Refs #631

🤖 Generated with Claude Code

@cristim cristim added urgency/this-sprint Within the current sprint triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm impact/many Affects most users effort/xl Multi-week / refactor type/feat New capability labels Oct 15, 2026
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 6 billable files and costs up to $1.50.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 63 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 60a2acdb-87bb-4530-82af-ad078bb3f9c9
📥 Commits

Reviewing files that changed from the base of the PR and between 2e7a535 and 67eed57.

📒 Files selected for processing: 6
  • internal/config/store_postgres_managed_root_integration_test.go
  • internal/config/store_postgres_plan_schedule_clock.go
  • internal/purchase/notifications.go
  • internal/purchase/plan_step_attach.go
  • internal/purchase/plan_step_managed.go
  • internal/purchase/plan_step_managed_integration_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim force-pushed the fix/631-m2-allocation branch from 7d9c807 to 03144c3 Compare October 15, 2026 23:35

@cristim cristim left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

  1. internal/purchase/plan_step_managed.go:31 root.Status == "scheduled" is redundant. managedRootClockAvailable (store_postgres_plan_schedule_clock.go) and adoptableRootRow (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.
  2. 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.
  3. 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 through getOrCreateExecution at 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, and ResolveManagedPlanRootTx adds FOR UPDATE on 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 UPDATE from ResolveManagedPlanRootTx makes TestManagedClaimWaitsForOutcome fail ("Condition never satisfied": the second client never waits on the execution row). Killed.
  • Mutation probe 2: the scheduled guard, 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. NextExecutionDate is only set on the local copy of the locked plan (plan_step_managed.go:36). Nothing in plan_step_*.go writes 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 replacement case). 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 clock and past clock cases). The cost is nit 2. I found no path that skips a purchase that should happen except the deliberate fail-closed cases.
  • Eligibility: managedPlanUnchanged keeps 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 Manager fixtures 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.

@cristim
cristim force-pushed the fix/631-m2-allocation branch from 03144c3 to cf12d41 Compare October 16, 2026 00:30
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Rebased onto the rebased #907 (cf12d41). Pure rebase, git range-diff 03144c39..ae15eeba cf12d41a..06a3368e:

1:  ae15eeba = 1:  06a3368e fix(purchase): claim the exact managed schedule root

Empty range-diff, so the verdict on ae15eeb carries to 06a3368. Retarget to main after #907 merges.

@cristim
cristim changed the base branch from fix/631-m2-allocation to main October 16, 2026 00:50
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.
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main after #907 merged (squash 2e7a535); retargeted to main. Pure rebase, git range-diff cf12d41a..06a3368e origin/main..67eed572:

1:  06a3368e = 1:  67eed572 fix(purchase): claim the exact managed schedule root

Empty range-diff, so the independent verdict on ae15eeb (carried to 06a3368) carries to 67eed57. Gate still needs CI green on 67eed57.

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

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.

@cristim
cristim merged commit 4f97e95 into main Oct 16, 2026
25 checks passed
@cristim
cristim deleted the fix/631-m2-claim branch October 16, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xl Multi-week / refactor impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant