Repository navigation
fix(plans): preserve managed next-root dates on completion - #906
Conversation
Use the canonical logical next root for managed ramp completion and target-met advancement. Stop on missing or unsafe roots, reject duplicate roots, and preserve raw plan metadata through a narrow progress write.
managedClockSibling built the other plan with newRampFixture, which starts a separate Postgres container, so the incoming retry link lived in another database and incoming_other_plan failed. Clone the plan inside f.store instead. Drop managedClockFixture's total parameter, which every caller set to 3 (unparam). Refs #631 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@coderabbitai review |
|
…cision CI on Linux keeps nanoseconds in time.Now, Postgres stores microseconds, so the exact Equal on the legacy clock failed there. Compare within a millisecond. Refs #631 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
CR round 1: zero actionable items. The docstring-coverage pre-merge warning is dismissed: this repo's comment policy is no comments unless they state a why the code cannot, so we do not add docstrings to hit a percentage. CI is green on 36d385e (the earlier Integration Tests failure was a nanosecond-vs-microsecond time comparison in the new test, fixed in 36d385e). Stacked PRs #907-#909 get no CI or CR run until their base is main, so they are re-gated after each parent merges. |
cristim
left a comment
There was a problem hiding this comment.
Independent review of 36d385e: no actionable findings
Base used: origin/main (d6ffd5d, includes #892 and #899). This PR is the stack root, base is main. Diff is 4 files, +319 additions (under the 400 cap). git merge-tree --write-tree origin/main 36d385eb is clean, so the moved main causes no conflict.
Findings (both non-blocking)
- Nit / follow-up,
internal/config/store_postgres_plan_schedule_clock.go:67-77: the next-root read (ListExecutionsForPlanStepTxplus the incoming-retryEXISTS) is not locked against writers ofpurchase_executions. The plan row lock serializes completions with each other, but a reschedule, cancel or new retry link on the step N+1 root that commits between the read and the commit leaves a stalenext_execution_date. This is acknowledged in the plan (plans/631-managed-scheduling-m2-20261015.md, generic-writer cuts) and is not reachable yet:dates_managedis only settable by SQL, because the APIPlanRequestrebuildsRampSchedulefrom preset fields (internal/api/types.go:832-870). Please confirm #907-#909 or a later cut own it before the marker is exposed. - Nit, same file
:30-32: a blocked or missing root writesnext_execution_date = NULLwith no log line or reason, so an operator sees a managed plan that silently stops. The tests assert this as intended. A debug or warn log naming the blocking cause would help.
Date handling (the requested attack)
- Timezone, month-end, leap year: the new code does no date arithmetic. It copies
PurchaseExecution.ScheduledDate(atimestamptzcolumn, migration 000001:79) intopurchase_plans.next_execution_date(alsotimestamptz, 000001:67) as an instant. There is noAddDate,Truncateor local-zone conversion in the diff, so month-end and leap-year drift cannot arise here. The only formula use is the unchanged unmanaged branch (GetNextPurchaseDate). - Completion racing the scheduler:
CompletePlanStepandCompleteTargetMetStepTxboth takeLockPurchasePlanTx(FOR UPDATE) beforeadvanceRampStepandpersistRampPosition; the managed write is a compare-and-set onupdated_at, returningErrPurchasePlanConflicton mismatch. Two concurrent completions cannot both advance (advanceRampSteprejects the second viaErrRampStepCountedBySibling). Two non-canceled canonical roots produceErrPurchasePlanConflictand roll back the progress (test "duplicate" asserts step stays 1). The residual unlocked-read window is finding 1. - Preserved date in the past: possible (a pending or scheduled root whose date has passed is copied as is). It cannot trigger a purchase from the plan date. Purchase eligibility is per execution row (
internal/purchase/manager.go:900skips rows not yet due; a due row is the existing root, independent of this PR). The plan-date consumers on main are guarded:plan_step_attach.go:93refuses an existing row once the plan date has passed,:104leaves an empty root due inside the veto window to the scheduler, andattachScheduledDate(:352-357) never schedules earlier than now + NotificationDaysBefore.shouldRetryOverduePlan(notifications.go:97) treats a past date as overdue only until the grace window ends, the same as the legacy formula date. A NULL date makesplanUnchangedfail (plan_step_attach.go:133), so a stopped clock skips rather than buys.
What I attacked
- Ran
go test -tags integration -run TestManagedCompletion ./internal/config/at the head SHA with testcontainers Postgres 16: all 3 test functions and all subtests pass;go build ./...andgo vet -tags integration ./internal/config/clean. - Mutation 1 (remove the 3-line
DatesManageddelegate inpersistRampPosition, equivalent to parent behavior):TestManagedCompletionKeepsNextRootAndRawPlanfails for both "bought" and "target met" ("Should be true" on the date equality). This is the fail-before proof; the tests set the marker via raw SQL, so they compile and run on the parent too. - Mutation 2 (drop the incoming
retry_execution_idcheck):StopsAtBlockedNextRoot/incoming_other_stepand/incoming_other_planfail. - Read
managedPlanNextRootagainst the table of cases in the test (missing, canceled, failed, paused, legacy, duplicate, zero date, delayed-fire, executed/completed/skip/purchased provenance, canceled predecessors, live child, replacement): each maps to a branch and is exercised. Finished ramp returns nil before the marker check (planScheduleNextDateTx:37-39).jsonb_set(... '{current_step}')leaves unknown nested ramp/service metadata and the baseline untouched (asserted byte-equal). - Not run: mutation probes beyond these two (optional per owner rule); the unmanaged path is covered by the existing suite plus the legacy assertion in
TestManagedCompletionFinishedAndLegacyClock.
Gate status (origin/main Review gate)
- Independent review: this comment, at 36d385e.
- Findings resolved: none actionable; two nits above.
- CI on SHA:
gh pr checks 906shows 25/25 SUCCESS; runs for this commit: "CI - Build & Test" and "pre-commit" both success. - CodeRabbit: commit status "Review completed" success on 36d385e; 0 inline comments and 0 review threads, so none unresolved. The author's dismissal of the docstring-coverage warning is on the PR; I agree with it given the repo comment policy.
- Local real-path evidence on the PR: the body states
go buildandgo test -race ./internal/config/... ./internal/purchase/...on macOS with native PostgreSQL fixtures, labeled as not a real-account scenario. I reproduced the config integration tests above. I did not re-run./internal/purchase/.... - Labels: match issue #631 (priority/p1, severity/high, urgency/this-sprint, impact/many, effort/xl, type/feat, triaged). The PR closes no issue (
Refs #631), so the mirror is against #631. - Verdict recorded: this comment. Any new commit or rebase restarts the gate; an empty
git range-diffafter a pure rebase keeps this verdict.
|
Gate record for 36d385e (origin/main CLAUDE.md "Review gate"):
Merging at 36d385e with --match-head-commit. #907 will be retargeted to main and rebased next. |
What
Persist and preserve the managed next-root date when a ramp step completes (
dates_managed), so completing a step throughCompletePlanSteporCompleteTargetMetStepTxno longer replaces an uneven managed schedule with the legacy formula. Also fixes the same-store sibling fixture (managedClockSiblingstarted a second Postgres container, so the incoming-retry link lived in another database) and drops the unusedtotalfixture parameter.Why
M2 managed-ramp chain for #631: the managed schedule root dates must survive step completion before allocation, claim and target-met can rely on them.
Local verification
go build ./...andgo test -race ./internal/config/... ./internal/purchase/...on this tip (macOS).Stack (M2 chain, merge in order)
main)Refs #631
🤖 Generated with Claude Code
Summary by CodeRabbit