Skip to content

The plan/runtime boundary

The executor is the engine’s hot, parallel, memory-bounded core — the part you least want running a half-checked plan. The last three lessons built up the validated pieces: a screened path, a strict located parse. This lesson is where they converge into a single typed handle — CompiledPlan — and where the engine makes “this plan is fully compiled” the same kind of unforgeable, compiler-checked fact that ValidatedPath made of “this path is screened.”

You’ll be able to: explain what CompiledPlan wraps, what the engine decides once at plan time versus per record at run time, and why the executor’s entry point refuses a raw config.

CompiledPlan is the output of compilation — and like ValidatedPath, its fields are private, so the only way to hold one is to have gone through the compiler:

clinker-plan ·compiled.rs ·CompiledPlan type @47d2e12
/// CompiledPlan is the typed-handle output of PipelineConfig::compile.
#[derive(Debug)]
pub struct CompiledPlan {
dag: ExecutionPlanDag, // the lowered execution DAG
config: PipelineConfig, // the validated config
artifacts: CompileArtifacts, // per-node CXL typecheck results, bound schemas
channel_identity: Option<ChannelIdentity>,
pipeline_hash: [u8; 32], // BLAKE3 of the source YAML
}

Everything expensive and checkable has already happened by the time this struct exists. The DAG is lowered (nodes resolved, topology fixed). Every CXL expression is typechecked, its typed program stored in artifacts. Schemas are bound. The source is hashed. The struct is read-only from the outside — accessors like dag(), config(), and artifacts() hand out shared references, nothing more.

The static/runtime split: decide once, run many

Section titled “The static/runtime split: decide once, run many”

This is the architectural heart of the lesson. Work in a streaming engine falls into two buckets:

  • Plan-time (static), done once per job: parse, validate, resolve references, lower the DAG, typecheck every CXL expression, bind schemas. Expensive, but paid a single time.
  • Run-time (dynamic), done once per record: pull a record, evaluate the already-typechecked expressions, route it, write it. This runs millions of times.

CompiledPlan is the membrane between them. Anything that can be settled before the first record flows is settled at compile time and frozen into the handle, so the per-record path never repeats it. The doc on artifacts() says exactly this: “The runtime executor reads this map directly to pull each transform’s Arc<TypedProgram> instead of re-typechecking.” The executor never typechecks a CXL expression — that was done once, at compile time, and the result is in the handle. (Lesson 23 shows the same “compile once, run per record” idea inside CXL itself.)

The nodes the DAG is built from are themselves self-describing — each PlanNode owns its topology, its display form, and its execution payload, plus a span back to the source YAML for diagnostics:

clinker-plan ·mod.rs ·PlanNode type @47d2e12
pub enum PlanNode {
Source { name: String, span: Span, /* ... */ },
Transform { name: String, span: Span, /* ... */ },
Output { name: String, span: Span, /* ... */ },
// ... each variant owns its topology, display, and execution payload
}

The executor accepts the handle and nothing else

Section titled “The executor accepts the handle and nothing else”

Here’s the enforcement. The executor’s public entry point takes &CompiledPlan. It does not accept a PipelineConfig. And a compile_fail doctest pins that refusal so it can never silently regress:

clinker-exec ·mod.rs ·run_plan_with_readers_writers fn @47d2e12
/// Accepts the typed `CompiledPlan` handle returned by `PipelineConfig::compile`.
///
/// ```compile_fail
/// let _ = PipelineExecutor::run_plan_with_readers_writers(
/// cfg, // ← should be &CompiledPlan, not &PipelineConfig
/// // ...
/// );
/// ```
pub fn run_plan_with_readers_writers<W: Into<WriterRegistry>>(
plan: &CompiledPlan,
// ...
) { /* ... */ }

Trace the guarantee, exactly as with ValidatedPath: to run, you need a &CompiledPlan; to get a CompiledPlan, you must call PipelineConfig::compile (the only constructor); and compile is where all validation lives. So “the executor ran an unvalidated plan” is not a bug you guard against at run time — it’s a program that does not compile. The type is the proof that compilation happened.

The whole pattern fits in one screen. A raw plan, a compile step that validates and bakes a result, a private-fielded handle, and a runner that accepts only the handle:

rust // editable

Uncomment the last block and the build fails: run wants a &CompiledPlan, you have a RawPlan, and you cannot hand-build a CompiledPlan because its fields are private. The only way forward is through compile — which is the only place validation happens. That refusal is the plan/runtime boundary in one compiler error.

// quick check

Why does PipelineExecutor accept &CompiledPlan rather than &PipelineConfig?

You’ve seen the engine concentrate all its “is this valid?” work behind one typed handle. But plenty can still go wrong at run time — a bad cast, a file error, an internal invariant tripped. How the engine classifies those failures, and which ones abort versus get quarantined, is the next lesson.