fix(symlinkguard): test the WithoutCancel final flush; log what shutdown drops
CI / Lint / Test / Vet (pull_request) Successful in 1m5s
CI / Mirror to GitHub (pull_request) Skipped

PR #180 round-2 review (comments 15467, 15487):
- toggleNotifier now fails on a done ctx, like audit.NtfyNotifier, so
  replacing context.WithoutCancel(ctx) with ctx in the final flush turns
  TestWatchFinalFlushSendsAQueuedAlertOnACtxThatOutlivesTheCancel (renamed
  from ...FlushesTheQueueOnceMoreAtShutdown) red.
- After the final flush, any undelivered or suppressed alerts are logged as
  one error with both counts, never silently.
- The !ok first-receive branch now swaps overflow into suppressed.
- Deterministic deliver() tests for the drain-on-close path and the
  closed-and-empty overflow case.
- Watch doc states the shutdown path only runs for a cancelling caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 16:27:54 +02:00
co-authored by Claude Opus 5.5
parent 52d4bfeeab
commit cadc0cc02f
2 changed files with 101 additions and 5 deletions
@@ -144,7 +144,11 @@ func Scan(brainDir string) (Result, error) {
// scan waited on it a planted link stayed readable for several intervals.
// The scan hands alerts over without blocking; any that do not fit are
// counted in the summary. When ctx ends, Watch returns after a final flush
// of what was handed over (bounded by shutdownFlushTimeout).
// of what was handed over (bounded by shutdownFlushTimeout); alerts that
// flush cannot deliver are counted in one error log line instead. That
// shutdown path only runs for a caller that cancels ctx: cmd/server passes
// context.Background(), so in production the flush on handoff is what keeps
// alerts from waiting on a restart.
func Watch(ctx context.Context, brainDir string, interval time.Duration, n Notifier) {
if interval <= 0 {
interval = DefaultInterval
@@ -211,7 +215,8 @@ func (s alertSender) push(msg string) {
// over, and again every interval while any are pending (outage retries).
// When alerts is closed (Watch is stopping) it drains it and flushes once
// more, bounded by shutdownFlushTimeout, so an alert handed over just before
// shutdown is not lost (PR #180 review).
// shutdown is not lost (PR #180 review). Whatever that flush cannot deliver
// is logged as one error with its counts, never dropped silently.
func deliver(ctx context.Context, interval time.Duration, n Notifier, alerts <-chan string, overflow *atomic.Int64) {
t := time.NewTicker(interval)
defer t.Stop()
@@ -240,9 +245,14 @@ func deliver(ctx context.Context, interval time.Duration, n Notifier, alerts <-c
ok = drain()
}
if !ok {
q.suppressed += int(overflow.Swap(0))
fctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), shutdownFlushTimeout)
q.flush(fctx, n)
cancel()
if len(q.items) > 0 || q.suppressed > 0 {
slog.Error("symlink tripwire alerts not delivered at shutdown",
"undelivered", len(q.items), "suppressed", q.suppressed)
}
return
}
q.flush(ctx, n)