feat(channels): implement the single-publish fixes — S-01..S-04 (reviews 010/011; ADR-047 am. 4, ADR-052, ADR-053, ADR-022 am.)

S-01 — the marked-pub open contract (ADR-047 amendment 4):
- ChannelCore::register_pub_openable + PubOpenHandler (the put path's
  handler returns its result for the merged single response; the
  wrapper awaits inline, teardown is EOF-driven, handler_task stays
  None); register_openable* refuses Pub-typed specs; the wrapper entry
  points refuse unmarked/non-Pub shapes
- the reserved input key `channel_id` (the initiator-allocated id):
  stripped pre-validation by resolve_sink_handler (the single source of
  truth, P-08), handed to the wrapper via OperationContext.metadata, a
  missing key rejected INVALID_INPUT; adopt parity enforced
  (ManagerError::InvalidAdopt) and adopt failures answer
  channel:open_failed / adopt_failed
- zero call.published on marked ops: the first publish chunk on a
  marked op is rejected loudly (channel:publish_chunks_rejected) in
  both dispatch loops; the handler is never fed
- relay put-translation replaces RelayError::PubTypedOpen: hub
  allocates odd on the producer leg, opens the spoke op in the
  background (put-acks-in-result), one-direction pump, response-mirror
  with the producer's reserved key stripped
- ChannelClient::open_pub_channel + PubOpenReply (the initiator-
  allocates put open capability)

S-02 — channel/control implemented (ADR-052): establisher-contributed
ChannelControl (Establishment::with_control), manager-owned registry,
teardown removals, channel:no_control_handle capability answer.

S-03 — channel/resources/subscribe implemented (ADR-053): the
ResourceEnumerators registry (shared per peer), snapshot-then-changes
enumerators, scan-once aggregation, the empty seed; the
always-failing one-shot error stream removed.

S-04 — op/register forward-through (ADR-022 amendment): announced
unmarked Sub/Pub register the real streaming/sink forwarding shapes;
marked announcements refused with MARKED_SPEC_UNSUPPORTED; marked
specs refuse the generic registration path and register_imported
(Result-returning now).

Guard (review 010 action 3): tests/stub_guard.rs greps src/ for
not_implemented; the four pinned stub tests are deleted and the
marked-spec registration refusals are pinned by tests.

Verified: cargo test (696 + guard), clippy --all-targets -D warnings,
fmt --check, fuzz corpus replay.
This commit is contained in:
glm-5.3-flash committed 2026-10-13 09:00:21 +00:00
1 parent df4d4d3a9e
commit 944c300c17
18 files changed
+2418 -326

No files matched your search

+76
View File
@@ -18,6 +18,9 @@ use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tracing::debug;
use crate::protocol::wire::CallError;
use serde_json::Value;
use super::mux::{MuxHandle, OpenerLedger};
use super::reassembly::{MpscRecvStream, MpscSendStream, DEFAULT_BUFFER_CAP};
@@ -38,6 +41,15 @@ pub enum ChannelSide {
/// `ChannelLifecyclePolicy` (ADR-041, amended by ADR-047 §7).
pub const DEFAULT_MAX_CHANNELS: usize = 256;
/// The per-channel out-of-band control callback (ADR-052): received
/// from the open-op establisher (`Establishment::with_control`),
/// installed by the open-op wrapper under the channel's id, routed to
/// by `channel/control`, removed by every teardown path. The manager
/// stays blind to what the closure does (ADR-039).
pub type ChannelControl = Arc<
dyn Fn(Value) -> futures::future::BoxFuture<'static, Result<Value, CallError>> + Send + Sync,
>;
/// The per-channel state — the reassembly sender (the demux feeds chunk
/// payloads into it; the handler reads them out via `MpscRecvStream`),
/// the handler task handle, and the ALPN (for observability).
@@ -62,6 +74,13 @@ pub enum ManagerError {
UnknownChannel(u32),
#[error("channel already exists: {0}")]
ChannelExists(u32),
#[error(
"channel {channel_id} is outside the remote side's allocation range (expected {expected})"
)]
InvalidAdopt {
channel_id: u32,
expected: &'static str,
},
}
/// The shared state for a channels connection. `Clone` (cheap — `Arc`
@@ -101,6 +120,10 @@ struct Inner {
/// drains them into the new receiver. `clear_all` drops the map.
early_arrivals: Mutex<HashMap<u32, VecDeque<Bytes>>>,
early_arrival_count: AtomicU64,
/// Per-channel out-of-band control callbacks (ADR-052). Installed
/// by the open-op wrapper from the establisher's
/// `Establishment::with_control`; removed by every teardown path.
control_handles: Mutex<HashMap<u32, ChannelControl>>,
}
/// The per-channel cap on parked early-arrival chunks (`route_payload`
@@ -151,6 +174,7 @@ impl ChannelManager {
dropped_unknown_chunks: AtomicU64::new(0),
early_arrivals: Mutex::new(HashMap::new()),
early_arrival_count: AtomicU64::new(0),
control_handles: Mutex::new(HashMap::new()),
}),
}
}
@@ -317,6 +341,32 @@ impl ChannelManager {
alpn: impl Into<String>,
handler_task: Option<JoinHandle<()>>,
) -> Result<(MpscSendStream, MpscRecvStream), ManagerError> {
// The adopt range is the remote side's allocation range (the
// odd/even split, ADR-047 §5): the connect side allocates odd,
// so the accept side adopts odd; the accept side allocates
// even, so the connect side adopts even. Adopting inside the
// local range would collide with this side's own monotonic
// allocator shortly after. The duplicate check comes first —
// adopting an id this side already holds returns
// `ChannelExists` and must never touch the live channel (the
// 0.8.1 regression pin).
let expected = match self.inner.side {
ChannelSide::Connect => "even (the accept side's allocated ids; ADR-047 §5)",
ChannelSide::Accept => "odd (the connect side's allocated ids; ADR-047 §5)",
};
if self.inner.channels.lock().contains_key(&channel_id) {
return Err(ManagerError::ChannelExists(channel_id));
}
let parity_ok = match self.inner.side {
ChannelSide::Connect => channel_id.is_multiple_of(2),
ChannelSide::Accept => !channel_id.is_multiple_of(2),
};
if !parity_ok {
return Err(ManagerError::InvalidAdopt {
channel_id,
expected,
});
}
let alpn = alpn.into();
let send = self
@@ -381,6 +431,30 @@ impl ChannelManager {
}
}
/// Install the out-of-band control callback for a channel
/// (ADR-052). Called by the open-op wrapper from the establisher's
/// `Establishment::with_control`. Replaces an existing callback for
/// the id (the establisher runs once per channel — replacement is
/// not an expected path; the last call wins).
pub fn set_control_handle(&self, channel_id: u32, control: ChannelControl) {
self.inner
.control_handles
.lock()
.insert(channel_id, control);
}
/// The control callback for a channel, for routing (the
/// `channel/control` handler). `None` when the channel's ALPN
/// registered no control surface (the op answers
/// `channel:no_control_handle`).
pub fn control_handle(&self, channel_id: u32) -> Option<ChannelControl> {
self.inner.control_handles.lock().get(&channel_id).cloned()
}
fn remove_control_handle(&self, channel_id: u32) {
self.inner.control_handles.lock().remove(&channel_id);
}
/// Install channel 0 (pre-negotiated as `alk/call`, ADR-036).
/// Channel 0 is special only in that it's pre-allocated (by
/// `channels-call`); the `ChannelsAdapter` hands the resulting
@@ -530,6 +604,7 @@ impl ChannelManager {
// Dropping `state.demux_sender` signals EOF to the
// handler's read half (REQ-CH-02).
drop(state.demux_sender);
self.remove_control_handle(channel_id);
Ok(state.handler_task)
}
None => Err(ManagerError::UnknownChannel(channel_id)),
@@ -550,6 +625,7 @@ impl ChannelManager {
task.abort();
}
}
self.inner.control_handles.lock().clear();
// The early-arrival buffers die with the connection too.
self.inner.early_arrivals.lock().clear();
// The opener ledger has the opener PeerIds.