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:
1 parent
df4d4d3a9e
commit
944c300c17
18 files changed
+2418
-326
No files matched your search
+130
-2
@@ -70,8 +70,8 @@ impl ChannelOpenError {
|
||||
|
||||
/// The establishment-failure reason code — `details.reason` of a
|
||||
/// `channel:open_failed` error (`dial_failed`, `unknown_resource`,
|
||||
/// `resource_shortage`, `handler_error`, `timeout`). `None` for
|
||||
/// any other failure shape.
|
||||
/// `resource_shortage`, `handler_error`, `timeout`, `adopt_failed`).
|
||||
/// `None` for any other failure shape.
|
||||
pub fn establishment_reason(&self) -> Option<&str> {
|
||||
match self {
|
||||
ChannelOpenError::CallFailed { error }
|
||||
@@ -88,6 +88,30 @@ impl ChannelOpenError {
|
||||
}
|
||||
}
|
||||
|
||||
/// The awaited single merged response of a put open
|
||||
/// ([`ChannelClient::open_pub_channel`], ADR-047 amendment 4 §4.9 —
|
||||
/// the put ack rides in the result, ADR-047 amendment 4 §4.5). The
|
||||
/// putter writes bytes on the returned `send` half (or spawns pumps)
|
||||
/// and awaits this for the put's wire contract (e.g. alkblobs' put
|
||||
/// replying `{ token, digest }`). Await this after (or alongside) the
|
||||
/// byte stream, never before it — the response cannot exist before the
|
||||
/// byte stream completes.
|
||||
pub struct PubOpenReply {
|
||||
rx: tokio::sync::oneshot::Receiver<Result<Value, ChannelOpenError>>,
|
||||
}
|
||||
|
||||
impl PubOpenReply {
|
||||
/// The putter-side await: the merged response output (`Ok`);
|
||||
/// `Err` carries the wire `CallError` verbatim
|
||||
/// (`channel:open_failed` + `details.reason` per ADR-049 §3).
|
||||
pub async fn resolve(self) -> Result<Value, ChannelOpenError> {
|
||||
match self.rx.await {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(ChannelOpenError::MissingChannelId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The client-side handle for a channels connection. Constructed via
|
||||
/// [`ChannelClient::from_connection`] from an established
|
||||
/// `Connection`. Holds the `ChannelManager` (for the demux/mux state)
|
||||
@@ -382,6 +406,110 @@ impl ChannelClient {
|
||||
.map(|(send, recv)| (channel_id, out, send, recv))
|
||||
}
|
||||
|
||||
/// The putter-facing open (ADR-047 amendment 4 §4.9 — the
|
||||
/// initiator-allocates capability `open_channel`/`open_channel_with_reply`
|
||||
/// cannot provide, because those await the open reply before
|
||||
/// returning the halves and a put's bytes flow before the reply
|
||||
/// exists, review 011 §2.7): allocates the connect-side odd id
|
||||
/// locally, inserts the reserved input key, sends the open op in a
|
||||
/// background task, and returns **immediately** — the caller
|
||||
/// writes bytes on `send` without waiting for anything (the
|
||||
/// put ack rides in the merged response; ADR-047 amendment 4 §4.5).
|
||||
///
|
||||
/// Returns `(channel_id, send, recv, reply)`; `reply` is the
|
||||
/// awaited single merged response — `Ok(Value)` (the full output,
|
||||
/// e.g. `{ token, digest }`) or the wire error. On a failed open
|
||||
/// the local adopt entry is torn down and `send`/`recv` are
|
||||
/// already EOF'd (teardown); the caller still holds them but every
|
||||
/// write is EOF-shaped. The `recv` half is the (bidirectional)
|
||||
/// channel's read side — a put that also receives reads it.
|
||||
pub async fn open_pub_channel(
|
||||
&self,
|
||||
operation_id: &str,
|
||||
input: Value,
|
||||
alpn: &str,
|
||||
) -> Result<(u32, MpscSendStream, MpscRecvStream, PubOpenReply), ChannelOpenError> {
|
||||
use crate::registry::spec::RESERVED_OPEN_INPUT_KEY;
|
||||
|
||||
let (channel_id, send, recv) = self
|
||||
.manager
|
||||
.open_channel(
|
||||
alpn,
|
||||
// The opener ledger is inert on the connect side (it
|
||||
// does not enforce the per-identity cap, ADR-041) —
|
||||
// the record is observability only.
|
||||
"channel-client",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
crate::channels::manager::ManagerError::TooManyChannels { count, max } => {
|
||||
ChannelOpenError::CallFailed {
|
||||
error: crate::protocol::wire::CallError::new(
|
||||
"channel:too_many_channels",
|
||||
format!("per-connection channel cap exceeded: {count}/{max}"),
|
||||
false,
|
||||
)
|
||||
.with_details(serde_json::json!({ "count": count, "max": max })),
|
||||
}
|
||||
}
|
||||
other => ChannelOpenError::CallFailed {
|
||||
error: crate::protocol::wire::CallError::new(
|
||||
"channel:allocation_failed",
|
||||
format!("channel allocation failed: {other}"),
|
||||
false,
|
||||
),
|
||||
},
|
||||
})?;
|
||||
|
||||
let mut op_input = input;
|
||||
match op_input.as_object_mut() {
|
||||
Some(obj) => {
|
||||
obj.insert(
|
||||
RESERVED_OPEN_INPUT_KEY.to_string(),
|
||||
serde_json::json!(channel_id),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
return Err(ChannelOpenError::CallFailed {
|
||||
error: crate::protocol::wire::CallError::invalid_input(
|
||||
"marked pub open op input must be an object (ADR-047 amendment 4 §4.3)",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let call_conn = {
|
||||
let guard = self.call_connection.lock().await;
|
||||
guard.as_ref().map(Arc::clone)
|
||||
};
|
||||
let manager = self.manager.clone();
|
||||
let op_id = operation_id.to_string();
|
||||
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
||||
tokio::spawn(async move {
|
||||
let response = match &call_conn {
|
||||
Some(conn) => conn.call(&op_id, op_input).await,
|
||||
None => ResponseEnvelope::error(
|
||||
"channel-client",
|
||||
crate::protocol::wire::CallError::internal("channel client closed"),
|
||||
),
|
||||
};
|
||||
// A failed open tears the allocator-side entry down: the
|
||||
// responder refused, the channel is dead, and the local
|
||||
// half must not linger in the connection's channel map.
|
||||
if response.result.is_err() {
|
||||
let _ = manager.teardown_channel(channel_id);
|
||||
}
|
||||
let reply = match response.result {
|
||||
Ok(out) => Ok(out),
|
||||
Err(error) => Err(ChannelOpenError::CallFailed { error }),
|
||||
};
|
||||
let _ = reply_tx.send(reply);
|
||||
});
|
||||
|
||||
Ok((channel_id, send, recv, PubOpenReply { rx: reply_rx }))
|
||||
}
|
||||
|
||||
/// Take the `CallConnection` — used by the consumer to register
|
||||
/// imported ops (`from_call`) on the connection's overlay. After
|
||||
/// this, `call_open_op` returns an error (the connection is owned
|
||||
|
||||
Reference in new issue
Block a user