feat(review 006 Unit 1): channel-open establishment phase + typed client error (E-01, N-1)
Implements ADR-049 Unit 1 — the open-op wrapper gains an awaited,
bounded establishment phase, and the client stops erasing the error.
- OpenEstablisher hook + Establishment/EstablishmentError types:
register_openable_with_establisher awaits the establisher bounded
(earlier of dispatch deadline and per-registration timeout, else
ESTABLISHMENT_TIMEOUT = 10s) after allocation, before the reply and
before the pump handler is spawned (ADR-049 §1/§2). Implementation
note: the establisher takes (input, auth) only — the channel's
yield-once BiStream belongs exclusively to the pump handler
(amendment recorded in ADR-049).
- Establishment failure: teardown_channel + opener-ledger take +
policy.on_close un-increment (allocation and teardown balance;
the ledger take is the atomic gate, ADR-047 §7), reply
channel:open_failed with details {reason, message} — reason ∈
dial_failed / unknown_resource / resource_shortage / handler_error
/ timeout (ADR-049 §3). SSH contract consumer-visible: a failed
open never returns a channel_id.
- register_openable unchanged (no establisher = always-OK; existing
registrations compile and behave identically — compat gate test).
- ChannelClient::open_channel returns ChannelOpenError (breaking at
0.5.0): CallFailed { error: CallError } carries the wire error
verbatim (establishment_reason() branches on details.reason);
MissingChannelId / AdoptFailed cover the local-only shapes
(ADR-049 §4, review 006 N-1).
- Tests cover all four verification gates from the review: e2e
establisher failure through a real channels connection (typed
reason + no-channel + ledger un-increment), bounded timeout,
no-establisher compat, establisher-success pump round-trip; plus
reason-vocabulary mapping and bound arithmetic.
- Bump to 0.5.0 (open_channel error-type change is semver-relevant).
Verification: cargo test (608 passed), clippy --all-targets -D
warnings, fmt --check, doc --no-deps, wasm32 check — all clean.
This commit is contained in:
1 parent
48ceeba55c
commit
2586c3b217
9 files changed
+1106
-31
No files matched your search
@@ -68,12 +68,19 @@ impl ChannelClient {
|
||||
/// the `MpscRecvStream` (read half). The caller can build a
|
||||
/// `Connection` from these via `channel_source` and
|
||||
/// `Connection::from_source`.
|
||||
///
|
||||
/// The error is typed (ADR-049 §4 — review 006 N-1):
|
||||
/// `ChannelOpenError::CallFailed` carries the wire `CallError`
|
||||
/// verbatim (branch on `establishment_reason()` for
|
||||
/// `channel:open_failed`'s `details.reason`); `MissingChannelId`
|
||||
/// covers a malformed success reply; `AdoptFailed` covers local
|
||||
/// adoption failure.
|
||||
pub async fn open_channel(
|
||||
&self,
|
||||
operation_id: &str,
|
||||
input: Value,
|
||||
alpn: &str,
|
||||
) -> Result<(u32, MpscSendStream, MpscRecvStream), String>;
|
||||
) -> Result<(u32, MpscSendStream, MpscRecvStream), ChannelOpenError>;
|
||||
|
||||
/// Take the `CallConnection` — used by the consumer to register
|
||||
/// imported ops (`from_call`) on the connection's overlay. After
|
||||
@@ -88,7 +95,11 @@ impl ChannelClient {
|
||||
ADR-047 dissolved the generic `channel/open` operation into per-ALPN
|
||||
open ops (`channels/<alpn>/sub`, `channels/<alpn>/pub`). Each ALPN
|
||||
crate registers its own open op via `ChannelCore::register_openable`
|
||||
(ADR-047 §3, as amended 2026-08-13 — per-connection registration).
|
||||
(ADR-047 §3, as amended 2026-08-13 — per-connection registration),
|
||||
optionally with an establisher via
|
||||
`register_openable_with_establisher` (ADR-049 — the awaited,
|
||||
bounded establishment phase; establishment failure resolves
|
||||
`Err` on the client with `channel:open_failed` + `details.reason`).
|
||||
The `ChannelClient` calls these ops by name on channel 0 via
|
||||
`call_open_op`; `open_channel` wraps `call_open_op` + `adopt_channel`.
|
||||
|
||||
|
||||
@@ -77,12 +77,39 @@ round-trip, so the open round-trip is not additive latency.
|
||||
| `channel:forbidden` | `AccessControl::check` denied the open | false |
|
||||
| `channel:allocation_failed` | Handler allocate failed (e.g., backend couldn't start) | true (often transient) |
|
||||
| `channel:too_many_channels` | Per-connection or per-identity channel limit hit (ADR-040, ADR-041) | false |
|
||||
| `channel:open_failed` | The establishment phase failed (ADR-049) — `details: { reason, message }`, reason ∈ `dial_failed` / `unknown_resource` / `resource_shortage` / `handler_error` / `timeout` | false |
|
||||
| `channel:no_channels_session` | The op was invoked outside a channels session (no `ChannelManager` — ADR-047 §2) | false |
|
||||
|
||||
`channel:unknown_alpn` and `channel:invalid_params` (ADR-037) are gone —
|
||||
an unregistered ALPN is an ordinary `NOT_FOUND` (op not registered); a
|
||||
bad `input` is ordinary schema rejection.
|
||||
|
||||
### The establishment phase (ADR-049)
|
||||
|
||||
An openable ALPN may register an **establisher** alongside its open
|
||||
handler (`ChannelCore::register_openable_with_establisher`). The
|
||||
establisher is the awaited preparation step ADR-047 §3 described
|
||||
("validate params, consult ownership, prepare the backend, return a
|
||||
channel plan"): the wrapper awaits it **bounded** (the dispatch
|
||||
deadline when the op carries one, else the registration's override or
|
||||
the 10s `ESTABLISHMENT_TIMEOUT` — the earlier of the two), **before
|
||||
replying and before spawning the pump handler**. On establisher
|
||||
failure (error or deadline) the wrapper tears down the just-allocated
|
||||
channel (demux sender, opener-ledger entry, `policy.on_close`
|
||||
un-increment — the same atomic ledger-take gate every teardown path
|
||||
runs) and replies `channel:open_failed` with
|
||||
`details: { reason, message }` (the table row above). The SSH
|
||||
contract holds consumer-visibly: a failed open never returns a
|
||||
`channel_id`.
|
||||
|
||||
Registrations without an establisher behave exactly as before (the
|
||||
open op cannot fail post-allocation; establishment work inside the
|
||||
handler is invisible to the open reply). The bound applies only to
|
||||
the establisher — the spawned pump handler's lifetime is governed by
|
||||
the existing teardown machinery, unchanged. Head-of-line safety: the
|
||||
serving loop spawns Once invocations as independent tasks, so a slow
|
||||
establisher on one open op does not block other calls on channel 0.
|
||||
|
||||
### `channels/<alpn>/pub` — publish a binary stream
|
||||
|
||||
`OperationType::Pub` (ADR-046). The initiator publishes a stream of
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (amends ADR-047 §3 — the open-op wrapper gains an awaited
|
||||
Accepted — implemented in alkcall 0.5.0 (Unit 1: E-01 + N-1; see the
|
||||
"Amendment (Unit 1 implementation, 2026-09-06)" at the bottom).
|
||||
Amends ADR-047 §3 — the open-op wrapper gains an awaited
|
||||
establishment phase ahead of the spawned pump handler; resolves review
|
||||
006 E-01 and N-1)
|
||||
006 E-01 and N-1
|
||||
|
||||
## Context
|
||||
|
||||
@@ -313,4 +315,42 @@ untouched, which is what keeps the split cheap to revise.
|
||||
- alktunnels OQ-TN-09 (dial-failure reporting — the first consumer of
|
||||
the new error) and `docs/research/ssh-socks5-survey.md`
|
||||
§"Open-failure path" (the reason-code prior art)
|
||||
- RFC 4254 §5.1, RFC 1928 §6 — SSH/SOCKS5 open-failure semantics
|
||||
- RFC 4254 §5.1, RFC 1928 §6 — SSH/SOCKS5 open-failure semantics
|
||||
|
||||
## Amendment (Unit 1 implementation, 2026-09-06)
|
||||
|
||||
Unit 1 landed in alkcall 0.5.0. Two implementation-shape notes, both
|
||||
within this ADR's two-way door (the hook shape is the revisable
|
||||
implementation detail; the wire surface is unchanged from §1/§3):
|
||||
|
||||
1. **The establisher does not receive the channel `Connection`.** §1's
|
||||
signature sketch passed `Connection` to the establisher, but the
|
||||
channel's `BiStream` is yield-once (`ChannelBidiStreamSource`) — it
|
||||
cannot be handed to both the establisher and the pump handler, and
|
||||
the establisher is pre-data-plane by design (its dial targets the
|
||||
backend, not the channel). The implemented signature is
|
||||
`Fn(Value, AuthContext) -> BoxFuture<'static,
|
||||
Result<Establishment, EstablishmentError>>`; the `Connection`
|
||||
belongs exclusively to the `OpenHandler` (unchanged).
|
||||
2. **The bound is the earlier of the dispatch deadline and the
|
||||
per-registration timeout.** §2 names the dispatch deadline "when
|
||||
the `OperationContext` carries one, else the crate constant";
|
||||
implemented as `min(deadline_remaining, timeout_override |
|
||||
ESTABLISHMENT_TIMEOUT)` — the registration override stays
|
||||
meaningful for `Query`/`Mutation`-typed open ops (whose dispatch
|
||||
carries a 30s deadline; `Sub` clears it), and a deadline already in
|
||||
the past yields a zero bound (immediate `timeout` reason).
|
||||
|
||||
Implemented surface: `OpenEstablisher`, `Establishment`,
|
||||
`EstablishmentError` (reasons `dial_failed`/`unknown_resource`/
|
||||
`resource_shortage`/`handler_error` + the wrapper's `timeout`),
|
||||
`ESTABLISHMENT_TIMEOUT` (10s), `CHANNEL_OPEN_FAILED`
|
||||
(`channel:open_failed`), `ChannelCore::register_openable_with_establisher`
|
||||
(`register_openable` delegates with `establisher: None`),
|
||||
`ChannelOpenError` (client-side typed error: `CallFailed { error:
|
||||
CallError }` / `MissingChannelId` / `AdoptFailed`, with
|
||||
`call_error()` + `establishment_reason()` accessors). All four
|
||||
verification gates from the review landed as tests (establisher
|
||||
failure e2e through a real channels connection with ledger
|
||||
un-increment + no-channel assertions, bounded timeout, no-establisher
|
||||
compat, establisher-success pump round-trip).
|
||||
Reference in new issue
Block a user