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
+214
-19
@@ -35,11 +35,13 @@ use crate::registry::spec::{OperationSpec, OperationType};
|
||||
|
||||
use super::manager::ChannelManager;
|
||||
use super::operations::{
|
||||
ChannelCore, Establishment, EstablishmentError, OpenEstablisher, OpenHandler,
|
||||
CHANNEL_OPEN_FAILED, RESERVED_REPLY_KEY,
|
||||
ChannelCore, ChannelPlan, Establishment, EstablishmentError, OpenEstablisher, OpenHandler,
|
||||
PubOpenHandler, CHANNEL_OPEN_FAILED, RESERVED_REPLY_KEY,
|
||||
};
|
||||
use super::pump::pump_bidi;
|
||||
|
||||
use super::reassembly::{MpscRecvStream, MpscSendStream};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// The producer-leg surface the relay holds (ADR-051 §2): the
|
||||
/// `CallConnection` for the translate hop's open call plus the
|
||||
@@ -81,8 +83,17 @@ struct RelayPlan {
|
||||
producer_manager: ChannelManager,
|
||||
spoke_send: Option<MpscSendStream>,
|
||||
spoke_recv: Option<MpscRecvStream>,
|
||||
/// The put path (ADR-047 amendment 4 §4.7): the producer leg's
|
||||
/// open-op response, awaited by the relay pub handler after the
|
||||
/// consumer EOF (put-acks-in-result — the spoke's response cannot
|
||||
/// exist before its byte stream completes, so the establisher
|
||||
/// never awaits it).
|
||||
pub_reply: Option<tokio::sync::oneshot::Receiver<CallConnectionResponse>>,
|
||||
}
|
||||
|
||||
/// The producer leg's open-op response, carried on the put path's plan.
|
||||
type CallConnectionResponse = crate::protocol::wire::ResponseEnvelope;
|
||||
|
||||
impl Drop for RelayPlan {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.producer_manager.teardown_channel(self.spoke_id);
|
||||
@@ -120,9 +131,10 @@ impl ChannelRelay {
|
||||
/// produced open op.
|
||||
/// - `spec` must carry the `channel_open` marker (reconstructed
|
||||
/// WITH the marker by discovery, ADR-047 amendment 3). A
|
||||
/// `Pub`-typed marked spec is a loud assembly error
|
||||
/// ([`RelayError::PubTypedOpen`] — the C-08 blocker, ADR-051
|
||||
/// §6); an unmarked spec is likewise an error — branch on the
|
||||
/// `Pub`-typed marked spec composes through the put-forwarding
|
||||
/// translation (see [`Self::register_relay_pub_openable`]); an
|
||||
/// unmarked spec is a
|
||||
/// [`RelayError::UnmarkedSpec`] error — branch on the
|
||||
/// marker before calling.
|
||||
/// - The translate hop calls the producer leg with the spec's own
|
||||
/// op name; a hub that re-names the consumer-visible op composes
|
||||
@@ -145,14 +157,12 @@ impl ChannelRelay {
|
||||
});
|
||||
}
|
||||
if spec.op_type == OperationType::Pub {
|
||||
return Err(RelayError::PubTypedOpen {
|
||||
message: format!(
|
||||
"spec `{}` is a Pub-typed channel-open op — the Pub open path is \
|
||||
not implemented (C-08 blocker, `channel:pub_open_not_implemented` \
|
||||
class; ADR-051 §6)",
|
||||
spec.name
|
||||
),
|
||||
});
|
||||
return self.register_relay_pub_openable(
|
||||
consumer_core,
|
||||
consumer_registry,
|
||||
spec,
|
||||
consumer_auth,
|
||||
);
|
||||
}
|
||||
|
||||
let producer_call = Arc::clone(&self.producer_leg.call);
|
||||
@@ -227,6 +237,76 @@ impl ChannelRelay {
|
||||
)
|
||||
.map_err(|e| RelayError::Registration { message: e })
|
||||
}
|
||||
|
||||
/// Register a from_call-imported **marked `Pub`** spec as a relay
|
||||
/// openable — the put-forwarding translation (ADR-047 amendment 4
|
||||
/// §4.7). The consumer leg runs the full marked-pub wrapper (adopt
|
||||
/// → establish → pub handler awaited inline); this side's compose:
|
||||
///
|
||||
/// - the establisher allocates the hub's connect-side id on the
|
||||
/// producer leg (`open_channel` — an odd id, disjoint from the
|
||||
/// spoke's even allocations), sends the open op **in the
|
||||
/// background** (the spoke's put response cannot exist before
|
||||
/// its byte stream completes; an establisher that awaited it
|
||||
/// deadlocks the leg), and plans the spoke-leg streams + the
|
||||
/// response's oneshot; the plan's Drop guard reclaims the
|
||||
/// producer-leg entry on every handler path, as ADR-051 §6 pins.
|
||||
/// - the handler pumps the consumer-leg bytes → the spoke leg
|
||||
/// (one direction, pub), EOFs the spoke side on consumer EOF,
|
||||
/// then awaits the spoke's single response, strips the
|
||||
/// producer's reserved reply key (the consumer reply carries the
|
||||
/// hub-allocated consumer-leg id; a spoke `channel_id` merged
|
||||
/// through would shadow it — the same reserved-key class the
|
||||
/// wrapper refuses), and returns the remainder — the consumer-leg
|
||||
/// wrapper merges it with the pinned order (ack fields are none
|
||||
/// here; the put acks in the spoke's result, §4.5).
|
||||
///
|
||||
/// A spoke open-time establishment failure surfaces verbatim in
|
||||
/// the final response (put-acks-in-result; the spoke's mapped
|
||||
/// `channel:open_failed` rides the response, not the establisher).
|
||||
fn register_relay_pub_openable(
|
||||
&self,
|
||||
consumer_core: &ChannelCore,
|
||||
consumer_registry: &OperationRegistry,
|
||||
spec: OperationSpec,
|
||||
consumer_auth: AuthContext,
|
||||
) -> Result<(), RelayError> {
|
||||
let producer_call = Arc::clone(&self.producer_leg.call);
|
||||
let producer_manager = self.producer_leg.manager.clone();
|
||||
let op_name = spec.name.clone();
|
||||
|
||||
let establisher: OpenEstablisher = Arc::new(move |input, per_call_auth| {
|
||||
let producer_call = Arc::clone(&producer_call);
|
||||
let producer_manager = producer_manager.clone();
|
||||
let op_name = op_name.clone();
|
||||
Box::pin(async move {
|
||||
open_pub_on_producer_leg(
|
||||
&producer_call,
|
||||
&producer_manager,
|
||||
&op_name,
|
||||
input,
|
||||
&per_call_auth,
|
||||
)
|
||||
.await
|
||||
})
|
||||
});
|
||||
|
||||
let pub_open_handler: PubOpenHandler =
|
||||
Arc::new(move |_input, plan, channel_conn, _auth| {
|
||||
Box::pin(async move { pub_relay_forward(channel_conn, plan).await })
|
||||
});
|
||||
|
||||
consumer_core
|
||||
.register_pub_openable(
|
||||
spec,
|
||||
Some(establisher),
|
||||
pub_open_handler,
|
||||
consumer_registry,
|
||||
consumer_auth,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| RelayError::Registration { message: e })
|
||||
}
|
||||
}
|
||||
|
||||
/// The translate hop (ADR-051 §1 layer 1, §2): call the producer leg's
|
||||
@@ -288,6 +368,7 @@ async fn open_on_producer_leg(
|
||||
producer_manager: producer_manager.clone(),
|
||||
spoke_send: Some(spoke_send),
|
||||
spoke_recv: Some(spoke_recv),
|
||||
pub_reply: None,
|
||||
}) as super::operations::ChannelPlan);
|
||||
Ok(if reply_fields.is_empty() {
|
||||
establishment
|
||||
@@ -334,6 +415,123 @@ fn map_spoke_error(spoke_error: &CallError) -> EstablishmentError {
|
||||
}
|
||||
}
|
||||
|
||||
/// The put path's translate hop (ADR-047 amendment 4 §4.7): allocate
|
||||
/// the hub's connect-side channel id on the producer leg, send the
|
||||
/// spoke's open op **in the background** with the reserved input key
|
||||
/// set, and plan the spoke-leg streams + the response oneshot. The
|
||||
/// establisher returns promptly; the spoke's response arrives when the
|
||||
/// spoke's put completes (or fails) — put-acks-in-result.
|
||||
async fn open_pub_on_producer_leg(
|
||||
producer_call: &Arc<CallConnection>,
|
||||
producer_manager: &ChannelManager,
|
||||
op_name: &str,
|
||||
input: Value,
|
||||
per_call_auth: &AuthContext,
|
||||
) -> Result<Establishment, EstablishmentError> {
|
||||
let (spoke_id, spoke_send, spoke_recv) = producer_manager
|
||||
.open_channel(
|
||||
"alk/relay",
|
||||
// The opener ledger is inert on the hub's connect side
|
||||
// (the connect side does not enforce the per-identity cap,
|
||||
// ADR-041) — the record is observability only.
|
||||
"hub-relay",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| EstablishmentError::ResourceShortage {
|
||||
message: format!("producer leg: channel allocation failed: {e}"),
|
||||
})?;
|
||||
|
||||
let mut hop_input = input.clone();
|
||||
if let Some(obj) = hop_input.as_object_mut() {
|
||||
obj.insert(
|
||||
crate::registry::spec::RESERVED_OPEN_INPUT_KEY.to_string(),
|
||||
serde_json::json!(spoke_id),
|
||||
);
|
||||
}
|
||||
let payload = build_relay_payload(op_name, hop_input, per_call_auth);
|
||||
|
||||
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
||||
let producer_call_task = Arc::clone(producer_call);
|
||||
tokio::spawn(async move {
|
||||
let _ = reply_tx.send(producer_call_task.call_with_payload(payload).await);
|
||||
});
|
||||
|
||||
Ok(Establishment::new(Arc::new(RelayPlan {
|
||||
spoke_id,
|
||||
producer_manager: producer_manager.clone(),
|
||||
spoke_send: Some(spoke_send),
|
||||
spoke_recv: Some(spoke_recv),
|
||||
pub_reply: Some(reply_rx),
|
||||
}) as ChannelPlan))
|
||||
}
|
||||
|
||||
/// The relay pub handler (ADR-047 amendment 4 §4.7): pump the
|
||||
/// consumer-leg bytes → the spoke leg (one direction, pub — the
|
||||
/// shutdown-on-completion contract, ADR-050's), then await the spoke's
|
||||
/// single response and strip its reserved reply key. The plan's Drop
|
||||
/// guard reclaims the producer-leg entry on every path.
|
||||
async fn pub_relay_forward(
|
||||
channel_conn: crate::core::types::Connection,
|
||||
plan: Option<ChannelPlan>,
|
||||
) -> Result<Value, CallError> {
|
||||
let Some(plan) = plan else {
|
||||
return Err(CallError::internal(
|
||||
"relay: pub establisher produced no plan",
|
||||
));
|
||||
};
|
||||
let Ok(relay_plan_arc) = plan.downcast::<RelayPlan>() else {
|
||||
return Err(CallError::internal(
|
||||
"relay: establisher plan of unexpected type",
|
||||
));
|
||||
};
|
||||
let Ok(mut relay_plan) = Arc::try_unwrap(relay_plan_arc) else {
|
||||
return Err(CallError::internal(
|
||||
"relay: establisher plan held elsewhere; skipping pump",
|
||||
));
|
||||
};
|
||||
let Some(mut spoke_send) = relay_plan.spoke_send.take() else {
|
||||
return Err(CallError::internal(
|
||||
"relay: pub plan missing the spoke send half",
|
||||
));
|
||||
};
|
||||
relay_plan.spoke_recv.take();
|
||||
|
||||
let Ok(bidi) = channel_conn.accept_bi().await else {
|
||||
return Err(CallError::internal(
|
||||
"relay: consumer leg data plane never opened",
|
||||
));
|
||||
};
|
||||
let (mut read_half, _write_half) = tokio::io::split(bidi);
|
||||
// The one-direction pump: consumer bytes → spoke; shutdown on
|
||||
// consumer EOF (the pump contract, ADR-050).
|
||||
let _copied = tokio::io::copy(&mut read_half, &mut spoke_send)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let _ = spoke_send.shutdown().await;
|
||||
drop(spoke_send);
|
||||
|
||||
match relay_plan.pub_reply.take() {
|
||||
Some(rx) => match rx.await {
|
||||
Ok(response) => match response.result {
|
||||
Ok(mut out) => {
|
||||
if let Some(obj) = out.as_object_mut() {
|
||||
obj.remove(RESERVED_REPLY_KEY);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
Err(_) => Err(CallError::internal(
|
||||
"relay: producer-leg open resolved without a response",
|
||||
)),
|
||||
},
|
||||
None => Err(CallError::internal(
|
||||
"relay: pub plan missing the response receiver",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `call.requested` payload for the translate hop: the hub
|
||||
/// as caller, the consumer as `forwarded_for` (ADR-026 §3, ADR-032 §3
|
||||
/// — the same shape `build_forwarded_payload` produces for the plain
|
||||
@@ -357,18 +555,15 @@ fn build_relay_payload(op_name: &str, input: Value, per_call_auth: &AuthContext)
|
||||
|
||||
/// The assembly-time errors `ChannelRelay::register_relay_openable`
|
||||
/// produces (ADR-051 §6 — the rejection postures are loud, never
|
||||
/// silent stubs).
|
||||
/// silent stubs; the `PubTypedOpen` variant was removed by ADR-047
|
||||
/// amendment 4 §4.7 — pub-typed marked specs translate now, never
|
||||
/// reject-and-strand).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum RelayError {
|
||||
/// The spec carries no `channel_open` marker — the relay only
|
||||
/// registers marked specs.
|
||||
#[error("unmarked spec: {message}")]
|
||||
UnmarkedSpec { message: String },
|
||||
/// A `Pub`-typed marked spec — the Pub open path is not
|
||||
/// implemented (C-08 blocker; `channel:pub_open_not_implemented`
|
||||
/// class).
|
||||
#[error("pub-typed open op: {message}")]
|
||||
PubTypedOpen { message: String },
|
||||
/// The consumer-leg registration itself failed (schema compile,
|
||||
/// handler-kind mismatch).
|
||||
#[error("registration failed: {message}")]
|
||||
|
||||
Reference in new issue
Block a user