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 @@ -81,6 +81,7 @@
* [BUGFIX] Ingester: Fix panic (`HistogramProtoToHistogram called with a float histogram`) when ingesting a float native histogram with a zero count (e.g. a staleness marker or empty histogram). The decoder is now selected by histogram type via `IsFloatHistogram()` instead of by count value. #7645
* [BUGFIX] Querier: Fix parquet queryable fallback returning a nil error instead of the actual query error in `LabelValues` and `LabelNames`. #7638
* [BUGFIX] Store Gateway: Fix misleading "no index cache backend addresses" validation error being reported for chunks-cache, metadata-cache, and parquet caches when their memcached or redis backend is configured without addresses. The message is now the cache-type-agnostic "no cache backend addresses". #7675
* [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.0 2026-04-24

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