thanos-io/thanos · warning
block querier already closed
Error message
block querier already closed
What it means
blockBaseQuerier.Close returns this error when the querier has already been closed. Closing twice is a usage bug: once closed, the underlying index, chunk, and tombstone readers have been released and cannot be closed again. The guard prevents double-release panics/errors from the underlying tsdb readers.
Solutions
- Track closure at the call site (sync.Once or a closed flag) so Close is invoked only once per querier
- Use `defer q.Close()` exclusively instead of mixing defer and manual Close calls
- Check whether intermediate querier-wrapping layers double-close; fix ownership so exactly one layer closes
- Ignore benign double-close only if you control the code and it is provably idempotent elsewhere
Example fix
// before defer querier.Close() ... querier.Close() // after defer querier.Close() ...(remove the second Close call or guard with sync.Once)
Defensive patterns
Strategy: try-catch
Validate before calling
type closedQuerier interface{ Closed() bool }
if cq, ok := q.(closedQuerier); ok && cq.Closed() { return errors.New("querier already closed") } Type guard
func isAlreadyClosed(err error) bool {
return err != nil && strings.Contains(err.Error(), "already closed")
} Try / catch
if err := q.Close(); err != nil && isAlreadyClosed(err) {
return nil // treat double close as no-op
}
return err Prevention
- Give querier ownership to exactly one code path responsible for Close
- Use sync.Once around Close when multiple goroutines may finish with the querier
- Prefer a single `defer q.Close()` over mixed defer + manual Close
- Return no-op on double close in wrappers if idempotency is desired
When it happens
Trigger: Calling Close() twice on the same blockBaseQuerier returned by the cached block chunk querier machinery (e.g. a defer-based cleanup path plus an explicit Close).
Common situations: Defensive `defer q.Close()` combined with manual Close in an error path; query-layer code that releases queriers both on success and on error without tracking state.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- invalid service state
- invalid service state
- failed to run pre compaction callback for plan
- hashring config watcher stopped unexpectedly
AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07).
Data as JSON: /api/errors/7f215df543b0088a.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/receive/expandedpostingscache/tsdb.go:82
index: indexr,
chunks: chunkr,
tombstones: tombsr,
}, nil
}
func (q *blockBaseQuerier) LabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) {
res, err := q.index.SortedLabelValues(ctx, name, hints, matchers...)
return res, nil, err
}
func (q *blockBaseQuerier) LabelNames(ctx context.Context, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) {
res, err := q.index.LabelNames(ctx, matchers...)
return res, nil, err
}
func (q *blockBaseQuerier) Close() error {
if q.closed {
return errors.New("block querier already closed")
}
errs := tsdb_errors.NewMulti(
q.index.Close(),
q.chunks.Close(),
q.tombstones.Close(),
)
q.closed = true
return errs.Err()
}
type cachedBlockChunkQuerier struct {
*blockBaseQuerier
cache ExpandedPostingsCache
}
func NewCachedBlockChunkQuerier(cache ExpandedPostingsCache, b prom_tsdb.BlockReader, mint, maxt int64) (storage.ChunkQuerier, error) {View on GitHub (pinned to 35b8b99117)