#[cfg(feature = "otel")]
use std::sync::LazyLock;
use std::{fmt, future::Future, marker::PhantomData};
#[cfg(feature = "otel")]
use opentelemetry::metrics::Histogram;
#[cfg(feature = "otel")]
use opentelemetry::{KeyValue, global};
#[cfg(all(test, feature = "memory", feature = "json"))]
use serde::de::DeserializeOwned;
use tracing::{error, warn};
use crate::{BuildBatchContext, IncomingMessage};
use super::context::Context;
use super::dispatch::{Delivery, Slot, Workers, settle_outcome};
use super::failure::{FailurePolicies, FailurePolicy};
use super::handle::Deserialized;
use super::handler::{HandlerOutcome, HandlerResult};
use super::input::{DecodeWith, InputKind, REPORTED};
use super::metadata::HandlerMetadata;
#[derive(Debug)]
#[non_exhaustive]
pub enum BatchResult {
Uniform(HandlerOutcome),
PerElement(Vec<HandlerOutcome>),
}
pub trait IntoBatchResult {
fn into_batch_result(self) -> BatchResult;
}
impl IntoBatchResult for BatchResult {
fn into_batch_result(self) -> BatchResult {
self
}
}
impl IntoBatchResult for HandlerOutcome {
fn into_batch_result(self) -> BatchResult {
BatchResult::Uniform(self)
}
}
impl IntoBatchResult for () {
fn into_batch_result(self) -> BatchResult {
BatchResult::Uniform(HandlerOutcome::ack())
}
}
impl<E> IntoBatchResult for Result<(), E> {
fn into_batch_result(self) -> BatchResult {
BatchResult::Uniform(match self {
Ok(()) => HandlerOutcome::ack(),
Err(_) => HandlerOutcome::drop(),
})
}
}
impl<E> IntoBatchResult for Result<HandlerOutcome, E> {
fn into_batch_result(self) -> BatchResult {
BatchResult::Uniform(self.unwrap_or_else(|_| HandlerOutcome::drop()))
}
}
impl IntoBatchResult for Vec<HandlerOutcome> {
fn into_batch_result(self) -> BatchResult {
BatchResult::PerElement(self)
}
}
#[doc(hidden)]
pub fn batch_verdict<O: IntoBatchResult>(
outcome: O,
batch_len: usize,
) -> Result<(), Vec<HandlerOutcome>> {
match outcome.into_batch_result() {
BatchResult::Uniform(one) => {
if one.is_ack() && !one.has_after() {
Ok(())
} else {
Err(uniform_batch(one, batch_len))
}
}
BatchResult::PerElement(outcomes) => Err(outcomes),
}
}
#[doc(hidden)]
#[must_use]
pub fn uniform_batch(outcome: HandlerOutcome, batch_len: usize) -> Vec<HandlerOutcome> {
if batch_len == 0 {
return Vec::new();
}
let status = outcome.outcome();
let mut settles: Vec<HandlerOutcome> = std::iter::repeat_with(|| HandlerOutcome::from(status))
.take(batch_len - 1)
.collect();
settles.push(outcome);
settles
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a batch handler over `{T}`",
note = "a batch handler takes (&[T], &mut Context) and returns an IntoBatchResult value; \
per-element header contracts ride a `&[Message<H, T>]` body instead"
)]
pub(crate) trait SliceHandler<T, C = (), S = ()>: Send + Sync {
fn handle_slice(
&self,
batch: &[T],
ctx: &mut Context<'_, C, S>,
) -> impl Future<Output = BatchResult> + Send;
}
impl<T, F, Fut, C, S> SliceHandler<T, C, S> for F
where
F: Fn(&[T], &mut Context<'_, C, S>) -> Fut + Send + Sync,
Fut: Future + Send,
Fut::Output: IntoBatchResult,
{
fn handle_slice(
&self,
batch: &[T],
ctx: &mut Context<'_, C, S>,
) -> impl Future<Output = BatchResult> + Send {
let fut = (self)(batch, ctx);
async move { fut.await.into_batch_result() }
}
}
pub trait BatchDef: Sized {
type Input: InputKind;
type Context;
type Handler;
type Source;
fn source(&self) -> Self::Source;
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 message_name(&self) -> Option<&'static str> {
None
}
fn message_description(&self) -> Option<&'static str> {
None
}
fn into_handler(self) -> Self::Handler;
}
pub(crate) fn batch_metadata<D: BatchDef>(name: String, def: &D) -> HandlerMetadata {
let mut meta = HandlerMetadata::raw(name).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
}
pub(crate) trait TakenBatch<C>: Send {
fn context(&self) -> Option<C>;
#[cfg(feature = "testing")]
fn unsettled(&mut self) -> Vec<crate::testing::coordinator::Delivered>;
}
impl<M, C> TakenBatch<C> for Vec<M>
where
M: IncomingMessage,
C: BuildBatchContext<M>,
{
fn context(&self) -> Option<C> {
self.first().map(C::build)
}
#[cfg(feature = "testing")]
fn unsettled(&mut self) -> Vec<crate::testing::coordinator::Delivered> {
self.iter()
.map(|msg| crate::testing::coordinator::Delivered {
raw: bytes::Bytes::copy_from_slice(msg.payload()),
value: None,
settle: None,
})
.collect()
}
}
pub(crate) trait BatchHandler<Batch, C = (), S = ()>: Send + Sync {
type Scratch: Default + Send;
type Taken: TakenBatch<C>;
fn take(batch: Batch) -> Self::Taken;
fn handle_batch(
&self,
batch: Self::Taken,
scratch: &mut Self::Scratch,
ctx: &mut Context<'_, C, S>,
) -> impl Future<Output = ()> + Send;
}
#[cfg(all(test, feature = "memory", feature = "json"))]
pub(crate) async fn handled<H, M, C, S>(
handler: &H,
batch: Vec<M>,
scratch: &mut H::Scratch,
ctx: &mut Context<'_, C, S>,
) where
H: BatchHandler<Vec<M>, C, S, Taken = Vec<M>>,
M: Send,
C: Send + Sync,
S: Send + Sync,
{
handler.handle_batch(batch, scratch, ctx).await;
}
#[cfg(all(test, feature = "memory", feature = "json"))]
pub(crate) fn typed_batch<M, T, C, H>(
codec: C,
inner: H,
) -> TypedBatch<M, super::input::Decoded<T>, C, H>
where
M: IncomingMessage,
T: DeserializeOwned + Send + Sync + 'static,
C: crate::codec::Codec,
{
TypedBatch {
codec,
inner,
decode: FailurePolicy::Drop,
_phantom: PhantomData,
}
}
pub struct TypedBatch<M, Input, DecodeCodec, Inner> {
codec: DecodeCodec,
inner: Inner,
decode: FailurePolicy,
_phantom: PhantomData<fn(M, Input)>,
}
impl<M, Input, DecodeCodec, Inner> TypedBatch<M, Input, DecodeCodec, Inner> {
#[must_use]
pub(crate) fn over(codec: DecodeCodec, inner: Inner) -> Self
where
Input: DecodeWith<DecodeCodec>,
{
Self {
codec,
inner,
decode: FailurePolicy::Drop,
_phantom: PhantomData,
}
}
#[must_use]
pub(crate) fn with_decode(mut self, decode: FailurePolicy) -> Self {
self.decode = decode;
self
}
}
impl<M, Input, DecodeCodec, Inner> fmt::Debug for TypedBatch<M, Input, DecodeCodec, Inner> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TypedBatch")
.field("decode", &self.decode)
.finish_non_exhaustive()
}
}
impl<Batch, M, Input, DecodeCodec, Inner, C, S> BatchHandler<Batch, C, S>
for TypedBatch<M, Input, DecodeCodec, Inner>
where
Batch: IntoIterator<Item = M>,
M: IncomingMessage,
Input: DecodeWith<DecodeCodec>,
DecodeCodec: Send + Sync,
Inner: SliceHandler<Input::Owned, C, S>,
C: BuildBatchContext<M> + Send + Sync + 'static,
S: Send + Sync,
{
type Scratch = Vec<Input::Owned>;
type Taken = Vec<M>;
fn take(batch: Batch) -> Vec<M> {
batch.into_iter().collect()
}
async fn handle_batch(
&self,
batch: Vec<M>,
values: &mut Self::Scratch,
ctx: &mut Context<'_, C, S>,
) {
let subscription = ctx.subscription();
let accepted = decode_batch::<M, Input, DecodeCodec, C, S>(
batch,
values,
&self.codec,
self.decode,
ctx,
)
.await;
if accepted.is_empty() {
return;
}
let delivery = ctx.delivery();
let result = self.inner.handle_slice(values, ctx).await;
values.clear();
settle_batch(accepted, result, subscription, delivery).await;
}
}
pub struct DeserializedBatch<M, F, Inner> {
inner: Inner,
decode: FailurePolicy,
_phantom: PhantomData<fn(M, F)>,
}
impl<M, F, Inner> DeserializedBatch<M, F, Inner> {
#[must_use]
pub(crate) fn over(inner: Inner) -> Self {
Self {
inner,
decode: FailurePolicy::Drop,
_phantom: PhantomData,
}
}
#[must_use]
pub(crate) fn with_decode(mut self, decode: FailurePolicy) -> Self {
self.decode = decode;
self
}
}
impl<M, F, Inner> fmt::Debug for DeserializedBatch<M, F, Inner> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DeserializedBatch")
.field("decode", &self.decode)
.finish_non_exhaustive()
}
}
impl<Batch, M, F, Inner, C, S> BatchHandler<Batch, C, S> for DeserializedBatch<M, F, Inner>
where
Batch: IntoIterator<Item = M>,
M: IncomingMessage,
F: Deserialized + Send + Sync + 'static,
Inner: for<'p> SliceHandler<F::Output<'p>, C, S>,
C: BuildBatchContext<M> + Send + Sync + 'static,
S: Send + Sync,
{
type Scratch = ();
type Taken = Vec<M>;
fn take(batch: Batch) -> Vec<M> {
batch.into_iter().collect()
}
async fn handle_batch(&self, batch: Vec<M>, _scratch: &mut (), ctx: &mut Context<'_, C, S>) {
let subscription = ctx.subscription();
if batch.is_empty() {
return;
}
let delivery = ctx.delivery();
let mut values = Vec::with_capacity(batch.len());
let mut rejected: Vec<(usize, HandlerResult)> = Vec::new();
for (index, msg) in batch.iter().enumerate() {
if let Some(outcome) = reported(msg, std::any::type_name::<F>(), self.decode, ctx) {
rejected.push((index, outcome));
continue;
}
match F::from_payload(msg.payload()) {
Ok(value) => values.push(value),
Err(err) => {
warn!(
target: "ruststream::dispatch",
subscription = %subscription,
message_type = std::any::type_name::<F>(),
error = %err,
"payload construction failed",
);
let outcome =
rejection(&err, "batch payload construction failed", self.decode, ctx);
rejected.push((index, outcome));
}
}
}
#[cfg(feature = "otel")]
if !values.is_empty() {
record_batch_size(subscription, values.len());
}
let result = if values.is_empty() {
BatchResult::PerElement(Vec::new())
} else {
self.inner.handle_slice(&values, ctx).await
};
drop(values);
settle_split_batch(batch, rejected, result, subscription, delivery).await;
}
}
async fn settle_split_batch<M, C>(
batch: Vec<M>,
rejected: Vec<(usize, HandlerResult)>,
result: BatchResult,
subscription: &str,
delivery: &Delivery<C>,
) where
M: IncomingMessage,
C: BuildBatchContext<M> + Send + Sync + 'static,
{
if rejected.is_empty() {
return settle_batch(batch, result, subscription, delivery).await;
}
let accepted_len = batch.len() - rejected.len();
let per_element = match result {
BatchResult::Uniform(outcome) => uniform_batch(outcome, accepted_len),
BatchResult::PerElement(results) => {
if results.len() != accepted_len {
error!(
target: "ruststream::dispatch",
subscription = %subscription,
expected = accepted_len,
returned = results.len(),
"per-element outcome count does not match the batch; \
retrying the unmatched remainder",
);
}
results
}
};
#[cfg(feature = "testing")]
let mut log = (accepted_len > 0).then(|| {
BatchLog::of(
batch
.iter()
.enumerate()
.filter(|(index, _)| rejected.binary_search_by_key(index, |&(at, _)| at).is_err())
.map(|(_, msg)| msg),
LentValues::default(),
)
});
let mut accepted_results = per_element.into_iter();
let mut rejected = rejected.into_iter().peekable();
for (index, msg) in batch.into_iter().enumerate() {
if rejected.peek().is_some_and(|(at, _)| *at == index) {
let (_, outcome) = rejected.next().expect("peeked");
settle_outcome(
&mut Slot::new(msg),
outcome,
subscription,
delivery,
C::build as fn(&M) -> C,
)
.await;
continue;
}
let mut result = accepted_results
.next()
.unwrap_or_else(HandlerOutcome::retry);
let after = result.take_after();
#[cfg(feature = "testing")]
if let Some(log) = log.as_mut() {
log.settled(result.outcome());
}
settle_outcome(
&mut Slot::new(msg),
result.outcome(),
subscription,
delivery,
C::build as fn(&M) -> C,
)
.await;
if let Some(after) = after {
delivery.spawn_after(after);
}
}
#[cfg(feature = "testing")]
if let Some(log) = log {
log.record(subscription);
}
}
pub(crate) async fn settle_batch<M, C>(
accepted: Vec<M>,
result: BatchResult,
subscription: &str,
delivery: &Delivery<C>,
) where
M: IncomingMessage,
C: BuildBatchContext<M> + Send + Sync + 'static,
{
settle_lent_batch(
accepted,
result,
subscription,
delivery,
LentValues::default(),
)
.await;
}
#[cfg(feature = "testing")]
pub(crate) type LentValues = Vec<crate::testing::coordinator::RecordedValue>;
#[cfg(not(feature = "testing"))]
pub(crate) type LentValues = ();
pub(crate) async fn settle_lent_batch<M, C>(
accepted: Vec<M>,
result: BatchResult,
subscription: &str,
delivery: &Delivery<C>,
lent: LentValues,
) where
M: IncomingMessage,
C: BuildBatchContext<M> + Send + Sync + 'static,
{
#[cfg(feature = "testing")]
let mut batch = BatchLog::of(&accepted, lent);
#[cfg(not(feature = "testing"))]
let () = lent;
match result {
BatchResult::Uniform(mut outcome) => {
let after = outcome.take_after();
let status = outcome.outcome();
for msg in accepted {
#[cfg(feature = "testing")]
batch.settled(status);
settle_outcome(
&mut Slot::new(msg),
status,
subscription,
delivery,
C::build as fn(&M) -> C,
)
.await;
}
if let Some(after) = after {
delivery.spawn_after(after);
}
}
BatchResult::PerElement(results) => {
if results.len() != accepted.len() {
error!(
target: "ruststream::dispatch",
subscription = %subscription,
expected = accepted.len(),
returned = results.len(),
"per-element outcome count does not match the batch; \
retrying the unmatched remainder",
);
}
let mut results = results.into_iter();
for msg in accepted {
let mut result = results.next().unwrap_or_else(HandlerOutcome::retry);
let after = result.take_after();
#[cfg(feature = "testing")]
batch.settled(result.outcome());
settle_outcome(
&mut Slot::new(msg),
result.outcome(),
subscription,
delivery,
C::build as fn(&M) -> C,
)
.await;
if let Some(after) = after {
delivery.spawn_after(after);
}
}
}
}
#[cfg(feature = "testing")]
batch.record(subscription);
}
#[cfg(feature = "testing")]
struct BatchLog {
deliveries: Vec<crate::testing::coordinator::Delivered>,
settled: usize,
_pending: crate::testing::coordinator::PendingRecord,
}
#[cfg(feature = "testing")]
impl BatchLog {
fn of<'m, M: IncomingMessage + 'm>(
accepted: impl IntoIterator<Item = &'m M>,
lent: LentValues,
) -> Self {
let mut lent = lent.into_iter();
Self {
deliveries: accepted
.into_iter()
.map(|msg| crate::testing::coordinator::Delivered {
raw: bytes::Bytes::copy_from_slice(msg.payload()),
value: lent.next(),
settle: None,
})
.collect(),
settled: 0,
_pending: crate::testing::coordinator::PendingRecord::new(),
}
}
fn settled(&mut self, status: HandlerResult) {
if let Some(one) = self.deliveries.get_mut(self.settled) {
one.settle = Some(status);
}
self.settled += 1;
}
fn record(self, subscription: &str) {
crate::testing::coordinator::record_batch(subscription, self.deliveries);
}
}
#[cfg(feature = "otel")]
static BATCH_SIZE: LazyLock<Histogram<u64>> = LazyLock::new(|| {
global::meter("ruststream")
.u64_histogram("ruststream.batch.size")
.with_unit("{message}")
.with_description("Decoded batch sizes handed to batch handlers.")
.build()
});
#[cfg(feature = "otel")]
pub(crate) fn record_batch_size(destination: &str, len: usize) {
#[allow(clippy::cast_possible_truncation)]
BATCH_SIZE.record(
len as u64,
&[KeyValue::new(
"messaging.destination.name",
destination.to_owned(),
)],
);
}
#[allow(clippy::needless_pass_by_ref_mut)]
pub(crate) async fn decode_batch<M, Input, DecodeCodec, C, S>(
batch: Vec<M>,
values: &mut Vec<Input::Owned>,
codec: &DecodeCodec,
decode: FailurePolicy,
ctx: &mut Context<'_, C, S>,
) -> Vec<M>
where
M: IncomingMessage,
Input: DecodeWith<DecodeCodec>,
DecodeCodec: Sync,
C: BuildBatchContext<M> + Send + Sync + 'static,
S: Send + Sync,
{
let subscription = ctx.subscription();
let delivery = ctx.delivery();
values.clear();
values.reserve(batch.len());
let mut rejected: Vec<(usize, HandlerResult)> = Vec::new();
for (index, msg) in batch.iter().enumerate() {
if let Some(outcome) = reported(msg, Input::input_label(), decode, ctx) {
rejected.push((index, outcome));
continue;
}
match Input::decode(codec, msg) {
Ok(value) => values.push(value),
Err(err) => {
warn!(
target: "ruststream::dispatch",
subscription = %subscription,
message_type = Input::input_label(),
error = %err,
"codec decode failed",
);
rejected.push((index, rejection(&err, "batch decode failed", decode, ctx)));
}
}
}
#[cfg(feature = "otel")]
if !values.is_empty() {
record_batch_size(subscription, values.len());
}
if rejected.is_empty() {
return batch;
}
let mut accepted = Vec::with_capacity(batch.len() - rejected.len());
let mut rejected = rejected.into_iter().peekable();
for (index, msg) in batch.into_iter().enumerate() {
if rejected.peek().is_some_and(|(at, _)| *at == index) {
let (_, outcome) = rejected.next().expect("peeked");
settle_outcome(
&mut Slot::new(msg),
outcome,
subscription,
delivery,
<C as BuildBatchContext<M>>::build as fn(&M) -> C,
)
.await;
continue;
}
accepted.push(msg);
}
accepted
}
pub(crate) fn reported<M, C, S>(
msg: &M,
message_type: &str,
decode: FailurePolicy,
ctx: &Context<'_, C, S>,
) -> Option<HandlerResult>
where
M: IncomingMessage,
{
let err = msg.decode_error()?;
warn!(
target: "ruststream::dispatch",
subscription = %ctx.subscription(),
message_type,
error = %err,
"{}",
REPORTED,
);
Some(rejection(err, "batch decode failed", decode, ctx))
}
pub(crate) fn rejection<C, S>(
err: &impl fmt::Display,
reason: &str,
decode: FailurePolicy,
ctx: &Context<'_, C, S>,
) -> HandlerResult {
match decode {
FailurePolicy::FailFast => {
ctx.fail_fast(&format!("{reason}: {err}"));
HandlerResult::drop()
}
other => other.settlement().unwrap_or_else(HandlerResult::drop),
}
}
mod carried;
pub(crate) use carried::{CarriedBatch, SourceCarriesBatch};
#[cfg(all(test, feature = "memory", feature = "json"))]
mod tests;