docs(adr): amendments pinning the single-publish fix — marked-pub contract (047 am.4), control surface (052), resource enumerators (053), op/register forward-through + marked refusal (022 am.)
Per review 011 §6 sequencing: the four amendment documents drafted before code. All pins, no options — the hedged alternatives are stated as rejected with rationale: - ADR-047 amendment 4: the marked-pub open contract (review 011 §1/§2's seven pins — marker semantics, publish_schema refusal, reserved input key + dispatch strip, wrapper-as-responder-of-record, ack-timing sentence, relay translation, client put-open capability) plus the registration invariants that make stubs unregistrable (§4.8). - ADR-052 (new): channel/control control-handle surface — establisher- contributed ChannelControl, wrapper-owned registry, channel:no_control_handle. - ADR-053 (new): channel/resources/subscribe live resource enumerators — snapshot-then-changes, scan-once aggregation, empty seed. - ADR-022 amendment: announced unmarked Sub/Pub forward through the real forwarding shapes; marked announcements refused (MARKED_SPEC_UNSUPPORTED).
This commit is contained in:
1 parent
4eae18f539
commit
df4d4d3a9e
4 files changed
+618
-3
No files matched your search
@@ -0,0 +1,149 @@
|
||||
# ADR-052: The `channel/control` Control-Handle Surface
|
||||
|
||||
## Status
|
||||
|
||||
Accepted — amends ADR-037 (`channel/control`'s contract) and ADR-047
|
||||
(amended §1: the generic ops stay). Resolves review 010 S-02 (the
|
||||
`channel/control` stub shipped nine published versions; the stub was
|
||||
pinned by tests and tracked as OQ-39) by implementing the surface the
|
||||
ADR-037 text always promised. The stub's tracked design work (OQ-39)
|
||||
is promoted here verbatim-ish: ALPN-crate-registered per-channel
|
||||
control callbacks, a channels-layer callback registry keyed by
|
||||
`channel_id`.
|
||||
|
||||
## Context
|
||||
|
||||
ADR-037 specified `channel/control` as the out-of-band control op on
|
||||
channel 0 (resize, signal, keepalive — anything that does not need
|
||||
ordering relative to data), with the channels layer routing the
|
||||
ALPN-specific `message` to a handler's control handle. The
|
||||
implementation shipped a registered, discoverable stub that checked
|
||||
`channel` existence and then returned `channel:control_not_implemented`
|
||||
on every invocation — which perversely returned *not found* for unknown
|
||||
channels and *not implemented* for real ones (review 010 S-02). The
|
||||
implementation was tracked as OQ-39 with the shape recorded; nothing
|
||||
blocked it except that no ALPN crate had asked yet. alktty's move of
|
||||
its control surface to the channels path (ADR-049 §5's migration list)
|
||||
and the general stub ruling (review 010) make that answer invalid.
|
||||
|
||||
The design constraint from ADR-039: `ChannelManager` is auth-blind,
|
||||
ALPN-blind, transport-blind. The control-handle surface must not
|
||||
reintroduce ALPN interpretation into the manager.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. The control callback type and registry
|
||||
|
||||
```rust
|
||||
pub type ChannelControl = Arc<
|
||||
dyn Fn(Value) -> BoxFuture<'static, Result<Value, CallError>> + Send + Sync,
|
||||
>;
|
||||
```
|
||||
|
||||
The callback receives the `message` value (ALPN-specific JSON; the
|
||||
channels layer does not interpret it) and returns the ALPN-specific
|
||||
reply (or a `CallError` it wants surfaced verbatim — e.g.
|
||||
`target_gone`). The live handle: `HashMap<u32, ChannelControl>` in the
|
||||
`ChannelManager`'s internals (the same map discipline as `channels`;
|
||||
`parking_lot` mutex).
|
||||
|
||||
The manager stays blind to what a callback does — it stores opaque
|
||||
closures, exactly as `ChannelState` already stores opaque `handler_task`
|
||||
handles.
|
||||
|
||||
### 2. Registration: the establisher contributes the control handle
|
||||
|
||||
The ALPN crate registers a control callback by returning it from its
|
||||
open-op establisher: `Establishment::with_control(callback)` (builder,
|
||||
alongside `with_reply_field` — ADR-049 amendment 3). The **wrapper**
|
||||
installs it under the allocated/adopted id
|
||||
(`manager.set_control_handle(id, callback)`) and every teardown path
|
||||
removes it (`teardown_channel` drops the entry; `clear_all` drains
|
||||
them) — same closure-lifecycle discipline as the opener ledger.
|
||||
|
||||
Why the establisher, not the handler: the establisher already returns
|
||||
typed payloads the wrapper routes (`Establishment`), it runs where the
|
||||
ALPN crate has prepared its backend (the resize target is the backend
|
||||
the establisher dialled/planned), and adding a registration path
|
||||
through the `OpenHandler` would require handing the handler the
|
||||
manager and the id — a signature break across every published consumer
|
||||
for zero benefit. The wrapper-owned installation also makes the
|
||||
callback's lifetime exactly the channel's lifetime by construction.
|
||||
|
||||
### 3. The op: routing and error surface
|
||||
|
||||
`make_control_handler` (the ADR-037 op, registered by
|
||||
`ChannelOps::register_on`) becomes: validate `channel_id` (unchanged)
|
||||
→ `channel` exists? (unchanged; `channel:unknown_channel`) → callback
|
||||
registered for the id? →
|
||||
|
||||
| case | reply |
|
||||
|------|-------|
|
||||
| unknown channel | `channel:unknown_channel` (existing class) |
|
||||
| no callback registered | `channel:no_control_handle` — typed, retryable false, message: the channel's ALPN registered no control surface (`details: { alpn }`) |
|
||||
| callback ok | `{ "control": <callback reply> }` |
|
||||
| callback err | the callback's `CallError` verbatim |
|
||||
|
||||
`channel:no_control_handle` is a real capability answer, not a stub:
|
||||
a consumer that calls `channel/control` on a channel whose ALPN chose
|
||||
not to expose control learns exactly that. The `message` body is
|
||||
ALPN-specific JSON (unchanged; the channels layer does not read it).
|
||||
|
||||
**The callback receives the message only** — no identity, no
|
||||
`OperationContext`. The authorization home is the op's own
|
||||
`access_control` (checked by the registry before the handler, per
|
||||
ADR-010/017 — the same op-level ACL every other surface runs); a
|
||||
per-owner resize is expressed as ADR-011 `resource_id_path` on the
|
||||
`channel/control` spec for ALPNs that want it, not by widening the
|
||||
callback signature. Wire-visibility: `channel:no_control_handle`
|
||||
joins the stable error set (ADR-016 — consumers branch on it
|
||||
deliberately); the callback's reply shape is the producing crate's
|
||||
schema concern.
|
||||
|
||||
### 4. What this does not decide
|
||||
|
||||
Ordering-sensitive control rides the data channel (unchanged —
|
||||
channel-operations.md's control-message division, DP-4 pinned). The
|
||||
manager keeps its blindness; the enumerators registry of ADR-053 is
|
||||
the analogous ALPN-level surface for discovery, not control.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:** the advertised op serves; S-02 dies; ALPN crates get
|
||||
out-of-band control for free (resize/signal arrive as one routed
|
||||
call-op instead of per-crate in-band framing); teardown-bound callback
|
||||
lifetime is structural.
|
||||
|
||||
**Negative:** the manager's internals gain one map (opaque closures —
|
||||
no blindness regression); a control callback racing teardown can
|
||||
observe a torn-down backend through its own error (`CallError`) — the
|
||||
same shape the handler EOF path already produces, not a new class.
|
||||
|
||||
## Door type
|
||||
|
||||
**One-way (wire-visible).** `channel:no_control_handle` and the reply
|
||||
envelope shape join the stable surface once consumers branch on them.
|
||||
The callback type, the establisher carrier, and the map are two-way-door
|
||||
implementation details inside it (the `Establishment` carrier is
|
||||
`#[non_exhaustive]` — ADR-049 amendment 2's posture).
|
||||
|
||||
## Verification gates
|
||||
|
||||
- `channel/control` routes to the establisher-contributed callback and
|
||||
the reply is the callback's output (e2e over a real channels
|
||||
connection);
|
||||
- `channel:no_control_handle` on a channel whose ALPN registered none;
|
||||
- `channel:unknown_channel` preserved (the pre-existing error);
|
||||
- teardown removes the handle (post-teardown control answers
|
||||
`channel:unknown_channel`, never a stale callback);
|
||||
- callback `Err` passes through verbatim.
|
||||
|
||||
## References
|
||||
|
||||
- Review 010 S-02 (the stub; the implement ruling), review 011 §4
|
||||
(downstream-safe; implement preferred)
|
||||
- OQ-39 (the promoted design note this ADR resolves)
|
||||
- ADR-037 (the op's contract), ADR-047 (openable ALPNs; the wrapper
|
||||
this registration rides), ADR-049 (the establisher carrier this
|
||||
registers through), ADR-039 (manager blindness), ADR-011
|
||||
(`resource_id_path` — the per-owner control-gate lever)
|
||||
Reference in new issue
Block a user