Repository navigation
Conversation
…loud#999) Migration 000143 adds outcome_settled_at, outcome_settled_by and outcome_settlement to purchase_executions, with a CHECK that they are set together. They hold an operator's audited settlement of a failed execution whose outcome is unknown. Settling never changes status or the ramp ledger's view of the row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lan can be deleted (LeanerCloud#999) SettleFailedExecution records an operator's acknowledged, reasoned settlement of a failed plan execution with no no_submission proof. It locks the plan FOR SHARE, then the row and its retry-chain tail FOR UPDATE, and refuses while the chain may still buy: a tail outside the terminal allowlist or a failed tail that can still be retried. Replaying the same request returns the stored settlement. The row stays failed and keeps counting as possibly bought in the ramp ledger. requireManagedDeleteTx skips a settled failed row, and LinkRetryExecutionAtomic refuses one, since a retry of a row detached from its deleted plan would buy with no plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng races (LeanerCloud#999) Covers the issue scenario (a failed row whose retry completed blocks the managed delete until settled; the ledger is unchanged), every refusal, chains of unknown outcomes, replay, the retry CAS, and the three orderings that matter: a retry that links first, a delete that holds the plan first, and a delete that queues behind a settle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilures (LeanerCloud#999) The endpoint needs execute:purchases, account access to the execution and the grant's constraints against what the execution would have bought (strict scope), a 10-300 character reason and acknowledge_unknown_outcome: true. It logs the settlement with the actor and credential kind. Retry's lost-race 409 now says the row may have been settled. Marks the known issue resolved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
Deleting an editor-managed plan is refused (409
managed_plan_has_live_executions) while any execution may hold or buy capacity.requireManagedDeleteTxcounts afailedrow without ano_submissionproof as live, because its outcome is unknown and it can still be retried (a retry of a row detached from a deleted plan would buy with no plan). Nothing could ever change that row: failed rows are never pruned, and the retry link isn't consulted. So even after a retry of the row completed, the plan could never be deleted, only disabled.Fix
As the issue asks, this adds an explicit, audited operator action and leaves delete without a flag. Delete still fails closed on an unknown outcome until a named person has accounted for it.
purchase_executions.outcome_settled_at,outcome_settled_by(TEXT, so the audit survives user deletion and records the admin API key sentinel) andoutcome_settlement(the complete fixed-format record), with a CHECK that the three are set together. 134 to 139 are taken by open PRs, and 140 to 142 by open PRs fix(recommendations): key stored recs, rec ID and purchase identity on RDS az_config (#408) #1239 and perf(api): page recommendations from an ID-ordered keyset scan instead of the full list #1240. Gaps are expected (previousMigrationVersion).SettleFailedExecution(internal/config/store_postgres_execution_settle.go):failedplan execution with nono_submissionproof. Any other status, ano_submissionrow, an ad-hoc row or a row whose plan is gone gets 409.Acknowledgeand a reason, and storesoperator:settled_unknown_outcome: ack=true chain_tail=<id> tail_status=<status> reason="...".FOR SHARE, then the rowFOR UPDATE, then its retry-chain tailFOR UPDATE. That's the same plan-first order asDeletePurchasePlanand the editor command footprint. It re-checksplan_idunder the locks.LadderChainMaxDepth.replayed: true). A different one gets 409.statusand never makes the row count as unbought.holdsCapacity, the ramp ledger (ListPlanCommittedTx),ramp_ledger_holdsand the retention trigger are untouched, so a possibly-submitted purchase is never hidden or re-bought. The row staysfailedin History after the plan is deleted (plan_id is set to NULL, as for completed rows today).RecoverReapedExecutioncan still record a late completion over it.requireManagedDeleteTxskips a settledfailedrow (blocksManagedDelete). Every other classification is unchanged.LinkRetryExecutionAtomicalso requiresoutcome_settled_at IS NULL, so a settled row can never be retried. Retry's lost-race 409 now reads "already retried or settled; reload it".POST /api/purchases/{id}/settle, body{"reason": "...", "acknowledge_unknown_outcome": true}:execute:purchases(admin:* alone is not enough, matching the separation-of-duties carve-out) and account access to the execution (out of scope gets 404).StrictScopeagainst what the execution would have bought: its recommendations' providers, services, regions and total commitment, plus its accounts. Any dimension the execution leaves unknown requires an unconstrained grant.purchase execution settled by operatorwith the actor, the credential kind and the stored record.known_issues/35_legacy_guard_followups.md: the feat(plans): settle a failed-with-submission row so a managed plan can be deleted #999 entry is marked resolved. The doc stays in place because its other entry belongs to the open PR fix(cleanup): only delete settled executions when their TTL lapses #1173. Whichever of the two merges second should move the doc toknown_issues/resolved/. The audit-status line will conflict between the two PRs, and it's a one-line merge.Closes #999
Verification
All of this ran locally on macOS against a real Postgres in Docker (testcontainers). It is unit and integration evidence, not real-cloud evidence. Tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid.New tests:
internal/config/store_postgres_execution_settle_integration_test.goTestSettleFailedExecutionLetsManagedDeleteProceedreproduces the issue scenario: a failed row whose retry completed blocks the delete. After a settle, the delete proceeds, the row is stillfailedand notno_submission, the audit survives, andListPlanCommittedTxreturns exactly the same ledger before and after.TestSettleFailedExecutionRefusalscovers completed, pending, canceled, failed+no_submission, retry tails pending/running/failed, no plan, not found, and no ack/actor/reason. None of them writes anything.TestSettleFailedExecutionChainOfUnknownOutcomeschecks that the head is refused while its failed tail can still be retried, and that the delete stays blocked until both are settled.TestSettleFailedExecutionReplayAndRetrycovers the replay, the 409 on a different reason, and the retry CAS refusing a settled row.pg_stat_activity:TestSettleWaitsForRetryAndRefusesItsLiveSuccessor: a retry linked first; the settle waits on the row lock, then refuses the pending successor.TestSettleWaitsForDeleteAndRefusesDetachedRow: a delete holding the plan lock commits first; the settle waits onFOR SHARE, then refuses the detached row.TestDeleteWaitsForSettleAndSeesIt: a delete queued behind a settle sees the committed settlement.internal/api/handler_execution_settle_integration_test.goTestSettleUnblocksManagedPlanDeleteOverHTTP: delete 409, thenPOST /api/purchases/{id}/settlethroughHandleRequest(routing and CSRF) returns 200, then delete succeeds.TestSettleAuthorizationAndValidation: admin without execute gets 403, out of account scope gets 404, execute constrained to another account or provider gets 403, a missing ack, short reason or unknown field gets 400, and a completed row gets 409. A refused settle writes nothing. Execute constrained to the plan's account succeeds.TestBlocksManagedDelete,TestSettlementRecordReplayMatchesOnlyTheSameReason,TestDecodeExecutionSettle,TestMapExecutionSettleError, plus the migration up/down/CHECK test.Fail-before / pass-after (mutation probes on this branch, each reverted afterwards):
...LetsManagedDeleteProceed,...ChainOfUnknownOutcomes,TestDeleteWaitsForSettleAndSeesIt...ReplayAndRetryplan_idre-check under the locksTestSettleWaitsForDeleteAndRefusesDetachedRowFOR SHARElockTestSettleWaitsForDeleteAndRefusesDetachedRow...Refusals,...ChainOfUnknownOutcomes...RefusalsTestSettleAuthorizationAndValidationOn main,
TestDeleteManagedPlanByExecutionStatus/failedalready shows the permanent 409, and no operation existed that could clear it.Coverage gaps: nothing here touches a real cloud account. The operator's cloud-side check stays a human step, recorded only as the free-text reason and the acknowledgement. The full
internal/apiintegration package was not run, only the subset above.Out of scope
blocking_execution_idsto the refusal details would be a small follow-up.Frontend follow-up for the UI owner
managed_plan_has_live_executionsfrom plan delete, explain that failed purchases with an unknown outcome must be settled first, and link to the plan's failed executions in History.failedplan execution that isn'tno_submission, offer Settle next to Retry. It should open a confirm dialog that asks for a reason (10 to 300 characters) and a required "I checked the cloud account; this may have been bought, and it will never be retried" checkbox. It then sendsPOST /api/purchases/{id}/settlewith{"reason", "acknowledge_unknown_outcome": true}. On 409, show the message (for example, a retry is still pending).Maintainer: please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,triaged,urgency/this-quarter,impact/few,effort/m,type/feat.Known unrelated CI failure: govulncheck (GO-2026-6809) until #1236 merges.
🤖 Generated with Claude Code