From 1f1931eab8eee06c32d66a343dcd1cb480417dad Mon Sep 17 00:00:00 2001 From: Ben Neigher Date: Thu, 14 Aug 2025 20:41:19 -0700 Subject: [PATCH] fix: prevent timer goroutine leaks with non-blocking channel operations - Use non-blocking select in Timer.Stop() to avoid goroutine leak when no reader is present - Add defer cleanup in timer functions to ensure channels are properly drained - Fixes potential goroutine accumulation in long-running applications This resolves issues where timer goroutines could leak when: 1. Timer.Stop() is called but no goroutine is reading from stopCh 2. Timer functions exit without properly draining the stop channel The fix ensures robust cleanup without blocking operations. --- utils/timer.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/utils/timer.go b/utils/timer.go index a5ccc24..45992c1 100644 --- a/utils/timer.go +++ b/utils/timer.go @@ -42,6 +42,14 @@ func SetTimeout(fn func(), sleep time.Duration) *Timer { stopCh: make(chan struct{}), } timer.fn = func() { + defer func() { + // Ensure channel is drained to prevent leaks + select { + case <-timer.stopCh: + default: + } + }() + select { case <-timer.timer.C: fn() @@ -61,7 +69,12 @@ func ClearTimeout(timer *Timer) { func (t *Timer) Stop() { if t.timer.Stop() { - t.stopCh <- struct{}{} + // Use non-blocking send to avoid goroutine leak if no reader + select { + case t.stopCh <- struct{}{}: + default: + // Channel is full or no reader, timer already stopped + } } } @@ -72,6 +85,14 @@ func SetInterval(fn func(), sleep time.Duration) *Timer { stopCh: make(chan struct{}), } timer.fn = func() { + defer func() { + // Ensure channel is drained to prevent leaks + select { + case <-timer.stopCh: + default: + } + }() + for { select { case <-timer.timer.C: