use std::future::Future;
use bytes::BytesMut;
use serde::Serialize;
use tracing::warn;
use crate::codec::Codec;
#[cfg(feature = "testing")]
use crate::testing::coordinator::{Origin, PipelinePublish, publishing_to};
use crate::{IncomingMessage, OutgoingMessage, PayloadForm, Publisher};
use super::context::Context;
use super::dispatch::Workers;
use super::failure::{FailurePolicies, FailurePolicy};
use super::handler::{Handler, HandlerOutcome};
use super::input::{DecodeWith, InputKind};
use super::metadata::{HandlerMetadata, OutgoingMessageMetadata};
use super::publish::{
ForReply, PublishContext, PublishIdentity, PublishPipeline, PublishTransform, TypedPublisher,
};
pub(crate) trait ReplySink<Reply, DeliveryCx, Pipeline>: Send + Sync {
type Payload: PayloadForm;
type Error: std::fmt::Display;
fn deliver<'e>(
&self,
name: &'e str,
reply: &'e Reply,
pipeline: &Pipeline,
cx: &PublishContext<'_, DeliveryCx>,
encode: <Self::Payload as PayloadForm>::Encode<'e>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be encoded as a reply",
note = "an encoded reply is a `serde::Serialize` value (derive it), or a \
`Message<Headers, Payload>` pair whose halves are; reply bytes that must leave \
unencoded ride a `#[derive(Serialized)]` reply type instead"
)]
pub(crate) trait EncodeReply: Send + Sync {
#[doc(hidden)]
fn deliver_typed<'e, Leaf, ReplyCodec, Transforms, Cx, PP>(
&self,
stack: &TypedPublisher<Leaf, ReplyCodec, Transforms>,
name: &'e str,
pipeline: &PP,
cx: &PublishContext<'_, Cx>,
encode: <Leaf::Payload as PayloadForm>::Encode<'e>,
) -> impl Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send
where
Leaf: Publisher,
ReplyCodec: Codec,
Transforms: PublishTransform<ForReply<Cx>, Leaf::Options>,
Cx: Sync,
PP: PublishPipeline;
}
impl<Reply: Serialize + Send + Sync> EncodeReply for Reply {
fn deliver_typed<'e, Leaf, ReplyCodec, Transforms, Cx, PP>(
&self,
stack: &TypedPublisher<Leaf, ReplyCodec, Transforms>,
name: &'e str,
pipeline: &PP,
cx: &PublishContext<'_, Cx>,
encode: <Leaf::Payload as PayloadForm>::Encode<'e>,
) -> impl Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send
where
Leaf: Publisher,
ReplyCodec: Codec,
Transforms: PublishTransform<ForReply<Cx>, Leaf::Options>,
Cx: Sync,
PP: PublishPipeline,
{
stack.publish(name, self, pipeline, cx, encode)
}
}
impl<Hd, Pd> EncodeReply for super::Message<Hd, Pd>
where
Hd: Serialize + Send + Sync,
Pd: Serialize + Send + Sync,
{
fn deliver_typed<'e, Leaf, ReplyCodec, Transforms, Cx, PP>(
&self,
stack: &TypedPublisher<Leaf, ReplyCodec, Transforms>,
name: &'e str,
pipeline: &PP,
cx: &PublishContext<'_, Cx>,
encode: <Leaf::Payload as PayloadForm>::Encode<'e>,
) -> impl Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send
where
Leaf: Publisher,
ReplyCodec: Codec,
Transforms: PublishTransform<ForReply<Cx>, Leaf::Options>,
Cx: Sync,
PP: PublishPipeline,
{
stack.publish_pair(name, &self.headers, &self.body, pipeline, cx, encode)
}
}
impl<Reply, DeliveryCx, Pipeline, Leaf, ReplyCodec, Transforms>
ReplySink<Reply, DeliveryCx, Pipeline> for TypedPublisher<Leaf, ReplyCodec, Transforms>
where
Reply: EncodeReply,
DeliveryCx: Sync,
Pipeline: PublishPipeline,
Leaf: Publisher,
ReplyCodec: Codec,
Transforms: PublishTransform<ForReply<DeliveryCx>, Leaf::Options>,
{
type Payload = Leaf::Payload;
type Error = Box<dyn std::error::Error + Send + Sync>;
fn deliver<'e>(
&self,
name: &'e str,
reply: &'e Reply,
pipeline: &Pipeline,
cx: &PublishContext<'_, DeliveryCx>,
encode: <Self::Payload as PayloadForm>::Encode<'e>,
) -> impl Future<Output = Result<(), Self::Error>> + Send {
reply.deliver_typed(self, name, pipeline, cx, encode)
}
}
impl<Reply, DeliveryCx, Pipeline, Bare> ReplySink<Reply, DeliveryCx, Pipeline> for Bare
where
Reply: super::Serialized + Sync,
DeliveryCx: Sync,
Pipeline: Send + Sync,
Bare: Publisher,
{
type Payload = Bare::Payload;
type Error = Box<dyn std::error::Error + Send + Sync>;
async fn deliver<'e>(
&self,
name: &'e str,
reply: &'e Reply,
_pipeline: &Pipeline,
_cx: &PublishContext<'_, DeliveryCx>,
encode: <Self::Payload as PayloadForm>::Encode<'e>,
) -> Result<(), Self::Error> {
let payload = <Bare::Payload as PayloadForm>::serialized(reply, encode)?;
let msg = OutgoingMessage::with_payload(name, payload);
#[cfg(feature = "testing")]
let recorded = PipelinePublish::capture(&msg);
let sent = self.publish(msg, None).await;
#[cfg(feature = "testing")]
if sent.is_ok() {
recorded.sent();
}
sent.map_err(Into::into)
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a mountable reply definition",
note = "a reply needs a destination before it mounts: declare one on the reply type \
(`#[derive(Outgoing)]` with `#[outgoing(name = \"..\")]`), or name the default with \
`publish(\"subject\")` on the attribute and `.to(\"subject\")` on the chain"
)]
pub trait PublishingDef: Send + Sync {
type Input: InputKind;
type Injections;
type Reply;
type Context;
type Source;
fn source(&self) -> Self::Source;
fn reply_name(&self) -> &str;
fn workers(&self) -> Workers {
Workers::sequential()
}
fn failure_policies(&self) -> FailurePolicies {
FailurePolicies::default()
}
fn description(&self) -> Option<&str> {
None
}
fn input_schema(&self) -> Option<String> {
None
}
fn headers_schema(&self) -> Option<String> {
None
}
fn outgoing(&self) -> Vec<OutgoingMessageMetadata> {
Vec::new()
}
fn message_name(&self) -> Option<&'static str> {
None
}
fn message_description(&self) -> Option<&'static str> {
None
}
}
pub(crate) trait PublishingCall<S>: PublishingDef {
fn call(
&self,
input: &<Self::Input as InputKind>::Target,
injections: &Self::Injections,
ctx: &mut Context<'_, Self::Context, S>,
) -> impl Future<Output = Result<Self::Reply, HandlerOutcome>> + Send;
}
pub(crate) fn publishing_metadata<D: PublishingDef>(name: String, def: &D) -> HandlerMetadata {
let mut meta = HandlerMetadata::raw(name)
.with_output_type(std::any::type_name::<D::Reply>())
.with_def_details(
def.description(),
def.input_schema(),
def.headers_schema(),
def.message_name(),
def.message_description(),
);
meta.input_type = <D::Input as InputKind>::input_label();
meta.deserialized = <D::Input as InputKind>::DESERIALIZED;
meta.outgoing = def.outgoing();
meta
}
pub(crate) struct PublishingHandler<
Def: PublishingDef,
DecodeCodec,
Wiring,
Pipeline = PublishIdentity,
> {
pub(crate) def: Def,
pub(crate) codec: DecodeCodec,
pub(crate) publisher: Wiring,
pub(crate) pipeline: Pipeline,
pub(crate) injections: Def::Injections,
pub(crate) decode: FailurePolicy,
#[cfg(feature = "testing")]
pub(crate) origin: Origin,
}
impl<Def: PublishingDef, DecodeCodec, Wiring, Pipeline> std::fmt::Debug
for PublishingHandler<Def, DecodeCodec, Wiring, Pipeline>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PublishingHandler").finish_non_exhaustive()
}
}
impl<Msg, Def, DecodeCodec, Wiring, Pipeline, State> Handler<Msg, Def::Context, State>
for PublishingHandler<Def, DecodeCodec, Wiring, Pipeline>
where
Msg: IncomingMessage,
Def: PublishingCall<State>,
Def::Input: DecodeWith<DecodeCodec>,
Def::Injections: Send + Sync,
Def::Reply: Send + Sync,
Def::Context: Send + Sync,
DecodeCodec: Send + Sync,
Wiring: ReplySink<Def::Reply, Def::Context, Pipeline>,
Pipeline: Send + Sync,
State: Send + Sync,
{
async fn handle(
&self,
msg: &Msg,
ctx: &mut Context<'_, Def::Context, State>,
) -> HandlerOutcome {
let owned = match <Def::Input as DecodeWith<DecodeCodec>>::decode(&self.codec, msg) {
Ok(value) => value,
Err(err) => {
warn!(
target: "ruststream::dispatch",
subscription = %ctx.name(),
message_type = <Def::Input as InputKind>::input_label(),
error = %err,
"codec decode failed",
);
#[cfg(any(feature = "testing", feature = "otel"))]
ctx.mark_decode_failed();
return match self.decode {
FailurePolicy::FailFast => {
ctx.fail_fast(&format!("decode failed: {err}"));
HandlerOutcome::drop()
}
other => other
.settlement()
.map_or_else(HandlerOutcome::drop, Into::into),
};
}
};
let view = <Def::Input as InputKind>::view(&owned, msg.payload());
let reply = match self.def.call(view, &self.injections, ctx).await {
Ok(reply) => reply,
Err(outcome) => return outcome,
};
let name = self.def.reply_name();
let mut own = BytesMut::new();
let encode = ctx.take_encode_buffer().unwrap_or(&mut own);
let slot = <<Wiring as ReplySink<Def::Reply, Def::Context, Pipeline>>::Payload as PayloadForm>::encode_slot(
encode,
);
let pubcx = PublishContext::new(ctx.name(), ctx.delivery_headers(), ctx.cx_ref());
let publish = self
.publisher
.deliver(name, &reply, &self.pipeline, &pubcx, slot);
#[cfg(feature = "testing")]
let publish = publishing_to(self.origin, publish);
if let Err(err) = publish.await {
warn!(
target: "ruststream::dispatch",
subscription = %ctx.name(),
reply = %name,
reply_type = std::any::type_name::<Def::Reply>(),
error = %err,
"reply publish failed",
);
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}
}
#[cfg(test)]
mod tests {
use std::future::ready;
use super::{PublishingCall, PublishingDef, publishing_metadata};
use crate::HeaderMap;
use crate::Name;
use crate::runtime::context::Context;
use crate::runtime::dispatch::{Delivery, Workers};
use crate::runtime::failure::FailurePolicies;
use crate::runtime::handler::HandlerOutcome;
struct ManualPub;
impl PublishingDef for ManualPub {
type Input = crate::runtime::input::Decoded<u32>;
type Injections = ();
type Reply = u32;
type Context = ();
type Source = Name;
fn source(&self) -> Name {
Name::new("in")
}
#[allow(clippy::unnecessary_literal_bound)]
fn reply_name(&self) -> &str {
"out"
}
}
impl<S: Send + Sync> PublishingCall<S> for ManualPub {
fn call(
&self,
input: &u32,
(): &(),
_ctx: &mut Context<'_, (), S>,
) -> impl Future<Output = Result<u32, HandlerOutcome>> {
ready(Ok(*input))
}
}
#[tokio::test]
async fn defaults_metadata_and_call() {
let def = ManualPub;
assert_eq!(def.workers(), Workers::sequential());
assert_eq!(def.failure_policies(), FailurePolicies::default());
assert!(def.description().is_none());
assert!(def.input_schema().is_none());
assert!(def.headers_schema().is_none());
assert!(def.outgoing().is_empty());
assert!(def.message_name().is_none());
assert!(def.message_description().is_none());
assert_eq!(def.reply_name(), "out");
let _source = def.source();
let meta = publishing_metadata("in".to_owned(), &def);
assert_eq!(meta.name, "in");
let state = ();
let delivery = Delivery::empty();
let headers = HeaderMap::new();
let mut ctx = Context::new("in", &headers, &state, (), &delivery);
assert_eq!(def.call(&5, &(), &mut ctx).await.unwrap(), 5);
}
#[cfg(all(feature = "memory", feature = "json", feature = "logging"))]
mod diagnostics {
use std::future::ready;
use super::ManualPub;
use crate::codec::JsonCodec;
use crate::memory::{MemoryBroker, MemoryError};
use crate::runtime::context::Context;
use crate::runtime::dispatch::Delivery;
use crate::runtime::failure::FailurePolicy;
use crate::runtime::handler::Handler;
use crate::runtime::publish::{PublishIdentity, TypedPublisher};
use crate::runtime::publishing::PublishingHandler;
#[cfg(feature = "testing")]
use crate::testing::coordinator::Origin;
use crate::testkit::delivery::one_delivery;
use crate::testkit::log_capture::{find, start};
use crate::{HeaderMap, Lend, OutgoingMessage, Publisher};
struct Rejecting;
impl Publisher for Rejecting {
type Payload = Lend;
type Error = MemoryError;
type Options = ();
fn publish(
&self,
_msg: OutgoingMessage<'_, &[u8]>,
_options: Option<&Self::Options>,
) -> impl Future<Output = Result<(), Self::Error>> {
ready(Err(MemoryError::ShutDown))
}
}
#[tokio::test]
async fn a_decode_failure_names_the_subscription_and_the_expected_type() {
let broker = MemoryBroker::new();
let msg = one_delivery(&broker, "in", b"not json").await;
let handler = PublishingHandler {
def: ManualPub,
codec: JsonCodec,
publisher: TypedPublisher::with_codec(broker.publisher(), JsonCodec),
pipeline: PublishIdentity,
injections: (),
decode: FailurePolicy::FailFast,
#[cfg(feature = "testing")]
origin: Origin::default(),
};
let state = ();
let delivery = Delivery::empty();
let headers = HeaderMap::new();
let mut ctx = Context::new("in", &headers, &state, (), &delivery);
let (events, guard) = start();
let settle = handler.handle(&msg, &mut ctx).await;
drop(guard);
let failure = find(&events, "codec decode failed");
assert_eq!(failure.get("subscription").map(String::as_str), Some("in"));
assert_eq!(failure.get("message_type").map(String::as_str), Some("u32"));
assert!(
failure.get("error").is_some_and(|e| e.contains("decode")),
"the diagnostic must carry the codec error: {failure:?}",
);
assert!(settle.is_drop());
}
#[tokio::test]
async fn a_failed_reply_publish_names_the_reply_channel_and_retries() {
let broker = MemoryBroker::new();
let msg = one_delivery(&broker, "in", b"5").await;
let handler = PublishingHandler {
def: ManualPub,
codec: JsonCodec,
publisher: TypedPublisher::with_codec(Rejecting, JsonCodec),
pipeline: PublishIdentity,
injections: (),
decode: FailurePolicy::Drop,
#[cfg(feature = "testing")]
origin: Origin::default(),
};
let state = ();
let delivery = Delivery::empty();
let headers = HeaderMap::new();
let mut ctx = Context::new("in", &headers, &state, (), &delivery);
let (events, guard) = start();
let settle = handler.handle(&msg, &mut ctx).await;
drop(guard);
let failure = find(&events, "reply publish failed");
assert_eq!(failure.get("subscription").map(String::as_str), Some("in"));
assert_eq!(failure.get("reply").map(String::as_str), Some("out"));
assert_eq!(failure.get("reply_type").map(String::as_str), Some("u32"));
assert!(
failure
.get("error")
.is_some_and(|e| e.contains("shut down")),
"the diagnostic must carry the broker error: {failure:?}",
);
assert!(settle.is_retry());
}
}
#[cfg(all(feature = "memory", feature = "json"))]
#[test]
fn a_mounted_publishing_handler_hides_its_wiring() {
use crate::codec::JsonCodec;
use crate::memory::MemoryPublish;
use crate::runtime::failure::FailurePolicy;
use crate::runtime::publish::{PublishIdentity, TypedPublisher};
use crate::runtime::publishing::PublishingHandler;
#[cfg(feature = "testing")]
use crate::testing::coordinator::Origin;
let handler = PublishingHandler {
def: ManualPub,
codec: JsonCodec,
publisher: TypedPublisher::with_codec(MemoryPublish, JsonCodec),
pipeline: PublishIdentity,
injections: (),
decode: FailurePolicy::Drop,
#[cfg(feature = "testing")]
origin: Origin::default(),
};
assert_eq!(format!("{handler:?}"), "PublishingHandler { .. }");
}
}