[omp major] Unbounded goroutine spawn: acquire-after-go in fan-out paths #153
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#153
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
[omp major] Unbounded goroutine spawn: acquire-after-
goin fan-out pathsinternal/notify/emit.go:322-342,tasks.go:364-389,webhooks.go:360-376: one goroutine per recipient/hook spawned BEFORE the cap-8 semaphore acquire; recipient set and hook count are uncapped. A burst (large team mention, many hooks) spawns unbounded goroutines first and throttles second.Fix
Acquire before
go(or errgroupSetLimit): bound in-flight fan-out work by construction. Regression test (large recipient set → bounded goroutine delta). Coverage gate holds.Acceptance criteria
-race.Fix ready for review: #159 (branch fix/issue-153). Acquire-before-spawn on resolve/createAll/fanoutOne/DeliverRepo; regression tests fail pre-fix (deltas 201/201/201/89) and pass after; package suite green -race, coverage 96.0%.
Review of PR #159 (fix/issue-153,
0704ebf): acquire-before-go on all four fan-out sites.No leaked-slot path found: every acquired slot has exactly one paired deferred release in the spawned goroutine; every unacquired path releases nothing.
Budget-expiry accounting preserved — PASS: compared against origin/main. createAll pre-fix counted semaphore-stranded workers failed inside the goroutine; post-fix counts never-launched recipients failed in the parent (same failed=true). fanoutOne identical via fail() on both parent-expiry and worker fctx.Err paths. DeliverRepo best-effort skip semantics unchanged (unlaunched hooks abandoned either way). resolve has no failed flag; dropping remaining recipients on request-cancel (vs pre-fix blocking sem<- with no ctx escape, which leaked) is strictly better.
No deadlock — PASS: no site holds any lock across the parent acquire (mu taken only briefly for failed++/add, never across sem<-; team-expansion path continues before acquire). sem is per-call local, never re-acquired by createOne/deliverHook/validPrincipal (no nested acquire, no pool-exhaustion cycle). Callers (emit :243 with fctx, drainFanout tasks.go:332 with Background, StartWebhooks tasks.go:287 detached ctx) hold no repo/task locks across the call. notify takes no syncMu/packMu/rw at all. Parent acquire always has a ctx/fctx escape, so no indefinite block.
chan-sem vs errgroup — SOUND: keeps the existing primitive (minimal diff, same cap-8 bound §4 permits both flavors). One doc nit (non-blocking): docs/features/06_notifications.md entry says 'the §4 errgroup.SetLimit(8) bound now holds literally' but the code is a chan-sem implementing the same limit — conceptually accurate, mechanism wording slightly loose. Left as-is; not worth a churn push.
Negative controls — GENUINE by construction: pre-fix resolve used blocking sem<- with no ctx escape, so all N goroutines provably coexisted (200 parked/blocked); createAll/fanoutOne spawned N before any select; DeliverRepo spawned 64. Deltas ~200/200/200/64+slack vs bound 40 (fanout_bound_test.go:fanoutGoroutineBound) fail pre-fix, pass fixed (~8 + slack). Claimed 201/201/201/89 all exceed 40 — plausible, wide margin either way.
Hygiene — PASS: coverage 96.0% (gate >=95%); no import-block changes in emit.go/tasks.go/webhooks.go/notify.go; test file imports stdlib + internal/identity + internal/store only (no new third-party; dependency budget intact). gofmt clean, go vet clean. Sender-owns-closes respected (sem never closed — correct for a counting semaphore; wg owns completion). wg.Add precedes go, Wait after loop at all sites.
Tests (scratch worktree /tmp/pr159 @
0704ebf, removed after): go vet ./internal/notify/... clean; gofmt -l clean; go test -race -run TestFanout.*Bounded -count=5 PASS; full go test -race ./internal/notify/... PASS; coverage 96.0%.MERGE RECOMMENDATION: ready to merge.
Fixed by PR #159 (review: release-on-all-paths + no-deadlock + genuine controls verified; 96.0% coverage), merged. Closing.