Skip to content

Commit a4e2f2f

Browse files
perf(ingester): lazy regex evaluation on head postings cache miss (#7553)
* perf(ingester): lazy regex evaluation on head postings cache miss When the expanded postings cache misses on the head block, regex matchers on high-cardinality labels (e.g. pod with 400K+ values) dominate query cost. This PR defers expensive regex matchers to a lazy per-series evaluation when a selective equality matcher already narrows the result set significantly. On cache miss, splitMatchersForHeadWithConfig splits matchers into: - Selective matchers (equality, low-card regex) for postings lookup - Lazy matchers (high-card regex) applied per-series via LabelValueFor A cost-ratio gate decides when deferral is worthwhile: - Simple regex (single contains, prefix): cardinality > selectivePostings * 6 - Complex regex (multi-substring, capture groups): cardinality > selectivePostings * 2 Label cardinality lookups are cached in an expirable LRU (60s TTL) to avoid repeated LabelValues calls under load. Benchmark (realistic pod names, 413K cardinality, 9K selective postings): - Eager: 62ms, 29.8MB per query - Lazy: 14ms, 12.6MB per query (4.5x faster, 58% less memory) New flags (disabled by default with max-cardinality=0): - blocks-storage.expanded_postings_cache.head.lazy-matcher-max-cardinality - blocks-storage.expanded_postings_cache.head.lazy-matcher-simple-cost-ratio - blocks-storage.expanded_postings_cache.head.lazy-matcher-complex-cost-ratio * Update pkg/storage/tsdb/expanded_postings_cache.go Co-authored-by: SungJin1212 <tjdwls1201@gmail.com> Signed-off-by: Alan Protasio <alanprot@gmail.com> * address review: mark flags experimental, use errors.Is, add error propagation test - Mark all lazy-matcher flags as [EXPERIMENTAL] per v1 guarantee - Use errors.Is(err, storage.ErrNotFound) instead of direct comparison - Propagate non-ErrNotFound errors from LabelValueFor - Add TestFetchWithLazyMatchers_PropagatesLabelValueForError - Skip SetMatches matchers from lazy candidates (already fast-pathed) - Remove dead SetMatches check from regexCostClass - Regenerate docs and schema --------- Signed-off-by: Alan Protasio <alanprot@gmail.com> Signed-off-by: Alan Protasio <approtas@amazon.com> Co-authored-by: SungJin1212 <tjdwls1201@gmail.com>
1 parent d4e51d5 commit a4e2f2f

11 files changed

Lines changed: 1639 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
* [ENHANCEMENT] Instrument Ingester CPU profile with source for read APIs. #7494
2929
* [ENHANCEMENT] Ingester: Convert expanded postings cache from FIFO to LRU eviction to retain frequently-queried entries under memory pressure. #7510
3030
* [ENHANCEMENT] Querier: Detach series label and chunk data from gRPC unmarshal buffers in store-gateway streaming path, allowing the Go GC to reclaim receive buffers. #7519
31+
3132
* [ENHANCEMENT] Distributor: Add `WrappedHistogram` with configurable size limit (`-validation.max-native-histogram-size-bytes`) to cap native histogram protobuf size before unmarshalling. #7570
33+
* [ENHANCEMENT] Ingester: Add lazy regex evaluation on head postings cache miss. Defers expensive regex matchers on high-cardinality labels to per-series filtering when a selective equality matcher already narrows the result set. Configured via `-blocks-storage.expanded_postings_cache.head.lazy-matcher-max-cardinality` (disabled by default). #7553
3234
* [BUGFIX] Querier: Fix queryWithRetry and labelsWithRetry returning (nil, nil) on cancelled context by propagating ctx.Err(). #7370
3335
* [BUGFIX] Metrics Helper: Fix non-deterministic bucket order in merged histograms by sorting buckets after map iteration, matching Prometheus client library behavior. #7380
3436
* [BUGFIX] Distributor: Return HTTP 401 Unauthorized when tenant ID resolution fails in the Prometheus Remote Write 2.0 path. #7389

docs/blocks-storage/querier.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,6 +1970,25 @@ blocks_storage:
19701970
# CLI flag: -blocks-storage.expanded_postings_cache.block.fetch-timeout
19711971
[fetch_timeout: <duration> | default = 0s]
19721972

1973+
# [EXPERIMENTAL] Maximum label cardinality for deferring regex matchers on
1974+
# the head block. When a regex matcher targets a label with more unique
1975+
# values than this threshold, it is applied lazily during iteration
1976+
# instead of postings lookup. 0 disables.
1977+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-max-cardinality
1978+
[lazy_matcher_max_cardinality: <int> | default = 0]
1979+
1980+
# [EXPERIMENTAL] Cardinality:postings ratio above which a simple regex
1981+
# (prefix-only, single contains) is deferred to lazy iteration. Lower =
1982+
# more aggressive deferral. Calibrated empirically; defaults to 6.
1983+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-simple-cost-ratio
1984+
[lazy_matcher_simple_cost_ratio: <int> | default = 6]
1985+
1986+
# [EXPERIMENTAL] Cardinality:postings ratio above which a complex regex
1987+
# (multi-substring, capture groups, character classes) is deferred. Lower
1988+
# = more aggressive deferral. Calibrated empirically; defaults to 2.
1989+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-complex-cost-ratio
1990+
[lazy_matcher_complex_cost_ratio: <int> | default = 2]
1991+
19731992
users_scanner:
19741993
# Strategy to use to scan users. Supported values are: list, user_index.
19751994
# CLI flag: -blocks-storage.users-scanner.strategy

docs/blocks-storage/store-gateway.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,6 +2028,25 @@ blocks_storage:
20282028
# CLI flag: -blocks-storage.expanded_postings_cache.block.fetch-timeout
20292029
[fetch_timeout: <duration> | default = 0s]
20302030

2031+
# [EXPERIMENTAL] Maximum label cardinality for deferring regex matchers on
2032+
# the head block. When a regex matcher targets a label with more unique
2033+
# values than this threshold, it is applied lazily during iteration
2034+
# instead of postings lookup. 0 disables.
2035+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-max-cardinality
2036+
[lazy_matcher_max_cardinality: <int> | default = 0]
2037+
2038+
# [EXPERIMENTAL] Cardinality:postings ratio above which a simple regex
2039+
# (prefix-only, single contains) is deferred to lazy iteration. Lower =
2040+
# more aggressive deferral. Calibrated empirically; defaults to 6.
2041+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-simple-cost-ratio
2042+
[lazy_matcher_simple_cost_ratio: <int> | default = 6]
2043+
2044+
# [EXPERIMENTAL] Cardinality:postings ratio above which a complex regex
2045+
# (multi-substring, capture groups, character classes) is deferred. Lower
2046+
# = more aggressive deferral. Calibrated empirically; defaults to 2.
2047+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-complex-cost-ratio
2048+
[lazy_matcher_complex_cost_ratio: <int> | default = 2]
2049+
20312050
users_scanner:
20322051
# Strategy to use to scan users. Supported values are: list, user_index.
20332052
# CLI flag: -blocks-storage.users-scanner.strategy

docs/configuration/config-file-reference.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2650,6 +2650,25 @@ tsdb:
26502650
# CLI flag: -blocks-storage.expanded_postings_cache.block.fetch-timeout
26512651
[fetch_timeout: <duration> | default = 0s]
26522652

2653+
# [EXPERIMENTAL] Maximum label cardinality for deferring regex matchers on
2654+
# the head block. When a regex matcher targets a label with more unique
2655+
# values than this threshold, it is applied lazily during iteration instead
2656+
# of postings lookup. 0 disables.
2657+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-max-cardinality
2658+
[lazy_matcher_max_cardinality: <int> | default = 0]
2659+
2660+
# [EXPERIMENTAL] Cardinality:postings ratio above which a simple regex
2661+
# (prefix-only, single contains) is deferred to lazy iteration. Lower = more
2662+
# aggressive deferral. Calibrated empirically; defaults to 6.
2663+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-simple-cost-ratio
2664+
[lazy_matcher_simple_cost_ratio: <int> | default = 6]
2665+
2666+
# [EXPERIMENTAL] Cardinality:postings ratio above which a complex regex
2667+
# (multi-substring, capture groups, character classes) is deferred. Lower =
2668+
# more aggressive deferral. Calibrated empirically; defaults to 2.
2669+
# CLI flag: -blocks-storage.expanded_postings_cache.head.lazy-matcher-complex-cost-ratio
2670+
[lazy_matcher_complex_cost_ratio: <int> | default = 2]
2671+
26532672
users_scanner:
26542673
# Strategy to use to scan users. Supported values are: list, user_index.
26552674
# CLI flag: -blocks-storage.users-scanner.strategy

docs/configuration/v1-guarantees.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,7 @@ Currently experimental features are:
133133
- Ingester: Active Series Tracker
134134
- Per-tenant `active_series_trackers` configuration in runtime config overrides
135135
- Counts active series matching PromQL label matchers and exposes `cortex_ingester_active_series_per_tracker` metric
136+
- Ingester: Lazy regex evaluation on head postings cache miss
137+
- `-blocks-storage.expanded_postings_cache.head.lazy-matcher-max-cardinality` (int) CLI flag
138+
- `-blocks-storage.expanded_postings_cache.head.lazy-matcher-simple-cost-ratio` (int) CLI flag
139+
- `-blocks-storage.expanded_postings_cache.head.lazy-matcher-complex-cost-ratio` (int) CLI flag

integration/query_fuzz_test.go

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,280 @@ func TestExpandedPostingsCacheFuzz(t *testing.T) {
665665
}
666666
}
667667

668+
// TestLazyMatchersFuzz fuzzes PromQL queries against two cortex instances with
669+
// identical data:
670+
// - cortex-1: head expanded-postings cache enabled, lazy matcher DISABLED
671+
// (the eager path - regex applied during postings lookup).
672+
// - cortex-2: head expanded-postings cache enabled, lazy matcher ENABLED
673+
// with aggressive thresholds (cardinality=1, both cost ratios=1) so the
674+
// optimization fires on every regex matcher.
675+
//
676+
// The test verifies:
677+
// 1. Query results match between the two instances (correctness).
678+
// 2. The cortex_ingester_expanded_postings_lazy_matcher_queries_total counter
679+
// is incremented on cortex-2 (the optimization actually triggers).
680+
func TestLazyMatchersFuzz(t *testing.T) {
681+
s, err := e2e.NewScenario(networkName)
682+
require.NoError(t, err)
683+
defer s.Close()
684+
685+
// Start dependencies.
686+
consul1 := e2edb.NewConsulWithName("consul1")
687+
consul2 := e2edb.NewConsulWithName("consul2")
688+
require.NoError(t, s.StartAndWaitReady(consul1, consul2))
689+
690+
baseFlags := mergeFlags(
691+
AlertmanagerLocalFlags(),
692+
map[string]string{
693+
"-store.engine": blocksStorageEngine,
694+
"-blocks-storage.backend": "filesystem",
695+
"-blocks-storage.tsdb.head-compaction-interval": "4m",
696+
"-blocks-storage.tsdb.block-ranges-period": "2h",
697+
"-blocks-storage.tsdb.ship-interval": "1h",
698+
"-blocks-storage.bucket-store.sync-interval": "15m",
699+
"-blocks-storage.tsdb.retention-period": "2h",
700+
"-blocks-storage.bucket-store.index-cache.backend": tsdb.IndexCacheBackendInMemory,
701+
"-blocks-storage.bucket-store.bucket-index.enabled": "true",
702+
"-blocks-storage.expanded_postings_cache.head.enabled": "true",
703+
"-blocks-storage.expanded_postings_cache.block.enabled": "true",
704+
"-distributor.replication-factor": "1",
705+
"-store-gateway.sharding-enabled": "false",
706+
"-alertmanager.web.external-url": "http://localhost/alertmanager",
707+
// The alertmanager initializes a memberlist gossip ring that auto-
708+
// detects a private RFC1918 IP. On Docker networks where containers
709+
// get non-private IPs (e.g. the 240.0.0.0/4 reserved range), this
710+
// detection hard-fails. Setting an explicit advertise address skips
711+
// the autodetection — the value is unused since we don't enable HA
712+
// peers, but presence of the flag is enough.
713+
"-alertmanager.cluster.advertise-address": "127.0.0.1:9094",
714+
},
715+
)
716+
717+
// cortex-1: eager path. Lazy matcher disabled (default).
718+
flags1 := mergeFlags(baseFlags, map[string]string{
719+
"-ring.store": "consul",
720+
"-consul.hostname": consul1.NetworkHTTPEndpoint(),
721+
"-ingester.matchers-cache-max-items": "10000",
722+
})
723+
724+
// cortex-2: lazy path. Aggressive thresholds force the optimization to
725+
// fire on essentially every regex matcher, so we exercise the lazy code
726+
// path repeatedly for correctness verification.
727+
flags2 := mergeFlags(baseFlags, map[string]string{
728+
"-ring.store": "consul",
729+
"-consul.hostname": consul2.NetworkHTTPEndpoint(),
730+
"-ingester.matchers-cache-max-items": "10000",
731+
"-blocks-storage.expanded_postings_cache.head.lazy-matcher-max-cardinality": "1",
732+
"-blocks-storage.expanded_postings_cache.head.lazy-matcher-simple-cost-ratio": "1",
733+
"-blocks-storage.expanded_postings_cache.head.lazy-matcher-complex-cost-ratio": "1",
734+
})
735+
736+
require.NoError(t, writeFileToSharedDir(s, "alertmanager_configs", []byte{}))
737+
738+
path1 := path.Join(s.SharedDir(), "cortex-1")
739+
path2 := path.Join(s.SharedDir(), "cortex-2")
740+
flags1 = mergeFlags(flags1, map[string]string{"-blocks-storage.filesystem.dir": path1})
741+
flags2 = mergeFlags(flags2, map[string]string{"-blocks-storage.filesystem.dir": path2})
742+
743+
// Both instances use the local build.
744+
cortex1 := e2ecortex.NewSingleBinary("cortex-1", flags1, "")
745+
cortex2 := e2ecortex.NewSingleBinary("cortex-2", flags2, "")
746+
require.NoError(t, s.StartAndWaitReady(cortex1, cortex2))
747+
748+
require.NoError(t, cortex1.WaitSumMetrics(e2e.Equals(float64(512)), "cortex_ring_tokens_total"))
749+
require.NoError(t, cortex2.WaitSumMetrics(e2e.Equals(float64(512)), "cortex_ring_tokens_total"))
750+
751+
c1, err := e2ecortex.NewClient(cortex1.HTTPEndpoint(), cortex1.HTTPEndpoint(), "", "", "user-1")
752+
require.NoError(t, err)
753+
c2, err := e2ecortex.NewClient(cortex2.HTTPEndpoint(), cortex2.HTTPEndpoint(), "", "", "user-1")
754+
require.NoError(t, err)
755+
756+
now := time.Now()
757+
start := now.Add(-24 * time.Hour)
758+
scrapeInterval := 30 * time.Second
759+
760+
// Build a fixture with multiple labels, including a high-cardinality
761+
// "pod"-style label so regex matchers from promqlsmith actually exercise
762+
// the deferral path. With lazy-matcher-max-cardinality=1, any label with
763+
// >1 unique value is eligible.
764+
numSeries := 10
765+
numberOfLabelsPerSeries := 5
766+
numSamples := 10
767+
ss := make([]prompb.TimeSeries, numSeries*numberOfLabelsPerSeries)
768+
lbls := make([]labels.Labels, numSeries*numberOfLabelsPerSeries)
769+
770+
for i := 0; i < numSeries; i++ {
771+
for j := 0; j < numberOfLabelsPerSeries; j++ {
772+
series := e2e.GenerateSeriesWithSamples(
773+
fmt.Sprintf("test_series_%d", i),
774+
start,
775+
scrapeInterval,
776+
i*numSamples,
777+
numSamples,
778+
prompb.Label{Name: "test_label", Value: fmt.Sprintf("test_label_value_%d", j)},
779+
prompb.Label{Name: "pod", Value: fmt.Sprintf("test_pod_%d_%d", i, j)},
780+
)
781+
ss[i*numberOfLabelsPerSeries+j] = series
782+
783+
builder := labels.NewBuilder(labels.EmptyLabels())
784+
for _, lbl := range series.Labels {
785+
builder.Set(lbl.Name, lbl.Value)
786+
}
787+
lbls[i*numberOfLabelsPerSeries+j] = builder.Labels()
788+
}
789+
}
790+
791+
for _, client := range []*e2ecortex.Client{c1, c2} {
792+
res, err := client.Push(ss)
793+
require.NoError(t, err)
794+
require.Equal(t, 200, res.StatusCode)
795+
}
796+
797+
rnd := rand.New(rand.NewSource(now.Unix()))
798+
opts := []promqlsmith.Option{
799+
promqlsmith.WithEnabledAggrs(enabledAggrs),
800+
}
801+
ps := promqlsmith.New(rnd, lbls, opts...)
802+
803+
// Regex patterns that exercise different cost classes in the lazy matcher gate.
804+
// Each pattern matches a SUBSET of pods (not all), so both =~ and !~ queries
805+
// return non-empty results, verifying correctness with actual data.
806+
regexPatterns := []string{
807+
".*_0_.*", // single contains (simple) — 5/50 pods
808+
".*_[0-4]_[0-2]", // character class (complex) — 15/50 pods
809+
"test_pod_[5-9]_.*", // prefix + class (complex) — 25/50 pods
810+
".*pod_3.*", // single contains (simple) — 5/50 pods
811+
"(test_pod_1|test_pod_2)_.*", // alternation (complex) — 10/50 pods
812+
}
813+
814+
testRun := 300
815+
queries := make([]string, 0, testRun*2)
816+
matchers := make([]string, 0, testRun)
817+
for i := 0; i < testRun; i++ {
818+
expr := ps.WalkRangeQuery()
819+
if !isValidQuery(expr, true) {
820+
continue
821+
}
822+
queries = append(queries, expr.Pretty(0))
823+
824+
// Each matcher set includes a __name__= anchor + a regex on pod,
825+
// guaranteeing the lazy matcher optimization fires on every cache miss.
826+
regex := regexPatterns[i%len(regexPatterns)]
827+
matchers = append(matchers, storepb.PromMatchersToString(
828+
append(
829+
ps.WalkSelectors(),
830+
labels.MustNewMatcher(labels.MatchEqual, "__name__", fmt.Sprintf("test_series_%d", i%numSeries)),
831+
labels.MustNewMatcher(labels.MatchRegexp, "pod", regex),
832+
)...))
833+
834+
// Also generate a direct PromQL query with the regex so the instant/range
835+
// query path exercises the lazy matcher too. Include iteration index in
836+
// a != matcher to force unique cache keys (cache miss on every query).
837+
queries = append(queries, fmt.Sprintf(`test_series_%d{pod=~"%s",test_label!="iter_%d"}`, i%numSeries, regex, i))
838+
// Also test negative regex (!~) to exercise that code path.
839+
queries = append(queries, fmt.Sprintf(`test_series_%d{pod!~"%s",test_label!="iter_%d_neg"}`, i%numSeries, regex, i))
840+
}
841+
842+
type testCase struct {
843+
query string
844+
qt string
845+
res1, res2 model.Value
846+
sres1, sres2 []model.LabelSet
847+
err1, err2 error
848+
}
849+
850+
cases := make([]*testCase, 0, len(queries)*2+len(matchers))
851+
852+
// Data spans [start, start + (numSamples-1)*scrapeInterval]. Constrain
853+
// fuzzed timestamps to this window so queries actually hit the head block.
854+
dataEnd := start.Add(scrapeInterval * time.Duration(numSamples-1))
855+
dataWindowMs := dataEnd.Sub(start).Milliseconds()
856+
857+
for _, query := range queries {
858+
fuzzyTime := time.Duration(rand.Int63n(dataWindowMs))
859+
queryEnd := start.Add(fuzzyTime * time.Millisecond)
860+
res1, err1 := c1.Query(query, queryEnd)
861+
res2, err2 := c2.Query(query, queryEnd)
862+
cases = append(cases, &testCase{
863+
query: query, qt: "instant",
864+
res1: res1, res2: res2, err1: err1, err2: err2,
865+
})
866+
res1, err1 = c1.QueryRange(query, start, queryEnd, scrapeInterval)
867+
res2, err2 = c2.QueryRange(query, start, queryEnd, scrapeInterval)
868+
cases = append(cases, &testCase{
869+
query: query, qt: "range query",
870+
res1: res1, res2: res2, err1: err1, err2: err2,
871+
})
872+
}
873+
874+
for _, m := range matchers {
875+
fuzzyTime := time.Duration(rand.Int63n(dataWindowMs))
876+
queryEnd := start.Add(fuzzyTime * time.Millisecond)
877+
res1, err := c1.Series([]string{m}, start, queryEnd)
878+
require.NoError(t, err)
879+
res2, err := c2.Series([]string{m}, start, queryEnd)
880+
require.NoError(t, err)
881+
cases = append(cases, &testCase{
882+
query: m, qt: "get series",
883+
sres1: res1, sres2: res2,
884+
})
885+
}
886+
887+
failures := 0
888+
for i, tc := range cases {
889+
if tc.err1 != nil || tc.err2 != nil {
890+
if !cmp.Equal(tc.err1, tc.err2) {
891+
t.Logf("case %d error mismatch.\n%s: %s\nerr1: %v\nerr2: %v\n", i, tc.qt, tc.query, tc.err1, tc.err2)
892+
failures++
893+
}
894+
} else if shouldUseSampleNumComparer(tc.query) {
895+
if !cmp.Equal(tc.res1, tc.res2, sampleNumComparer) {
896+
t.Logf("case %d # of samples mismatch.\n%s: %s\nres1: %s\nres2: %s\n", i, tc.qt, tc.query, tc.res1.String(), tc.res2.String())
897+
failures++
898+
}
899+
} else if !cmp.Equal(tc.res1, tc.res2, comparer) {
900+
t.Logf("case %d results mismatch.\n%s: %s\nres1: %s\nres2: %s\n", i, tc.qt, tc.query, tc.res1.String(), tc.res2.String())
901+
failures++
902+
} else if !cmp.Equal(tc.sres1, tc.sres2, labelSetsComparer) {
903+
t.Logf("case %d series results mismatch.\n%s: %s\nsres1: %s\nsres2: %s\n", i, tc.qt, tc.query, tc.sres1, tc.sres2)
904+
failures++
905+
}
906+
}
907+
if failures > 0 {
908+
require.Failf(t, "finished lazy matcher fuzzing tests", "%d test cases failed", failures)
909+
}
910+
911+
// Verify the lazy-matcher optimization was actually triggered on cortex-2.
912+
// If the gate is misconfigured or the test fixture doesn't exercise the
913+
// path, this guards against silent regressions where the optimization
914+
// becomes a no-op.
915+
916+
// Diagnostic: print related counters before the assertion so failures
917+
// can be debugged from the test output.
918+
for _, m := range []string{
919+
"cortex_ingester_queries",
920+
"cortex_ingester_queried_series",
921+
"cortex_ingester_queried_chunks",
922+
"cortex_ingester_expanded_postings_cache_requests_total",
923+
"cortex_ingester_expanded_postings_cache_hits_total",
924+
"cortex_ingester_expanded_postings_non_cacheable_queries_total",
925+
"cortex_ingester_expanded_postings_lazy_matcher_queries_total",
926+
} {
927+
v, _ := cortex2.SumMetrics([]string{m})
928+
t.Logf("cortex-2 %s = %v", m, v)
929+
}
930+
931+
require.NoError(t, cortex2.WaitSumMetrics(e2e.Greater(0),
932+
"cortex_ingester_expanded_postings_lazy_matcher_queries_total"))
933+
934+
// Sanity check: cortex-1 (eager) should NEVER increment this counter.
935+
c1Lazy, err := cortex1.SumMetrics([]string{"cortex_ingester_expanded_postings_lazy_matcher_queries_total"})
936+
if err == nil && len(c1Lazy) > 0 {
937+
require.Equal(t, float64(0), c1Lazy[0],
938+
"cortex-1 has lazy matcher disabled but the metric is non-zero")
939+
}
940+
}
941+
668942
func TestVerticalShardingFuzz(t *testing.T) {
669943
s, err := e2e.NewScenario(networkName)
670944
require.NoError(t, err)

0 commit comments

Comments
 (0)