Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 51 additions & 106 deletions control_plane/src/accuracy.rs
Original file line number Diff line number Diff line change
@@ -1,93 +1,43 @@
//! Theoretical accuracy profile of a chosen `SketchParams`.
//!
//! Mirrors `ASAPQuery-backend/src/stores/sketch_db/accuracy.rs` —
//! both sides compute the same (ε, δ) from the same sketch type
//! + parameters, so a plan the control plane validates here meets
//! the same bound the backend will later surface on query
//! responses. The two files must be kept in lockstep; a golden
//! test at the bottom of this module pins the numeric parity.
//!
//! ## Why the control plane needs this
//!
//! The planner picks `SketchParams` (width/depth/K/α/precision)
//! to meet a user-supplied `accuracy_sla`. Today the cost model
//! approximates the bound inline in a few places
//! (`cost_model.rs:156-158`). Centralising the derivation here:
//!
//! * lets `DeploymentPlanCompiler` / `DeploymentCostPlanner` / any future
//! planner compute the post-hoc ε of the chosen plan and
//! verify it actually meets the SLA.
//! * surfaces the bound to downstream systems (backend via
//! `/api/v1/plan` response; dashboards via
//! `asap_otel_processor_accuracy_epsilon` gauge) without the
//! caller re-deriving from scratch.
//!
//! ## Bounds we encode
//!
//! | Sketch | `kind` | ε formula | δ formula |
//! |-------------------|-----------------------|----------------------|--------------------|
//! | CountMinSketch | `AdditiveFrequency` | e / w | 1 / 2^d |
//! | CountSketch(ε, δ) | `AdditiveFrequency` | ε (user-supplied) | δ (user-supplied) |
//! | HLL(p) | `RelativeCardinality` | 1.04 / √(2^p) | — (Gaussian σ) |
//! | KLL(k) | `RankQuantile` | 2.296 / √k | 0.01 (fixed) |
//! | DDSketch(α) | `RelativeQuantile` | α | 0 (deterministic) |
//!
//! Citations (verbatim from the backend):
//! * CMS — Cormode & Muthukrishnan, *J. Algorithms* 55(1) 2005
//! * CountSketch — Charikar, Chen, Farach-Colton, ICALP 2002
//! * HLL — Flajolet et al., DMTCS 2007
//! * KLL — Karnin, Lang, Liberty, FOCS 2016
//! * DDSketch — Masson, Rim, Lee, VLDB 2019
//! Adapt planner sketch parameters to ASAPPlanner accuracy guarantees.

use crate::types::SketchParams;
pub use asap_types::{AccuracyKind, AccuracyProfile};
pub use asap_types::accuracy::{AccuracyKind, AccuracyProfile};
use planner_types::post_asap::SketchParams as PlannerParams;

/// Planner-specific derivation over Planner-owned sketch parameters.
pub fn derive(params: &SketchParams) -> AccuracyProfile {
let normalized = match params {
SketchParams::CountMinSketch { rows, cols, .. } => PlannerParams::Cms {
depth: (*rows).max(1),
width: (*cols).max(1),
},
SketchParams::CountSketch { epsilon, delta } => {
return AccuracyProfile {
epsilon: *epsilon,
delta: Some(*delta),
kind: AccuracyKind::AdditiveFrequency,
}
}
SketchParams::HLL { precision } => PlannerParams::Hll {
precision: u8::try_from(*precision).unwrap_or(14),
},
SketchParams::KLL { k, .. } => PlannerParams::Kll { k: (*k).max(1) },
SketchParams::DDSketch {
relative_accuracy, ..
} => PlannerParams::DDSketch {
alpha: *relative_accuracy,
},
};
AccuracyProfile::from_sketch_params(&normalized)
.expect("shared family has a numeric planner bound")
}

/// Source adapter for planner configuration parameters.
pub trait PlannerAccuracyProfile {
fn derive(params: &SketchParams) -> Self;
}

impl PlannerAccuracyProfile for AccuracyProfile {
fn derive(params: &SketchParams) -> Self {
match params {
SketchParams::CountMinSketch { rows, cols, .. } => {
let cols = (*cols).max(1) as f64;
let rows = (*rows).max(1) as i32;
Self {
epsilon: std::f64::consts::E / cols,
delta: 0.5_f64.powi(rows),
kind: AccuracyKind::AdditiveFrequency,
}
}
SketchParams::CountSketch { epsilon, delta } => Self {
epsilon: *epsilon,
delta: *delta,
kind: AccuracyKind::AdditiveFrequency,
},
SketchParams::HLL { precision } => {
let m = (1u64 << precision) as f64;
Self {
epsilon: 1.04 / m.sqrt(),
delta: 0.0,
kind: AccuracyKind::RelativeCardinality,
}
}
SketchParams::KLL { k, .. } => {
let k = (*k).max(1) as f64;
Self {
epsilon: 2.296 / k.sqrt(),
delta: 0.01,
kind: AccuracyKind::RankQuantile,
}
}
SketchParams::DDSketch {
relative_accuracy, ..
} => Self {
epsilon: *relative_accuracy,
delta: 0.0,
kind: AccuracyKind::RelativeQuantile,
},
}
derive(params)
}
}

Expand All @@ -97,87 +47,82 @@ mod tests {

#[test]
fn cms_bound_is_e_over_w() {
let p = AccuracyProfile::derive(&SketchParams::CountMinSketch {
let p = derive(&SketchParams::CountMinSketch {
rows: 3,
cols: 1000,
metric_name: "m".into(),
});
assert_eq!(p.kind, AccuracyKind::AdditiveFrequency);
assert!((p.epsilon - std::f64::consts::E / 1000.0).abs() < 1e-12);
assert!((p.delta - 0.125).abs() < 1e-12);
assert!((p.delta.unwrap() - (-3.0_f64).exp()).abs() < 1e-12);
}

#[test]
fn countsketch_passes_through_user_supplied_bounds() {
let p = AccuracyProfile::derive(&SketchParams::CountSketch {
let p = derive(&SketchParams::CountSketch {
epsilon: 0.01,
delta: 0.001,
});
assert_eq!(p.kind, AccuracyKind::AdditiveFrequency);
assert_eq!(p.epsilon, 0.01);
assert_eq!(p.delta, 0.001);
assert_eq!(p.delta, Some(0.001));
}

#[test]
fn hll_p14_matches_flajolet_bound() {
let p = AccuracyProfile::derive(&SketchParams::HLL { precision: 14 });
let p = derive(&SketchParams::HLL { precision: 14 });
assert_eq!(p.kind, AccuracyKind::RelativeCardinality);
// 1.04 / √16384 = 0.008125
assert!((p.epsilon - 0.008125).abs() < 1e-9);
}

#[test]
fn kll_k200_matches_karnin_lang_liberty_bound() {
let p = AccuracyProfile::derive(&SketchParams::KLL {
let p = derive(&SketchParams::KLL {
k: 200,
quantiles: vec![0.5, 0.95, 0.99],
});
assert_eq!(p.kind, AccuracyKind::RankQuantile);
assert!((p.epsilon - 2.296 / 200.0_f64.sqrt()).abs() < 1e-12);
assert!((p.delta - 0.01).abs() < 1e-12);
assert!((p.epsilon - 2.296 / 200.0_f64.powf(0.9723)).abs() < 1e-12);
assert!((p.delta.unwrap() - 0.01).abs() < 1e-12);
}

#[test]
fn ddsketch_alpha_passes_through_verbatim() {
for alpha in [0.005, 0.01, 0.02, 0.05] {
let p = AccuracyProfile::derive(&SketchParams::DDSketch {
let p = derive(&SketchParams::DDSketch {
relative_accuracy: alpha,
quantiles: vec![0.5, 0.99],
});
assert_eq!(p.kind, AccuracyKind::RelativeQuantile);
assert_eq!(p.epsilon, alpha);
assert_eq!(p.delta, 0.0);
assert_eq!(p.delta, Some(0.0));
}
}

/// **Parity contract** with the backend. Pinned numeric
/// values are identical to what `ASAPQuery-backend`'s
/// `AccuracyProfile::derive` produces for the matching
/// `AggregationConfig`. Any change that drifts these tests
/// likely needs a matching change on the backend side —
/// and vice versa.
/// Keep the user-facing summary explicit about unknown HLL confidence.
#[test]
fn golden_parity_with_backend() {
// HLL precision=14: ε = 0.008125, kind = relative_cardinality
let p = AccuracyProfile::derive(&SketchParams::HLL { precision: 14 });
let p = derive(&SketchParams::HLL { precision: 14 });
assert_eq!(
p.summary(),
"accuracy: ε=0.008125, δ=0, kind=relative_cardinality"
"accuracy: ε=0.008125, δ=unknown, kind=relative_cardinality"
);

// KLL k=200: ε = 2.296 / 200, kind = rank_quantile, δ = 0.01
let p = AccuracyProfile::derive(&SketchParams::KLL {
// KLL k=200: ε = 2.296 / 200^0.9723, kind = rank_quantile, δ = 0.01
let p = derive(&SketchParams::KLL {
k: 200,
quantiles: vec![],
});
let expected_eps = 2.296_f64 / 200.0_f64.sqrt();
let expected_eps = 2.296_f64 / 200.0_f64.powf(0.9723);
assert_eq!(
p.summary(),
format!("accuracy: ε={}, δ=0.01, kind=rank_quantile", expected_eps)
);

// DDSketch α=0.02: passes verbatim, δ=0, kind=relative_quantile
let p = AccuracyProfile::derive(&SketchParams::DDSketch {
let p = derive(&SketchParams::DDSketch {
relative_accuracy: 0.02,
quantiles: vec![],
});
Expand All @@ -186,7 +131,7 @@ mod tests {

#[test]
fn kind_serialises_to_snake_case() {
let p = AccuracyProfile::derive(&SketchParams::HLL { precision: 14 });
let p = derive(&SketchParams::HLL { precision: 14 });
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"kind\":\"relative_cardinality\""));
}
Expand All @@ -195,7 +140,7 @@ mod tests {
fn round_trip_through_serde() {
let src = AccuracyProfile {
epsilon: 0.008125,
delta: 0.0,
delta: None,
kind: AccuracyKind::RelativeCardinality,
};
let json = serde_json::to_string(&src).unwrap();
Expand Down
69 changes: 4 additions & 65 deletions control_plane/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
//! Control plane crate — library surface for the ASAPQuery-backend host.
//!
//! Refactor-2026-05 (Phase 9 / `refactor/controller-layered-cleanup`):
//! the controller previously ran as a standalone binary with its own
//! OpAMP server and HTTP API. After the controller crate moved into
//! ASAPQuery-backend, the same modules are exposed as a Rust library so
//! `asap-query-engine` can call them in-process — capability mapping,
//! plan emission, OpAMP push from the backend host. This `lib.rs`
//! declares the public module surface; the existing `main.rs` continues
//! to provide the standalone binary entrypoint for any deployments that
//! still want to run the control plane out-of-process.
//! Control-plane planning, configuration emission, and deployment services.
//!
//! These modules support in-process integration and the standalone control-plane
//! binary. Internal module paths are not a wire-protocol compatibility contract.

#![allow(
clippy::collapsible_match,
clippy::doc_lazy_continuation,
Expand All @@ -23,52 +16,6 @@
clippy::vec_init_then_push
)]

//! ## 2026-05 layered-cleanup refactor — old → new module mapping
//!
//! The internal module layout was restructured to mirror
//! `control_plane/docs/design.md` §5 target layout without splitting into
//! multiple crates:
//!
//! | Old path | New path |
//! |---|---|
//! | `controller/src/algebra/expr.rs` | `controller/src/intent_algebra/relational.rs` |
//! | `controller/src/algebra/lower.rs` | `controller/src/intent_algebra/legacy_lower.rs` |
//! | `controller/src/algebra/directory.rs` | `controller/src/physical/sketch_catalog.rs` |
//! | `controller/src/algebra/physical.rs` | `controller/src/physical/planner.rs` |
//! | `controller/src/algebra/allocator.rs` | `controller/src/physical/allocator.rs` |
//! | `controller/src/algebra/plan.rs` | `controller/src/physical/plan.rs` |
//! | `controller/src/algebra/optimizer.rs` | `controller/src/optimizer/engine.rs` |
//! | `controller/src/planner/cost_model.rs` | `controller/src/optimizer/cost/mod.rs` |
//! | `controller/src/planner/{delta,online}_cost_model.rs` | `controller/src/optimizer/cost/{delta,online}.rs` |
//! | `controller/src/planner/{pareto,tco,wire_cost}.rs` | `controller/src/optimizer/cost/{pareto,tco,wire}.rs` |
//! | `controller/src/planner/rules.rs` | `controller/src/optimizer/rules/mod.rs` |
//! | `controller/src/planner/baseline_planner.rs` | `controller/src/optimizer/baseline.rs` |
//! | `controller/src/planner/stage_split.rs` | `controller/src/physical/stage_split.rs` |
//! | `controller/src/analyzer.rs` | `controller/src/pipeline.rs` |
//! | `controller/src/stage_split/` | `controller/src/physical/colored_dag/` |
//! | `controller/src/query_language/` | `controller/src/query_parser/language/` |
//! | `controller/src/config/workloads.rs` | `controller/src/workload.rs` |
//! | `controller/src/config/{stage_config*,agent,backend,asapquery_backend,precompute}.rs` | `controller/src/emit/{...}.rs` |
//!
//! Public modules to consume from `asap-query-engine`:
//! - `intent_algebra` — `AggIntent` + `QueryExpr` DAG (canonical L3 IR;
//! `relational` carries the L2 relational IR the parsers emit and
//! `lower` lowers it to the canonical L3 types).
//! - `query_parser` — L1 entry point: `parse_query_expr_canonical`/`parse_query`
//! call `asap_frontend_promql::lower_promql` directly (no local parser
//! since design-target-architecture.md Part B; SQL not yet adopted).
//! - `physical` — L5 framework (allocator, planner, plan, sketch_catalog,
//! colored_dag, stage_split, topology).
//! - `optimizer` — L4 rule engine + cost model traits/impls + baseline
//! planner.
//! - `opamp` — OpAMP server (will be invoked from the backend's
//! service startup once Phase 4 wires the in-process integration).
//! - `types`, `types_v2` — control-plane-internal data model.
//!
//! NOT intended for public consumption from outside the workspace —
//! these modules expose the control plane's L1–L5 internals and are not
//! part of any wire/protocol contract.

pub mod accuracy;
pub mod backend_client;
pub mod clickhouse;
Expand All @@ -92,14 +39,6 @@ pub mod types;
pub mod types_v2;
pub mod workload;

// 2026-05 layered-cleanup follow-up: the back-compat shims previously
// defined here (`pub use emit as config`, `pub use pipeline as analyzer`,
// `pub mod algebra { … }`, `pub use physical::colored_dag as stage_split`,
// `pub mod planner { … }`) have been removed. `main.rs` and other
// consumers now reference the canonical module names directly
// (`emit`, `pipeline`, `intent_algebra::relational`, `optimizer`,
// `physical`, `physical::colored_dag`, etc.) per the layered-cleanup
// follow-up task.
/// PromQL → ASAPPlanner's canonical post-ASAP plan via
/// `asap_aware_mapping::bind::implement_tree`.
pub mod asap_tier_implement;
Expand Down
Loading
Loading