diff --git a/crates/asap-aware-mapping/src/cost_model.rs b/crates/asap-aware-mapping/src/cost_model.rs index 98b4f91..b1780c8 100644 --- a/crates/asap-aware-mapping/src/cost_model.rs +++ b/crates/asap-aware-mapping/src/cost_model.rs @@ -440,7 +440,7 @@ pub trait CostModel { /// how many structurally exist (`candidate.consumer_count`). See /// `crate::recurrence`'s module docs for the full design. /// - /// - `recurrence.is_empty()` (no [`RepeatingEntry`]/[`DataCharacteristics`]-derived + /// - `recurrence.is_empty()` (no [`RepeatingEntry`]/[`DataWorkload`]-derived /// metadata available): delegates to /// [`cse_share_decision`](Self::cse_share_decision), preserving /// today's structural-consumer-count behavior exactly — issue #287's @@ -456,7 +456,7 @@ pub trait CostModel { /// rates is equivalent to comparing `rate * H` for any fixed `H > 0`). /// /// [`RepeatingEntry`]: asap_types::workload::RepeatingEntry - /// [`DataCharacteristics`]: asap_types::workload::DataCharacteristics + /// [`DataWorkload`]: asap_types::workload::DataWorkload fn cse_share_decision_with_recurrence( &self, candidate: &CseCandidate, diff --git a/crates/asap-aware-mapping/src/lib.rs b/crates/asap-aware-mapping/src/lib.rs index d8adda7..613d506 100644 --- a/crates/asap-aware-mapping/src/lib.rs +++ b/crates/asap-aware-mapping/src/lib.rs @@ -204,9 +204,9 @@ pub use explanation::{ }; pub use grouping::{has_subpopulations, HydraGroupingStrategy}; pub use recurrence::{ - evaluation_rate_of, total_cost, update_rate_from_data_characteristics, CostRate, - EvaluationRate, Horizon, RecurrenceCostExplanation, RecurrenceError, RecurrenceProfile, - RootRecurrence, UpdateRate, + evaluation_rate_of, total_cost, update_rate_from_data_workload, CostRate, EvaluationRate, + Horizon, RecurrenceCostExplanation, RecurrenceError, RecurrenceProfile, RootRecurrence, + UpdateRate, }; pub use replacement::{ default_strategies, default_strategies_with, search_workload, search_workload_with, diff --git a/crates/asap-aware-mapping/src/recurrence.rs b/crates/asap-aware-mapping/src/recurrence.rs index 2fdc9b4..8b38058 100644 --- a/crates/asap-aware-mapping/src/recurrence.rs +++ b/crates/asap-aware-mapping/src/recurrence.rs @@ -2,7 +2,7 @@ //! //! ASAPPlanner already models recurring-workload metadata //! ([`asap_types::workload::RepeatingEntry`]) and ingest-rate metadata -//! ([`asap_types::workload::DataCharacteristics`]), but until this module +//! ([`asap_types::workload::DataWorkload`]), but until this module //! neither reached [`CostModel`]'s CSE share-vs-recompute decision //! ([`CostModel::cse_share_decision`]): that decision only ever compared a //! *structural* consumer count (how many workload locations reference a @@ -79,12 +79,10 @@ //! for a whole workload). A one-shot ([`asap_types::workload::BatchEntry`]) //! consumer contributes to [`RecurrenceProfile::one_shot_consumers`] //! instead, never to this rate. -//! - [`UpdateRate`]: derived from workload-level -//! [`asap_types::workload::DataCharacteristics`] via -//! [`update_rate_from_data_characteristics`] (`series_count * -//! samples_per_sec_per_series`) — a deployment with a more precise -//! per-target ingest measurement should compute its own `UpdateRate` -//! instead of relying on this proxy. +//! - [`UpdateRate`]: read from workload-level +//! [`asap_types::workload::DataWorkload::ingestion_rate`] via +//! [`update_rate_from_data_workload`]. Missing evidence remains unknown; +//! data at rest is not assigned a fabricated update rate. //! - `maintenance_cost_per_update` / `summary_read_cost` / //! `raw_recompute_cost`: [`CostModel`] hooks (defaults documented on the //! trait itself, in `cost_model.rs`) — illustrative placeholders, like @@ -95,7 +93,7 @@ //! //! [`RecurrenceProfile::is_empty`] is `true` exactly when a caller supplied //! no [`RepeatingEntry`](asap_types::workload::RepeatingEntry)/ -//! [`DataCharacteristics`](asap_types::workload::DataCharacteristics)-derived +//! [`DataWorkload`](asap_types::workload::DataWorkload)-derived //! information at all (no evaluation rate, no update rate, zero recorded //! one-shot consumers — [`RecurrenceProfile::EMPTY`], its `Default`). //! [`CostModel::cse_share_decision_with_recurrence`]'s default body checks @@ -106,7 +104,7 @@ use std::fmt; -use asap_types::workload::{DataCharacteristics, RepetitionInterval}; +use asap_types::workload::{DataWorkload, RepetitionInterval}; use crate::cost_model::{Cost, CostModel, CseCandidate, ShareDecision}; @@ -114,8 +112,8 @@ use crate::cost_model::{Cost, CostModel, CseCandidate, ShareDecision}; /// How often the *raw* data underlying a maintained summary changes — /// ingest/update events per second (Hz). See the module docs' provenance -/// table: normally derived from [`DataCharacteristics`] via -/// [`update_rate_from_data_characteristics`]. +/// table: normally read from [`DataWorkload::ingestion_rate`] via +/// [`update_rate_from_data_workload`]. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct UpdateRate(pub f64); @@ -234,7 +232,7 @@ pub enum RecurrenceError { /// discipline [`evaluation_rate_of`] applies to each [`RepetitionInterval`], /// applied at every point an `UpdateRate` enters a [`RecurrenceProfile`] /// ([`RecurrenceProfile::with_update_rate`], -/// [`update_rate_from_data_characteristics`], +/// [`update_rate_from_data_workload`], /// [`crate::replacement::PlanSpace::recurrence_profiles`]'s own parameter) /// *and*, as a backstop that can't be bypassed by constructing a /// `RecurrenceProfile` via its public fields directly, inside [`decide`] @@ -284,24 +282,17 @@ where Ok(any.then_some(EvaluationRate(total_hz))) } -/// Derive an [`UpdateRate`] from workload-level [`DataCharacteristics`]: -/// `series_count * samples_per_sec_per_series` — the total number of raw -/// ingest samples per second across every series this characteristics -/// value describes. A proxy, not a measurement: a deployment with a more -/// precise per-target ingest rate should compute its own `UpdateRate` -/// rather than rely on this conversion. -/// -/// Validated via [`validate_update_rate`]: `samples_per_sec_per_series` is -/// caller-supplied `f64` with no type-level guarantee of being finite or -/// non-negative, so a garbage `DataCharacteristics` value (NaN, infinite, -/// or negative) is rejected here rather than silently propagating into a -/// [`RecurrenceProfile`]. -pub fn update_rate_from_data_characteristics( - dc: &DataCharacteristics, -) -> Result { - validate_update_rate(UpdateRate( - dc.series_count as f64 * dc.samples_per_sec_per_series, - )) +/// Read an [`UpdateRate`] from workload-level [`DataWorkload`] evidence. +/// Missing evidence remains `None`; a present non-finite or negative rate is +/// rejected rather than propagated into a [`RecurrenceProfile`]. +pub fn update_rate_from_data_workload( + workload: &DataWorkload, +) -> Result, RecurrenceError> { + workload + .ingestion_rate + .value + .map(|rate| validate_update_rate(UpdateRate(rate.0))) + .transpose() } // ── RecurrenceProfile ──────────────────────────────────────────────────── @@ -326,7 +317,7 @@ pub struct RecurrenceProfile { pub one_shot_consumers: usize, /// The ingest/update rate of the raw data this target (if maintained) /// would be kept up to date against. `None` when no - /// [`DataCharacteristics`] were available. + /// [`DataWorkload::ingestion_rate`] evidence was available. pub update_rate: Option, } @@ -365,7 +356,7 @@ impl RecurrenceProfile { self } - /// Attach an ingest/update rate (from [`update_rate_from_data_characteristics`] + /// Attach an ingest/update rate (from [`update_rate_from_data_workload`] /// or a deployment-specific measurement). Validated via /// [`validate_update_rate`] — rejects a NaN, infinite, or negative rate /// rather than silently storing it. @@ -670,34 +661,42 @@ mod tests { assert_eq!(err, RecurrenceError::InvalidInterval(interval(0))); } - // ── update_rate_from_data_characteristics ──────────────────────────── + // ── update_rate_from_data_workload ─────────────────────────────────── #[test] - fn update_rate_from_data_characteristics_multiplies_series_by_sample_rate() { - let dc = DataCharacteristics { - series_count: 1_000, - samples_per_sec_per_series: 0.1, - bytes_per_raw_sample: 80, - distinct_keys_per_window: None, - data_distribution: Default::default(), + fn update_rate_from_data_workload_reads_ingestion_rate_evidence() { + let workload = DataWorkload { + ingestion_rate: asap_types::workload::Evidence { + value: Some(asap_types::workload::Rate(100.0)), + ..Default::default() + }, + ..Default::default() }; - let rate = update_rate_from_data_characteristics(&dc).unwrap(); + let rate = update_rate_from_data_workload(&workload).unwrap().unwrap(); assert!((rate.0 - 100.0).abs() < 1e-9); } #[test] - fn update_rate_from_data_characteristics_rejects_a_negative_sample_rate() { - let dc = DataCharacteristics { - series_count: 1_000, - samples_per_sec_per_series: -0.1, - bytes_per_raw_sample: 80, - distinct_keys_per_window: None, - data_distribution: Default::default(), + fn update_rate_from_data_workload_rejects_a_negative_rate() { + let workload = DataWorkload { + ingestion_rate: asap_types::workload::Evidence { + value: Some(asap_types::workload::Rate(-0.1)), + ..Default::default() + }, + ..Default::default() }; - let err = update_rate_from_data_characteristics(&dc).unwrap_err(); + let err = update_rate_from_data_workload(&workload).unwrap_err(); assert!(matches!(err, RecurrenceError::InvalidUpdateRate(_))); } + #[test] + fn update_rate_from_data_workload_preserves_missing_evidence() { + assert_eq!( + update_rate_from_data_workload(&DataWorkload::default()).unwrap(), + None + ); + } + // ── RecurrenceProfile ───────────────────────────────────────────────── #[test] @@ -1074,7 +1073,7 @@ mod tests { assert_eq!(explanation.recompute_total, Some(Cost(50.0))); } - /// A batch-only workload (no `DataCharacteristics`, only one-shot + /// A batch-only workload (no `DataWorkload`, only one-shot /// consumers) with the *default* `DefaultCostModel` must not /// unconditionally prefer `Share` regardless of how many one-shot /// consumers there are — issue #287 review bug 1's original repro, diff --git a/crates/asap-aware-mapping/src/replacement.rs b/crates/asap-aware-mapping/src/replacement.rs index 5c40e01..5835ada 100644 --- a/crates/asap-aware-mapping/src/replacement.rs +++ b/crates/asap-aware-mapping/src/replacement.rs @@ -2311,7 +2311,7 @@ impl PlanSpace { /// One [`RecurrenceProfile`] per discovered [`MemoGroup`] target, built by /// [`PlanSpace::recurrence_profiles`] — the "carry `RepeatingEntry.interval` -/// and relevant `DataCharacteristics` into ASAP-aware search/cost context" +/// and relevant `DataWorkload` into ASAP-aware search/cost context" /// half of issue #287. Looked up by `Rc` pointer identity, the same /// currency [`PlanSpace::group_for`]/[`GlobalSelection::for_target`] already /// use. @@ -2372,10 +2372,10 @@ impl PlanSpace { /// `update_rate` is applied uniformly to every discovered site *that /// this walk actually reached from some root* (see the "unreachable /// sites" note below): today's - /// [`asap_types::workload::DataCharacteristics`] is a single + /// [`asap_types::workload::DataWorkload`] is a single /// workload-level value (applies to every query in a `QueryWorkload`), /// not per-target, so there is no finer-grained source to attach - /// instead. `None` when no `DataCharacteristics` were available — + /// instead. `None` when no `DataWorkload` evidence was available — /// preserves "missing metadata" behavior for the update-rate term alone /// even when repeating/one-shot consumer information is present. /// diff --git a/crates/frontend-promql/tests/promql_lowering.rs b/crates/frontend-promql/tests/promql_lowering.rs index 192118d..b872508 100644 --- a/crates/frontend-promql/tests/promql_lowering.rs +++ b/crates/frontend-promql/tests/promql_lowering.rs @@ -769,7 +769,7 @@ fn batch_lowers_each_entry_and_reads_per_query_accuracy() { }, ]), repeating_queries: None, - data_characteristics: None, + data_workload: None, }; let results = lower_promql_batch(&workload); assert_eq!(results.len(), 2); @@ -787,7 +787,7 @@ fn batch_rejects_non_promql_language() { requirements: None, }]), repeating_queries: None, - data_characteristics: None, + data_workload: None, }; let results = lower_promql_batch(&workload); assert_eq!(results.len(), 1); diff --git a/crates/types/src/workload.rs b/crates/types/src/workload.rs index 9fbc98f..7a444fd 100644 --- a/crates/types/src/workload.rs +++ b/crates/types/src/workload.rs @@ -50,8 +50,8 @@ pub struct BatchEntry { pub requirements: Option, } -/// One entry in a repeating (streaming) workload: a query that fires -/// every `interval` milliseconds. +/// One query that fires every `interval` milliseconds. Its recurrence does +/// not imply that the queried data is continuously ingesting. #[derive(Debug, Clone)] pub struct RepeatingEntry { pub query: Query, @@ -60,7 +60,18 @@ pub struct RepeatingEntry { pub requirements: Option, } -// ── Data characteristics ────────────────────────────────────────────────────── +// ── Data workload ───────────────────────────────────────────────────────────── + +/// Whether the data queried by this workload is static, still arriving, or a +/// mixture of both. This is independent of whether queries repeat. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum DataArrival { + AtRest, + ContinuouslyIngesting, + Mixed, + #[default] + Unknown, +} /// Statistical distribution of keys in the incoming data stream. #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -78,23 +89,51 @@ pub enum DataDistribution { Bursty, } -/// Characteristics of the data arriving at the ingestion layer. -/// Used by the cost model and sketch-parameter binder to size sketches -/// and estimate transmission cost without running the query. -#[derive(Debug, Clone)] -pub struct DataCharacteristics { - /// Number of distinct active time series for this metric. - pub series_count: u64, - /// Sample rate per series at the SDK / agent (Hz). - pub samples_per_sec_per_series: f64, - /// Wire size of one raw OTLP metric data point after protobuf encoding - /// (bytes). Typical range: 50–200 bytes. - pub bytes_per_raw_sample: u32, - /// Known distinct key values per flush period for frequency / cardinality - /// sketches. `None` → inferred analytically from inserts and distribution. - pub distinct_keys_per_window: Option, - /// Statistical distribution of keys in the stream. - pub data_distribution: DataDistribution, +/// Where an empirical workload value came from. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum EvidenceSource { + Declared, + Observed, + Derived, + #[default] + Unknown, +} + +/// A workload value together with the provenance and freshness needed to +/// decide whether it is safe to use. Times and durations are milliseconds. +#[derive(Debug, Clone, PartialEq)] +pub struct Evidence { + pub value: Option, + pub source: EvidenceSource, + pub observed_at_ms: Option, + pub valid_for_ms: Option, +} + +impl Default for Evidence { + fn default() -> Self { + Self { + value: None, + source: EvidenceSource::Unknown, + observed_at_ms: None, + valid_for_ms: None, + } + } +} + +/// Queries per second, samples per second, or another rate whose unit is +/// established by the field that contains it. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rate(pub f64); + +/// Workload-level facts about the data being queried. Unlike the former +/// ingestion-only `DataCharacteristics`, this also represents data at rest. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct DataWorkload { + pub arrival: DataArrival, + pub ingestion_volume: Evidence, + pub ingestion_rate: Evidence, + pub input_cardinality: Evidence, + pub distribution: Evidence, } // ── Top-level workload ──────────────────────────────────────────────────────── @@ -111,9 +150,9 @@ pub struct QueryWorkload { pub language: QueryLanguage, /// One-shot queries executed together as a batch. pub query_batch: Option>, - /// Queries that repeat on a fixed interval (streaming / continuous). + /// Queries that repeat on a fixed interval. pub repeating_queries: Option>, - /// Workload-level data characteristics used for sketch sizing and cost - /// estimation. Applies to all queries in this workload. - pub data_characteristics: Option, + /// Workload-level data facts used for accuracy and cost estimation. + /// Applies to all queries in this workload. + pub data_workload: Option, } diff --git a/docs/design_docs/asap-aware-mapping/README.md b/docs/design_docs/asap-aware-mapping/README.md index 1d32afe..9034fb9 100644 --- a/docs/design_docs/asap-aware-mapping/README.md +++ b/docs/design_docs/asap-aware-mapping/README.md @@ -90,6 +90,9 @@ The design is split into focused documents: summaries and optimizations can be composed safely. - [End-to-end accuracy guarantees](end-to-end-accuracy-guarantees.md) specifies the typed guarantee IR, sketch contracts, composition rules, target checking, and fail-closed boundaries. +- [Query workloads, data workloads, and summary lifecycle maintenance](workload-demand-and-summary-lifecycle.md) separates + query-workload properties from data-workload properties and defines ephemeral, prepared, + shared, and continuously maintained summary-state alternatives. - [Explainability](explainability.md) describes how the planner reports available replacements using the same candidate space it optimizes. diff --git a/docs/design_docs/asap-aware-mapping/workload-demand-and-summary-lifecycle.md b/docs/design_docs/asap-aware-mapping/workload-demand-and-summary-lifecycle.md new file mode 100644 index 0000000..fef2aff --- /dev/null +++ b/docs/design_docs/asap-aware-mapping/workload-demand-and-summary-lifecycle.md @@ -0,0 +1,738 @@ +# Design: Query Workloads, Data Workloads, and Summary Lifecycle Maintenance + +## Audience and context + +This document is for ASAPPlanner designers, architects, researchers, and +developers working on workload-aware plan selection. It defines how the +planner should describe query workload, data workload, and the lifecycle of +summary state. It is a design contract, not a description of the current +public Rust API. + +The terminology follows the ProjectASAP +[glossary](https://github.com/ProjectASAP/internal-docs/blob/03e1c70f5af3ae9221471898541067eee7f86338/glossary.md). +That glossary is authoritative for the meanings of data workload, query +workload, ad-hoc and predictable queries, one-time and repeated queries, +real-time and longitudinal queries, output cardinality, and lookback window. +This document maps those concepts into planner responsibilities and records +where the current model is incomplete. + +This design is orthogonal to +[end-to-end accuracy guarantees](end-to-end-accuracy-guarantees.md). Accuracy +decides whether a candidate is correct enough. Workload demand and state +lifecycle decide whether building, maintaining, sharing, or recomputing that +candidate is worthwhile. Neither decision may override the other. + +## Problem and why now + +A summary operator does not imply one execution lifecycle. The same exact or +approximate summary can be: + +- built once from data at rest and discarded after one query; +- prepared before a known future query and retired afterward; +- shared across a bounded set of requests; or +- maintained incrementally as data continues to arrive. + +Likewise, an exact stateless operator may run once over a batch, once per +update in an incremental pipeline, or once per readout. Operator statefulness, +execution schedule, and output representation are separate properties. + +The query expression alone cannot determine those properties. The same query +may arrive unexpectedly during exploration, run once at a scheduled time, or +repeat every ten seconds on a dashboard. Planning summary state from syntax +alone either misses reuse or invents reuse that the workload does not justify. + +The current normalized workload distinguishes a one-shot `query_batch` from +fixed-interval `repeating_queries`, and the recurrence cost model distinguishes +one-shot consumers from evaluation and update rates. This is a useful base, but +it does not represent predictability, uncertain demand, real-time versus +longitudinal scope, at-rest versus continuously ingesting data, or summary-state +lifecycle. It also risks treating "repeating query" and "streaming data" as the +same fact even though the glossary defines them on different axes. + +## Inputs, outputs, and end-to-end behavior + +The planner receives four logically distinct inputs: + +1. logical queries, which define query semantics; +2. query workload, including per-query accuracy and latency requirements, + predictability, recurrence, and queried time scope; +3. data-workload characteristics, including arrival, volume, cardinality, and + distribution; +4. existing summaries and the lifecycle actions available to the deployment. + +The output is a legal physical-plan choice plus explicit state deployments. A +state deployment states whether a summary is ephemeral, prepared, shared for a +bounded period, or continuously maintained. Its cost explanation identifies +the demand and data evidence used in the decision. + +```text + logical queries ---+ + query workload -----+ + data workload ---+--> candidate plans + available summaries ---+ -> semantic and accuracy legality + -> lifecycle alternatives + -> horizon-normalized cost + -> selected plan + deployments +``` + +For an unpredictable one-time query, the planner may read an existing summary, +build an ephemeral summary, or recompute from raw data. It must not assume +future reuse. For a predictable one-time query, it may additionally compare +preparing state in advance with building or recomputing at execution time. For +repeated queries, it may amortize build and maintenance cost across reads over +an explicit horizon. + +### End-to-end decision order + +```text +normalize query and data workloads + -> derive recurrence, time-scope, and data evidence + -> enumerate semantic plan alternatives + -> enumerate legal execution contracts and state lifecycles + -> validate summary capabilities and phase constraints + -> derive and check accuracy guarantees + -> normalize one-time and rate costs over an explicit horizon + -> rank legal alternatives + -> emit plan, deployments, assumptions, and rejected alternatives +``` + +## Goals and non-goals + +### Goals + +- Represent glossary-defined query-workload and data-workload concepts without + collapsing independent axes into one enum. +- Separate an operator's statefulness from its execution schedule and the + lifecycle of the state it produces. +- Make unknown demand explicit and fail closed rather than treating it as zero + or infinite reuse. +- Compare one-time and rate-valued costs only through an explicit horizon. +- Explain why a selected plan builds, reuses, maintains, or avoids summary + state. +- Preserve a minimal path from the current batch/repeating workload and + recurrence profile to the proposed model. + +### Non-goals + +- Scheduling jobs, assigning machines, admission control, or executing queries. +- Predicting future query text inside ASAPPlanner. +- Defining a sketch runtime or state-storage protocol. +- Choosing a concrete forecasting algorithm for uncertain demand. +- Changing accuracy targets or guarantee algebra. + +## Heilmeier questions + +- **What are we trying to do?** Choose whether summary state should be built, + maintained, shared, reused, or avoided for different query workloads + and data workload. +- **How is it done today, and what are the limits?** The planner distinguishes + one-shot counts, fixed repeating intervals, and an ingest-rate proxy. It + cannot distinguish an unexpected exploratory query from a scheduled one-time + report, or data at rest from continuous ingestion as an explicit mode. +- **What is new, and why will it succeed?** Orthogonal workload axes and an + explicit state lifecycle let the existing recurrence formulas compare the + same summary under different deployment choices without changing query + semantics. +- **Who cares?** Users need predictable latency and cost; operators need to + know what state will exist and for how long; planner developers need demand + assumptions to be auditable. +- **What are the risks and costs?** More inputs can make planning harder to + configure, forecasts may be stale, and a large lifecycle search space can + increase planning cost. +- **What are the checks for success?** The acceptance cases below must produce + different lifecycle alternatives and cost terms for identical query syntax + under different workload contracts. + +## Proposed design + +### Authoritative concepts and ownership + +| Concept | Authoritative layer | Reason | +| --- | --- | --- | +| Query meaning | Pre-ASAP query IR | Workload metadata must not change semantics | +| Accuracy requirement | Query workload (per-query) | The required result fidelity may be explicit or supplied by the normalization default | +| Response-latency requirement | Query workload (per-query) | The optional end-to-end response-time bound belongs to one query execution | +| Query workload | Workload input | Arrival and recurrence are not inferable from syntax | +| Data workload | Workload input | Ingestion and distribution describe the data, not query workload | +| Summary capability | Summary properties | Merge, delete, and update support constrain legal lifecycles | +| State lifecycle | Physical planning decision | Lifecycle is selected, not declared by `SummaryAgg` | +| Cost | Cost model and explanation | Cost consumes all inputs but does not define their meaning | + +### Query workload + +Accuracy and latency are separate per-query requirements within the query +workload. They constrain different planner decisions and must not be collapsed +into one SLA value: + +```rust +enum AccuracyRequirement { + /// The caller supplied the required result fidelity. + Explicit(AccuracyTarget), + /// The source omitted accuracy; normalization applies the exact default. + ImplicitExact, +} + +enum LatencyRequirement { + /// Maximum permitted end-to-end latency for one query execution. + ExplicitMax(Duration), + /// The caller supplied no latency bound. + Unspecified, +} + +struct QueryRequirements { + accuracy: AccuracyRequirement, + response_latency: LatencyRequirement, +} +``` + +An omitted accuracy field is not an unknown accuracy target and does not permit +arbitrary approximation: the current normalization policy makes it +`ImplicitExact`. Keeping that variant distinct from `Explicit(Exact)` preserves +whether the caller chose exactness or inherited the default. An unspecified +response-latency requirement imposes no response-time constraint; it is not a +zero-duration bound or evidence that every latency is acceptable. Accuracy is +checked as a legality constraint, while response latency is used to reject +plans that cannot meet the bound. + +#### Classification axes + +The glossary classifications must be modeled independently. + +##### Predictability + +```rust +enum Predictability { + /// The query shape is not known before arrival. + AdHoc, + /// The query or parameterized template is known before execution. + Predictable { + known_at: Option, + }, + /// The caller supplied no reliable classification. + Unknown, +} +``` + +`AdHoc` does not mean repeated or one-time. It means the query shape was not +known in advance. The glossary currently places exploratory/ad-hoc queries in +the one-time category, so the MVP should accept `AdHoc + OneTime` and reserve +other combinations until a concrete use case establishes their semantics. + +##### Recurrence + +```rust +enum QueryRecurrence { + OneTime { + invocations: u64, + execute_at: Option, + }, + Repeated { + demand: RepeatedDemand, + }, + Unknown, +} + +enum RepeatedDemand { + FixedInterval(Duration), + Scheduled(Vec), + EstimatedRate(DemandEstimate), +} + +struct DemandEstimate { + /// Time range over which the demand was measured or forecast. + observation_window: ObservationWindow, + /// Expected demand, expressed in exactly one form. + expected: ExpectedDemand, + /// Highest expected invocation rate within the observation window. + peak_rate: Option, + /// Highest expected number of simultaneously executing invocations. + max_concurrency: Option, + /// Confidence in this estimate, in the inclusive range [0.0, 1.0]. + confidence: Confidence, + source: EvidenceSource, + observed_at: Option, + valid_for: Option, +} + +enum ExpectedDemand { + /// Expected total invocations over `observation_window`. + InvocationCount(u64), + /// Expected average invocations per second over `observation_window`. + AverageRate(Rate), +} + +struct ObservationWindow { + start: Timestamp, + end: Timestamp, +} + +struct Confidence(f64); +``` + +One-time means no recurrence is expected for that workload entry. Several +one-time consumers may still share a subplan within a submitted workload. +Repeated means the same query expression over its selected data is evaluated +over time, matching the glossary. Parameterized templates require an explicit +equivalence policy before their executions count as the same query. + +Query-workload volume is more than an average rate. Cost and latency can differ +for the same total request count when requests arrive in bursts or concurrently. +`ExpectedDemand` makes invocation count and average rate alternative +representations, preventing conflicting values in one estimate. The observation +window must be non-empty, rates must be finite and non-negative, and +`Confidence` must be between zero and one. Fixed intervals and explicit +schedules are declarations rather than estimates and do not need fabricated +confidence. The MVP may cost only invocation count and evaluation rate, but it +must preserve unsupported volume characteristics for explanation rather than +silently discarding them. + +##### Queried time scope + +```rust +enum QueryTimeScope { + RealTime, + Longitudinal, + Mixed, + Unknown, +} +``` + +`QueryTimeScope` is not a response-latency requirement. It classifies the event +time of the data selected by the query; `LatencyRequirement` constrains the +wall-clock time allowed to produce the result. They are independent: a +longitudinal query over archived data may require a 100 ms response, while a +real-time query over the latest data may permit a 30 second response. + +This classification is not derived only from a numeric lookback. A five-minute +lookback over recent data is real-time; the same duration over archived data is +not. Planning input should therefore carry the classification and the concrete +time selection separately: + +```rust +struct TimeSelection { + scope: QueryTimeScope, + lookback: Option, + as_of: Option, +} +``` + +For example, the same five-minute lookback has a different scope depending on +whether it is anchored at the current planning time or at a historical time: + +```rust +// The last five minutes: real-time. +TimeSelection { + scope: QueryTimeScope::RealTime, + lookback: Some(Duration::minutes(5)), + as_of: None, +} + +// A five-minute interval from archived data: longitudinal. +TimeSelection { + scope: QueryTimeScope::Longitudinal, + lookback: Some(Duration::minutes(5)), + as_of: Some(timestamp!("2024-01-01T12:05:00Z")), +} +``` + +`lookback` is a query property already represented by temporal query nodes in +some frontends. The normalized workload should reference or derive it rather +than introduce a second conflicting value. + +### Data workload is separate from query workload + +```rust +enum DataArrival { + AtRest, + ContinuouslyIngesting, + Mixed, + Unknown, +} + +/// Statistical distribution of keys in the input data. +enum DataDistribution { + /// A small number of keys account for most observations. + Zipf, + /// Keys are approximately equally likely. + Uniform, + /// Observations arrive in bursts with a temporarily concentrated key set. + Bursty, +} + +struct DataWorkload { + arrival: DataArrival, + ingestion_volume: Evidence, + ingestion_rate: Evidence, + input_cardinality: Evidence, + distribution: Evidence, +} +``` + +`DataDistribution` reuses the existing ASAPPlanner classification. It describes +the key-frequency shape used by summary accuracy and cost models, not whether +data arrives continuously. An unavailable or unsupported distribution is +represented by `Evidence.value = None` rather than by assuming the default +distribution. + +The former `DataCharacteristics` was a stale, continuous-ingestion-specific +case built around series count and samples per second. `DataWorkload` replaces +it as the normalized input rather than embedding that special case in the +general model. Data at rest may have row count and scan statistics without a +nonzero ingestion rate. Unknown arrival must not be interpreted as continuously +ingesting or at rest. + +Every empirical value uses an evidence wrapper conceptually containing: + +```rust +struct Evidence { + value: Option, + source: EvidenceSource, + observed_at: Option, + valid_for: Option, +} +``` + +This reuses the provenance and freshness principles from empirical summary +parameter configuration. A missing or stale value remains unknown. + +### Output cardinality is a derived or evidenced cost input + +Output cardinality depends on input cardinality and grouping columns. The +planner may derive it analytically, accept a catalog estimate, or leave it +unknown. The source and freshness metadata must be preserved because output +cardinality affects summary size, read cost, post-processing cost, and network +cost. It is not a query correctness requirement. + +### Separate operator state, schedule, and output + +The physical design must not use `SummaryAgg` as shorthand for incremental +maintenance. + +```rust +enum OperatorState { + Stateless, + Stateful { + mergeable: bool, + deletable: bool, + }, +} + +enum EvaluationSchedule { + OneShot, + PerUpdate, + OnRead, +} + +enum OutputRepresentation { + PlainRows, + SummaryState, + FinalizedValue, +} +``` + +A one-shot sketch builder is stateful while it consumes its input, but it does +not imply long-lived incremental maintenance. A stateless transform can run +`PerUpdate` before a downstream maintained summary. These types describe an +execution contract; they do not replace semantic operators in the post-ASAP IR. + +### State lifecycle is a plan alternative + +```rust +enum StateLifecycle { + Ephemeral, + Prepared { + activate_at: Timestamp, + retire_at: Timestamp, + }, + Shared { + retention: Duration, + }, + ContinuouslyMaintained, +} +``` + +- `Ephemeral` builds state for one submitted workload and discards it afterward. +- `Prepared` builds or begins maintaining state before a predictable query and + retires it after the known need ends. +- `Shared` retains state for multiple consumers over a bounded lifetime. +- `ContinuouslyMaintained` applies data updates until an explicit later + deployment decision retires the state. + +The summary family and its properties constrain which lifecycles are legal. +For example, an append-only sketch may support continuous inserts but not a +sliding-window lifecycle requiring deletion. Lifecycle legality is checked +before cost ranking, like accuracy legality. + +### Existing summaries are planning input + +An ad-hoc query cannot justify creating permanent state from unknown future +demand, but it may use compatible state that already exists. The planning +problem therefore needs a state catalog describing identity, parameters, +coverage, freshness, accuracy guarantee, lifecycle, and ownership. Catalog +integration is a separate implementation increment; this design only requires +that "reuse existing" and "create new" remain distinguishable alternatives. + +### Cost over a horizon + +`H` is the optimization horizon: the future wall-clock duration over which the +planner compares one-time and recurring costs. The existing cost model +represents it in seconds: + +```rust +/// A finite, strictly positive optimization duration, in seconds. +struct Horizon(f64); +``` + +The horizon is not the query lookback, the queried time scope, or the response +latency bound. It answers only "over how much future execution time should +these alternatives be costed?" All alternatives in one decision must use the +same `H`. `reads(H)` is the number of query evaluations expected or scheduled +within that horizon; for a fixed evaluation rate it is +`H * evaluation_rate`, plus any separately modeled one-time invocations. +Who supplies `H`, and whether a deployment may default it, remains an explicit +architecture decision below. If no horizon is available, the planner must not +compare a one-time cost with a rate-valued cost. + +For a stateful incremental alternative over horizon `H`: + +```text +total(H) = build_cost + + H * update_rate * maintenance_cost_per_update + + reads(H) * summary_read_cost + + H * retention_cost_rate + + retirement_cost +``` + +For repeated raw recomputation: + +```text +total(H) = reads(H) * raw_recompute_cost +``` + +For an ephemeral summary: + +```text +total = invocations * (build_cost + summary_read_cost + retirement_cost) +``` + +`retirement_cost` consistently means the one-time cost of ending a summary +state lifecycle, including deallocation or other cleanup. For ephemeral state, +retirement happens immediately after each invocation; for prepared, shared, or +continuously maintained state, it happens when that deployment is retired. + +For prepared state, update and retention terms apply only between activation +and retirement. Existing state does not pay a new build cost, but its catalog +provenance must establish that assumption. + +The existing `Cost`, `CostRate`, `EvaluationRate`, `UpdateRate`, `Horizon`, and +`total_cost` types are the minimum viable foundation. The implementation should +extend their explanations and lifecycle coverage instead of creating a second +recurrence cost system. + +### Unknown and uncertain demand + +Unknown demand is not zero demand and is not evidence of future reuse. The MVP +policy is: + +- do not select newly created long-lived state solely on unknown future reuse; +- allow raw recomputation, ephemeral build, and reuse of already available + compatible state; +- retain an explicit explanation of the missing demand evidence; and +- require an explicit planning objective before using an estimated demand + distribution. + +Future uncertain-demand support may add expected-cost, percentile-cost, +worst-case, or regret objectives. Those policies must consume a typed estimate +with confidence and provenance; they are not implicit behavior of +`Predictability::Unknown`. + +## Review against the ProjectASAP glossary + +The glossary review found the following required coverage and current gaps. + +| Glossary concept | Current ASAPPlanner representation | Missing design support | +| --- | --- | --- | +| Data at rest vs continuously ingesting | Continuous ingest characteristics are available; no explicit arrival mode | Add `DataArrival`; support at-rest statistics without inventing update rate | +| Ingestion volume | Not a first-class workload input | Add evidenced volume with a time basis | +| Ingestion rate | Derived from series count and sample rate | Preserve as evidenced rate; do not conflate with query evaluation rate | +| Input cardinality | Partial `series_count` and distinct-key inputs | Associate each estimate with its dataset, metric, columns, and observation window | +| Data distribution | Small built-in enum | Preserve source/freshness; permit deployment-specific distributions later | +| Ad-hoc vs predictable | Not represented | Add predictability independently from recurrence | +| One-time vs repeated | Batch entries and fixed-interval repeating entries | Add scheduled one-time, unknown recurrence, and estimated/scheduled repetition | +| Query volume and characteristics | Fixed interval or structural consumer count | Add observation window, peak/burst and concurrency evidence where latency or capacity models require it | +| Real-time vs longitudinal | Temporal IR can carry ranges; no workload classification | Add time scope plus concrete selection; avoid inferring scope from lookback alone | +| Output cardinality | May be inferred locally; no common evidenced input | Add derived/evidenced value and provenance for costing | +| Lookback window | Represented in temporal query shapes/frontends | Establish query IR as authority and expose it to workload costing | +| CTSA pipeline | Not explicitly modeled | Keep as architectural context; planner consumes collect/store/analyze facts but does not model transmission topology in the MVP | +| CSP(F) | Cost and fidelity partly modeled | Treat scale/performance/fidelity as objectives and constraints; do not collapse fidelity into cost | + +Two terminology constraints apply: + +1. A repeated query is not inherently a streaming-data workload. It may + repeatedly query data at rest. +2. A one-time query is not inherently stateless. A predictable one-time query + may justify prepared state, while an ephemeral summary is stateful during + its one execution. + +## Minimal complexity + +The minimum input model is determined by the downstream applications selected +for integration, not by a context-free notion of the fewest possible fields. +Each supported use case must contribute the workload facts that can change +plan legality, accuracy, lifecycle, or cost: + +- Time-series metric queries require queried time scope and lookback. +- Repeated dashboard queries, including an ASAPQuery integration, require + recurrence and evaluation frequency so the planner can cost reuse and + maintenance across executions. +- Batch queries over data at rest require an explicit at-rest arrival mode and + must not be assigned a fabricated ingestion rate. +- Summary techniques whose accuracy depends on the input distribution require + evidenced distribution characteristics; omitting them must produce unknown + accuracy or a conservative fallback rather than a favorable assumption. + +The initial implementation should include the union of fields required by its +committed integrations. Additional workload dimensions should be added when a +new downstream use case demonstrates that they affect a planning decision. + +The simplest alternative is to extend `BatchEntry` with optional schedule and +classification fields and extend `RepeatingEntry` with time scope. That is a +reasonable serialization migration, but it is not a sufficient conceptual +model: it continues to make predictability and recurrence mutually exclusive +container choices, and it has no place for data arrival or state lifecycle. + +The minimum new conceptual layers are therefore: + +1. orthogonal query-demand metadata, required because glossary categories are + not one taxonomy; +2. data-workload metadata, required because ingestion does not describe query + recurrence; +3. state lifecycle as a physical alternative, required because one summary + operator can be deployed ephemerally or incrementally. + +No separate scheduler, forecasting framework, or replacement cost model is +introduced. Existing query IR, summary properties, accuracy model, and +recurrence cost types remain authoritative in their domains. + +## Alternatives and decisions + +### Encode workload class as one enum + +Rejected. Variants such as `AdHoc`, `OneShot`, and `Repeated` overlap: +predictability and recurrence are different facts, and time scope is a third. + +### Infer demand from query syntax or submitted root count + +Rejected. Syntax contains no evidence of future arrival, and several roots in +one request establish only current structural sharing. + +### Treat every summary as continuously maintained + +Rejected. It excludes ephemeral construction over data at rest and overcharges +one-time plans. It also hides deployment lifetime from explanations. + +### Treat every one-time query as raw recomputation + +Rejected. An ephemeral summary may reduce memory or network cost during one +execution, an existing summary may already answer the query, and a predictable +future query may justify preparation. + +### Fold fidelity into a scalar cost + +Rejected. Accuracy and semantic correctness are constraints checked before +ranking. A cheaper plan cannot purchase permission to violate fidelity. + +### Extend the existing recurrence profile only + +Partially accepted for implementation reuse, rejected as the whole model. +`RecurrenceProfile` is an aggregated cost context for a target. It should remain +the derived input to cost decisions, while normalized workload metadata retains +predictability, time scope, provenance, and lifecycle information needed before +and after aggregation. + +## Quality attributes and evidence + +- **Understandability:** explanations use glossary terms and show each axis + separately. Proxy: reviewers can distinguish repeated queries from continuous + ingestion in exported plan evidence. +- **Debuggability:** selected and rejected lifecycle alternatives record demand, + horizon, data statistics, and provenance. Proxy: no lifecycle decision is + explained only as a scalar cost. +- **Maintainability:** current recurrence types remain the cost authority; + normalized workload types remain the source authority. No duplicate formula + system is introduced. +- **Extensibility:** scheduled and estimated recurrence fit without changing + query semantics. Forecasting policies remain pluggable planning objectives. +- **Performance:** lifecycle enumeration expands the candidate space. The MVP + should generate only capability-compatible alternatives and deduplicate + equivalent deployments before ranking. +- **Operability:** every long-lived state has activation, retention or retirement + semantics and ownership in output. Concrete runtime APIs are future work. +- **Security and privacy:** query logs and empirical distributions may be + sensitive. Provenance must identify a source without requiring raw query-log + contents to be embedded in exported plans. + +## Acceptance and test design + +Implementation acceptance is defined by identical logical queries producing +different legal lifecycle choices under different workload contracts: + +1. **Unpredictable one-time query:** offers raw recomputation, compatible + existing state, and ephemeral build; does not justify new continuous state. +2. **Predictable scheduled one-time query:** may offer prepared state with a + bounded activation and retirement period. +3. **Repeated query over continuously ingesting data:** compares incremental + maintenance and repeated recomputation using distinct update and evaluation + rates over an explicit horizon. +4. **Repeated query over data at rest:** uses evaluation rate without inventing + maintenance updates. +5. **Real-time and longitudinal queries with the same expression:** preserve + different time selections and may receive different scan, retention, and + summary alternatives. +6. **Unknown demand:** remains unknown in explanation and cannot make a newly + created long-lived state win through assumed reuse. +7. **Mixed one-time and repeated consumers:** requires an explicit horizon and + accounts for shared build cost once. +8. **Accuracy failure:** rejects a lifecycle regardless of favorable workload + cost. + +Focused unit tests should cover normalization, invalid combinations, evidence +freshness, lifecycle capability checks, and dimensional cost arithmetic. +End-to-end tests should cover cases 1–8 through candidate selection and exported +explanations. A reviewer who did not implement the workload types should design +or review at least the unknown-demand and mixed-consumer cases; that independent +review has not occurred for this design document. + +## Risks, rollout, and exit criteria + +The implementation should roll out additively: + +1. add normalized metadata and explanations while preserving current + batch/repeating behavior; +2. derive the existing `RecurrenceProfile` from the richer model; +3. add ephemeral and existing-state alternatives; +4. add prepared and continuously maintained lifecycle selection; +5. integrate empirical demand and state catalogs only when provenance and + freshness contracts are available. + +Compatibility requires old workloads to normalize without changing their +current decisions when no new metadata is supplied. Unknown new fields must +take the documented conservative path rather than acquire optimistic defaults. + +Open decisions requiring architecture or product input: + +- whether predictable parameterized query templates count as the same repeated + query and under which equivalence relation; +- who supplies the optimization horizon and whether a deployment may define a + default for purely repeated workloads; +- which planning objective governs uncertain demand; +- how state ownership, quota, and retirement requests cross the planner/runtime + boundary; +- whether real-time versus longitudinal is supplied by the caller, derived by a + policy using `as_of` and lookback, or both with conflict diagnostics; and +- the minimum evidence freshness required before empirical workload data may + affect selection. + +The design exits draft status when these decisions have owners, the normalized +input has a compatibility plan, and acceptance cases 1–8 can be expressed in +fixtures without runtime-specific assumptions.