Skip to content

Per-pipeline middleware, composed once at Bind - #97

Merged
dangra merged 3 commits into
masterfrom
pipeline-middleware
Sep 17, 2026
Merged

dangra merged 3 commits into
masterfrom
pipeline-middleware

Conversation

@dangra

@dangra dangra commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

A pipeline can now declare its own middleware chain. pipelinedef.Config.Middleware carries it, and the generated constructor takes it as an option — NewXxx(h, opts ...Option), with the generated package's WithMiddleware (an alias of pipelinedef.WithMiddleware, so wiring code never imports pipelinedef) the first option, since the middleware is optional:

deploy, err := deploypb.NewDeployService(&deploy{w}, deploypb.WithMiddleware(notFoundIsPermanent)).Bind(eng)
  • Scope: wraps only that pipeline's operations, forward and unwind alike (Invocation.Phase distinguishes). A rule that belongs to one pipeline — the flyd stop port's "a store not-found ends any forward operation as a permanent failure" — no longer reaches every pipeline bound to the engine, and no longer needs a PipelineID string compare on every attempt.
  • Order: engine middleware outermost, then the pipeline's own in declaration order (first listed outermost), then the handler: engine[0](…engine[n](pipeline[0](…pipeline[m](h)))). A tracing span installed on the engine wraps a pipeline's rules.
  • Composed once at Bind. boundStep carries each step's composed forward and unwind operations; the per-attempt wrap and the unwind closure invokeUnwind allocated are gone. Wrapper factories run once per step and phase, the inner handler once per attempt, so invariant 71 holds and durableotel.Middleware is unaffected. The engine chain is final by Bind: options apply only in engine.New.
  • Bind rejects a nil middleware entry. Per-step middleware stays out of scope; a pipeline middleware switching on inv.StepID() covers it.

Spec: 04-engine Middleware section gains the pipeline level and the composition line; invariant 119; http-analogy gets the "wrapping at mux registration" row (and its stale XFunc row fixed); 05-codegen and 02-authoring show the constructor option, and 02-authoring's stale positional-constructor sections are brought to the one-implementor form. README and tour observability sentences mention the per-pipeline form.

Tests: scope and order across two pipelines on one engine (engine/p1/p2 onion on one, engine alone on the other, forward attempts and unwind); the flyd case (one pipeline's not-found is permanent, the other's retries); factories run once at Bind regardless of attempts; Bind rejects a nil entry; New copies the slice; a machines example test passes middleware through the constructor option.

Perf gate (3 interleaved slices vs master): zero regressions; allocs and bytes within ±1%.

Additive for hand-rolled definitions; generated packages regenerate (the constructor gains an options variadic, existing call sites compile unchanged).

Copilot AI lite review requested due to automatic review settings September 17, 2026 17:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

A pipeline declares its own middleware chain: pipelinedef.Config.Middleware,
exposed by the generated constructor as NewXxx(h, mw...). It wraps only
that pipeline's operations, forward and unwind alike, inside the engine
chain — engine middleware outermost, then the pipeline's own in order,
then the handler. A rule that belongs to one pipeline, such as a store's
not-found ending any forward operation of that pipeline permanently, no
longer reaches every pipeline bound to the engine.

Both chains are composed once at Bind: boundStep carries the composed
forward and unwind operations, so the per-attempt wrap and the unwind
closure are gone. Bind rejects a nil middleware entry. Invariant 119.
The generated constructor takes opts ...pipelinedef.Option, the knobs a
proto cannot declare; WithMiddleware is the first. Wiring code imports
pipelinedef for it; handler code still never does.
Each generated package carries Option (= pipelinedef.Option) and
WithMiddleware, so wiring code configures a pipeline as
machinespb.NewProvisionMachine(h, machinespb.WithMiddleware(mw)) and
never imports pipelinedef. Emitted once per Go package.
@dangra
dangra force-pushed the pipeline-middleware branch from 7bbf075 to bdd9062 Compare September 17, 2026 20:08
@dangra
dangra merged commit 94f9280 into master Sep 17, 2026
7 checks passed
@dangra dangra mentioned this pull request Sep 18, 2026
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.

2 participants