Skip to content

Commit 2891919

Browse files
xbglowxclaude
andauthored
fix: prevent goroutine leak in downstreamer on context cancellation (#21062)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7155bd5 commit 2891919

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

‎pkg/querier/queryrange/downstreamer.go‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,16 @@ func (in instance) For(
195195
return nil
196196
})
197197
if err != nil {
198-
ch <- logql.Resp{
198+
// Send the error unless the context has been canceled and the
199+
// reader has already exited. Without this select, a canceled
200+
// context causes this goroutine to block on the channel send
201+
// forever, leaking the goroutine.
202+
select {
203+
case ch <- logql.Resp{
199204
I: -1,
200205
Err: err,
206+
}:
207+
case <-ctx.Done():
201208
}
202209
}
203210
close(ch)

‎pkg/querier/queryrange/downstreamer_test.go‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"runtime"
78
"strconv"
89
"strings"
910
"sync"
@@ -587,6 +588,74 @@ func TestCancelWhileWaitingResponse(t *testing.T) {
587588
"but the For method did not return as expected.")
588589
}
589590

591+
func TestCancelDoesNotLeakGoroutines(t *testing.T) {
592+
mkIn := func() *instance {
593+
return DownstreamHandler{
594+
limits: fakeLimits{},
595+
next: nil,
596+
}.Downstreamer(context.Background()).(*instance)
597+
}
598+
in := mkIn()
599+
600+