Skip to content

Commit 859b7d9

Browse files
zzylolclaude
andcommitted
fix: restore grouped TopK heap item labels from their stored group
A topk by heap partitions by its group labels, so Planner's weighted frequency item identity excludes them. Stored-state readout now merges the stored group labels back into each series identity before rendering it, and install no longer rejects identity items that exclude labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8272a7a commit 859b7d9

6 files changed

Lines changed: 157 additions & 62 deletions

File tree

‎crates/asap_summary_state/src/stored_state/delta_apply.rs‎

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,12 @@ fn decode_full(
237237

238238
/// Render a ranked heap item as the legacy heap key: item parts joined by
239239
/// `;`, with a canonical series identity (it names `__name__`) shown as its
240-
/// series key.
241-
fn heap_item_key(items: &[asap_physical_operators::values::Value]) -> String {
240+
/// series key. A grouped heap's identity omits the labels it is partitioned
241+
/// by; those are stored as the heap's group labels and restored here.
242+
fn heap_item_key(
243+
items: &[asap_physical_operators::values::Value],
244+
group: &std::collections::BTreeMap<String, String>,
245+
) -> String {
242246
use asap_physical_operators::values::Value;
243247
items
244248
.iter()
@@ -247,7 +251,13 @@ fn heap_item_key(items: &[asap_physical_operators::values::Value]) -> String {
247251
asap_physical_operators::physical_planner::promql_rows::decode_series_identity(text)
248252
.ok()
249253
.filter(|labels| labels.contains_key("__name__"))
250-
.map(|labels| series_key(&labels))
254+
.map(|mut labels| {
255+
// An empty group value is a series without that label.
256+
for (name, value) in group.iter().filter(|(_, v)| !v.is_empty()) {
257+
labels.entry(name.clone()).or_insert_with(|| value.clone());
258+
}
259+
series_key(&labels)
260+
})
251261
.unwrap_or_else(|| text.to_string())
252262
}
253263
Value::Null => String::new(),
@@ -543,8 +553,11 @@ impl SummaryState {
543553
/// `None` for anything other than a heap-bearing state — the
544554
/// heap-less Frequency states (`Cms`/`CountSketch`) carry no item
545555
/// universe to enumerate, and the quantile/cardinality states have
546-
/// no heap at all.
547-
pub fn topk_items(&self) -> Option<Vec<(String, f64)>> {
556+
/// no heap at all. `group` is the stored group the state belongs to.
557+
pub fn topk_items(
558+
&self,
559+
group: &std::collections::BTreeMap<String, String>,
560+
) -> Option<Vec<(String, f64)>> {
548561
match self {
549562
SummaryState::CmsWithHeap(h) => Some(
550563
h.topk_heap_items()
@@ -566,7 +579,7 @@ impl SummaryState {
566579
else {
567580
return None;
568581
};
569-
Some((heap_item_key(&row), score))
582+
Some((heap_item_key(&row, group), score))
570583
})
571584
.collect(),
572585
),
@@ -890,7 +903,7 @@ mod tests {
890903
let state = cumulative_summary_state(&[(1000, &first), (2000, &second)], heap)
891904
.unwrap()
892905
.unwrap();
893-
let mut items = state.topk_items().unwrap();
906+
let mut items = state.topk_items(&Default::default()).unwrap();
894907
items.sort_by(|a, b| a.0.cmp(&b.0));
895908
assert_eq!(
896909
items,
@@ -912,6 +925,36 @@ mod tests {
912925
};
913926
assert!(cumulative_summary_state(&[(1000, &first)], narrower).is_err());
914927
}
928+
929+
// A grouped heap's item identity omits its partition labels; readout
930+
// restores non-empty ones from the stored group and keeps identity values.
931+
#[test]
932+
fn weighted_frequency_items_restore_group_labels() {
933+
use crate::summary_kernels::weighted_frequency::{
934+
FrequencyAlgorithm, PhysicalWeightedFrequency,
935+
};
936+
use asap_physical_operators::values::Value;
937+
let mut state = PhysicalWeightedFrequency::new(FrequencyAlgorithm::Cms, 64, 3, 8).unwrap();
938+
let identity = r#"{"__name__":"m","endpoint":"a"}"#;
939+
state.update(&[Value::Utf8(identity.into())], 2.0).unwrap();
940+
let state = SummaryState::WeightedFrequency(state);
941+
let group = |pairs: &[(&str, &str)]| {
942+
pairs
943+
.iter()
944+
.map(|(k, v)| (k.to_string(), v.to_string()))
945+
.collect::<std::collections::BTreeMap<_, _>>()
946+
};
947+
assert_eq!(
948+
state.topk_items(&group(&[("job", "j")])).unwrap(),
949+
vec![(r#"m{endpoint="a",job="j"}"#.to_string(), 2.0)]
950+
);
951+
assert_eq!(
952+
state
953+
.topk_items(&group(&[("job", ""), ("endpoint", "other")]))
954+
.unwrap(),
955+
vec![(r#"m{endpoint="a"}"#.to_string(), 2.0)]
956+
);
957+
}
915958
use asap_sketchlib::HllVariant;
916959

917960
fn encode_dd(sk: &DdSketch) -> Vec<u8> {

‎crates/asap_summary_state/src/stored_state/readout.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,12 @@ pub fn sketch_query_value(rs: &SummaryState, query: &SketchQuery) -> Result<f64,
8282
/// heap-less family (`Dd`/`Hll`/`Kll`/`Cms`/`CountSketch` -- no item
8383
/// universe to rank), not for an empty heap (a heap-bearing family that
8484
/// simply never received any updates yields `Ok(vec![])`, not an error).
85-
pub fn topk_ranked(rs: &SummaryState, k: usize) -> Result<Vec<(String, f64)>, Error> {
86-
let mut items = rs.topk_items().ok_or(Error::Unsupported(
85+
pub fn topk_ranked(
86+
rs: &SummaryState,
87+
k: usize,
88+
group: &std::collections::BTreeMap<String, String>,
89+
) -> Result<Vec<(String, f64)>, Error> {
90+
let mut items = rs.topk_items(group).ok_or(Error::Unsupported(
8791
"TopK requires a heap-bearing family (CmsWithHeap/CountSketchWithHeap) -- \
8892
this state's family carries no item universe to rank",
8993
))?;

‎data_plane/src/precompute_engine/raw_dag.rs‎

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,6 @@ impl RawDagProgram {
192192
{
193193
return Err("raw precompute graph does not read the bound raw source".into());
194194
}
195-
// Stored heap readout renders identity items as whole series
196-
// keys; one that omits labels would not name its series.
197-
fn partial_identity(expr: &SummaryInputExpr) -> bool {
198-
match expr {
199-
SummaryInputExpr::EntityIdentity(
200-
planner_types::post_asap::EntityIdentity::PromqlLabelSet { excluding },
201-
) => !excluding.is_empty(),
202-
SummaryInputExpr::Tuple(items) => items.iter().any(partial_identity),
203-
_ => false,
204-
}
205-
}
206-
if input.item.as_ref().is_some_and(partial_identity) {
207-
return Err(
208-
"raw heap items that exclude identity labels are not readable".into(),
209-
);
210-
}
211195
let program = Self {
212196
source,
213197
program: compiled.encode().map_err(|e| e.to_string())?.into(),

‎data_plane/src/query_engines/asap_query_engine/post_asap_readout.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ impl PhysicalQueryRuntime<'_> {
212212
for (key, state) in groups {
213213
let value = self
214214
.context
215-
.readout_bound(state, &query)
215+
.readout_bound(key, state, &query)
216216
.map_err(PhysicalNodeError::Store)?;
217217
let (mut rows, row_coverage) = expand_item_readout(key, value, item_labels)?;
218218
if self.language == control_plane::query_plan::QueryLanguage::MetricsQl

‎data_plane/src/query_engines/asap_query_engine/summary_executor.rs‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -548,12 +548,14 @@ impl QueryExecutionContext<'_> {
548548
Ok(result)
549549
}
550550

551+
/// `group` is the state's stored group labels, as `read_bound_materialization` returns them.
551552
pub fn readout_bound(
552553
&self,
554+
group: &BTreeMap<String, String>,
553555
state: &GroupState,
554556
query: &SketchQuery,
555557
) -> Result<SummaryValue, SummaryExecutorError> {
556-
self.readout(state, query)
558+
self.readout(group, state, query)
557559
}
558560

559561
pub fn merge_bound_states(
@@ -618,16 +620,17 @@ impl QueryExecutionContext<'_> {
618620

619621
fn readout(
620622
&self,
623+
group: &BTreeMap<String, String>,
621624
state: &GroupState,
622625
query: &SketchQuery,
623626
) -> Result<SummaryValue, SummaryExecutorError> {
624627
let GroupState::Sketch { entries, kind } = state else {
625628
return Err(SummaryExecutorError::UnsupportedFamily);
626629
};
627630
if self.is_cumulative {
628-
readout_cumulative(entries, *kind, query, self.t1_ms as i64)
631+
readout_cumulative(entries, *kind, query, self.t1_ms as i64, group)
629632
} else {
630-
readout_per_window(entries, *kind, query, self.t0_ms as i64)
633+
readout_per_window(entries, *kind, query, self.t0_ms as i64, group)
631634
}
632635
}
633636
}
@@ -640,6 +643,7 @@ fn readout_cumulative(
640643
kind: DeltaSketchKind,
641644
query: &SketchQuery,
642645
t1_ms: i64,
646+
group: &BTreeMap<String, String>,
643647
) -> Result<SummaryValue, SummaryExecutorError> {
644648
let mut merged: Option<SummaryState> = None;
645649
let mut latest_window_end: Option<i64> = None;
@@ -675,7 +679,7 @@ fn readout_cumulative(
675679
let w_end = latest_window_end.unwrap_or(t1_ms);
676680
if let SketchQuery::TopK { k } = query {
677681
Ok(SummaryValue::TopK(
678-
vec![(w_end, topk_ranked(&merged, *k)?)],
682+
vec![(w_end, topk_ranked(&merged, *k, group)?)],
679683
coverage,
680684
))
681685
} else {
@@ -697,6 +701,7 @@ fn readout_per_window(
697701
kind: DeltaSketchKind,
698702
query: &SketchQuery,
699703
t0_ms: i64,
704+
group: &BTreeMap<String, String>,
700705
) -> Result<SummaryValue, SummaryExecutorError> {
701706
let mut by_window: BTreeMap<i64, SummaryState> = BTreeMap::new();
702707
// Tracked from RAW window-ends, before the `w_end < t0_ms` carry-in
@@ -738,7 +743,7 @@ fn readout_per_window(
738743
if let SketchQuery::TopK { k } = query {
739744
let points = by_window
740745
.into_iter()
741-
.map(|(w_end, rs)| topk_ranked(&rs, *k).map(|items| (w_end, items)))
746+
.map(|(w_end, rs)| topk_ranked(&rs, *k, group).map(|items| (w_end, items)))
742747
.collect::<Result<Vec<_>, _>>()?;
743748
Ok(SummaryValue::TopK(points, coverage))
744749
} else {
@@ -763,8 +768,12 @@ fn sketch_query_value(
763768
},
764769
)
765770
}
766-
fn topk_ranked(state: &SummaryState, k: usize) -> Result<Vec<(String, f64)>, SummaryExecutorError> {
767-
asap_summary_state::stored_state::readout::topk_ranked(state, k).map_err(
771+
fn topk_ranked(
772+
state: &SummaryState,
773+
k: usize,
774+
group: &BTreeMap<String, String>,
775+
) -> Result<Vec<(String, f64)>, SummaryExecutorError> {
776+
asap_summary_state::stored_state::readout::topk_ranked(state, k, group).map_err(
768777
|asap_summary_state::stored_state::readout::Error::Unsupported(reason)| {
769778
SummaryExecutorError::Unsupported(reason)
770779
},
@@ -1187,7 +1196,11 @@ mod tests {
11871196
};
11881197
let states = context.read_bound_materialization(&binding).unwrap();
11891198
let SummaryValue::Points(points, coverage) = context
1190-
.readout_bound(&states[0].1, &SketchQuery::Quantile { q: 0.5 })
1199+
.readout_bound(
1200+
&states[0].0,
1201+
&states[0].1,
1202+
&SketchQuery::Quantile { q: 0.5 },
1203+
)
11911204
.unwrap()
11921205
else {
11931206
panic!("expected points");
@@ -1267,8 +1280,9 @@ mod tests {
12671280
(SketchQuery::FrequencyL2, 6.0f64.sqrt()),
12681281
(SketchQuery::FrequencyEntropy, 1.5),
12691282
] {
1270-
let SummaryValue::Points(points, _) =
1271-
context.readout_bound(&states[0].1, &query).unwrap()
1283+
let SummaryValue::Points(points, _) = context
1284+
.readout_bound(&states[0].0, &states[0].1, &query)
1285+
.unwrap()
12721286
else {
12731287
panic!("expected scalar points")
12741288
};

0 commit comments

Comments
 (0)