Skip to content

feat: emit precondition/invalid-argument ConduitError codes across pipeline, connector, orchestrator#2538

Merged
devarismeroxa merged 2 commits into
mainfrom
feat/conduiterr-precondition-codes
Jul 6, 2026
Merged

feat: emit precondition/invalid-argument ConduitError codes across pipeline, connector, orchestrator#2538
devarismeroxa merged 2 commits into
mainfrom
feat/conduiterr-precondition-codes

Conversation

@devarismeroxa

Copy link
Copy Markdown
Contributor

Summary

Continues the structured-error rollout (item 2). Steps 1-4 (#2524, #2527, #2528, #2530,
#2532) wired the ConduitError foundation into the API boundary and migrated every
not-found sentinel. This PR migrates the non-not-found sentinels that guard pipeline
lifecycle and mutation preconditions, following the exact conduiterr.Wrap pattern from
those PRs.

Purely additive: pkg/http/api/status/status.go's conduitErrorStatus already turns any
*ConduitError into the right gRPC status + ErrorInfo detail; the sentinel-based
codeFromError/PipelineError/ConnectorError switches are untouched and remain the
fallback for anything not yet migrated. Every sentinel stays in the error chain via
conduiterr.Wrap, so errors.Is(err, ErrX) checks throughout the codebase are
unaffected — verified below.

Codes added

Code (reason) gRPC code Sentinel Package
pipeline.running FailedPrecondition ErrPipelineRunning pipeline (CodePipelineRunning)
pipeline.not_running FailedPrecondition ErrPipelineNotRunning pipeline (CodePipelineNotRunning)
pipeline.name_already_exists AlreadyExists ErrNameAlreadyExists pipeline (CodePipelineNameAlreadyExists)
pipeline.name_missing InvalidArgument ErrNameMissing pipeline (CodePipelineNameMissing)
connector.running FailedPrecondition ErrConnectorRunning connector (CodeConnectorRunning)
connector.invalid_type InvalidArgument ErrInvalidConnectorType connector (CodeConnectorInvalidType)
orchestrator.invalid_processor_parent_type InvalidArgument ErrInvalidProcessorParentType orchestrator (new codes.go)
orchestrator.pipeline_has_processors_attached FailedPrecondition ErrPipelineHasProcessorsAttached orchestrator
orchestrator.pipeline_has_connectors_attached FailedPrecondition ErrPipelineHasConnectorsAttached orchestrator
orchestrator.connector_has_processors_attached FailedPrecondition ErrConnectorHasProcessorsAttached orchestrator
orchestrator.immutable_provisioned_by_config FailedPrecondition ErrImmutableProvisionedByConfig orchestrator

Per-sentinel raise sites: migrated vs deferred

  • ErrPipelineRunning / ErrPipelineNotRunning — sentinel is defined in pkg/pipeline
    but raised in pkg/lifecycle and pkg/orchestrator. Migrated the clean control-plane
    sites: lifecycle.Service.Start/Stop (the authoritative check) and every
    orchestrator.{Pipeline,Connector,Processor}Orchestrator CRUD guard (9 sites for
    ErrPipelineRunning, 2 for ErrPipelineNotRunning).
    Deferred (reported, not touched): the lifecycle-poc (PipelineArchV2 preview
    engine) mirror of both Start/Stop checks, and the internal
    lifecycle.Service.runPipeline tomb-alive defensive check (redundant with Start's
    check, several layers below the API boundary). Both remain bare sentinels — no
    behavior change, errors.Is still holds.
  • ErrNameAlreadyExists / ErrNameMissing (pipeline package only —
    connector.ErrNameMissing is a distinct sentinel, out of scope per the task) —
    migrated both raise sites: Service.Update (direct return) and validatePipeline
    (joined via cerrors.Join, same pattern already proven in
    pkg/provisioning/config/validate.go's fieldError helper).
  • ErrConnectorRunning — single choke point, connector.Instance.Connector.
    Migrated.
  • ErrInvalidConnectorType — 4 raise sites total. Migrated the one user-facing site,
    connector.Service.Create (validates a caller-supplied type). Deferred: the three
    default: branches in Service.SetState, Store.decode, and
    Instance.Connector — all guard against a Type value already validated at Create
    time (storage-layer/defensive, effectively unreachable in practice); left as bare
    sentinels.
  • ErrInvalidProcessorParentType, ErrPipelineHasProcessorsAttached,
    ErrPipelineHasConnectorsAttached, ErrConnectorHasProcessorsAttached,
    ErrImmutableProvisionedByConfig — all defined and raised entirely within
    pkg/orchestrator (3, 1, 1, 1, and 9 sites respectively). Migrated every site; added
    small unexported helpers (pipelineRunningErr, immutableProvisionedByConfigErr,
    invalidProcessorParentTypeErr in the new pkg/orchestrator/codes.go) to avoid
    repeating the wrap+suggestion boilerplate across the 9 ErrImmutableProvisionedByConfig
    sites.

Adversarial self-review

  • Re-checked every conduiterr.Wrap call: Wrap's msg replaces (does not
    concatenate with) the cause's Error() text, so for sites that previously did
    cerrors.Errorf("<prefix>: %w", sentinel) I reconstructed the equivalent text via
    fmt.Sprintf/string-concat with sentinel.Error() so the human-readable message is
    unchanged.
  • Confirmed pkg/pipeline/service.go's validatePipeline joins multiple wrapped
    *ConduitErrors via cerrors.Join; conduiterr.Get (via errors.As) still finds the
    first one, matching the existing pkg/provisioning/config/validate.go precedent (Go
    1.20+ Unwrap() []error support).
  • Found four existing tests that asserted strict identity (is.Equal(err, sentinel),
    stronger than errors.Is) on sentinels that used to be returned bare:
    TestPipelineOrchestrator_{Update,Delete,UpdateDLQ}_PipelineRunning,
    TestPipelineOrchestrator_Delete_PipelineHas{Processors,Connectors}Attached,
    TestConnectorOrchestrator_{Delete,Update}_PipelineRunning,
    TestProcessorOrchestrator_{CreateOnPipeline,UpdateOnPipeline,DeleteOnPipeline}_PipelineRunning.
    Wrapping necessarily breaks is.Equal identity (the concrete error value changes from
    the sentinel to a *ConduitError); updated these to errors.Is + a new
    conduiterr.Get check for the code and suggestion — this is not a weakened
    assertion, it verifies the same fact (this exact sentinel is in the chain) the way
    every other migrated site's test does.
  • Added minimal new tests where no test previously exercised the raise site directly:
    TestInstance_Connector_AlreadyRunning (connector), TestServiceLifecycle_Start_AlreadyRunning
    / TestServiceLifecycle_Stop_NotRunning (lifecycle).
  • Did not touch pkg/http/api/status/status.go — its sentinel-based fallback switches
    are intentionally left alone per the established pattern (they still catch the
    deferred/bare-sentinel sites above).

Test plan

  • go build ./...
  • go test ./pkg/pipeline/... ./pkg/connector/... ./pkg/orchestrator/... ./pkg/http/api/... ./pkg/provisioning/... -count=1 — all green
  • go test -race on the same packages plus ./pkg/lifecycle/... — all green, no data races
  • golangci-lint run ./pkg/pipeline/... ./pkg/connector/... ./pkg/orchestrator/... — 0 issues
  • GOOS=linux GOARCH=386 go build ./... — OK
  • Verified every errors.Is(err, ErrX) check across the repo still holds for all
    migrated and deferred sentinels (grepped for Is( usages before and after)

Risk tier: Tier 2 (API/orchestrator error surface, not a data-path/serialization
change — no wire format, no protocol, no state layer touched). One reviewer approval
required per CLAUDE.md; not merging this myself per the task instructions.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Tapg9dLWXKMZJoL65R24vd

…peline, connector, orchestrator

Continues the structured-error rollout (steps 2-4 covered not-found errors in
#2527-#2532) by migrating the non-not-found sentinels used to guard pipeline
lifecycle and mutation preconditions:

- pipeline.running / pipeline.not_running (codes.FailedPrecondition):
  ErrPipelineRunning / ErrPipelineNotRunning, wrapped at their clean
  control-plane raise sites in pkg/lifecycle/service.go (Start/Stop) and
  pkg/orchestrator/{pipelines,connectors,processors}.go (Update/Delete/Create
  guards).
- pipeline.name_already_exists (codes.AlreadyExists) / pipeline.name_missing
  (codes.InvalidArgument): pkg/pipeline/service.go Update and
  validatePipeline (the latter joined via cerrors.Join, same pattern as
  pkg/provisioning/config/validate.go).
- connector.running (codes.FailedPrecondition): the single choke point,
  Instance.Connector in pkg/connector/instance.go.
- connector.invalid_type (codes.InvalidArgument): the user-facing check in
  connector.Service.Create.
- orchestrator.invalid_processor_parent_type (codes.InvalidArgument),
  orchestrator.pipeline_has_{processors,connectors}_attached,
  orchestrator.connector_has_processors_attached,
  orchestrator.immutable_provisioned_by_config (all
  codes.FailedPrecondition): new pkg/orchestrator/codes.go, wrapped at every
  raise site in the orchestrator package.

Every sentinel stays in the error chain via conduiterr.Wrap (invariant
comment at each site); existing errors.Is checks are preserved, though tests
that previously asserted strict `is.Equal(err, sentinel)` identity had to be
updated to errors.Is + a ConduitError code/suggestion check, since wrapping
necessarily changes the concrete error value (documented in the PR).

Deferred (reported, not migrated, to keep blast radius tight): the
lifecycle-poc (PipelineArchV2 preview) mirror of ErrPipelineRunning/
ErrPipelineNotRunning; the internal runPipeline tomb-alive defensive check;
and three defensive/unreachable ErrInvalidConnectorType switch defaults
(SetState, store.decode, Instance.Connector) guarding already-validated
data.

Design: docs/design-documents/20260705-conduit-error-and-structured-output.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tapg9dLWXKMZJoL65R24vd
@devarismeroxa devarismeroxa requested a review from a team as a code owner July 6, 2026 01:58
@devarismeroxa devarismeroxa merged commit 5440cf2 into main Jul 6, 2026
5 checks passed
@devarismeroxa devarismeroxa deleted the feat/conduiterr-precondition-codes branch July 6, 2026 03:16
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.

1 participant