Skip to content
Open
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 @@ -187,6 +187,7 @@ pub mod cost_model;
pub mod exact_composition;
pub mod explanation;
pub mod grouping;
pub mod lifecycle_dag_export;
pub mod recurrence;
pub mod replacement;
pub mod rewrite;
Expand All @@ -210,6 +211,10 @@ pub use explanation::{
explain_replacements, explain_replacements_with, ExplanationKind, ReplacementExplanation,
};
pub use grouping::{has_subpopulations, HydraGroupingStrategy};
pub use lifecycle_dag_export::{
export_summary_maintenance_plan, SummaryMaintenanceDagExport,
SummaryMaintenanceDeploymentExport, SummaryMaintenanceLifecycleAlternativeExport,
};
pub use recurrence::{
evaluation_rate_of, total_cost, update_rate_from_data_workload, CostRate, EvaluationRate,
Horizon, RecurrenceCostExplanation, RecurrenceError, RecurrenceProfile, RootRecurrence,
Expand Down
207 changes: 207 additions & 0 deletions crates/asap-aware-mapping/src/lifecycle_dag_export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//! 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 lifecycle alternatives and their
//! typed rejection reasons are available, and emits both views together.

use serde::Serialize;

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

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 evaluation_schedule: EvaluationScheduleExport,
pub output_representation: OutputRepresentationExport,
}

#[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 {
SummaryMaintenanceDagExport {
graph: dag_export::export_summary(&plan.root),
deployments: 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(),
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),
}
}

fn export_guarantee(
guarantee: &SummaryMaintenanceLifecycleGuarantee,
) -> SummaryMaintenanceLifecycleGuaranteeExport {
SummaryMaintenanceLifecycleGuaranteeExport {
lifecycle: export_lifecycle(&guarantee.summary_maintenance_lifecycle),
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
}
}
}
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"] }
20 changes: 19 additions & 1 deletion crates/integration-tests/tests/workload_lifecycle_e2e.rs
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 @@ -170,4 +170,22 @@ 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"
);
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());
}