ADR-0058: Outcome-aware Segment Composition Contract
Date: 2026-08-13 Status: Accepted References: ADR-0024, ADR-0025 Bd: rc-65fs (epic); ADR number reserved rc-zfov.
Context
A demo surfaced three defects that share one missing contract. Outcome-aware
Segments (EIPs that implement OutcomePipeline from ADR-0025, or that reach the
outcome layer through the Tower Result<Exchange, CamelError> adapter) had no
stated rule for what they may report as Completed after their work failed.
The visible defects:
- rc-20yn:
recipient_listreturnedOk(original)when every recipient failed. The adapter wrapped that intoCompleted(original). Thecache:Segment then wrote the inbound body back under the key for the full time-to-live. The corruption was silent ontimer:routes. - rc-n8rc:
Body::Streamwas consumed twice on the error path (the HTTP symptom-masker of rc-20yn). - rc-65yi: the body was lost when
cache_peek_staleran inside ado_trycatch that shared a key with acache:step.
ADR-0025 defined the PipelineOutcome type (Completed, Stopped, Failed).
It did not state the body-propagation contract for a Segment whose work produced
zero successes. This ADR pins that contract.
Decision
The invariant
When a Segment's attempted work results in zero successes (an operational
failure), the Segment SHALL report Failed(error). It MAY report
Stopped(exchange) only when the zero-success outcome is an intentional halt
governed by the Stop EIP (ADR-0025 section 3). It SHALL NOT report Completed.
The invariant is outcome-based. It is not body-equality-based. A zero-success
Segment may not return Completed even when its body differs from the inbound
body.
A Segment that attempted no work is not an operational failure. It MAY report
Completed(original). Example: a recipient_list whose expression resolves to
an empty list attempts no recipient call.
Per-Segment definitions
Each governed Segment defines "attempted work", "success", "operational failure", and "intentional halt" as follows.
recipient_list(TowerService<Exchange>, reaches the invariant through theResulttoPipelineOutcomeadapter): attempt = one recipient endpoint call. Success = the call returnedOk. Operational failure = at least one call was attempted and zero calls returnedOk. Intentional halt = none (recipient_listdoes not produceStopped).multicast(OutcomePipeline Segment, parallel sibling ofrecipient_list): attempt = one branch sub-pipeline run. Success = the branch returnedCompleted. Operational failure = at least one branch ran and zero branches returnedCompleted. Intentional halt = a branch returnedStoppedand that halt propagates.cache(OutcomePipeline Segment; propagator, not generator): attempt = theon_misssub-pipeline run on a cache MISS. Success =on_missreturnedCompleted. AStoppedorFailedpropagated fromon_missis not the cache's operational failure. The cache propagates it with no write-back. The cache's own operational failure is a repository error surfaced asFailed(Contract C1 from ADR-0023). Intentional halt = aStoppedpropagated fromon_miss.do_try(OutcomePipeline Segment): attempt =try_bodyrun. Success =try_bodyreturnedCompleted, ortry_bodyreturnedFailedand a matching catch clause ran and returnedCompleted. Operational failure =try_bodyreturnedFailedand no catch matched, or every matching catch re-propagated. Intentional halt = aStoppedfromtry_body(propagated; skips catch and finally per ADR-0025 section 5.1), or aStoppedfrom a catch body.
Cache write-back trust rule
The cache: Segment SHALL write back a body only when on_miss reports
Completed. It SHALL skip write-back when on_miss reports Stopped or
Failed. This is already true at crates/camel-processor/src/cache_eip.rs step
3 (the on_miss outcome match returns Stopped/Failed before any
repository.set call). The rule is the downstream guarantor of the invariant
for the cache-warming pattern. See the eip-cache spec capability.
Last-error determinism
For recipient_list and multicast, the last_error carried by a zero-success
Failed outcome is determined as follows.
- Sequential arm: the last error encountered in iteration order.
- Parallel arm (
recipient_list): the error from the task returned by the lastJoinSet::join_next().awaitcall that completed with an error (completion order). - Parallel arm (
multicast): the error from the highest-index branch that returnedFailed(branch-index order — results are sorted by branch index before the representative error is selected). This is the legacy LastWins semantics.
The parallel-arm error is a representative error in both siblings. It is not
the causally-first error. The two siblings use different selection orders
(completion vs. branch-index), but both yield a representative, not a
causal, error. A test may assert a specific error identity only when it
controls the selection order through a synchronization primitive
(recipient_list) or fixed branch arrangement (multicast).
Multicast outcome
multicast is governed by the zero-success invariant and the Stopped-wins rule.
When at least one branch returns Stopped, multicast propagates Stopped
(ADR-0025 section 3). When zero branches return Stopped and zero branches
return Completed, multicast reports Failed(last_error).
The partial-success aggregation policy is out of scope for this ADR. The current
multicast returns Failed when any branch fails, even when other branches
succeed. This is inconsistent with recipient_list, which aggregates the
successful results on partial success. The inconsistency is tracked as bd
rc-b41j. A future change that reconciles the two siblings SHALL update this
section.
Governed Segments
This ADR is the compliance authority for: recipient_list, multicast,
cache, do_try, split, streaming-split, load_balance.
The normative test scenarios in the segment-outcome-composition spec
capability cover the first four (delivered by the outcome-aware-segment-composition change). The remaining three (split, streaming-split,
load_balance) inherit compliance from this ADR. They are verified separately.
Migration / existing-code alignment
multicast already complies with the zero-success invariant. Verified in
crates/camel-processor/src/multicast_segment.rs: sequential_multicast and
parallel_multicast track last_error, return Stopped on the first
Stopped branch, and return Failed(last_error) when outputs is empty and
last_error is set.
recipient_list is the non-compliant Segment. It is corrected in
outcome-aware-segment-composition Task 2.1, with this ADR as authority. The
fix changes the zero-success path of RecipientListService::call to return
Err(last_error) instead of Ok(original). The existing Result to
PipelineOutcome adapter then yields Failed, and cache: skips write-back.
stop_on_exception defaults to false in RecipientListConfig. This matches
Apache Camel. The default is unchanged.
Consequences
- A zero-success
recipient_listinside acache:on_miss no longer poisons the cache. The cache retains the previously seeded stale entry. do_trycatches keyed onFailedreceive the error. They do not receive a launderedCompleted(original).- A Segment author can determine, from this ADR alone, whether a new Segment implementation is compliant.
Alternatives considered
- Report
Stopped(original)on zero-success. Rejected.Stoppedis reserved for the intentional Stop EIP (ADR-0025 section 3). Using it for an operational error would hide the failure fromdo_trycatches keyed onFailed. - Change
stop_on_exceptiondefault totrue. Rejected. It would break Apache Camel parity. Partial-multicast routes legitimately continue past a failed branch. - Body-equality invariant ("a Segment must not report
Completedwith an unchanged body when its work failed"). Rejected. It would reject legitimate no-op Segments that returnCompleted(original)after no attempted work. The outcome-based rule keys on "attempted work produced zero successes".
Glossary
- Operational failure: a Segment attempted one or more units of work and zero succeeded.
- Intentional halt: a
Stoppedgoverned by the Stop EIP (ADR-0025 section 3). - Representative error: the parallel-arm
last_error. Forrecipient_list, selection is byJoinSet::join_nextcompletion order; formulticast, by highest branch index (legacy LastWins). Neither is the causally-first error.