Files
alknet/tasks/call/protocol/abort-cascade-wiring.md
glm-5.2 d149932e2a tasks: decompose review #004 findings into 4 fix tasks + review gate
W1 (call/protocol/abort-cascade-wiring): wire AbortCascade into CallAdapter
handle_stream for EVENT_ABORTED. W2 (core/endpoint-client-fingerprint):
extract TLS client cert fingerprint in dispatch_quinn/dispatch_iroh.
W3 (vault/mnemonic-debug-redaction): replace Mnemonic derive(Debug) with
redacting impl. W4 (core/auth-apikey-resources, level: research): decide
whether ApiKeyEntry should carry resources, then implement or drop from
spec. review-post-impl-fixes gates on all four. Graph: 33 tasks, 12 gens.
2026-06-24 10:02:03 +00:00

130 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: call/protocol/abort-cascade-wiring
name: Wire AbortCascade into CallAdapter inbound event path (ADR-016)
status: pending
depends_on: [call/protocol/abort-cascade]
scope: narrow
risk: medium
impact: component
level: implementation
---
## Description
`AbortCascade::cascade_abort` is implemented and unit-tested in
`crates/alknet-call/src/protocol/abort.rs` (20 tests cover depth-3 cascades,
mixed Call/Subscribe entries, both `AbortPolicy` variants, and the
"unknown root silently discarded" rule), but no caller invokes it.
`CallAdapter::handle_stream` (adapter.rs:210213) only dispatches
`EVENT_REQUESTED`; an inbound `call.aborted` event is logged at `debug!`
and dropped. As a result, ADR-016's cascade is a documented property
that does not hold at runtime — when a wire client aborts a parent
request, the parent's composed descendants keep running to completion,
which is exactly the wasted-work / unwanted-side-effects case ADR-016
was written to prevent.
This task adds the missing bolt between the two halves.
### Wire visibility (read before implementing)
ADR-016 Decision 2 (decisions/016-...:220224) and the abort-cascade
task (tasks/call/protocol/abort-cascade.md:6874) are explicit:
**composed child request_ids are internal**. The client only sees
`call.aborted` for the root ID it sent; the server cascades internally.
So the wiring should:
1. Receive the inbound `call.aborted` for `root_request_id`.
2. Call `AbortCascade::cascade_abort(root_request_id, AbortPolicy::AbortDependents)`
— the wire caller does not choose the policy (ADR-016 Decision 6:
the root gets the default `AbortDependents`; `ContinueRunning` is a
handler-level opt-in for children, not a wire field).
3. Drop each descendant's pending entry (which cancels its future via
Rust's async drop semantics — no `call.aborted` is sent on the wire
for descendants).
4. Drop the root's pending entry (the trigger event already came from
the wire; the server mirrors the abort locally).
Do **not** send `call.aborted` frames back to the client for descendant
IDs — that would leak internal composition structure to the wire.
### Implementation sketch
In `CallAdapter::handle_stream` (adapter.rs:200228), replace the
`continue` branch with a match on event type:
```rust
match envelope.r#type.as_str() {
EVENT_REQUESTED => { /* existing dispatch path */ }
EVENT_ABORTED => {
let request_id = envelope.id.clone();
let mut pending = connection.pending().lock();
let mut cascade = AbortCascade::new(&mut pending);
let aborted = cascade.cascade_abort(&request_id, AbortPolicy::AbortDependents);
// also abort the root itself (cascade_abort does not touch the root)
pending.handle_aborted(&request_id);
if !aborted.is_empty() {
tracing::debug!(count = aborted.len(), "abort cascade evicted descendants");
}
}
other => {
debug!(event_type = %other, id = %envelope.id, "ignoring non-requested/non-aborted event on inbound stream");
}
}
```
`AbortCascade` already takes `&mut PendingRequestMap`; `CallConnection::pending()`
returns `&Arc<Mutex<PendingRequestMap>>` (connection.rs:5355). Import
`AbortCascade` and `AbortPolicy` from `super::abort` and
`crate::registry::context` respectively.
### Integration test (the test that would have caught the gap)
Add an integration test in `adapter.rs`'s `tests` module that exercises
the full path:
1. Build a `CallAdapter` with a registry containing one parent op
(`parent/run`) whose handler calls `env.invoke("child", "run", ...)`.
2. Register `child/run` in the same registry.
3. Open a stub `Connection`, construct a `CallConnection`, manually
register a pending entry for the parent's request_id, and simulate
a composed child by registering a second pending entry with
`parent_request_id: Some(parent_id)`.
4. Send an `EventEnvelope::aborted(parent_id)` frame through
`handle_stream`.
5. Assert both the parent and child entries are gone from
`PendingRequestMap`.
The existing unit tests on `AbortCascade` cover the tree-walking logic;
this test only needs to confirm the wiring — that an inbound abort
frame actually reaches `cascade_abort`.
## Acceptance Criteria
- [ ] `CallAdapter::handle_stream` handles `EVENT_ABORTED` (not just `EVENT_REQUESTED`)
- [ ] Inbound `call.aborted` triggers `AbortCascade::cascade_abort` with `AbortPolicy::AbortDependents`
- [ ] Root request's pending entry is also removed (cascade_abort skips the root)
- [ ] No `call.aborted` frames are sent on the wire for descendant IDs (internal-only cascade)
- [ ] Non-requested, non-aborted events still log at `debug!` and continue
- [ ] Integration test: parent abort removes parent + child from `PendingRequestMap`
- [ ] Integration test: abort for unknown request_id is a no-op (no panic, no removal)
- [ ] `cargo test -p alknet-call` succeeds
- [ ] `cargo clippy -p alknet-call --all-targets` succeeds with no warnings
## References
- docs/reviews/004-post-implementation-sanity-check.md — W1 (full finding)
- docs/architecture/decisions/016-abort-cascade-for-nested-calls.md — ADR-016 (Decision 2: server-side cascade; Decision 6: policy is handler-set, not wire-set)
- docs/architecture/crates/call/call-protocol.md:497 — spec requiring the CallAdapter to walk the tree
- tasks/call/protocol/abort-cascade.md — completed task that built `AbortCascade` in isolation
- crates/alknet-call/src/protocol/abort.rs — existing `AbortCascade` impl
- crates/alknet-call/src/protocol/adapter.rs:200228 — `handle_stream` (the site to modify)
## Notes
> The abort-cascade task (call/protocol/abort-cascade) built and tested
> `AbortCascade` but its acceptance criteria did not include wiring it
> into `CallAdapter::handle_stream`. The call-adapter task's acceptance
> criteria likewise omitted "inbound `call.aborted` triggers cascade."
> This task closes that integration gap — all the hard logic already
> exists and is tested; this task adds the ~30-line bolt and the one
> integration test that would have caught the gap.