Repository navigation
Conversation
…r partial runs A run that ended partially_completed bought part of its commitments, but the approve funnel returned before minting the revocation token, so the row could not be revoked by link and nobody got the executed email. transitionApproveAndExecute now mints the token for a saved partially_completed run too (IsCommittedPartialRun: partial status and no audit loss) and returns it with the run's error. Session approve, direct execute and run-now send the executed email for such a run before the 502, and the 502 message says the run partially completed. The token approve emails it when ApproveExecution returns a token with its error; its status mapping is unchanged. The executed email marks a partial run and lists only the commitments it bought when the row records them. Refs LeanerCloud#532 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…flight sync run The stale-approved recovery (claimAndRedrive) claims only "approved" rows, and approve, direct execute and run-now hold their row "running" for the whole provider call (LeanerCloud#829), so the CAS to "running" is the single claimer. Replay the race on real PostgreSQL: a re-drive that read the row as approved arrives mid-run, loses the claim and buys nothing. Refs LeanerCloud#532 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.
Slices 2 and 4 of the #532 plan (plan comment, checked against main
feec673c). Slice 1 merged as #644. This PR leaves slice 3 (token-mode mapping) open, so it says Refs, not Closes.Refs #532
Status of each plan item on current main (
a14dc8f2)executeFailureError)executed_atbefore the claimManager.DirectExecutestamps after the CAS)ApproveExecutionreturns no row andtokenActionErrorfalls through to the raw errorApproveExecution/transitionApproveAndExecute(token pin in the CAS). Changing the same signature here would conflict, so slice 3 should follow #1242. This PR only adds the partial-run email on that path (below).transitionApproveAndExecutereturned beforerotateApprovalTokenon anyexecErrrunning,claimAndRedriveclaims onlyapproved, andTestSingleClaim_*covers cron/SQS. Not tested yet: the stale-approved re-drive arriving during a sync runRoot cause (slice 2)
executeAndFinalizereturns the partial sentinel for a run that bought some commitments and saved the rowpartially_completed.transitionApproveAndExecutetreated that like any other failure: it returned(row, "", err)before minting the revocation token. The handlers then answered 502 without the executed email. So a partially bought purchase had no revoke link, and the approver got no executed email. The row also kept its pre-approve approval-token hash.Fix
purchase: a newIsCommittedPartialRun(row, err)is true when the row ispartially_completedand the error is not an audit loss. On an audit loss the stored row is stillrunning. The tail oftransitionApproveAndExecutemoved intorunClaimedAndMint, which also mints the token for a committed partial run and returns it with the run's error. Completed runs, failed runs and audit losses behave as before.api: session approve, direct execute and run-now go through a newexecuteFailure. For a committed partial run it sends the executed email with the row the funnel returned and the minted token, then returns the sameexecuteFailureErrormapping. That is still 502 with{execution_id, status: "partially_completed"}, keeping the 502-for-partial decision from fix(purchase): map session approve and direct execute failures by cause #644. The message now reads "partially completed: some commitments were purchased and can be revoked, others failed".approveViaToken):ApproveExecutionalready passes the funnel's token through, so a token returned with an error marks a committed partial run. The handler emails it from the re-fetched row (the re-fetch moved intoemailTokenApproveOutcome). The response mapping is unchanged (slice 3).NotificationData.PartialPurchase. For a partial run, the subject, text and HTML say "partially executed" / "Not every commitment in this purchase went through", instead of "N commitment(s) were purchased successfully". For a single-account partial row, the email lists only the recs markedPurchased, with totals from those recs. A multi-account root has no per-recPurchasedflags, so it lists every rec under the partial notice.openapi.yaml: the approve and execute 502 descriptions now say a partially_completed run is revocable and emailed.No migration. No frontend change.
Slice 4 (approved-row claim race)
Since #833, the single claimer is the CAS to
running.TestSingleClaim_StrandedRedriveMidSyncRunCannotBuyAgainreplays the remaining case from the plan on real PostgreSQL, once each for approve, direct execute and run-now. During the sync run's provider call, aclaimAndRedrivethat saw the row asapproved(a stale listing) tries to claim it. It must lose and buy nothing, and the row must endcompletedafter exactly one provider call. This test passes on main (it pins #833) and fails under a mutation that lets the re-drive claimrunningrows.Verification
All local, Linux, Go 1.26.9. The integration tests use Docker testcontainers PostgreSQL with the repos' fake/mock providers and recording email notifiers. Nothing calls a cloud API. All tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null.Fail before, on
origin/maina14dc8f. I copied the new integration tests into a detached worktree of main. Assertions on symbols that don't exist there yet (IsCommittedPartialRun,PartialPurchase, the rec list) were removed from the copy.go test -tags integration -count=1 -run 'TestApproveAndExecute_PartialRunMintsRevocationToken|TestApproveAndExecute_FailedRunMintsNoToken|TestSingleClaim_StrandedRedriveMidSyncRunCannotBuyAgain' ./internal/purchase/failed withTestApproveAndExecute_PartialRunMintsRevocationToken: Should NOT be empty ... a partial run is revocable, so it needs a revocation token. The failed-run control and the slice-4 pin pass on main, as expected.go test -tags integration -count=1 -run TestRunNow_PartialRunEmailsRevocationToken ./internal/api/failed withNot equal ... a partial run must send the executed email(0 calls).claimAndRedriveclaiming{"approved","running"}fails in all three subtests.Pass after, on this branch.
RevocationWindow.go test -tags integration -count=1 -timeout 20m -run 'TestRunNow_' ./internal/api/: ok. The new test runs a real run-now through the handler, real manager and real store. One of two purchases fails. The test checks the 502, a single executed email, that the emailed token passesvalidateRevokeTokenagainst the stored row, the expiry window,PartialPurchase, and that only the bought rec is listed.go test -count=1on./internal/email/,./internal/purchase/,./internal/api/and./internal/mocks/: all ok.executeFailure: the session and direct partial tests fail.TestExecutedEmailCommitmentsfail.PartialPurchase: the three partial tests fail.IsCommittedPartialRun:UncommittedFailureSendsNoEmail/partial_audit_lossfails.go build ./...,go vet ./...andgo vet -tags integration ./...: clean. golangci-lint v2.10.1 oninternal/api,internal/purchaseandinternal/email, with and without--build-tags integration: 0 issues.gocyclo -over 10on the changed non-test files: silent.gofmt -l: clean.git merge-tree): no conflicts. The combined tree builds,go vet -tags integrationpasses oninternal/purchaseandinternal/api, and the unit tests for both packages pass.Combined-tree verification. This branch was tested together with six other queued branches on a local, never-pushed tree:
queue/platform@77d116c6= origin/maina14dc8f2plus these heads, merged in this order:Linux, Go 1.26.9,
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....--build-tags integration, andgocyclo -over 10is silent on the changed non-test files. These ran atb61a9c30, which differs from77d116c6only by one.gitallowedline on fix/issue-1233.-tags integration -count=1):cmd/cleanup-lambda,internal/analytics,internal/auth,internal/database/postgres/migrationsandinternal/purchase.internal/apipasses, except two tests whose Postgres container didn't start, or whose migrations hit a timeout, under machine load. Rerun alone on the same tree, both pass:TestDeleteManagedPlanByExecutionStatusandTestNotifiedRow_RescheduleDoesNotOverwriteConcurrentCancel.internal/configpasses exceptTestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).TESTCONTAINERS_RYUK_DISABLED=trueto avoid a testcontainers reaper race under load.scripts/setup-git-secrets.shpatterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Out of scope / notes
ApproveExecutionshould return the row so that post-claim failures on the email-link/SQS approve map to 502 with the row status. This should land after sec(purchase): pin the approval token in the token approve/cancel CAS; compare revoke token first #1242, which changes the same signature. Until then, a partial token approve sends the email but still answers 500.rotateApprovalTokenwrites the token with a full-rowSavePurchaseExecutionright after the terminal save. A session revoke landing in that window would be overwritten back to the terminal status. This already applied to completed runs and now also applies to partial ones. A status-guarded save (SaveExecutionIfStatuson the terminal status) would close it, but it touches many mock expectations.completed_at, so the email's "revoke until" line is blank. The partial path emails the returned row, which has it.Maintainer: please mirror the issue's triage labels on this PR:
priority/p2,severity/medium,urgency/this-quarter,impact/few,effort/l,type/bug,triaged.🤖 Generated with Claude Code