Repository navigation
test(purchase): assert multi-account fan-out parallelism structurally - #1219
Conversation
TestExecuteMultiAccount_RunsAccountsInParallel proved the parallel fan-out by timing it: each PurchaseCommitment mock slept 300ms and the whole executePurchase pipeline had to finish under 500ms, leaving a 200ms margin for scheduling on whatever runner executed it. A loaded CI runner or -race could eat that margin and fail a correct implementation. Replace the wall-time bound with a structural check. Each PurchaseCommitment call bumps an atomic in-flight counter, records the peak, and waits at a barrier until both accounts' calls have arrived. A parallel fan-out opens the barrier immediately and peaks at 2; a serial loop leaves the first call alone at the barrier until its 2s timeout and peaks at 1, so the test fails. The timeout bounds only the failure path; a passing run never waits on it, so the test drops from ~300ms to ~1ms. Closes LeanerCloud#38 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 2 billable files and costs up to $0.50. Or wait 59 minutes for your next included review. View limit details
Comment |
TestFanOutPerAccount_RespectsParallelismLimit only asserted that the peak in-flight count stayed at or below CUDLY_MAX_ACCOUNT_PARALLELISM. A serial fanOutPerAccount (peak 1) satisfied every assertion, so a regression to a serial loop, or a limit that stopped being honored on the low side, stayed green (audit finding A16-011 on LeanerCloud#38). The first `limit` calls now wait at a one-shot barrier that opens once `limit` calls are in flight, so a correct fan-out deterministically peaks at exactly the limit. The test asserts peak == limit, keeps the peak <= limit upper bound, and requires that no call timed out at the barrier. A serial or below-limit fan-out leaves the first call alone at the barrier until its 2s timeout, which then opens the barrier for the remaining calls, so the failure is reported after one timeout rather than one per account. A passing run never waits on the timeout. Refs LeanerCloud#38 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review 2 (claude-fleet, cold): APPROVE 5c02695 Scope: two test-only commits, 74135be test(purchase) and 5c02695 test(scheduler). No production code, migrations or exported APIs change. Base production code equals head, so the mutation probes below stand in for fail-before. Findings (none blocking):
Attacked: mutation probes on a scratch copy, each restored and confirmed identical with
Hang analysis:
Verified locally:
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 5c02695 Independent adversarial review of PR #1219 (closes #38) at 5c02695, base main. Test-only: internal/purchase/execution_test.go and internal/scheduler/scheduler_test.go. Both halves of #38 are addressed.
Reasoning checked: testify runs Findings: none. 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: none. I spot-checked the mutation table (serial probe) and the purchase parallelism=1 probe, and both reproduce as described. Reviewer: rB3. 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 |
Brings the branch up to date with main so the PR is mergeable again. Main's LeanerCloud#1219 (issue LeanerCloud#38) rewrote the same two parallelism tests this branch converts. Conflicted files: - internal/purchase/execution_test.go (TestExecuteMultiAccount_RunsAccountsInParallel): main's version is kept. It already replaces the 300ms sleep and the elapsed-time bound with an in-flight peak counter and a barrier whose timeout only bounds the failure path, which is what this branch did. The branch's duplicate is dropped, so this PR no longer touches the file. - internal/scheduler/scheduler_test.go (TestFanOutPerAccount_RespectsParallelismLimit): the branch's version is kept. Main's barrier version still holds each call with a 5ms sleep so an over-limit fan-out overlaps; the branch parks every call and reads the in-flight count once the fan-out is blocked in errgroup.Go, with no wall-clock wait. It takes main's `limit` constant for the env value and both exact-equality assertions, and its comment now cites LeanerCloud#38 next to LeanerCloud#36. The "strings" import stays for fanOutParkedOnLimit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause
Issue #38 has two halves.
TestExecuteMultiAccount_RunsAccountsInParallel(internal/purchase/execution_test.go) proved the parallel multi-account fan-out with a wall-clock bound. EachPurchaseCommitmentmock sleptperCallDelay = 300ms, and the wholeexecutePurchasepipeline had to finish underparallelMaxBound = 500ms. That leaves 200ms of scheduling margin on whatever runner executes it. The three cited spots (old lines 853/925/957, now 1177/1249/1280) are that test's constants, its sleep and itsassert.Less(elapsed, ...). The invariant is sound, but on a loaded runner or under-racea correct implementation can fail.TestFanOutPerAccount_RespectsParallelismLimit(internal/scheduler/scheduler_test.go) only assertedpeak <= 3. A serialfanOutPerAccountpeaks at 1 and passed every assertion. A regression to a serial loop, or a limit honored only on the high side, stayed green.Fix
Both changes are test-only. Each test now asserts parallelism structurally with an atomic in-flight counter, a CAS-loop peak tracker and a channel barrier. No production code changed.
Purchase (commit 74135be): each
PurchaseCommitmentcall counts itself in flight and waits at a barrier that closes when both accounts' calls have arrived. The test assertspeak >= 2and that no call timed out at the barrier. A parallel fan-out opens the barrier at once, so the test never sleeps on the passing path (~1ms, down from ~300ms). A serial loop leaves the first call alone at the barrier untilbarrierTimeout = 2s, the peak stays 1, and the test fails.Scheduler (commit 5c02695): with 20 accounts and
CUDLY_MAX_ACCOUNT_PARALLELISM=3, the first 3 calls wait at a one-shot barrier that opens once 3 calls are in flight. The test asserts:peak == 3, so the fan-out must reach the limit;peak <= 3(kept), so it never exceeds it;The existing 5ms hold after the barrier stays, so a fan-out that ignored the limit still pushes the peak above 3. On a timeout the barrier opens for all later calls, so a serial fan-out fails after one 2s timeout rather than one per account. A passing run never waits on the timeout (~40ms).
Verification
All evidence is unit/mock-level, with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullset because this machine's default AWS profile is invalid. Nothing touches real cloud APIs. Every mutation below was applied locally and reverted, and the final tree has no changes outside the two test files.Purchase: fail when the code under test is sequential
go test ./internal/purchase/ -run TestExecuteMultiAccount_RunsAccountsInParallel -count=3:CUDLY_MAX_ACCOUNT_PARALLELISM=1execution.RunForAccountsWithConcurrency(...)inexecuteMultiAccountreplaced by a plain serialforloopBoth probes fail with the same messages:
Scheduler: fail-before / fail-after
go test ./internal/scheduler/ -run '^TestFanOutPerAccount_RespectsParallelismLimit$' -count=3. Each probe mutatesg.SetLimit(execution.ConcurrencyFromEnv())infanOutPerAccount:SetLimit(min(ConcurrencyFromEnv(), 1))peak in-flight was 1, want 3;1 account call(s) waited 2s at the barrier without 3 calls in flightSetLimit(min(ConcurrencyFromEnv(), 2))peak in-flight was 2, want 3+ barrier timeoutSetLimit(-1)peak in-flight must not exceed ...;peak in-flight was 20, want 3SetLimit(execution.DefaultMaxConcurrency)peak in-flight was 20, want 3Stability and speed on the real code
go test -race -count=20 -run '^(TestFanOutPerAccount_RespectsParallelismLimit|TestExecuteMultiAccount_RunsAccountsInParallel)$' ./internal/scheduler/ ./internal/purchase/passes: schedulerok 2.308s, purchaseok 1.808s, 4.1s wall in total.-racebinaries with-test.v, 20 runs each:go test -count=1 ./internal/purchase/ ./internal/scheduler/ ./internal/mocks/passes.internal/mocksholds the repo-wide test guards.Note on reproducing the old purchase flake: the old purchase test did not flake locally under CPU contention. The case for that half rests on the 200ms margin argument in the issue, not on an observed local failure. Neither new test has a timing margin on its passing path. They only need the fan-out goroutines to reach the fake within 2s of each other.
Static checks
go build ./...andgo vet ./internal/purchase/ ./internal/scheduler/are clean.golangci-lintv2.10.1run --allow-parallel-runnerson./internal/purchase/...and./internal/scheduler/...reports 0 issues.gofmt -lis clean.gocyclo -over 10 -ignore "_test\.go"on both packages is silent (test-only changes).scripts/gosec-hook.shon both changed files is clean.git initrepo with an isolated HOME, afterscripts/setup-git-secrets.sh.--scanpassed on both changed files and on all tracked files, andscripts/test-git-secrets-allowlist.shgave 109 PASS, 0 FAIL.Out of scope
internal/purchase/execute_detached_test.go:106usestime.Sinceonly as a lower bound (> 30sdeadline headroom). Load can't make that fail, so it was left alone.Labels
Could a maintainer please mirror the issue's triage labels onto this PR:
impact/internal,type/chore,priority/p3,severity/low,urgency/eventually,effort/s.Closes #38
🤖 Generated with Claude Code