Skip to content

fix(gorilla-merger): route old cold-only queries to the merger StoreAPI - #341

Merged
zzylol merged 1 commit into
mainfrom
fix/gorilla-merger-cold-timerange-routing
May 26, 2026
Merged

zzylol merged 1 commit into
mainfrom
fix/gorilla-merger-cold-timerange-routing

Conversation

@zzylol

@zzylol zzylol commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Root cause: customStore.timeRange() (gorilla-merger internal/merger/customstore.go) advertised MinTime = tsdb StartTime (~the recent head min), ignoring cold-part coverage. thanos-query prunes a store from a query's fan-out when the query window falls below the store's advertised MinTime (used in StoreInfo + TsdbInfos). So a query for old, cold-only data was never routed to the merger, streamColdSeries never ran, and stored cold/*.part objects were served EMPTY — even though the write path worked.
  • Fix: when a cold querier is attached, lower the advertised MinTime to the oldest cold part's block_start_ms (new ColdPartStore.MinBlockStart / ColdQuerier.MinBlockStart). The per-series [min_ts,max_ts] index entries remain the authoritative time filter inside the query path; this only widens the advertised floor of what the merger might serve.
  • Added focused debug logs at streamColdSeries entry and emit (cold_series count) so cold-serve activity is observable.

Why the existing union test passed but live failed

TestStoreAPIUnionsColdAndTSDB calls customStore.Series directly, bypassing thanos-query's Info-based store pruning. Live, thanos-query consults the advertised time range first and never dispatched the RPC — exactly the wiring/time-units class of bug the task flagged.

Test plan

  • go build ./..., go vet ./..., gofmt -l clean
  • go test ./... green (existing tests unbroken)
  • TestColdPartStoreMinBlockStart — min block start across out-of-order parts; empty/nil guards
  • TestCustomStoreTimeRangeIncludesCold — tsdb-only min = StartTime; with cold attached, min drops to the old cold block start
  • TestCustomStoreSeriesServesOldColdWindow — end-to-end Series RPC over a window covering ONLY an old cold part returns the stored series' labels + samples

🤖 Generated with Claude Code

The decode-on-read cold path stored parts but served EMPTY for old
windows: customStore.timeRange() advertised MinTime = tsdb StartTime
(roughly the recent head min), ignoring cold coverage. thanos-query
prunes a store whose advertised range does not overlap the query, so a
query for old (cold-only) data was never routed to the merger and
streamColdSeries never ran.

Lower the advertised MinTime to the oldest cold part's block_start_ms
when a cold querier is attached (via ColdPartStore.MinBlockStart /
ColdQuerier.MinBlockStart), so the StoreInfo + TsdbInfos cover the cold
window and thanos-query fans the query out to the merger. Add debug logs
at streamColdSeries entry and emit so cold-serve activity is observable.

Tests: MinBlockStart accessor, timeRange folds in cold coverage, and an
end-to-end Series RPC over an old cold-only window returns the stored
part's series + samples.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 3c05053 into main May 26, 2026
zzylol added a commit that referenced this pull request May 26, 2026
…ld reload so cold queries serve (#342)

Cold parts were stored but a live cold query still returned empty after a
restart, even with #341's MinTime floor in place. Two compounding causes:

1. customStore.timeRange() advertised MaxInt64 for an EMPTY tsdb head:
   tsdb.DB.StartTime() returns math.MaxInt64 when the head holds no samples
   (e.g. right after a restart, before the first warm fragment lands). That
   sentinel leaked into the StoreAPI's advertised MinTime, so thanos-query
   pruned the merger from EVERY query (no window can be >= MaxInt64) and
   streamColdSeries never ran. Take the MIN of the tsdb StartTime and the
   cold-part floor, and never advertise MaxInt64 (fall back to MinInt64 when
   the store is genuinely empty so it stays discoverable, returning no series).

2. ColdPartStore.Reload ran synchronously in main before the StoreAPI/HTTP
   servers started. It fetches + OpenParts every stored part, so a large
   accumulated cold tier (thousands of parts) blocked startup for tens of
   seconds — during which the gRPC endpoint was down and every query (warm
   and cold) returned empty. Run the reload in the background; the manifest
   is mutex-guarded, so concurrent queries see a growing manifest until it
   completes and the warm/open path serves immediately.

Live: after a restart the StoreAPI now comes up ~80ms after WAL replay
(vs ~53s before) and warm queries return data while the cold reload runs in
the background; cold-only-window queries return the stored parts' series once
reload completes (streamColdSeries emits N series). Adds a regression test
asserting the empty-head MinTime never advertises MaxInt64 and drops to the
cold floor when parts exist.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol deleted the fix/gorilla-merger-cold-timerange-routing branch July 17, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant