use std::future::Future;
use crate::{BuildBatchContext, IncomingMessage};
use super::batch::{BatchHandler, BatchResult, decode_batch, settle_batch};
use super::context::Context;
use super::dispatch::Workers;
use super::failure::{FailurePolicies, FailurePolicy};
use super::input::{DecodeWith, InputKind};
use super::metadata::{HandlerMetadata, OutgoingMessageMetadata};
pub trait BatchInjectDef: Send + Sync {
type Input: InputKind;
type Source;
type Injections;
type Context;
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 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 BatchInjectCall<S>: BatchInjectDef {
fn call(
&self,
batch: &[<Self::Input as InputKind>::Owned],
injections: &Self::Injections,
ctx: &mut Context<'_, Self::Context, S>,
) -> impl Future<Output = BatchResult> + Send;
}
pub(crate) fn batch_inject_metadata<D: BatchInjectDef>(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.outgoing = def.outgoing();
meta
}
pub(crate) struct BatchInjectHandler<Def: BatchInjectDef, DecodeCodec> {
pub(crate) def: Def,
pub(crate) codec: DecodeCodec,
pub(crate) injections: Def::Injections,
pub(crate) decode: FailurePolicy,
}
impl<Def: BatchInjectDef, DecodeCodec> std::fmt::Debug for BatchInjectHandler<Def, DecodeCodec> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BatchInjectHandler").finish_non_exhaustive()
}
}
impl<Msg, Def, DecodeCodec, State> BatchHandler<Msg, Def::Context, State>
for BatchInjectHandler<Def, DecodeCodec>
where
Msg: IncomingMessage,
Def: BatchInjectCall<State>,
Def::Input: DecodeWith<DecodeCodec>,
Def::Injections: Send + Sync,
Def::Context: BuildBatchContext<Msg> + Send + Sync + 'static,
DecodeCodec: Send + Sync,
State: Send + Sync,
{
type Scratch = Vec<<Def::Input as InputKind>::Owned>;
async fn handle_batch(
&self,
batch: Vec<Msg>,
values: &mut Self::Scratch,
ctx: &mut Context<'_, Def::Context, State>,
) {
let subscription = ctx.subscription();
let accepted = decode_batch::<Msg, Def::Input, DecodeCodec, Def::Context, State>(
batch,
values,
&self.codec,
self.decode,
ctx,
)
.await;
if accepted.is_empty() {
return;
}
let delivery = ctx.delivery();
let result = self.def.call(values, &self.injections, ctx).await;
values.clear();
settle_batch(accepted, result, subscription, delivery).await;
}
}
#[cfg(all(test, feature = "memory", feature = "json"))]
mod tests {
use std::future::ready;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use futures::StreamExt;
use super::super::dispatch::Delivery;
use super::super::handler::HandlerOutcome;
use super::super::input::Decoded;
use super::*;
use crate::codec::JsonCodec;
use crate::memory::{ConnectedMemoryBroker, MemoryBroker};
use crate::testkit::batch::{publish_payloads, pull_batch};
use crate::{HeaderMap, Name, Subscriber, SubscriptionSource};
struct Scale {
calls: Arc<AtomicUsize>,
seen: Arc<Mutex<Vec<u32>>>,
}
impl Scale {
fn new() -> Self {
Self {
calls: Arc::new(AtomicUsize::new(0)),
seen: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl BatchInjectDef for Scale {
type Input = Decoded<u32>;
type Source = Name;
type Injections = u32;
type Context = ();
fn source(&self) -> Self::Source {
Name::new("scale")
}
}
impl<S: Send + Sync> BatchInjectCall<S> for Scale {
fn call(
&self,
batch: &[u32],
factor: &u32,
_ctx: &mut Context<'_, (), S>,
) -> impl Future<Output = BatchResult> {
self.calls.fetch_add(1, Ordering::SeqCst);
self.seen
.lock()
.unwrap()
.extend(batch.iter().map(|n| n * factor));
ready(BatchResult::Uniform(HandlerOutcome::ack()))
}
}
#[test]
fn batch_inject_def_defaults_register_without_documentation() {
let def = Scale::new();
let source = def.source();
let name = SubscriptionSource::<ConnectedMemoryBroker>::name(&source).to_owned();
let meta = batch_inject_metadata(name, &def);
assert_eq!(meta.name, "scale");
assert_eq!(meta.input_type, "u32");
assert!(meta.description.is_none());
assert!(meta.payload_schema.is_none());
assert!(meta.headers_schema.is_none());
assert!(meta.message_name.is_none());
assert!(meta.message_description.is_none());
assert!(meta.outgoing.is_empty());
assert_eq!(def.workers(), Workers::sequential());
assert_eq!(def.failure_policies(), FailurePolicies::default());
}
#[test]
fn handler_debug_hides_the_injections() {
let handler = BatchInjectHandler {
def: Scale::new(),
codec: JsonCodec,
injections: 10,
decode: FailurePolicy::Drop,
};
assert!(format!("{handler:?}").contains("BatchInjectHandler"));
}
#[tokio::test]
async fn resolved_injections_reach_the_body_with_the_batch() {
let broker = MemoryBroker::new();
let mut sub = broker.subscribe("scale");
publish_payloads(&broker, "scale", &[b"1", b"2"]).await;
let def = Scale::new();
let seen = Arc::clone(&def.seen);
let handler = BatchInjectHandler {
def,
codec: JsonCodec,
injections: 10,
decode: FailurePolicy::Drop,
};
let state = ();
let delivery = Delivery::empty();
let headers = HeaderMap::new();
let mut ctx = Context::new("scale", &headers, &state, (), &delivery);
let batch = pull_batch(&mut sub).await;
assert_eq!(batch.len(), 2);
handler.handle_batch(batch, &mut Vec::new(), &mut ctx).await;
assert_eq!(*seen.lock().unwrap(), [10, 20]);
let mut stream = std::pin::pin!(sub.stream());
assert!(futures::poll!(stream.next()).is_pending());
}
#[tokio::test]
async fn a_fully_undecodable_batch_never_reaches_the_body() {
let broker = MemoryBroker::new();
let mut sub = broker.subscribe("scale");
publish_payloads(&broker, "scale", &[b"not json", b"also not json"]).await;
let def = Scale::new();
let calls = Arc::clone(&def.calls);
let handler = BatchInjectHandler {
def,
codec: JsonCodec,
injections: 10,
decode: FailurePolicy::Drop,
};
let state = ();
let delivery = Delivery::empty();
let headers = HeaderMap::new();
let mut ctx = Context::new("scale", &headers, &state, (), &delivery);
let batch = pull_batch(&mut sub).await;
assert_eq!(batch.len(), 2);
handler.handle_batch(batch, &mut Vec::new(), &mut ctx).await;
assert_eq!(calls.load(Ordering::SeqCst), 0);
let mut stream = std::pin::pin!(sub.stream());
assert!(futures::poll!(stream.next()).is_pending());
}
}