[omp major-2] Overflow fan-out not restart-safe despite doc claim #77
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#77
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-2] Overflow fan-out not restart-safe despite doc claim
06 §335 claims "the activity payload is the only queue that survives a restart", but the only driver is
Run(wake channel + 1-min webhook sweep + daily retention);sweepWebhooks(internal/notify/tasks.go:177-203) starts onlywebhookstasks. Nothing on startup or in any sweep re-drains activity events whosenotify-fanouttask was in flight at process death. Overflow emissions (>100 recipients) and shortfall repairs pending at restart are lost.Fix
Either add a startup/sweep pass that re-drains undrained fan-out events from the activity log, or correct the doc claim if restart-safety is deliberately out of scope (law 12 — but fix preferred: the claim is load-bearing for correctness). Regression test (seed undrained events, restart/sweep, assert delivery). Coverage gate holds.
Acceptance criteria
Fixed by PR #86 (#86): pending-marked activity events now redrain via a startup + minutely sweep that reuses the webhooks repo enumeration — no new global LIST. Regression tests + doc Decisions entries included.
Review: PR #86 (fix/issue-77 @
ff23bc0)Reviewed in scratch worktree /tmp/pr86. One real bug found and fixed (see below); everything else checks out.
BUG (fixed in
fix/issue-77-sweep-retry@b35725c— needs folding into this PR branch)internal/notify/tasks.go:477-481(was),internal/notify/activity.go:87-98(was).readActivityreturns nil for both honest gaps AND transient store errors, and the sweep loopcontinued on nil while the high-water advanced toendunconditionally. A single flaked GET on a pending seq during the restart sweep permanently skipped it (high-water never revisits) — the exact loss this PR fixes. Fix: newreadActivityErrdistinguishes error from absent/corrupt; on error the window stops (end = seq-1, next pass retries), gaps/corrupt still skip.docs/features/06_notifications.md:311-312updated in the same commit (law 12). NewTestSweepFanoutTransientProbeRetriesfails on the unfixed code (high-water = 1 after a failed probe, want 0) and passes with the fix.Verified OK (no change needed)
sweepFanoutreuses the sharedeachRepo(repos/+repos/<owner>/prefix LISTs,tasks.go:237-258), the same enumerationsweepWebhooksuses. Fan-out probes are GET-by-key only (collab_state,collab-events/<seq>,collab-fanout/<seq>); no LIST ofcollab-events/orcollab-fanout/in prod code. All backends implementListPrefixes(memory, filesystem, S3, Prefixed).tasks.go:462-472:collab_stateGET,NextSeq <= seen→ return, no fan-out). Note: early passes after a restart probe up to 32 activity GETs/repo with history — bounded by the cap, converges, no hidden fan-out (sweep only enqueues in-memory; the worker writes).end; deeper backlogs converge across minute passes; restart rebuild (empty map → from seq 1) is what makes the restart pass complete. With the fix above, no path permanently skips a pending seq.tasks.go:331-335; gaps never marked sincefanoutOne==falseskips the mark); retention deletes event + marker together (tasks.go:722-727). Orphan markers are inert (probe reads the event first).pending=false(emit.go:230);omitemptykeeps payload bytes unchanged; no enqueue/mark/CAS added on the hot path.NotificationIDs: same object key (412 collapses),indexAdddedups by ID (emit.go:505-548). Durable double-delivery is impossible. (Transient duplicate SSEpublishunder a sweep/worker race is possible —tasks.go:398publishes even on the 412 path — but that is the pre-existing at-least-once pattern shared withcreateOne, ephemeral only.)fanoutMuguards only the map, never held across store calls (law 3 ✓). Lock order table→entry only inendIfQuiescent✓.gofmt/go vetclean; stdlib-only imports (addedsyncinnotify.go,errors/contextin test only).Test results (with fix)
go test -race -count=1 ./internal/notify/...— okTestSweepRedrains|TestOverflowDrain|TestSweepFanout|TestRetentionDeletesFanout)-race -count=5— okMERGE RECOMMENDATION
Blocked: fold
fix/issue-77-sweep-retry(b35725c) intofix/issue-77first (cherry-pick or merge — 4 files, +96/-6). With that in, ready to merge.Follow-up to PR #86: reviewer commit
b35725ctransplanted as PR #87 (branch fix/issue-77-retry). Cherry-pick applied cleanly onto origin/main (cf3383c); notify tests + vet/gofmt green. Not merging per instructions.PR #87 review (branch fix/issue-77-retry, head
950d64b) — transplant ofb35725c, follow-up for #77FIDELITY: PASS. git diff
b35725corigin/fix/issue-77-retry is empty — identical modulo hash (re-cherry-pick). 4 files: internal/notify/tasks.go, internal/notify/activity.go, internal/notify/redrain_test.go, docs/features/06_notifications.md.CORRECTNESS:
TEST GENUINENESS: TestSweepFanoutTransientProbeRetries fails without the fix by construction — old code conflated error->nil->continue, loop ran to end=1, fanoutSeen advanced to 1, and the seen!=0 assertion fires (plus the healed pass would never drain the orphaned seq). No base-check run needed; the assertion directly distinguishes.
RESULTS (scratch worktree /tmp/pr87 @
950d64b, removed after):MERGE RECOMMENDATION: ready to merge (not merging per instructions).
Follow-up PR #87 (transplant of the reviewed sweep-retry fix) merged. #77 complete. Closing.