Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
* [BUGFIX] Compactor: Fix spurious `bucket operation fail after retries` error logs emitted during partial block cleanup. #7749
* [BUGFIX] Alertmanager: Fix panic in `validateAlertmanagerConfig` when receiver config traversal encounters nil interface values. #7751
* [BUGFIX] Parquet Converter: Fix `auto_forget_delay` having no effect. The ring lifecycler was created without the auto-forget delegate, so unhealthy instances were never automatically removed from the ring. #7752
* [BUGFIX] Querier: Close the per-query `storage.Querier` in the remote read handler once each sub-query completes. The handler was the only `storage.Querier` call site that did not call `Close()`, so it did not honor the interface contract and would leak any resources a querier releases in `Close()`. #7672

## 1.21.1 2026-06-04

Expand Down
1 change: 1 addition & 0 deletions pkg/querier/remote_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func RemoteReadHandler(q storage.Queryable, logger log.Logger) http.Handler {
errors <- err
return
}
defer querier.Close()

params := &storage.SelectHints{
Start: int64(from),
Expand Down
46 changes: 46 additions & 0 deletions pkg/querier/remote_read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/go-kit/log"
"github.com/gogo/protobuf/proto"
Expand All @@ -17,10 +18,12 @@ import (
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/util/annotations"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"

"github.com/cortexproject/cortex/pkg/cortexpb"
"github.com/cortexproject/cortex/pkg/ingester/client"
"github.com/cortexproject/cortex/pkg/querier/series"
"github.com/cortexproject/cortex/pkg/util/test"
)

func TestRemoteReadHandler(t *testing.T) {
Expand Down Expand Up @@ -88,6 +91,37 @@ func TestRemoteReadHandler(t *testing.T) {
require.Equal(t, expected, response)
}

func TestRemoteReadHandler_Closes_Querier(t *testing.T) {
t.Parallel()
var closed atomic.Int64
q := storage.QueryableFunc(func(mint, maxt int64) (storage.Querier, error) {
return &closeCountingQuerier{closed: &closed}, nil
})
handler := RemoteReadHandler(q, log.NewNopLogger())

const numQueries = 3
queries := make([]*client.QueryRequest, numQueries)
for i := range queries {
queries[i] = &client.QueryRequest{StartTimestampMs: 0, EndTimestampMs: 10}
}
requestBody, err := proto.Marshal(&client.ReadRequest{Queries: queries})
require.NoError(t, err)
requestBody = snappy.Encode(nil, requestBody)
request, err := http.NewRequest("GET", "/query", bytes.NewReader(requestBody))
require.NoError(t, err)
request.Header.Set("X-Prometheus-Remote-Read-Version", "0.1.0")

recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)

require.Equal(t, 200, recorder.Result().StatusCode)
// Each per-query querier is closed by a deferred call in its own goroutine,
// which may run after ServeHTTP returns, so poll until every querier is closed.
test.Poll(t, time.Second, int64(numQueries), func() any {
return closed.Load()
})
}

type mockQuerier struct {
matrix model.Matrix
}
Expand All @@ -110,3 +144,15 @@ func (m mockQuerier) LabelNames(ctx context.Context, _ *storage.LabelHints, matc
func (mockQuerier) Close() error {
return nil
}

// closeCountingQuerier records how many times Close is called so a test can
// assert the remote read handler releases every querier it creates.
type closeCountingQuerier struct {
mockQuerier
closed *atomic.Int64
}

func (q *closeCountingQuerier) Close() error {
q.closed.Add(1)
return nil
}
Loading