Skip to content

feat: add workload-level library integration API (#453) - #458

Merged
zzylol merged 5 commits into
refactor/issue-456-interface-namesfrom
feat/issue-453-workflow-api
Sep 29, 2026
Merged

zzylol merged 5 commits into
refactor/issue-456-interface-namesfrom
feat/issue-453-workflow-api

Conversation

@zzylol

@zzylol zzylol commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

Two requirements, split out of #423:

This PR also closes #453.

What

e2e_plan gives library users one call from a prepared workload to the selected Post-ASAP DAG for every query. OptimizationPass gives developers a replaceable optimization stage — not an interface users call, but the slot the stage sits in, with the shipped algorithm as one implementation.

Design doc: docs/design_docs/architecture/updated_interface_with_pluggable_optimization.md.

PlanSpace, cost_sorted, and global_selection are unchanged.

Before this PR

Planning one SQL workload with lifecycle decisions, from a PlanningWorkload and a catalog:

// 1. Lower every normalized entry, and record which entry each root came from.
//    Not lower_sql_batch: it walks query_batch alone and drops repeating entries.
let mut roots = Vec::new();
let mut entry_indices = Vec::new();
for (index, entry) in workload.query_workload.entries().enumerate() {
    let accuracy = entry.requirements.accuracy.target();
    let expr = lower_sql_dialect(&entry.query.0, &catalog, dialect.clone(), accuracy.clone()).await?;
    roots.push((index, Rc::new(expr), Some(accuracy)));
    entry_indices.push(index);
}

// 2. Search.
let strategies = default_strategies_with_evidence(&cost_model, &evidence);
let space = search_workload_with_targets(roots, &strategies, &accuracy_model);

// 3. Select once, re-binding roots to workload entries.
let demand = WorkloadDemand {
    workload: &workload.query_workload,
    data_workload: workload.data_workload.as_ref(),
    entry_indices: &entry_indices,
};
let selection = global_selection_with_summary_maintenance_lifecycles(
    &space, demand, now_ms, horizon, capabilities, &cost_model)?;

// 4. Assemble once per root.
for (index, root) in &space.roots {
    assemble_selected_dag_with_summary_maintenance_lifecycles(
        &selection, root, demand, now_ms, horizon, capabilities, &cost_model)?;
}

Steps 1 and 3 carry two bindings — the Id in the roots tuple and the &[usize] in WorkloadDemand — that both have to agree with entries() order, and nothing checks that they do.

After this PR

let output = e2e_plan(
    UserInput::new(&workload, FrontendInput::Sql { catalog: &catalog },
                   PlanningModels::builtin())
        .with_lifecycle(LifecycleInput::new(now_ms, capabilities).with_horizon(horizon))
).await?;

And a different algorithm replaces the stage without touching the pipeline:

impl OptimizationPass for MyPass {
    fn name(&self) -> &'static str { "my-pass" }
    fn optimize(&self, input: OptimizationInput<'_>) -> Result<PlanOutput, OptimizeError> { .. }
}

let output = e2e_plan(user_input.with_pass(&my_pass)).await?;

How

Crate Added
asap-types ParsedWorkload — the frontend/optimizer boundary; its constructor checks one lowered expression per normalized entry, so the binding above is an invariant rather than a convention
asap-aware-mapping OptimizationPass, OptimizationInput, PlanOutput, PlanningModels, LifecycleInput, the optimize harness, PassRegistry, MajorPass
asap-planner (new) e2e_plan, UserInput, FrontendInput, lowering dispatch

Three points worth a reviewer's attention:

  • MajorPass is a move, not a rewrite. The shipped pipeline runs unchanged behind the trait. One consequence: ReplacementStrategy is now a concept of MajorPass rather than of the optimization stage.
  • The optimize free function is the harness. A pass is third-party code, but downstream reads every pass's output against one contract, so optimize validates the input and then checks that the variant matches the request, that there is one plan per workload entry, and that plans[k].entry_index == k. Structural only.
  • Lowering no longer goes through lower_sql_batch. It walks query_batch alone and silently drops repeating entries — exactly the ones whose RepeatedDemand the lifecycle stage reads.

asap-planner is a separate crate because it is the only one depending on every frontend; a pass depends on asap-aware-mapping and asap-types only, so writing one does not pull in DataFusion.

Tests

12 new tests; cargo test --workspace is 1190 passing, 0 failing.

  • 5 unit tests in pass: the contract check rejects a variant the input did not ask for and a plan count that drops a query; the registry refuses a duplicate name and lists names in order.
  • 7 end-to-end tests in asap-planner: every query planned in entry order; repeating SQL entries lowered; a caller-supplied pass runs instead of the shipped one; the harness catches a pass that mislabels entry_index; a frontend that cannot lower the workload's language is rejected; disagreeing planning clocks are rejected; lifecycle input selects the lifecycle variant.

Not in this PR

  • Candidate-level diagnostics (dag_export's replacements) stay outside the trait — they only exist for a two-phase algorithm. Tools that want them talk to MajorPass and the candidate-search API directly.
  • dag_export does not yet take a --pass flag.
  • The developer guide's extension-point table (Part 3 §7) does not yet have a row for OptimizationPass.
  • Replanning remains undefined.

@Selvomega

Selvomega commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Although I still don't understand why PlanSpace is exposed, this actually looks good to me.

@zzylol

zzylol commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

@Selvomega please feel free to work based on this PR, or please guide whether I should merge this PR. Thanks!

Selvomega and others added 4 commits September 23, 2026 02:41
Lowering turns a PlanningWorkload into pre-ASAP IR, but the optimization
stage still needs the workload's demand facts — recurrence, predictability,
execution time, accuracy requirement — none of which live in the IR. Today a
caller carries the two halves separately and links them with a hand-built
`&[usize]` whose ordering contract is easy to get wrong and never checked.

ParsedWorkload holds both behind a constructor that verifies one lowered
expression per normalized entry, so positional correspondence is an invariant
rather than a convention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Optimization was the hard-coded two-phase pipeline: generate candidates, then
select among them. The only extension points sat inside that paradigm, so an
algorithm shaped differently — a greedy MQO loop with no candidate-generation
phase at all — could only be added by disguising itself as a rule.

OptimizationPass states the stage's end-to-end behaviour instead: pre-ASAP IR
in, post-ASAP DAG out. It names none of this crate's two-phase vocabulary, so
an implementation is not obliged to have phases. MajorPass is the shipped
algorithm moved behind it unchanged, which also makes ReplacementStrategy a
concept of that pass rather than of the interface.

The `optimize` free function is the harness callers use: it validates the
input once for every pass and checks the output contract downstream consumers
rely on — that the requested variant came back, that every workload entry is
accounted for, and that no plan is mislabelled. A defective pass therefore
fails at the boundary rather than reaching a deployment.

PassRegistry resolves a pass by name for callers driven by a CLI flag, a
config file, or a sweep over every registered baseline. It is caller-owned
rather than a link-time global so that two tests in one binary cannot see each
other's registrations, and a duplicate name is an error rather than an
overwrite so a comparison run cannot silently measure one pass twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Embedding ASAPPlanner meant assembling the pipeline by hand: lower per query
with the right frontend, search, select, then assemble once per root, and make
a second call with its own arguments for the deployment half. The only facade
re-exporting more than one frontend was asap-devtools, a developer-tools crate.

asap-planner is that facade. `e2e_plan` takes the prepared input and returns
the selected DAG per query — and the maintenance decisions too, when lifecycle
input is supplied. PlanSpace and GlobalSelection no longer appear in a user's
code. It is async because the SQL frontend plans through DataFusion.

Two details worth calling out:

- The SQL and MetricsQL frontends are driven one normalized entry at a time
  rather than through `lower_sql_batch`, which walks `query_batch` alone and
  would silently drop every repeating query — exactly the entries whose
  recurrence the lifecycle stage reads.
- `UserInput::validate` runs before lowering and rejects a frontend that
  cannot lower the workload's language, a non-positive horizon, and two
  disagreeing planning clocks. The last would otherwise build the DAG for one
  instant and price it for another, with neither stage able to notice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pass

Covers what `e2e_plan` and `OptimizationPass` are, the three interfacing types
a caller meets (`UserInput`, `OptimizationInput`, `PlanOutput`), how `MajorPass`
fills the stage by default and how another pass replaces it, how the three
workflows in input-output-workflow.md map onto this shape, and where the code
lives.

Written for the architecture audience: it states the interface and the reasons
behind its shape, and leaves per-call argument detail to the library reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 4dd64e5 into refactor/issue-456-interface-names Sep 29, 2026
2 checks passed
@zzylol
zzylol deleted the feat/issue-453-workflow-api branch September 29, 2026 17:56
Selvomega added a commit that referenced this pull request Sep 29, 2026
Cherry-pick of the PR #458 merge commit (4dd64e5) onto main. PR #458 was
merged into refactor/issue-456-interface-names after that branch had
already reached main via #457 and #445, so its content never landed on
main. Adds the asap-planner facade crate (#429), the pluggable
optimization pass (#430), and the ParsedWorkload boundary type.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants