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
2 changes: 2 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/asap-aware-mapping/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ edition = "2021"
[dependencies]
asap-types = { path = "../types" }
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
5 changes: 5 additions & 0 deletions crates/asap-aware-mapping/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ pub mod recurrence;
pub mod replacement;
pub mod rewrite;
pub mod rollup;
pub mod summary_maintenance_dag_export;
pub mod summary_maintenance_lifecycle;
pub mod topk_reuse;

Expand Down Expand Up @@ -218,6 +219,10 @@ pub use replacement::{
MAX_SEARCH_ITERATIONS,
};
pub use rewrite::AvgToSumOverCountStrategy;
pub use summary_maintenance_dag_export::{
export_summary_maintenance_plan, SummaryMaintenanceDagExport,
SummaryMaintenanceDeploymentExport, SummaryMaintenanceLifecycleAlternativeExport,
};
pub use summary_maintenance_lifecycle::{
global_selection_with_summary_maintenance_lifecycles,
materialize_with_summary_maintenance_lifecycles, plan_summary_maintenance_lifecycles,
Expand Down
277 changes: 277 additions & 0 deletions crates/asap-aware-mapping/src/summary_maintenance_dag_export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
//! Serializable DAG export for a materialized summary-maintenance plan.
//!
//! `asap-types::dag_export` owns the crate-neutral post-ASAP graph shape. This
//! adapter lives in the mapping layer, where summary-maintenance lifecycle
//! alternatives and their typed rejection reasons are available, and emits
//! both views together.

use std::collections::HashMap;
use std::rc::Rc;

use serde::Serialize;

use asap_types::dag_export::{self, SummaryDagGraph};
use asap_types::post_asap::{
EvaluationSchedule, OutputRepresentation, SummaryExpr, SummaryMaintenanceLifecycle,
SummaryMaintenanceLifecycleGuarantee, SummaryMaintenanceMode, SummaryNode,
};

use crate::summary_maintenance_lifecycle::{
SummaryMaintenanceLifecyclePlan, SummaryMaintenanceLifecycleRejection,
};

#[derive(Debug, Clone, Serialize)]
pub struct SummaryMaintenanceDagExport {
pub graph: SummaryDagGraph,
pub deployments: Vec<SummaryMaintenanceDeploymentExport>,
pub horizon_seconds: Option<f64>,
pub evaluation_rate_per_second: Option<f64>,
pub update_rate_per_second: Option<f64>,
pub expected_reads: Option<f64>,
pub selected_raw_recompute: bool,
pub summary_total_cost: Option<f64>,
pub raw_recompute_total_cost: Option<f64>,
}

#[derive(Debug, Clone, Serialize)]
pub struct SummaryMaintenanceDeploymentExport {
pub summary_index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected: Option<SummaryMaintenanceLifecycleGuaranteeExport>,
pub alternatives: Vec<SummaryMaintenanceLifecycleAlternativeExport>,
}

#[derive(Debug, Clone, Serialize)]
pub struct SummaryMaintenanceLifecycleAlternativeExport {
pub lifecycle: SummaryMaintenanceLifecycleExport,
pub total_cost: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rejection: Option<SummaryMaintenanceLifecycleRejectionExport>,
pub assumptions: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct SummaryMaintenanceLifecycleGuaranteeExport {
pub lifecycle: SummaryMaintenanceLifecycleExport,
pub maintenance_mode: SummaryMaintenanceModeExport,
pub evaluation_schedule: EvaluationScheduleExport,
pub output_representation: OutputRepresentationExport,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SummaryMaintenanceModeExport {
DirectBuild,
Incremental,
}

#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SummaryMaintenanceLifecycleExport {
Ephemeral,
Prepared {
activate_at_ms: u64,
retire_at_ms: u64,
},
Shared {
retention_ms: u64,
},
ContinuouslyMaintained,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EvaluationScheduleExport {
OneShot,
PerUpdate,
OnRead,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OutputRepresentationExport {
PlainRows,
SummaryState,
FinalizedValue,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SummaryMaintenanceLifecycleRejectionExport {
UnsupportedByRuntime,
RequiresPredictableOneTimeQuery,
RequiresMultipleReads,
RequiresHorizon,
RequiresContinuousData,
MissingOrStaleIngestionRate,
SummaryDoesNotSupportIncrementalUpdates,
SummaryDoesNotSupportDeletion,
MissingCostEvidence,
}

pub fn export_summary_maintenance_plan(
plan: &SummaryMaintenanceLifecyclePlan,
) -> SummaryMaintenanceDagExport {
let deployments: Vec<_> = plan
.deployments
.iter()
.map(|deployment| SummaryMaintenanceDeploymentExport {
summary_index: deployment.summary_index,
selected: deployment
.summary_maintenance_lifecycle_guarantee
.as_ref()
.map(export_guarantee),
alternatives: deployment
.alternatives
.iter()
.map(|alternative| SummaryMaintenanceLifecycleAlternativeExport {
lifecycle: export_lifecycle(&alternative.summary_maintenance_lifecycle),
total_cost: alternative.total_cost.map(|cost| cost.0),
rejection: alternative.rejection.as_ref().map(export_rejection),
assumptions: alternative.assumptions.clone(),
})
.collect(),
})
.collect();
let mut graph = dag_export::export_summary(&plan.root);
let deployment_by_summary: HashMap<_, _> = plan
.deployments
.iter()
.zip(&deployments)
.map(|(deployment, export)| (Rc::as_ptr(&deployment.summary), export))
.collect();
let mut next_node_id = 0;
annotate_lifecycle_deployments(
&plan.root,
&mut graph,
&deployment_by_summary,
&mut next_node_id,
);

SummaryMaintenanceDagExport {
graph,
deployments,
horizon_seconds: plan.horizon.map(|horizon| horizon.0),
evaluation_rate_per_second: plan.evaluation_rate.map(|rate| rate.0),
update_rate_per_second: plan.update_rate.map(|rate| rate.0),
expected_reads: plan.expected_reads,
selected_raw_recompute: plan.selected_raw_recompute,
summary_total_cost: plan.summary_total_cost.map(|cost| cost.0),
raw_recompute_total_cost: plan.raw_recompute_total_cost.map(|cost| cost.0),
}
}

/// Walk in the same post-order as `dag_export::export_summary` and attach a
/// deployment directly to every flattened occurrence of its `SummaryAgg`.
/// This makes the decision visible to graph consumers without asking them to
/// reconstruct pointer identity from `summary_index` or graph position.
fn annotate_lifecycle_deployments(
node: &SummaryNode,
graph: &mut SummaryDagGraph,
deployments: &HashMap<*const SummaryNode, &SummaryMaintenanceDeploymentExport>,
next_node_id: &mut usize,
) {
if !matches!(node.expr, SummaryExpr::KeepPreAsap(_)) {
for child in summary_children(&node.expr) {
annotate_lifecycle_deployments(child, graph, deployments, next_node_id);
}
}
let graph_node = &mut graph.nodes[*next_node_id];
if let Some(deployment) = deployments.get(&(node as *const SummaryNode)) {
graph_node.detail["summary_maintenance"] =
serde_json::to_value(deployment).expect("lifecycle export is serializable");
}
*next_node_id += 1;
}

fn summary_children(expr: &SummaryExpr) -> Vec<&Rc<SummaryNode>> {
match expr {
SummaryExpr::KeepPreAsap(_) => vec![],
SummaryExpr::SummaryAgg { child, .. } => vec![child],
SummaryExpr::SummaryJoin { outer, inner, .. }
| SummaryExpr::SummarySubtract {
left: outer,
right: inner,
} => vec![outer, inner],
SummaryExpr::SummaryDelete { summary_input, .. }
| SummaryExpr::SummaryEstimate { summary_input, .. } => vec![summary_input],
SummaryExpr::SummaryMerge { children } => children.iter().collect(),
}
}

fn export_guarantee(
guarantee: &SummaryMaintenanceLifecycleGuarantee,
) -> SummaryMaintenanceLifecycleGuaranteeExport {
SummaryMaintenanceLifecycleGuaranteeExport {
lifecycle: export_lifecycle(&guarantee.summary_maintenance_lifecycle),
maintenance_mode: match guarantee.summary_maintenance_mode {
SummaryMaintenanceMode::DirectBuild => SummaryMaintenanceModeExport::DirectBuild,
SummaryMaintenanceMode::Incremental => SummaryMaintenanceModeExport::Incremental,
},
evaluation_schedule: match guarantee.evaluation_schedule {
EvaluationSchedule::OneShot => EvaluationScheduleExport::OneShot,
EvaluationSchedule::PerUpdate => EvaluationScheduleExport::PerUpdate,
EvaluationSchedule::OnRead => EvaluationScheduleExport::OnRead,
},
output_representation: match guarantee.output_representation {
OutputRepresentation::PlainRows => OutputRepresentationExport::PlainRows,
OutputRepresentation::SummaryState => OutputRepresentationExport::SummaryState,
OutputRepresentation::FinalizedValue => OutputRepresentationExport::FinalizedValue,
},
}
}

fn export_lifecycle(lifecycle: &SummaryMaintenanceLifecycle) -> SummaryMaintenanceLifecycleExport {
match lifecycle {
SummaryMaintenanceLifecycle::Ephemeral => SummaryMaintenanceLifecycleExport::Ephemeral,
SummaryMaintenanceLifecycle::Prepared {
activate_at,
retire_at,
} => SummaryMaintenanceLifecycleExport::Prepared {
activate_at_ms: activate_at.0,
retire_at_ms: retire_at.0,
},
SummaryMaintenanceLifecycle::Shared { retention } => {
SummaryMaintenanceLifecycleExport::Shared {
retention_ms: retention.0,
}
}
SummaryMaintenanceLifecycle::ContinuouslyMaintained => {
SummaryMaintenanceLifecycleExport::ContinuouslyMaintained
}
}
}

fn export_rejection(
rejection: &SummaryMaintenanceLifecycleRejection,
) -> SummaryMaintenanceLifecycleRejectionExport {
match rejection {
SummaryMaintenanceLifecycleRejection::UnsupportedByRuntime => {
SummaryMaintenanceLifecycleRejectionExport::UnsupportedByRuntime
}
SummaryMaintenanceLifecycleRejection::RequiresPredictableOneTimeQuery => {
SummaryMaintenanceLifecycleRejectionExport::RequiresPredictableOneTimeQuery
}
SummaryMaintenanceLifecycleRejection::RequiresMultipleReads => {
SummaryMaintenanceLifecycleRejectionExport::RequiresMultipleReads
}
SummaryMaintenanceLifecycleRejection::RequiresHorizon => {
SummaryMaintenanceLifecycleRejectionExport::RequiresHorizon
}
SummaryMaintenanceLifecycleRejection::RequiresContinuousData => {
SummaryMaintenanceLifecycleRejectionExport::RequiresContinuousData
}
SummaryMaintenanceLifecycleRejection::MissingOrStaleIngestionRate => {
SummaryMaintenanceLifecycleRejectionExport::MissingOrStaleIngestionRate
}
SummaryMaintenanceLifecycleRejection::SummaryDoesNotSupportIncrementalUpdates => {
SummaryMaintenanceLifecycleRejectionExport::SummaryDoesNotSupportIncrementalUpdates
}
SummaryMaintenanceLifecycleRejection::SummaryDoesNotSupportDeletion => {
SummaryMaintenanceLifecycleRejectionExport::SummaryDoesNotSupportDeletion
}
SummaryMaintenanceLifecycleRejection::MissingCostEvidence => {
SummaryMaintenanceLifecycleRejectionExport::MissingCostEvidence
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,13 @@ mod tests {
assert_eq!(plan.summary_total_cost, None);
assert!(plan.deployments.is_empty());
assert!(matches!(plan.root.expr, SummaryExpr::KeepPreAsap(_)));

let exported =
crate::summary_maintenance_dag_export::export_summary_maintenance_plan(&plan);
assert!(exported.selected_raw_recompute);
assert_eq!(exported.raw_recompute_total_cost, Some(1.0));
assert_eq!(exported.summary_total_cost, None);
assert!(exported.deployments.is_empty());
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ asap-frontend-sql = { path = "../frontend-sql" }
asap-aware-mapping = { path = "../asap-aware-mapping" }

[dev-dependencies]
serde_json = "1"
tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread"] }
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::rc::Rc;
use asap_aware_mapping::cost_model::Cost;
use asap_aware_mapping::CostRate;
use asap_aware_mapping::{
global_selection_with_summary_maintenance_lifecycles,
export_summary_maintenance_plan, global_selection_with_summary_maintenance_lifecycles,
materialize_with_summary_maintenance_lifecycles, search_workload_with, CostModel, Horizon,
SummaryMaintenanceCapabilities, SummaryMaintenanceLifecycleCapabilities,
SummaryMaintenanceLifecycleCostInputs, SummaryMaintenanceLifecycleRejection, WorkloadDemand,
Expand Down Expand Up @@ -176,4 +176,36 @@ fn promql_dashboard_materializes_continuous_summary_with_explained_rejections()
) && alternative.rejection
== Some(SummaryMaintenanceLifecycleRejection::UnsupportedByRuntime)
}));

let exported = serde_json::to_value(export_summary_maintenance_plan(&plan)).unwrap();
assert_eq!(
exported["deployments"][0]["selected"]["lifecycle"]["kind"],
"continuously_maintained"
);
assert_eq!(
exported["deployments"][0]["selected"]["maintenance_mode"],
"incremental"
);
let alternatives = exported["deployments"][0]["alternatives"]
.as_array()
.expect("exported lifecycle alternatives");
assert!(alternatives.iter().any(|alternative| {
alternative["lifecycle"]["kind"] == "prepared"
&& alternative["rejection"] == "requires_predictable_one_time_query"
}));
assert!(alternatives.iter().any(|alternative| {
alternative["lifecycle"]["kind"] == "shared"
&& alternative["rejection"] == "unsupported_by_runtime"
}));
assert!(exported["graph"]["nodes"].as_array().is_some());
let summary_node = exported["graph"]["nodes"]
.as_array()
.unwrap()
.iter()
.find(|node| node["kind"] == "SummaryAgg")
.expect("exported SummaryAgg node");
assert_eq!(
summary_node["detail"]["summary_maintenance"]["selected"]["lifecycle"]["kind"],
"continuously_maintained"
);
}
Loading
Loading