From b0862f7a418422370780f4d4e691464259bc2b9c Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Sat, 4 Apr 2026 10:38:37 -0700 Subject: [PATCH] fix: replace time.Tick with NewTicker in prune goroutine for graceful shutdown (#593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replace `time.Tick()` with `time.NewTicker()` in the auto-prune goroutine so it stops cleanly during graceful shutdown. ## Problem `time.Tick` creates a ticker that can never be garbage collected or stopped. While the prune goroutine runs for the process lifetime, it won't stop during graceful shutdown — the goroutine leaks past the shutdown sequence. ## Fix - Create a `time.NewTicker` and a done channel - Use `select` to listen on both the ticker and done channel - Stop the ticker and close the done channel in the shutdown path (after `poller.Stop()`) - Pattern matches the existing `StartEvictionTicker()` approach ## Testing - `go build ./...` — compiles cleanly - `go test ./...` — all tests pass Fixes #377 Co-authored-by: you --- cmd/server/main.go | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 3b30709d..a498b697 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -224,8 +224,15 @@ func main() { defer stopEviction() // Auto-prune old packets if retention.packetDays is configured + var stopPrune func() if cfg.Retention != nil && cfg.Retention.PacketDays > 0 { days := cfg.Retention.PacketDays + pruneTicker := time.NewTicker(24 * time.Hour) + pruneDone := make(chan struct{}) + stopPrune = func() { + pruneTicker.Stop() + close(pruneDone) + } go func() { time.Sleep(1 * time.Minute) if n, err := database.PruneOldPackets(days); err != nil { @@ -233,11 +240,16 @@ func main() { } else { log.Printf("[prune] deleted %d transmissions older than %d days", n, days) } - for range time.Tick(24 * time.Hour) { - if n, err := database.PruneOldPackets(days); err != nil { - log.Printf("[prune] error: %v", err) - } else { - log.Printf("[prune] deleted %d transmissions older than %d days", n, days) + for { + select { + case <-pruneTicker.C: + if n, err := database.PruneOldPackets(days); err != nil { + log.Printf("[prune] error: %v", err) + } else { + log.Printf("[prune] deleted %d transmissions older than %d days", n, days) + } + case <-pruneDone: + return } } }() @@ -262,6 +274,11 @@ func main() { // 1. Stop accepting new WebSocket/poll data poller.Stop() + // 1b. Stop auto-prune ticker + if stopPrune != nil { + stopPrune() + } + // 2. Gracefully drain HTTP connections (up to 15s) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel()