From 30b7814aaf4ce23e598e5cdf663feb0655975ef5 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 20 Apr 2026 03:08:42 -0700 Subject: [PATCH] pkg/tools/batcher: stop scheduler panicking when b.data is closed externally scheduler()'s defer unconditionally calls close(b.data). If the channel was closed by the caller (or an upstream producer) instead of via the normal Close()-sends-nil path, the receive on b.data returns ok == false, scheduler returns, and the deferred close(b.data) then fires on an already-closed channel: panic: close of closed channel reliably reproducible under the #3653 steps (manually closing b.data while Start() is running). Track whether we observed the external-close via a local `externallyClosed` flag set in the `ok == false` branch. The defer only closes b.data when that flag is false, i.e. when the scheduler exited through the nil-message or ticker paths and still owns the channel. No behaviour change on the graceful Close() path. Fixes #3653 --- pkg/tools/batcher/batcher.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/tools/batcher/batcher.go b/pkg/tools/batcher/batcher.go index 93a31ed8f..bbaa2ca93 100644 --- a/pkg/tools/batcher/batcher.go +++ b/pkg/tools/batcher/batcher.go @@ -151,12 +151,21 @@ func (b *Batcher[T]) Put(ctx context.Context, data *T) error { func (b *Batcher[T]) scheduler() { ticker := time.NewTicker(b.config.interval) + // Track whether b.data was closed by an external caller so the + // cleanup below does not close it a second time. The only routes + // out of this function that leave b.data open are the nil-message + // and ticker paths; the ok == false branch means someone already + // closed the channel, and calling close(b.data) again there would + // panic with "close of closed channel" (#3653). + externallyClosed := false defer func() { ticker.Stop() for _, ch := range b.chArrays { close(ch) } - close(b.data) + if !externallyClosed { + close(b.data) + } b.wait.Done() }() @@ -169,6 +178,7 @@ func (b *Batcher[T]) scheduler() { case data, ok := <-b.data: if !ok { // If the data channel is closed unexpectedly + externallyClosed = true return } if data == nil {