Skip to content

Commit 1910429

Browse files
committed
test(plan-selection): pin that the hook never overrides a guarantee that misses
Add a Stage 3 test with evidence that misses the target and an always-accepting hook (still AccuracyTargetMissed), and an end-to-end test that candidates whose guarantee misses under the built-in models stay rejected under an accepting model. Drop the unused crate-root re-export of PropagationStats and re-indent the accuracy rejection reasons.
1 parent 5b23746 commit 1910429

3 files changed

Lines changed: 137 additions & 11 deletions

File tree

‎crates/logical-asap-optimization/src/lib.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ pub mod realization;
5454
#[cfg(any(test, feature = "test-support"))]
5555
pub mod test_support;
5656

57-
pub use accuracy::PropagationStats;
5857
pub use deployment_capabilities::{
5958
DeploymentCapabilities, DeploymentCapabilitiesError, SupportedSummary,
6059
};

‎crates/plan-selection/src/lib.rs‎

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,24 +1534,23 @@ fn accuracy_check(
15341534
{
15351535
format!(
15361536
"q{query}: the output's guarantee holds for each value, and the number \
1537-
of values one evaluation returns is not bound (no declared series count \
1538-
for a source a PromQL vector reads; no bound on a SQL result's groups \
1539-
yet), \
1540-
so it does not meet whole-result target {target:?}, and the deployment \
1541-
has no evidence that it meets it"
1537+
of values one evaluation returns is not bound (no declared series \
1538+
count for a source a PromQL vector reads; no bound on a SQL result's \
1539+
groups yet), so it does not meet whole-result target {target:?}, and \
1540+
the deployment has no evidence that it meets it"
15421541
)
15431542
}
15441543
Some(g) => format!(
1545-
"q{query}: the output guarantees a {:?} bound {:?}, failure probability {:?}, \
1546-
which misses target {target:?}, and the deployment has no evidence that it \
1547-
meets it",
1544+
"q{query}: the output guarantees a {:?} bound {:?}, failure probability \
1545+
{:?}, which misses target {target:?}, and the deployment has no evidence \
1546+
that it meets it",
15481547
g.spec.accuracy_metric,
15491548
g.spec.error.evaluate(),
15501549
g.spec.failure_probability.evaluate(),
15511550
),
15521551
None => format!(
1553-
"q{query}: the output has no guarantee, and the deployment has no evidence \
1554-
that it meets target {target:?}"
1552+
"q{query}: the output has no guarantee, and the deployment has no \
1553+
evidence that it meets target {target:?}"
15551554
),
15561555
},
15571556
));
@@ -4249,6 +4248,87 @@ mod tests {
42494248
}
42504249
}
42514250

4251+
/// Deployment evidence of a large absolute error (1.0) for every output.
4252+
struct EveryOutputMeasuredLoose;
4253+
4254+
impl AccuracyEvidenceProvider for EveryOutputMeasuredLoose {
4255+
fn output_guarantee(&self, _output: &QueryRoot) -> Option<AccuracyGuarantee> {
4256+
Some(AccuracyGuarantee::new(
4257+
AccuracyMetric::ABSOLUTE,
4258+
BoundExpr::Constant { value: 1.0 },
4259+
ProbabilityExpr::Constant { value: 1e-6 },
4260+
vec![],
4261+
))
4262+
}
4263+
}
4264+
4265+
/// A model that accepts every output that has no guarantee.
4266+
struct AcceptsEveryOutputWithoutGuarantee;
4267+
4268+
impl AccuracyModel for AcceptsEveryOutputWithoutGuarantee {
4269+
fn satisfies(&self, guarantee: &AccuracyGuarantee, target: &AccuracyTarget) -> bool {
4270+
BuiltinAccuracyModel.satisfies(guarantee, target)
4271+
}
4272+
4273+
fn accepts_output_without_guarantee(
4274+
&self,
4275+
_output: &QueryRoot,
4276+
_target: &AccuracyTarget,
4277+
) -> bool {
4278+
true
4279+
}
4280+
}
4281+
4282+
/// The model's hook decides only when there is no guarantee at all: an
4283+
/// output with no theoretical guarantee whose evidence guarantee misses
4284+
/// the target is rejected as `AccuracyTargetMissed`, though the hook
4285+
/// would accept it. Without that evidence the hook accepts it.
4286+
#[test]
4287+
fn a_missed_evidence_guarantee_is_not_overridden_by_the_hook() {
4288+
let target = AccuracyTarget::max_error(0.01).failure_probability(0.001);
4289+
let candidates = all_candidates(
4290+
"quantile_over_time(0.99, m[1m]) - quantile_over_time(0.5, m[1m])",
4291+
&target,
4292+
);
4293+
let summaries: Vec<_> = candidates
4294+
.iter()
4295+
.filter(|c| has_summary_estimate(c))
4296+
.map(|c| c.id.as_str())
4297+
.collect();
4298+
assert!(!summaries.is_empty());
4299+
let select = |evidence: &dyn AccuracyEvidenceProvider| {
4300+
select_plan(
4301+
&candidates,
4302+
&[entry_with(
4303+
target.clone().accuracy_metric(AccuracyMetric::ABSOLUTE),
4304+
repeating(10_000),
4305+
)],
4306+
&data(),
4307+
PlanningModels::builtin()
4308+
.with_accuracy(&AcceptsEveryOutputWithoutGuarantee)
4309+
.with_evidence(evidence),
4310+
)
4311+
.unwrap()
4312+
};
4313+
let loose = select(&EveryOutputMeasuredLoose);
4314+
for id in &summaries {
4315+
let rejection = loose.rejected.iter().find(|r| r.id == *id).unwrap();
4316+
assert_eq!(
4317+
rejection.cause,
4318+
RejectionCause::AccuracyTargetMissed,
4319+
"{}",
4320+
rejection.reason
4321+
);
4322+
}
4323+
let no_evidence = select(&NoAccuracyEvidence);
4324+
for id in &summaries {
4325+
assert!(
4326+
no_evidence.accepted[*id][0].accepted_without_guarantee,
4327+
"{id}"
4328+
);
4329+
}
4330+
}
4331+
42524332
/// `candidates` with every state node (a summary build or an exact
42534333
/// aggregate) kept at ingestion time, so selection charges its kept
42544334
/// bytes every second (a `MatAtQuery` state is held only for one

‎crates/planner/tests/deployment_accuracy_model.rs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,50 @@ async fn the_deployment_accuracy_model_decides_outputs_without_a_guarantee() {
255255
assert!(unguaranteed.contains(&accepting_selection.selected));
256256
assert!(reads_sketch(&accepting.plans[0].root));
257257
}
258+
259+
/// The hook decides only outputs with no guarantee: a candidate whose
260+
/// guarantee misses its target under the built-in models (a relative bound
261+
/// for a rank target) is still rejected for that under the accepting model.
262+
#[tokio::test]
263+
async fn the_hook_does_not_override_a_guarantee_that_misses() {
264+
// The user's workload: one p99 query with a 1% rank-error target.
265+
let workload = rank_target_workload("quantile_over_time(0.99, lat[5m])");
266+
let promql = || FrontendInput::Promql { histograms: None };
267+
// The candidates whose guarantee misses the target under the built-in models.
268+
let builtin = e2e_plan(UserInput::new(
269+
&workload,
270+
promql(),
271+
PlanningModels::builtin(),
272+
))
273+
.await
274+
.expect("plans with the built-in models");
275+
let missed: BTreeSet<String> = builtin
276+
.selection
277+
.as_ref()
278+
.unwrap()
279+
.rejected
280+
.iter()
281+
.filter(|r| r.cause == RejectionCause::AccuracyTargetMissed)
282+
.map(|r| r.id.clone())
283+
.collect();
284+
assert!(!missed.is_empty());
285+
// The same workload with the model that accepts every output without a guarantee.
286+
let accepting_model = AcceptsOutputsWithoutGuarantee;
287+
let accepting = e2e_plan(UserInput::new(
288+
&workload,
289+
promql(),
290+
PlanningModels::builtin().with_accuracy(&accepting_model),
291+
))
292+
.await
293+
.expect("plans with the accepting model");
294+
let accepting_selection = accepting.selection.as_ref().unwrap();
295+
for id in &missed {
296+
assert!(
297+
accepting_selection
298+
.rejected
299+
.iter()
300+
.any(|r| &r.id == id && r.cause == RejectionCause::AccuracyTargetMissed),
301+
"{id}"
302+
);
303+
}
304+
}

0 commit comments

Comments
 (0)