diff --git a/crates/asap-aware-mapping/src/pass/major.rs b/crates/asap-aware-mapping/src/pass/major.rs index 6deffdfcd..7956ea094 100644 --- a/crates/asap-aware-mapping/src/pass/major.rs +++ b/crates/asap-aware-mapping/src/pass/major.rs @@ -70,7 +70,7 @@ impl OptimizationPass for MajorPass { demand, lifecycle.now_ms, lifecycle.horizon, - lifecycle.capabilities, + models.capabilities, models.cost, ) .map_err(OptimizeError::LifecycleSelection)?; @@ -132,7 +132,7 @@ impl OptimizationPass for MajorPass { }, lifecycle.now_ms, lifecycle.horizon, - lifecycle.capabilities, + models.capabilities, models.cost, ) .map_err(|source| OptimizeError::LifecycleAssembly { @@ -171,7 +171,7 @@ impl OptimizationPass for MajorPass { }, lifecycle.now_ms, lifecycle.horizon, - lifecycle.capabilities, + models.capabilities, models.cost, ) .map_err(|source| OptimizeError::LifecycleAssembly { diff --git a/crates/asap-aware-mapping/src/pass/mod.rs b/crates/asap-aware-mapping/src/pass/mod.rs index f9dd0ac2d..34e691cc0 100644 --- a/crates/asap-aware-mapping/src/pass/mod.rs +++ b/crates/asap-aware-mapping/src/pass/mod.rs @@ -48,6 +48,7 @@ pub struct PlanningModels<'a> { pub cost: &'a dyn CostModel, pub accuracy: &'a dyn AccuracyModel, pub evidence: &'a dyn AccuracyEvidenceProvider, + pub capabilities: SummaryMaintenanceLifecycleCapabilities, } impl<'a> PlanningModels<'a> { @@ -60,6 +61,7 @@ impl<'a> PlanningModels<'a> { cost, accuracy, evidence, + capabilities: SummaryMaintenanceLifecycleCapabilities::ALL, } } @@ -71,6 +73,7 @@ impl<'a> PlanningModels<'a> { cost: &DEFAULT_COST_MODEL, accuracy: &DEFAULT_ACCURACY_MODEL, evidence: &NO_ACCURACY_EVIDENCE, + capabilities: SummaryMaintenanceLifecycleCapabilities::ALL, } } @@ -88,6 +91,14 @@ impl<'a> PlanningModels<'a> { self.evidence = evidence; self } + + pub fn with_capabilities( + mut self, + capabilities: SummaryMaintenanceLifecycleCapabilities, + ) -> Self { + self.capabilities = capabilities; + self + } } /// Supplying this asks the pass to also decide summary maintenance versus raw @@ -99,15 +110,13 @@ pub struct LifecycleInput { pub now_ms: u64, /// Seconds. Required to turn recurring demand into a finite total. pub horizon: Option, - pub capabilities: SummaryMaintenanceLifecycleCapabilities, } impl LifecycleInput { - pub fn new(now_ms: u64, capabilities: SummaryMaintenanceLifecycleCapabilities) -> Self { + pub fn new(now_ms: u64) -> Self { Self { now_ms, horizon: None, - capabilities, } } @@ -123,7 +132,8 @@ pub struct OptimizationInput<'a> { pub workload: &'a ParsedWorkload, pub models: PlanningModels<'a>, /// Every plan carries the maintenance-versus-recomputation decision, so - /// the planning clock and runtime capabilities are always required. + /// the planning clock is always required. Runtime capabilities are in + /// `models` with the deployment's cost and accuracy models. pub lifecycle: LifecycleInput, } diff --git a/crates/integration-tests/tests/operator_design_examples.rs b/crates/integration-tests/tests/operator_design_examples.rs index ace32b1c4..4800ad528 100644 --- a/crates/integration-tests/tests/operator_design_examples.rs +++ b/crates/integration-tests/tests/operator_design_examples.rs @@ -261,8 +261,7 @@ async fn batch_planning_replaces_and_shares_summary_operators() { use asap_aware_mapping::cost_model::{Cost, DefaultCostModel}; use asap_aware_mapping::pass::PlanningModels; use asap_aware_mapping::{ - CostModel, CostRate, LifecycleInput, SummaryMaintenanceLifecycleCapabilities, - SummaryMaintenanceLifecycleCostInputs, + CostModel, CostRate, LifecycleInput, SummaryMaintenanceLifecycleCostInputs, }; use asap_physical_operators::{ physical_planner::{compile, InputContract}, @@ -336,7 +335,7 @@ async fn batch_planning_replaces_and_shares_summary_operators() { &workload, FrontendInput::Sql { catalog: &catalog }, PlanningModels::builtin().with_cost(&Costs), - LifecycleInput::new(0, SummaryMaintenanceLifecycleCapabilities::default()), + LifecycleInput::new(0), )) .await .unwrap(); diff --git a/crates/planner/src/lib.rs b/crates/planner/src/lib.rs index 4f527f148..501bc9ead 100644 --- a/crates/planner/src/lib.rs +++ b/crates/planner/src/lib.rs @@ -53,8 +53,8 @@ pub struct UserInput<'a> { pub workload: &'a PlanningWorkload, pub frontend_specific: FrontendInput<'a>, pub models: PlanningModels<'a>, - /// Planning clock and runtime capabilities for the - /// maintenance-versus-recomputation decision every plan carries. + /// Planning clock for the maintenance-versus-recomputation decision every + /// plan carries. Runtime capabilities are supplied with `models`. pub lifecycle: LifecycleInput, /// `None` uses [`MajorPass`]. A black-box caller never sets this. pub pass: Option<&'a dyn OptimizationPass>, diff --git a/crates/planner/tests/e2e_plan.rs b/crates/planner/tests/e2e_plan.rs index 3ab55b101..20d83d4e7 100644 --- a/crates/planner/tests/e2e_plan.rs +++ b/crates/planner/tests/e2e_plan.rs @@ -7,9 +7,7 @@ use asap_aware_mapping::pass::{ OptimizationInput, OptimizationPass, OptimizeError, PlanOutput, PlanningModels, }; use asap_aware_mapping::replacement::default_strategies_with_evidence; -use asap_aware_mapping::{ - search_workload_with_targets, Horizon, LifecycleInput, SummaryMaintenanceLifecycleCapabilities, -}; +use asap_aware_mapping::{search_workload_with_targets, Horizon, LifecycleInput}; use asap_frontend_sql::{lower_sql_dialect, SqlCatalog}; use asap_planner::{e2e_plan, FrontendInput, PlanError, UserInput, UserInputError}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; @@ -43,7 +41,7 @@ fn batch(sql: &str) -> BatchEntry { /// The planning clock and default capabilities, no horizon: the least a /// caller can supply. fn lifecycle() -> LifecycleInput { - LifecycleInput::new(NOW_MS, SummaryMaintenanceLifecycleCapabilities::default()) + LifecycleInput::new(NOW_MS) } fn lineitem_catalog() -> SqlCatalog { @@ -165,6 +163,49 @@ async fn builtin_cost_model_cannot_price_lifecycles_and_falls_back_to_raw_recomp } } +/// Runtime capabilities are deployment inputs alongside the cost and +/// accuracy models, while LifecycleInput carries only the planning clock. +#[tokio::test] +async fn deployment_inputs_control_lifecycle_capabilities() { + let workload = sql_workload( + vec![batch("SELECT COUNT(DISTINCT l_orderkey) FROM lineitem")], + None, + ); + let catalog = lineitem_catalog(); + let models = PlanningModels::builtin().with_capabilities( + asap_aware_mapping::SummaryMaintenanceLifecycleCapabilities { + supports_ephemeral: true, + supports_prepared: false, + supports_shared: false, + supports_continuously_maintained: false, + }, + ); + + let output = e2e_plan(UserInput::new( + &workload, + FrontendInput::Sql { catalog: &catalog }, + models, + lifecycle(), + )) + .await + .expect("workload remains plannable with restricted capabilities"); + + assert!(output + .plans + .iter() + .flat_map(|plan| &plan.plan.deployments) + .all( + |deployment| deployment.alternatives.iter().all(|alternative| { + !matches!( + alternative.summary_maintenance_lifecycle, + asap_types::post_asap::SummaryMaintenanceLifecycle::Prepared { .. } + | asap_types::post_asap::SummaryMaintenanceLifecycle::Shared { .. } + | asap_types::post_asap::SummaryMaintenanceLifecycle::ContinuouslyMaintained + ) || alternative.rejection.is_some() + }) + )); +} + /// A repeating SQL query reaches the optimizer. `lower_sql_batch` walks /// `query_batch` alone, so driving the frontend through it would drop exactly /// the entries whose recurrence the lifecycle stage reads. @@ -338,10 +379,7 @@ fn rejects_disagreeing_planning_clocks() { histograms: None, }, PlanningModels::builtin(), - LifecycleInput::new( - NOW_MS + 1, - SummaryMaintenanceLifecycleCapabilities::default(), - ), + LifecycleInput::new(NOW_MS + 1), ); assert!(matches!( diff --git a/crates/planner/tests/summary_sharing.rs b/crates/planner/tests/summary_sharing.rs index 9b7a204ef..6cad6b8bf 100644 --- a/crates/planner/tests/summary_sharing.rs +++ b/crates/planner/tests/summary_sharing.rs @@ -10,7 +10,7 @@ use asap_aware_mapping::pass::{PlanOutput, PlanningModels}; use asap_aware_mapping::replacement::{default_size_params, DEFAULT_DELTA}; use asap_aware_mapping::{ CostModel, CostRate, DefaultCostModel, Horizon, LifecycleInput, SummaryMaintenanceCapabilities, - SummaryMaintenanceLifecycleCapabilities, SummaryMaintenanceLifecycleCostInputs, + SummaryMaintenanceLifecycleCostInputs, }; use asap_frontend_sql::SqlCatalog; use asap_planner::{e2e_plan, FrontendInput, UserInput}; @@ -103,8 +103,7 @@ fn repeating(query: &str, epsilon: f64) -> RepeatingEntry { } fn lifecycle() -> LifecycleInput { - LifecycleInput::new(NOW_MS, SummaryMaintenanceLifecycleCapabilities::default()) - .with_horizon(Horizon(HORIZON_S)) + LifecycleInput::new(NOW_MS).with_horizon(Horizon(HORIZON_S)) } fn promql_workload(queries: &[(&str, f64)]) -> PlanningWorkload {