Skip to content

engine: labels should order steps within a deploy, and deploy cycles must diagnose not panic #438

Description

@pskry

Principle

promises/inputs labels are a user-facing ordering mechanism. If a user can
write them, they must work everywhere — both across deploys and between
steps of the same deploy. Today they only work across deploys; using a label
to order two steps inside one deploy doesn't order anything and instead crashes
scampi with an "internal error".

Repro

Chain three run steps in one deploy via labels:

std.deploy(name = "base", targets = [host]) {
  posix.run { desc = "warm 1", check = "sleep 1", promises = ["warm:1"] }
  posix.run { desc = "warm 2", check = "sleep 1", inputs = ["warm:1"], promises = ["warm:2"] }
  posix.run { desc = "warm 3", check = "sleep 1", inputs = ["warm:2"] }
}
[scampi] fatal internal error
This is a BUG in scampi, not in your configuration.
...
BUG: engine.Plan returned unexpected error: deploy block cycle: base/local

Root causes

1. Labels don't reach the intra-deploy step graph. There are two separate
resource paths:

  • step_graph.go builds the within-deploy step DAG from the step-level
    spec.Promiser interface (step.Promises()/Inputs()).
  • deploy_graph.go builds the cross-deploy DAG from the step config's
    spec.ResourceDeclarer (ResourceDeclarations()), which is where user
    promises/inputs labels actually land.

So user labels feed only the cross-deploy graph. run steps expose nothing via
spec.Promiser, so intra-deploy they're always barriers and labels between them
order nothing.

2. The cross-deploy graph fabricates a self-cycle. Because collectPromises/
collectInputs aggregate over all of a deploy's steps, a deploy whose steps
promise and input the same label both produces and consumes it. The wiring loop
then adds a self-edge (deploy_graph.go:69-77) with no guard:

n.deps = append(n.deps, prods[0])   // no "prods[0] != n" check

The step graph gets this right (step_graph.go:59 skips producer == n); the
deploy graph doesn't. findCycle sees base -> base and returns
DeployCycleError.

3. Deploy cycles crash instead of diagnosing. DeployCycleError is a proper
typed diagnostic (it has a Diagnostic() method), but handleEngineError
(main.go:348) only handles AbortError/CancelledError; everything else hits
panic(errs.BUG(...)). So a config error prints "This is a BUG in scampi, not
in your configuration" with a stack trace — the opposite of the truth.
MultipleProducersError has the same fate.

Desired behavior

  1. Same-deploy labels order steps. A step that inputs a label another step in
    the same deploy promises depends on that step (promiser runs first) — via the
    intra-deploy step graph. This makes run steps orderable by labels, not just
    fenced by barriers.
  2. Cross-deploy graph ignores intra-deploy-satisfied labels. A label produced
    and consumed within one deploy is internal; it must not create a cross-deploy
    edge or a self-cycle. Add the producer != n guard, mirroring the step graph.
  3. Genuine deploy cycles are diagnostics, not panics. Route
    DeployCycleError and MultipleProducersError through the diagnostic path in
    handleEngineError so they render as guiding user errors (and carry a
    SourceSpan — see engine: MultipleProducersError / DeployCycleError carry no SourceSpan #367), never the internal-bug panic.

Files

  • internal/engine/step_graph.go — feed user labels into the step DAG.
  • internal/engine/deploy_graph.go — self-edge guard; treat intra-deploy-satisfied
    labels as internal.
  • cmd/scampi/main.go — handle DeployCycleError/MultipleProducersError as
    diagnostics in handleEngineError.
  • Relates to engine: MultipleProducersError / DeployCycleError carry no SourceSpan #367 (DeployCycleError lacks SourceSpan).

Discovered while building the multi-deploy live-region demo: chaining run
sleeps within a lane via labels crashed rather than ordering them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugSomething is not workingkind/enhancementImprove existing functionality

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions