Skip to content

Commit 59c977a

Browse files
zzylolclaude
andcommitted
refactor(revisions): admit and build revision windows with the Planner graph
Remote Write revisions buffer each stored window's samples and build it with the installed Planner graph; request admission runs the same graph and rejects the whole request if it fails. Known-empty counter panes of a captured revision keep the family's empty state. The per-sample interpreter in raw_dag.rs is now private to heaps, whose stored readout still decodes only the backend heap kernel. Tests cover backfill equivalence and Planner admission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f04e020 commit 59c977a

2 files changed

Lines changed: 140 additions & 70 deletions

File tree

‎data_plane/src/precompute_engine/raw_dag.rs‎

Lines changed: 70 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ pub struct RawDagProgram {
2727
pub input: SummaryUpdate,
2828
pub grouping: GroupingStrategy,
2929
pub reduction: planner_types::pre_asap::Reduction,
30-
projected_column: Option<String>,
3130
/// Planner's encoded precompute graph from the raw sample boundary to this
3231
/// output. Decoded graphs are not `Send`, so each execution decodes it.
3332
program: std::sync::Arc<[u8]>,
@@ -197,12 +196,7 @@ impl RawDagProgram {
197196
input: input.clone(),
198197
grouping: grouping.clone(),
199198
reduction: reduction.clone(),
200-
projected_column: config
201-
.effective_value_projection()
202-
.column()
203-
.map(str::to_owned),
204199
};
205-
program.validate()?;
206200
if let Some(old) = &selected {
207201
if old.family != program.family
208202
|| old.input != program.input
@@ -222,6 +216,16 @@ impl RawDagProgram {
222216
selected.ok_or_else(|| "raw materialization has no selected post-ASAP DAG producer".into())
223217
}
224218

219+
/// Heap readout decodes only the backend heap kernel, not Planner's weighted
220+
/// frequency state, so heaps stay on that kernel until it does.
221+
fn heap(&self) -> bool {
222+
matches!(&self.family, SummaryFamilyType::Sketch(kind, _) if matches!(
223+
kind.algorithm(),
224+
planner_types::post_asap::SketchAlgorithm::CmsWithHeap
225+
| planner_types::post_asap::SketchAlgorithm::CountSketchWithHeap
226+
))
227+
}
228+
225229
/// Execute the Planner graph over one pane's samples as one typed batch.
226230
/// Returns `None` when the graph admits no population from them.
227231
pub fn build<'a>(
@@ -230,21 +234,67 @@ impl RawDagProgram {
230234
pane: (i64, i64),
231235
max_bytes: usize,
232236
) -> Result<Option<Box<dyn AggregateCore>>, String> {
233-
use futures::StreamExt;
234237
let samples = pane_batch(samples);
235-
// Stored heap readout decodes only the backend heap kernel, not
236-
// Planner's weighted frequency state, so heaps stay on that kernel.
237-
if matches!(&self.family, SummaryFamilyType::Sketch(kind, _) if matches!(
238-
kind.algorithm(),
239-
planner_types::post_asap::SketchAlgorithm::CmsWithHeap
240-
| planner_types::post_asap::SketchAlgorithm::CountSketchWithHeap
241-
)) {
242-
let mut updater = self.updater()?;
238+
if self.heap() {
239+
let mut updater = self.heap_updater()?;
243240
for (series, time, value) in samples {
244241
self.apply(&mut *updater, series, value, time)?;
245242
}
246243
return Ok(Some(updater.take_accumulator()));
247244
}
245+
// The router assigns one population per group, so one pane yields
246+
// at most one state.
247+
match self.execute(samples, pane, max_bytes)?.as_slice() {
248+
[] => Ok(None),
249+
[row] => match row.as_slice() {
250+
[_, _, Value::Summary { state, .. }] => {
251+
asap_summary_state::physical::from_physical(state.as_ref())
252+
.map(Some)
253+
.map_err(|e| e.to_string())
254+
}
255+
_ => Err("raw precompute output is not a population state".into()),
256+
},
257+
_ => Err("one routed group produced several populations".into()),
258+
}
259+
}
260+
261+
/// Check that the Planner graph admits every sample, without storing a result.
262+
pub fn validate<'a>(
263+
&self,
264+
samples: impl IntoIterator<Item = (&'a str, i64, f64)>,
265+
max_bytes: usize,
266+
) -> Result<(), String> {
267+
let samples = pane_batch(samples);
268+
if self.heap() {
269+
let updater = self.heap_updater()?;
270+
for (_, _, value) in samples {
271+
self.validate_sample(updater.as_ref(), value)?;
272+
}
273+
return Ok(());
274+
}
275+
if samples.is_empty() {
276+
return Ok(());
277+
}
278+
let bounds = samples
279+
.iter()
280+
.fold((i64::MAX, i64::MIN), |(lo, hi), (_, time, _)| {
281+
(lo.min(*time), hi.max(time.saturating_add(1)))
282+
});
283+
self.execute(samples, bounds, max_bytes).map(|_| ())
284+
}
285+
286+
/// The family's empty state, for a pane known to have no samples.
287+
pub fn empty_state(&self) -> Result<Box<dyn AggregateCore>, String> {
288+
Ok(self.heap_updater()?.take_accumulator())
289+
}
290+
291+
fn execute<'a>(
292+
&self,
293+
samples: impl IntoIterator<Item = (&'a str, i64, f64)>,
294+
pane: (i64, i64),
295+
max_bytes: usize,
296+
) -> Result<Vec<Vec<Value>>, String> {
297+
use futures::StreamExt;
248298
let schema = precompute::raw_sample_schema();
249299
let rows = samples
250300
.into_iter()
@@ -272,7 +322,7 @@ impl RawDagProgram {
272322
},
273323
)
274324
.map_err(|e| e.to_string())?;
275-
let rows = futures::executor::block_on(async {
325+
futures::executor::block_on(async {
276326
let mut stream = graph.execute(program.roots(), context)?.pop().ok_or(
277327
asap_physical_operators::Error::Invalid("missing output".into()),
278328
)?;
@@ -282,58 +332,14 @@ impl RawDagProgram {
282332
}
283333
Ok::<_, asap_physical_operators::Error>(rows)
284334
})
285-
.map_err(|e| e.to_string())?;
286-
// The router assigns one population per group, so one pane yields
287-
// at most one state.
288-
match rows.as_slice() {
289-
[] => Ok(None),
290-
[row] => match row.as_slice() {
291-
[_, _, Value::Summary { state, .. }] => {
292-
asap_summary_state::physical::from_physical(state.as_ref())
293-
.map(Some)
294-
.map_err(|e| e.to_string())
295-
}
296-
_ => Err("raw precompute output is not a population state".into()),
297-
},
298-
_ => Err("one routed group produced several populations".into()),
299-
}
335+
.map_err(|e| e.to_string())
300336
}
301337

302-
pub fn updater(&self) -> Result<Box<dyn AccumulatorUpdater>, String> {
338+
fn heap_updater(&self) -> Result<Box<dyn AccumulatorUpdater>, String> {
303339
create_planner_accumulator(&self.family, &self.input, &self.grouping)
304340
}
305341

306-
fn validate(&self) -> Result<(), String> {
307-
match &self.input.weight {
308-
SummaryInputExpr::Column(ColumnRef::SampleValue) | SummaryInputExpr::Constant(_) => {}
309-
SummaryInputExpr::Column(
310-
ColumnRef::Named(name) | ColumnRef::Qualified { name, .. },
311-
) if self.projected_column.as_ref() == Some(name) => {}
312-
_ => return Err("raw DAG weight expression is unsupported".into()),
313-
}
314-
fn item(expr: &SummaryInputExpr) -> bool {
315-
match expr {
316-
SummaryInputExpr::Column(ColumnRef::Named(_) | ColumnRef::SampleValue) => true,
317-
SummaryInputExpr::Tuple(items) => items.iter().all(item),
318-
SummaryInputExpr::EntityIdentity(
319-
planner_types::post_asap::EntityIdentity::PromqlLabelSet { excluding },
320-
) => excluding.is_empty(),
321-
_ => false,
322-
}
323-
}
324-
if self.input.item.as_ref().is_some_and(|e| !item(e)) {
325-
return Err("raw DAG item expression is unsupported".into());
326-
}
327-
self.updater().map(|_| ())
328-
}
329-
330-
/// Validate admission with the same weight semantics used during execution,
331-
/// without mutating an accumulator or accepting part of a request.
332-
pub fn validate_sample(
333-
&self,
334-
updater: &dyn AccumulatorUpdater,
335-
value: f64,
336-
) -> Result<f64, String> {
342+
fn validate_sample(&self, updater: &dyn AccumulatorUpdater, value: f64) -> Result<f64, String> {
337343
let weight = match &self.input.weight {
338344
SummaryInputExpr::Constant(c) => *c,
339345
// The worker retains one previous value per series across pane rotation.
@@ -347,7 +353,7 @@ impl RawDagProgram {
347353
Ok(weight)
348354
}
349355

350-
pub fn apply(
356+
fn apply(
351357
&self,
352358
updater: &mut dyn AccumulatorUpdater,
353359
series: &str,

‎data_plane/src/precompute_engine/worker.rs‎

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4658,8 +4658,17 @@ mod dag_execution_tests {
46584658
plans
46594659
}
46604660

4661-
// Live raw ingest builds each pane with the installed Planner graph, and
4662-
// every stored state equals feeding the selected kernel sample by sample.
4661+
/// The pre-Planner interpreter's unkeyed update: a constant weight or the
4662+
/// sample value; unit-frequency summaries observe the sample value.
4663+
fn reference_weight(input: &planner_types::post_asap::SummaryUpdate, value: f64) -> f64 {
4664+
match (&input.item, &input.weight) {
4665+
(None, planner_types::post_asap::SummaryInputExpr::Constant(weight)) => *weight,
4666+
_ => value,
4667+
}
4668+
}
4669+
4670+
// Live raw ingest and backfill build each pane with the installed Planner
4671+
// graph, and every stored state equals feeding the kernel sample by sample.
46634672
#[test]
46644673
fn live_panes_execute_planner_dag_with_identical_states() {
46654674
let mut outputs = 0;
@@ -4747,6 +4756,7 @@ mod dag_execution_tests {
47474756
.and_then(serde_json::Value::as_bool)
47484757
.unwrap_or(false);
47494758
let mut expected = BTreeMap::new();
4759+
let mut backfill = BTreeMap::new();
47504760
for (key, samples) in &groups {
47514761
let group_key = Arc::new(GroupKey::new(
47524762
key.iter().map(|(k, v)| (k.as_str(), v.as_str())),
@@ -4761,10 +4771,27 @@ mod dag_execution_tests {
47614771
start as u64,
47624772
end as u64,
47634773
))
4764-
.or_insert_with(|| program.updater().unwrap());
4765-
program
4766-
.apply(&mut **updater, series, *value, *time)
4767-
.unwrap();
4774+
.or_insert_with(|| {
4775+
asap_summary_state::factory::create_planner_accumulator(
4776+
&program.family,
4777+
&program.input,
4778+
&program.grouping,
4779+
)
4780+
.unwrap()
4781+
});
4782+
updater.update_single(reference_weight(&program.input, *value), *time);
4783+
backfill
4784+
.entry((
4785+
group_key.values().labels.join(";"),
4786+
start as u64,
4787+
end as u64,
4788+
))
4789+
.or_insert_with(Vec::new)
4790+
.push(crate::storage_engines::sketch_db::backfill::RawSample {
4791+
labels: series.clone(),
4792+
timestamp_ms: *time,
4793+
value: *value,
4794+
});
47684795
}
47694796
}
47704797
}
@@ -4810,6 +4837,25 @@ mod dag_execution_tests {
48104837
config.metric
48114838
);
48124839
assert_eq!(actual, expected, "{:?}", config.aggregation_type);
4840+
// Backfill builds each window with the same installed graph.
4841+
let backfilled = backfill
4842+
.into_iter()
4843+
.map(|((key, start, end), samples)| {
4844+
let state = crate::storage_engines::sketch_db::build_dag_accumulator(
4845+
&program,
4846+
&samples,
4847+
(start, end),
4848+
)
4849+
.unwrap()
4850+
.unwrap();
4851+
((key, start, end), state.serialize_to_bytes())
4852+
})
4853+
.collect::<BTreeMap<_, _>>();
4854+
assert_eq!(
4855+
backfilled, expected,
4856+
"backfill {:?}",
4857+
config.aggregation_type
4858+
);
48134859
outputs += 1;
48144860
families.insert(
48154861
format!("{:?}", config.accumulator_spec().unwrap().family)
@@ -4986,6 +5032,24 @@ mod dag_execution_tests {
49865032
}
49875033
}
49885034

5035+
// Revision admission checks samples with the installed Planner graph.
5036+
#[test]
5037+
fn admission_validates_with_the_planner_graph() {
5038+
let plan = plan("sum_over_time(asap_demo_gauge[5s])");
5039+
let config = plan.precompute_plan.materializations[0].clone();
5040+
let program = InstalledPrecomputePlan::from_precompute_plan(plan.precompute_plan)
5041+
.unwrap()
5042+
.raw_programs[&config.policy_fp_u64()]
5043+
.clone();
5044+
let series = config.metric.as_str();
5045+
assert!(program
5046+
.validate([(series, 1000, 1.0), (series, 2000, 2.0)], 1 << 20)
5047+
.is_ok());
5048+
assert!(program
5049+
.validate([(series, 1000, f64::INFINITY)], 1 << 20)
5050+
.is_err());
5051+
}
5052+
49895053
// A raw output installs only with its Planner-compiled precompute graph.
49905054
#[test]
49915055
fn raw_output_requires_its_planner_precompute_graph() {

0 commit comments

Comments
 (0)