Repository navigation
chore(api): remove dead submit-dedupe code and lock the retry-successor path - #1217
Conversation
…or path persistRetrySuccessorTx inserts a web-source pending row for the retrying user and the failed row's recommendations, the same fingerprint an identical fresh submit carries, but it took no per-creator submit lock and ran no duplicate check. A retry racing (or following) an identical submit by the same user left two approvable pending rows. The retry transaction now starts with ListRecentSubmitsTx, the same per-creator advisory lock at the same point as persistExecutionAndSuppressions, and after the link CAS refuses with a 409 naming the existing execution when the creator already has a live submit with the successor's fingerprint inside the idempotency window. The lock comes before the ladder tranche lock; the submit path takes only this lock and the reconciler never takes it, so no new lock-order cycle. Also removes findDuplicatePendingExecution and GetPendingExecutionsTx (interface, Postgres store, MockConfigStore and the server/analytics fakes), which have had no production caller since LeanerCloud#761, and moves the remaining cases of TestFindDuplicatePendingExecution onto matchDuplicateInList and persistExecutionAndSuppressions. Closes LeanerCloud#760 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 11 billable files and costs up to $2.75. Or wait 8 minutes for your next included review. View limit details
Comment |
|
Independent review 2 (claude-fleet, cold): APPROVE f30184e Findings (none blocking):
Dead-code claim: confirmed.
Against #760:
Attacked:
Verified locally on the merged tree, with GOTOOLCHAIN=go1.26.10 and no cloud credentials, under the heavy lock:
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
VERDICT: APPROVE f30184e Independent adversarial review of PR #1217 (closes #760) at f30184e, base main. Delta reviewed: The fix addresses both halves of #760. Findings (none blocking):
Attacked:
|
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rB2. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on |
…nd e2e findings (#1270) * chore(test): fix the 2026-07-28 review's mock, CI-doc, known-issues and e2e findings - internal/mocks: a mistyped .Return value no longer panics when the test registered itself with the mock's Test method; it fails that test (Errorf + FailNow) under the method's name. 82 of the 83 type-assertion sites in MockConfigStore, MockAuthStore and the Secrets Manager, SES and SNS mocks go through testReporter (GetPendingExecutionsTx is left alone: open PR #1217 deletes it). Without a registered test the mock still panics, now naming the method. - frontend-build.yml: drop the paths filter so `Build frontend` reports on every PR (a path-filtered required check blocks every PR outside its paths); a fail-closed first step skips the build when nothing under frontend/ changed. docs/ci-required-checks.md explains why. - known-issues.md: the #731 entry named a role the ARM template does not define and a stale branch; correct the name and move it to Resolved, pointing re-deploys at the #1545 revoke-first steps. - tests/e2e: TestVersionEndpoint asserts version, git_sha and build_time instead of any non-empty JSON object. Refs #353 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(mocks): fail the test on a mistyped HistorySummaryAggregate return #1200 added HistorySummaryAggregate with the old panic on a mistyped .Return, which ends the whole test binary. Route it through wrongReturnType like the other sites so a registered test fails under the method's name instead. Refs #353 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings the branch up to date with main so the PR is mergeable again. Conflicted files: - internal/analytics/collector_test.go: main edited the hand-rolled mockConfigStore stubs this branch deletes. LeanerCloud#1299 added GetMarketplaceListingAttempt and ResolveMarketplaceListingAttempt, and LeanerCloud#1217 removed GetPendingExecutionsTx. The branch's version is kept, where mockConfigStore embeds mocks.MockConfigStore and overrides only GetActivePurchaseHistory. Main's two PRs made the matching changes to mocks.MockConfigStore, so the embedded mock already has the new methods and lacks the removed one. The file is identical to the branch head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ure account once, validate registration email (#1285) Three of the four internal/api findings in issue #311: - revokeViaSession no longer binds TransitionExecutionStatus's returned row only to discard it, and its doc comment now says how CancelledBy is written and that neither revoke path applies the 4-eyes policy. - Delete the two bare t.Skip revoke+4-eyes placeholders; revocation is not dual-controlled, and whether it should be is an owner decision. - The Azure exchange execute path looks the subscription's CloudAccount up once and uses that row for the scope check, the client credentials and the constraint AccountIDs, instead of three lookups that could disagree. - POST /api/register validates contact_email with validateEmailFormat and stores the bare address, instead of accepting any 5+ character string containing "@" (which stored "a@b\r\nBcc: x@y"). The findDuplicatePendingExecution removal is left to the open #1217, which already deletes it. Refs #311 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
What
Two follow-ups left by #761 (atomic submit dedupe, #758):
persistRetrySuccessorTxinserts acudly-webpending row for the retrying user with the failed row's recommendations and capacity. That is the same fingerprint an identical fresh submit by that user carries. It took no per-creator submit lock and ran no duplicate check, so a retry racing an identical submit (or arriving just after one) left two approvable pending rows for the same purchase.findDuplicatePendingExecutionandGetPendingExecutionsTxhave had no production caller since fix(api): make the purchase submit duplicate guard atomic #761 movedexecutePurchaseontoListRecentSubmitsTx.Root cause and fix
The retry transaction (now
writeRetrySuccessorTx, called frompersistRetrySuccessorTx) does this:ListRecentSubmitsTx(creator, now-2m). This takes the samepg_advisory_xact_lock('purchase-submit:'+creator)at the same point aspersistExecutionAndSuppressions, then reads the creator's live web submits. The creator is the successor'sCreatedByUserID(the retrier), and is empty for API-key sessions, exactly as on the submit side.purchaseIdempotencyKey+matchDuplicateInList, the same predicate the submit uses), the tx rolls back. The caller gets 409an identical purchase was submitted moments ago as execution X; act on it instead of retrying Ywithdetails.duplicate_execution_id = X. No suppressions are written and no approval email is sent.Why the lock alone is not enough: with the lock and no check, a submit that commits first is followed by a retry that still inserts. The mutation probe below shows this. Why the check alone is not enough: without the lock, both sides read before either commits. The probe below shows this too.
Design notes:
guardLadderTrancheForRetry's comment is updated to say this.already retried; act on its descendant409, not the duplicate 409. Existing tests that assert that message pass unchanged.idempotency_key, not the predecessor's (fix(plans): refuse Add Purchases and account changes on managed plans #1012). Linking the failed row to it would misstate the lineage.Details), the retry fails with a 500 rather than skip the check. The same recs already fail key derivation on the submit side.Dead code removed:
findDuplicatePendingExecution;GetPendingExecutionsTxfromconfig.StoreInterface,PostgresStore,mocks.MockConfigStore, theinternal/serverfake (mockConfigStoreForHealth) and theinternal/analyticsfake (mockConfigStore). Its two lines in thecreated_by_systemintegration test are dropped; the other readers there still cover the flag. Before removing it I grepped the repo and the diffs of all 80 open PRs. None referenceGetPendingExecutionsTx,findDuplicatePendingExecution,persistRetrySuccessorTx,ListRecentSubmitsTxormatchDuplicateInList.Test move:
TestFindDuplicatePendingExecutionis deleted.TestMatchDuplicateInList(added in #1030) already covers its window, creator, source and rec-set cases. The two cases it did not cover are now inTestMatchDuplicateInList_ReturnsTheMatchingRow: the scan returns the matching row by ID from a mixed list, and an undecodable candidate is skipped without ending the scan. The "lookup error is surfaced" case now runs against the live path inTestPersistExecution_RecentSubmitsErrorIsSurfaced.Mock change:
MockConfigStore.ListRecentSubmitsTxwith neitherListRecentSubmitsTxnorGetPendingExecutionsregistered now returns no rows instead of panicking. Without that, 19 existing retry unit tests would each need a stub, because the retry path now reads recent submits. Submit tests that registerGetPendingExecutionsbehave as before.No migration. No frontend change. The 409 is a normal client error, and the retry button already surfaces client-error messages.
Verification
All local on macOS, with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid. No cloud calls, purchases or email.Race test on real Postgres (testcontainers, Postgres 16, migrations to 000133):
internal/api/handler_purchases_retry_submit_race_integration_test.go,TestRetrySuccessorRacesFreshSubmit. Each round seeds a failed web execution owned by a real user. It then runspersistRetryExecution(the production successor builder) andpersistExecutionAndSuppressionsfor an identical fresh submit by the same user concurrently. Rounds alternate which side starts first. Each round asserts:pendingrow;40P01on either side;There are two modes:
Fail before (handler file swapped for
origin/main's, test unchanged): both modes FAIL in round 0 withexpected: 1, actual: 2pending rows.Mutation probes (each applied alone, then reverted):
RefusesIdenticalLiveSubmitandRecentSubmitsErrorFailsRetryfailOther tests (one package at a time):
go test -short -count=1for./internal/api/,./internal/config/,./internal/mocks/(repo-wide test guards),./internal/server/,./internal/analytics/,./internal/purchase/: okgo test -race -short -count=1 -run 'Retry|Dedupe|Duplicate|PersistExecution' ./internal/api/: okgo test -tags integration -count=1 ./internal/api/(whole package, 270s, including the existing retry, ladder-reconcile, managed-plan race and submit-dedupe integration tests): okgo test -tags integration -count=1 -run CreatedBySystem ./internal/config/: okinternal/api/handler_purchases_retry_dedupe_test.go(mock store): an identical live submit gets a 409 withduplicate_execution_id, no suppressions and no email; a different live submit lets the retry through; a read error fails the retry before any write.Static checks:
go build ./...,go vet ./...,go vet -tags integration ./internal/...: clean--allow-parallel-runnerson api/config/mocks/server/analytics, with and without--build-tags integration: 0 issuesgocyclo -over 10 -ignore "_test\.go" internal/: silent (writeRetrySuccessorTxis 9)--scanof every changed file, run in a freshgit initrepo with an isolated HOME and current main'ssetup-git-secrets.shand.gitallowed: clean.scripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAILEvidence level: real Postgres through a test harness, plus mock-store unit tests. I did not run this against the deployed stack or through the browser UI.
Out of scope
docs/audits/codebase-audit-2026-09-02.mdstill mentions the removed functions (A01-015, A04-005). I left it alone because it is a dated audit record.// executePurchase handles direct purchase execution from recommendationscomment sits abovematchDuplicateInList. It is pre-existing, so I did not touch it, to keep the diff clear of sec(api): bound update-any by its Constraints on planned-purchase run #1192.Maintainer: please mirror the issue's triage labels on this PR:
impact/internal,type/chore,triaged,priority/p3,severity/low,urgency/eventually,effort/s.Closes #760
🤖 Generated with Claude Code