The `composed` feature's ASSOCIATE egress (ADR-008's composed variant, ADR-009's gate): `composed::direct_egress` — each destination dials through an alktunnels 0.2.0 direct-op channel (`TunnelSession::open_direct`, `Substrate::Udp`), per-target ACL for free, the POC #2 `ComposedEgress` shape ported to the landed seam types. - `Cargo.toml` — `composed = ["dep:alktunnels"]` (alktunnels pinned "0.2.0"; rides `composed` alone, `cargo tree --edges normal` spot-checked); dev-dep for the composed suite. - `src/composed/mod.rs` — the lazy destination→tunnel table (LRU-capped, `DEFAULT_MAX_TUNNELS` = 256), per-tunnel relay tasks draining a shared inbound queue, `SessionOwner` (the owner wrapper shared with the composed accept — the batch-1 review's Sync-bound wrinkle), the readiness contract (real async park; ended relays → association-end `Err`, never a hot `Ok(None)`), framing-once, domain pass-through unresolved (ADR-012), the backpressure bound documented on the constructor. - Two deliberate deviations from the POC shape, documented in the task's Notes: the closed-queue condition is a live-relay count (the egress holds its own `Sender`, so the POC's queue-close arm was unreachable through this surface); the tunnel codec is alktunnels' (`[len]` + payload), not the RFC-framed reader. - `tests/composed-egress.rs` — round-trips per destination → one tunnel each, table reuse, LRU eviction/re-dial, the readiness park, the closed-relay-set `Err`, framing-once through the wire layer, domain pass-through — over a `wire_tunnel_direct` harness topology (the consumer-side shape with the direct op on the producer's serving registry). Verification: `cargo test` + `--all-features` pass (13 suites); `cargo check --features composed --target wasm32-unknown-unknown` clean (the hard gate); clippy `--all-targets --all-features -- -D warnings` clean; `cargo fmt --check` clean; `cargo doc --no-deps` clean.
8.7 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level |
|---|---|---|---|---|---|---|---|
| implementation/composed-egress | Composed — the direct-op egress (lazy destination→tunnel table) | completed | moderate | medium | component | implementation |
Description
The composed feature's egress provider (ADR-008's composed
variants): the ASSOCIATE egress that dials each destination through
an alktunnels 0.2.0 direct-op channel (TunnelSession::open_direct,
Substrate::Udp) — per-target ACL for free (each destination rides
its own tunnel:direct-governed channel), the lazy
destination→tunnel table LRU-evicted at a cap (POC #2's
ComposedEgress shape).
Deliverables:
Cargo.toml—composed = ["dep:alktunnels"](the only feature that adds the alktunnels dependency, ADR-009); alktunnels pinned"0.2.0"(the sibling pin posture — convention 11's deliberate-bump rule applies). Verify the composed graph stays wasm-clean (alktunnels' default crate is wasm-clean; the composed providers use only its ungated channels surface — the wasm check with--features composedis a hard gate).src/composed/mod.rs(featurecomposed) — the composed providers, named per producer.md §Wiring:direct_egress(...)→EgressFactory: the POC #2ComposedEgressshape ported to the landed seam types — a lazy destination→tunnel table: each new destination opens one alktunnels UDP-tunnel channel (open_direct,Substrate::Udp), a relay task per tunnel drains inbound datagrams into a shared queue, the write half stays in the table (LRU-capped; dropping a write half EOFs the tunnel producer-side; the session's Drop reaps the channel entry — alktunnels ADR-005).- The Sync-bound wrinkle (the batch-1 review's seam contract vs
alktunnels' surface): the landed
TargetHandle/BindTarget Handlehalves areBox<dyn AsyncRead + Send + Sync + Unpin>, but alktunnels'TakenHalvesyieldsSend + Unpinonly. The composed providers need an owner-wrapper inside the handle payloads (egress.rs's documented posture: "keep-alive state a substrate needs rides inside the halves — a provider-side owner wrapper"). For the egress the wrapper is the table entry's session (the egress owns it — not a handle payload); for the composed accept (the sibling task) the wrapper is load-bearing — design it here, share it. - The readiness contract (load-bearing):
next_replyparks on the shared inbound queue (recv().await— Pending until a relay task delivers); a closed queue (all relays ended) returnsErr— never a hotOk(None)(the POC's closed-queue arm; the stage's zero-yield defense, review 002 R-1, is the backstop, not the design). Replies carry inner bytes (addr + data— the reply address is the entry's recorded destination; framing-once). - Per-datagram
sendis awaited inline (the stage interleaves sends with reply drains) — a slow egress applies backpressure to the association, not to other channels (wire.md's note: "the batch-2 composed-egress author inherits this bound knowingly"). The table's open cost rides the send path (lazy open on first datagram — the POC's measured ~0.8 ms). - The resource/target mapping:
open_directtakes aSubstrateAddr— the destination'sTargetAddrrenders to it (IP forms direct; domain form passes through unresolved — producer-side resolution, the ssh-Dsemantic; ADR-012). Constructor config carries the LRU cap (the 256-channel default cap bounds fan-out — ADR-010's posture; the cap is config, not wire). - Module doc: names the ADRs (008/009), the per-target-ACL benefit, the readiness contract, the framing-once invariant, the backpressure bound.
- Tests (
tests/composed-egress.rs): the POC #2 shape end to end over channels — a tunnel-producing peer (the alktunnels registry shape,register_tunnel_direct_openable), the composed egress against it, datagram round-trips through the tunnel (multiple destinations → multiple tunnels; the LRU eviction at cap pinned by test); the readiness contract re-pinned (park-then- datagram, no spin); the closed-queue → association-end shape; framing-once (no double prefix — the POC #2 bug class).
Acceptance Criteria
cargo testpasses;cargo test --all-featurespassescargo check --features composed --target wasm32-unknown- unknownclean — the composed graph is wasm-clean (hard gate)cargo clippy --all-targets --all-features -- -D warningsclean;cargo fmt --checkcleanalktunnelspinned"0.2.0"; the dep ridescomposedalone (default graph unchanged —cargo treespot-check)- The lazy table + LRU eviction land per the POC shape — pinned by test
- The readiness contract honored (real async park; closed queue
→
Err, never a hotOk(None)) — pinned by test - Framing-once (replies are inner bytes; no double prefix) — pinned by test
- Per-datagram send backpressure documented on the constructor
References
- docs/architecture/decisions/008-provider-defaults.md (the composed variants), 009-feature-gates.md, 003 (the handle swap), 010 (the cap), 012 (resolution)
- docs/architecture/producer.md §Wiring (
composed::direct_egress), §The Three Seams - POC reference:
/workspace/alksocks-udp-poc/src/egress.rs(ComposedEgress— the table, the relay tasks, the closed-queue posture) - Landed surfaces:
src/egress.rs(the seam contracts + the owner-wrapper posture), alktunnels 0.2.0 (TunnelSession::open_direct,TakenHalves,register_tunnel_direct_openable)
Notes
Landed as
src/composed/mod.rs(featurecomposed) +tests/composed-egress.rs, with awire_tunnel_directtopology added totests/harness.rs(the consumer-side topology with alktunnels' direct op on the producer's serving registry — the composed egress'sChannelClientis the direct-op caller).Two deviations from the POC shape, both deliberate:
- The closed-queue condition is a live-relay count, not a queue close. The egress holds its own factory
Sender, so the sharedmpscqueue can never fully disconnect —recv()would never returnNoneand the POC's closed-queue arm was unreachable through this surface. Relays instead emit anEndedmarker on their tunnel's EOF;next_replycounts them (live_relays) and returns the association-endErrwhen the count reaches zero and the queue is drained. The readiness contract is unchanged (the wait is a real async park; a drained-but-live relay set parks).- The tunnel codec is alktunnels'
wire::DatagramReader([len]+ payload), not this crate's RFC-framed one — the POC fed the tunnel read half into the SOCKS wire reader (a bug class the round-trip test caught during bring-up); the composed egress frames the tunnel wire once with the tunnel codec and the RFC wire once downstream (framing-once holds at both boundaries).The Sync-bound wrinkle resolves as designed: the shared
SessionOwnerwrapper (half + session in one owner) is exported for the composed accept (the sibling task); the egress itself owns the session in the table entry (the POC's shape) and the relay task owns the bare read half.
cargo tree --edges normalspot-check:alktunnelsridescomposedalone (absent from the default normal graph; the dev-dep copy is dev-only).
Summary
The composed egress landed:
composed = ["dep:alktunnels"](alktunnels pinned "0.2.0"; dep ridescomposedalone),src/composed/mod.rs—direct_egress(theEgressFactory), the lazy destination→tunnel table with LRU eviction at cap (DEFAULT_ MAX_TUNNELS= 256), per-tunnel relay tasks draining into a shared inbound queue,SessionOwner(the owner wrapper shared with the composed accept), the readiness contract (real async park; ended relays → association-endErr, never a hotOk(None)), framing- once, per-target ACL via the direct op, domain pass-through unresolved (ADR-012), and the backpressure bound documented on the constructor.tests/composed-egress.rs(6 tests over thewire_tunnel_directharness topology): round-trips per destination → one tunnel each, table reuse, LRU eviction/re-dial, the readiness park, the closed-relay-setErr, framing-once through the wire layer, and the domain pass-through. Verification:cargo test(default) +--all-featurespass; the wasm check with--features composedclean; clippy (--all-targets --all-features -- -D warnings) clean;cargo fmt --checkclean;cargo doc --no-depsclean.