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
+110
-2
@@ -86,6 +86,11 @@ pub struct SinkDispatch {
|
||||
pub(crate) handler: Pin<Box<dyn Future<Output = ResponseEnvelope> + Send>>,
|
||||
pub(crate) chunk_tx: mpsc::Sender<Result<Value, CallError>>,
|
||||
pub(crate) publish_validator: Option<jsonschema::Validator>,
|
||||
/// Marked (`channel_open`) `Pub` op (ADR-047 amendment 4 §4.1):
|
||||
/// publish chunks never feed the handler — the first
|
||||
/// `call.published` chunk is rejected loudly in the initiator's
|
||||
/// direction and the rest are ignored.
|
||||
pub(crate) reject_publish: bool,
|
||||
}
|
||||
|
||||
/// An in-flight Pub (Sink) on the single-stream dispatch loop
|
||||
@@ -100,6 +105,11 @@ struct InFlightSink {
|
||||
chunk_tx: mpsc::Sender<Result<Value, CallError>>,
|
||||
publish_validator: Option<jsonschema::Validator>,
|
||||
handler_handle: JoinHandle<()>,
|
||||
/// Marked (`channel_open`) `Pub` op (ADR-047 amendment 4 §4.1):
|
||||
/// the first `call.published` chunk is rejected loudly in the
|
||||
/// initiator's direction and never fed to the handler.
|
||||
reject_publish: bool,
|
||||
reject_written: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DispatchResult {
|
||||
@@ -356,7 +366,7 @@ impl Dispatcher {
|
||||
.get("forwarded_for")
|
||||
.and_then(|v| serde_json::from_value::<Identity>(v.clone()).ok());
|
||||
|
||||
let input = payload.get("input").cloned().unwrap_or(Value::Null);
|
||||
let mut input = payload.get("input").cloned().unwrap_or(Value::Null);
|
||||
|
||||
let op_type = self
|
||||
.registry
|
||||
@@ -391,9 +401,13 @@ impl Dispatcher {
|
||||
context.deadline = None;
|
||||
match self
|
||||
.registry
|
||||
.resolve_sink_handler(&operation_name, &input, &context)
|
||||
.resolve_sink_handler(&operation_name, &mut input, &mut context)
|
||||
{
|
||||
Ok(sink_handler) => {
|
||||
let reject_publish = self
|
||||
.registry
|
||||
.registration(&operation_name)
|
||||
.is_some_and(|r| r.spec.channel_open.is_some());
|
||||
let publish_validator = self.registry.publish_validator(&operation_name);
|
||||
let (chunk_tx, chunk_rx) =
|
||||
mpsc::channel::<Result<Value, CallError>>(PUBLISH_CHANNEL_BUFFER);
|
||||
@@ -403,6 +417,7 @@ impl Dispatcher {
|
||||
handler: Box::pin(handler),
|
||||
chunk_tx,
|
||||
publish_validator,
|
||||
reject_publish,
|
||||
})
|
||||
}
|
||||
Err(envelope) => StartedDispatch::Once(Box::pin(std::future::ready(envelope))),
|
||||
@@ -657,6 +672,7 @@ impl Dispatcher {
|
||||
handler,
|
||||
mut chunk_tx,
|
||||
publish_validator,
|
||||
reject_publish,
|
||||
} = sink;
|
||||
|
||||
let (abort_tx, mut abort_rx) = oneshot::channel::<()>();
|
||||
@@ -668,6 +684,7 @@ impl Dispatcher {
|
||||
let mut handler = handler.fuse();
|
||||
let mut response: Option<ResponseEnvelope> = None;
|
||||
let mut aborted = false;
|
||||
let mut reject_written = false;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -698,6 +715,31 @@ impl Dispatcher {
|
||||
}
|
||||
match envelope.r#type.as_str() {
|
||||
EVENT_PUBLISHED => {
|
||||
// Marked ops: the data plane is the adopted
|
||||
// channel's `BiStream` — publish chunks are
|
||||
// rejected loudly, never consumed (ADR-047
|
||||
// amendment 4 §4.1). The handler continues on
|
||||
// the channel plane; the initiator's pending
|
||||
// call resolves with this rejection.
|
||||
if reject_publish {
|
||||
if !reject_written {
|
||||
reject_written = true;
|
||||
let rejection: EventEnvelope = ResponseEnvelope::error(
|
||||
request_id.clone(),
|
||||
CallError::new(
|
||||
crate::registry::spec::MARKED_PUB_CHUNKS_REJECTED,
|
||||
"the data plane of a marked pub op is the adopted \
|
||||
channel's BiStream — call.published chunks are \
|
||||
rejected (ADR-047 amendment 4 §4.1)",
|
||||
false,
|
||||
),
|
||||
).into();
|
||||
if let Err(err) = writer.write_frame(&rejection).await {
|
||||
warn!(error = %err, request_id = %request_id, "failed to write marked-op publish rejection");
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let chunk = envelope
|
||||
.payload
|
||||
.get("input")
|
||||
@@ -923,6 +965,7 @@ impl Dispatcher {
|
||||
handler,
|
||||
chunk_tx,
|
||||
publish_validator,
|
||||
reject_publish,
|
||||
} = sink;
|
||||
let handle = Self::spawn_sink_response_writer(
|
||||
&writer,
|
||||
@@ -935,6 +978,8 @@ impl Dispatcher {
|
||||
chunk_tx,
|
||||
publish_validator,
|
||||
handler_handle: handle,
|
||||
reject_publish,
|
||||
reject_written: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -977,6 +1022,36 @@ impl Dispatcher {
|
||||
.unwrap_or(Value::Null);
|
||||
let entry = in_flight_sinks.lock().remove(&request_id);
|
||||
if let Some(mut entry) = entry {
|
||||
if entry.reject_publish {
|
||||
// Marked ops: the data plane is the adopted
|
||||
// channel's `BiStream` — publish chunks are
|
||||
// rejected loudly, never consumed (ADR-047
|
||||
// amendment 4 §4.1). The loud error is written
|
||||
// once per request; the entry stays registered
|
||||
// so the handler's own final response still gets
|
||||
// written. The handler continues on the channel
|
||||
// plane; the initiator's pending call resolves
|
||||
// with this rejection immediately.
|
||||
if !entry.reject_written {
|
||||
entry.reject_written = true;
|
||||
let rejection: EventEnvelope = ResponseEnvelope::error(
|
||||
request_id.clone(),
|
||||
CallError::new(
|
||||
crate::registry::spec::MARKED_PUB_CHUNKS_REJECTED,
|
||||
"the data plane of a marked pub op is the \
|
||||
adopted channel's BiStream — call.published \
|
||||
chunks are rejected (ADR-047 amendment 4 §4.1)",
|
||||
false,
|
||||
),
|
||||
)
|
||||
.into();
|
||||
if let Err(err) = writer.write_frame(&rejection).await {
|
||||
warn!(error = %err, request_id = %request_id, "failed to write marked-op publish rejection");
|
||||
}
|
||||
}
|
||||
in_flight_sinks.lock().insert(request_id, entry);
|
||||
continue;
|
||||
}
|
||||
let validated = match &entry.publish_validator {
|
||||
Some(validator) => {
|
||||
if validator.is_valid(&chunk) {
|
||||
@@ -1146,6 +1221,7 @@ impl Dispatcher {
|
||||
handler,
|
||||
chunk_tx,
|
||||
publish_validator,
|
||||
reject_publish,
|
||||
} = sink;
|
||||
let handle = Self::spawn_sink_response_writer(
|
||||
&writer,
|
||||
@@ -1158,6 +1234,8 @@ impl Dispatcher {
|
||||
chunk_tx,
|
||||
publish_validator,
|
||||
handler_handle: handle,
|
||||
reject_publish,
|
||||
reject_written: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1200,6 +1278,36 @@ impl Dispatcher {
|
||||
.unwrap_or(Value::Null);
|
||||
let entry = in_flight_sinks.lock().remove(&request_id);
|
||||
if let Some(mut entry) = entry {
|
||||
if entry.reject_publish {
|
||||
// Marked ops: the data plane is the adopted
|
||||
// channel's `BiStream` — publish chunks are
|
||||
// rejected loudly, never consumed (ADR-047
|
||||
// amendment 4 §4.1). The loud error is written
|
||||
// once per request; the entry stays registered
|
||||
// so the handler's own final response still gets
|
||||
// written. The handler continues on the channel
|
||||
// plane; the initiator's pending call resolves
|
||||
// with this rejection immediately.
|
||||
if !entry.reject_written {
|
||||
entry.reject_written = true;
|
||||
let rejection: EventEnvelope = ResponseEnvelope::error(
|
||||
request_id.clone(),
|
||||
CallError::new(
|
||||
crate::registry::spec::MARKED_PUB_CHUNKS_REJECTED,
|
||||
"the data plane of a marked pub op is the \
|
||||
adopted channel's BiStream — call.published \
|
||||
chunks are rejected (ADR-047 amendment 4 §4.1)",
|
||||
false,
|
||||
),
|
||||
)
|
||||
.into();
|
||||
if let Err(err) = writer.write_frame(&rejection).await {
|
||||
warn!(error = %err, request_id = %request_id, "failed to write marked-op publish rejection");
|
||||
}
|
||||
}
|
||||
in_flight_sinks.lock().insert(request_id, entry);
|
||||
continue;
|
||||
}
|
||||
let validated = match &entry.publish_validator {
|
||||
Some(validator) => {
|
||||
if validator.is_valid(&chunk) {
|
||||
|
||||
Reference in new issue
Block a user