mod forward;
use std::borrow::Cow;
use std::fmt;
use std::marker::PhantomData;
use std::num::NonZeroUsize;
use crate::codec::Codec;
use crate::{FromName, StartAt, Unnamed};
use super::dispatch::Workers;
use super::failure::FailurePolicies;
use super::input::{Decoded, DecodedPair, Provided};
use super::router::{IncludeDef, InputCodec};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Open;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Fixed;
pub type AllOpen = (Open, Open, Open, Open);
pub trait Declared: Sized {
type Form;
type Settings;
fn declare(self) -> Self::Settings;
}
impl<T: IncludeDef> Declared for T {
type Form = <T as IncludeDef>::Form;
type Settings = Self;
fn declare(self) -> Self {
self
}
}
pub struct SubscriberBuilder<Def, Src, State, DefCodec = ()> {
def: Def,
source: Src,
workers: Workers,
failures: FailurePolicies,
batch_size: Option<NonZeroUsize>,
codec: DefCodec,
_state: PhantomData<fn() -> State>,
}
impl<Def, Src> SubscriberBuilder<Def, Src, AllOpen> {
#[must_use]
pub fn new(def: Def, source: Src) -> Self {
Self {
def,
source,
workers: Workers::sequential(),
failures: FailurePolicies::default(),
batch_size: None,
codec: (),
_state: PhantomData,
}
}
}
impl<Def, Src, State> SubscriberBuilder<Def, Src, State> {
#[must_use]
pub fn codec<C: Codec>(self, codec: C) -> SubscriberBuilder<Def, Src, State, C> {
SubscriberBuilder {
def: self.def,
source: self.source,
workers: self.workers,
failures: self.failures,
batch_size: self.batch_size,
codec,
_state: PhantomData,
}
}
}
type Collected<DefCodec> = (Workers, FailurePolicies, Option<NonZeroUsize>, DefCodec);
impl<Def, Src, State, DefCodec> SubscriberBuilder<Def, Src, State, DefCodec> {
fn into_parts(self) -> (Def, Src, Collected<DefCodec>) {
(
self.def,
self.source,
(self.workers, self.failures, self.batch_size, self.codec),
)
}
fn from_parts<NewSrc, NewState>(
def: Def,
source: NewSrc,
(workers, failures, batch_size, codec): Collected<DefCodec>,
) -> SubscriberBuilder<Def, NewSrc, NewState, DefCodec> {
SubscriberBuilder {
def,
source,
workers,
failures,
batch_size,
codec,
_state: PhantomData,
}
}
#[cfg(test)]
pub(crate) fn into_def(self) -> Def {
self.def
}
pub(crate) fn map_def<NewDef>(
self,
f: impl FnOnce(Def) -> NewDef,
) -> SubscriberBuilder<NewDef, Src, State, DefCodec> {
let (def, source, (workers, failures, batch_size, codec)) = self.into_parts();
SubscriberBuilder {
def: f(def),
source,
workers,
failures,
batch_size,
codec,
_state: PhantomData,
}
}
}
#[diagnostic::on_unimplemented(
message = "no codec is available to decode this subscriber's input",
label = "nothing in this chain names a codec",
note = "enable a codec feature on `ruststream` (`json`, `cbor` or `msgpack`), name one for \
the scope (`with_broker_codec(broker, JsonCodec, |b| ..)`) or the registration \
(`.codec(JsonCodec)`), or give the input type its own decoding with \
`#[derive(Deserialized)]` so no codec is needed"
)]
#[doc(hidden)]
pub trait DefinitionInputCodec<Input, Surface> {
type Codec: Clone + Send + Sync + 'static;
fn resolve(&self, surface: &Surface) -> Self::Codec;
}
impl<Input, Surface: InputCodec<Input>> DefinitionInputCodec<Input, Surface> for () {
type Codec = Surface::Codec;
fn resolve(&self, surface: &Surface) -> Self::Codec {
surface.input_codec()
}
}
impl<T, Surface, C> DefinitionInputCodec<Decoded<T>, Surface> for C
where
C: Codec + Clone + Send + Sync + 'static,
{
type Codec = C;
fn resolve(&self, _surface: &Surface) -> C {
self.clone()
}
}
impl<H, P, Surface, C> DefinitionInputCodec<DecodedPair<H, P>, Surface> for C
where
C: Codec + Clone + Send + Sync + 'static,
{
type Codec = C;
fn resolve(&self, _surface: &Surface) -> C {
self.clone()
}
}
impl<F, Surface, C: Codec> DefinitionInputCodec<Provided<F>, Surface> for C {
type Codec = ();
fn resolve(&self, _surface: &Surface) {}
}
#[doc(hidden)]
pub trait MountsWith<Input, Surface> {
type Codec: Clone + Send + Sync + 'static;
fn mounted_codec(&self, surface: &Surface) -> Self::Codec;
}
impl<Def, Src, State, DC, Input, Surface> MountsWith<Input, Surface>
for SubscriberBuilder<Def, Src, State, DC>
where
DC: DefinitionInputCodec<Input, Surface>,
{
type Codec = DC::Codec;
fn mounted_codec(&self, surface: &Surface) -> Self::Codec {
self.codec.resolve(surface)
}
}
pub(crate) type DefMountCodec<D, I, S> = <D as MountsWith<I, S>>::Codec;
impl<Def, Src, State, DefCodec> Declared for SubscriberBuilder<Def, Src, State, DefCodec>
where
Def: Declared,
{
type Form = Def::Form;
type Settings = Self;
fn declare(self) -> Self {
self
}
}
impl<Def, Src, State, DefCodec> fmt::Debug for SubscriberBuilder<Def, Src, State, DefCodec> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SubscriberBuilder")
.field("workers", &self.workers)
.field("failures", &self.failures)
.finish_non_exhaustive()
}
}
#[diagnostic::on_unimplemented(
message = "this subscriber's subscription is already named",
label = "the name is fixed in the `#[subscriber(..)]` attribute",
note = "a setting is named once: keep the name in the attribute, or leave the attribute's \
source open (`#[subscriber]`, `#[subscriber(Kind)]`) and name it here"
)]
pub trait NameStep: Sized {
type Out;
fn apply_name(self, name: Cow<'static, str>) -> Self::Out;
}
impl<Def, S: FromName, State, DC> NameStep for SubscriberBuilder<Def, Unnamed<S>, State, DC> {
type Out = SubscriberBuilder<Def, S, State, DC>;
fn apply_name(self, name: Cow<'static, str>) -> Self::Out {
let (def, _unnamed, collected) = self.into_parts();
Self::from_parts(def, S::from_name(name), collected)
}
}
#[diagnostic::on_unimplemented(
message = "this subscriber's worker policy is already fixed",
label = "`workers(..)` or `threads(..)` is named in the `#[subscriber(..)]` attribute",
note = "a subscription has one concurrency policy, named once: keep the clause in the \
attribute, or drop it there and name the policy here"
)]
pub trait WorkersStep: Sized {
type Out;
fn apply_workers(self, workers: Workers) -> Self::Out;
}
impl<Def, Src, F, P, B, DC> WorkersStep for SubscriberBuilder<Def, Src, (Open, F, P, B), DC> {
type Out = SubscriberBuilder<Def, Src, (Fixed, F, P, B), DC>;
fn apply_workers(self, workers: Workers) -> Self::Out {
let (def, source, (_default, failures, batch_size, codec)) = self.into_parts();
Self::from_parts(def, source, (workers, failures, batch_size, codec))
}
}
#[diagnostic::on_unimplemented(
message = "this subscriber's failure policies are already fixed",
label = "`on_failure(..)` is named in the `#[subscriber(..)]` attribute",
note = "a setting is named once: keep `on_failure(..)` in the attribute, or drop it there \
and name the policies here"
)]
pub trait FailureStep: Sized {
type Out;
fn apply_failures(self, policies: FailurePolicies) -> Self::Out;
}
impl<Def, Src, W, P, B, DC> FailureStep for SubscriberBuilder<Def, Src, (W, Open, P, B), DC> {
type Out = SubscriberBuilder<Def, Src, (W, Fixed, P, B), DC>;
fn apply_failures(self, policies: FailurePolicies) -> Self::Out {
let (def, source, (workers, _defaults, batch_size, codec)) = self.into_parts();
Self::from_parts(def, source, (workers, policies, batch_size, codec))
}
}
#[diagnostic::on_unimplemented(
message = "this subscriber's start position is already fixed",
label = "`start_at(..)` is named in the `#[subscriber(..)]` attribute",
note = "a setting is named once: keep `start_at(..)` in the attribute, or drop it there and \
name the position here"
)]
pub trait StartAtStep<P>: Sized {
type Out;
fn apply_start_at(self, position: P) -> Self::Out;
}
impl<Def, Src, W, F, P, B, DC> StartAtStep<P> for SubscriberBuilder<Def, Src, (W, F, Open, B), DC> {
type Out = SubscriberBuilder<Def, StartAt<Src, P>, (W, F, Fixed, B), DC>;
fn apply_start_at(self, position: P) -> Self::Out {
let (def, source, collected) = self.into_parts();
Self::from_parts(def, StartAt::new(source, position), collected)
}
}
#[diagnostic::on_unimplemented(
message = "this subscriber has no batches to size",
label = "`batch(..)` sizes the batches a batch body is handed",
note = "the batch size belongs to a batch body (`&[T]`, `&[F<'_>]`, `&[Message<H, P>]`), \
with or without a reply and `Out` slots; a single-message body takes no batch, and \
how many of those are in flight at once is `workers(n)` instead"
)]
#[doc(hidden)]
pub trait CapsBatches {}
#[diagnostic::on_unimplemented(
message = "this subscriber's batch size is already named",
label = "the batch size is named once",
note = "`batch(n)` is the one batching parameter the framework carries; the broker's own \
batching options ride its subscription source"
)]
pub trait BatchStep: Sized {
type Out;
fn apply_batch(self, size: NonZeroUsize) -> Self::Out;
}
impl<Def, Src, W, F, P, DC> BatchStep for SubscriberBuilder<Def, Src, (W, F, P, Open), DC>
where
Def: CapsBatches,
{
type Out = SubscriberBuilder<Def, Src, (W, F, P, Fixed), DC>;
fn apply_batch(self, size: NonZeroUsize) -> Self::Out {
let (def, source, (workers, failures, _open, codec)) = self.into_parts();
Self::from_parts(def, source, (workers, failures, Some(size), codec))
}
}
#[diagnostic::on_unimplemented(
message = "this batch subscriber has no batch size",
label = "a batch handler needs one",
note = "add `.batch(nonzero!(n))` at the mount site: the batch size is the one parameter the \
framework passes to the broker, and the broker's own batching options (a block \
timeout, a consumer group) ride its subscription source"
)]
pub trait BatchSized {
fn batch_size(&self) -> NonZeroUsize;
}
impl<Def, Src, W, F, P, DC> BatchSized for SubscriberBuilder<Def, Src, (W, F, P, Fixed), DC> {
fn batch_size(&self) -> NonZeroUsize {
self.batch_size
.expect("the fixed batch-size slot carries its size")
}
}
pub trait MapSourceStep<F>: Sized {
type Out;
fn apply_map_source(self, f: F) -> Self::Out;
}
impl<Def, Src, State, F, NewSrc, DC> MapSourceStep<F> for SubscriberBuilder<Def, Src, State, DC>
where
F: FnOnce(Src) -> NewSrc,
{
type Out = SubscriberBuilder<Def, NewSrc, State, DC>;
fn apply_map_source(self, f: F) -> Self::Out {
let (def, source, collected) = self.into_parts();
Self::from_parts(def, f(source), collected)
}
}
pub trait SubscriberSettings: Declared {
fn name(self, name: impl Into<Cow<'static, str>>) -> <Self::Settings as NameStep>::Out
where
Self::Settings: NameStep,
{
self.declare().apply_name(name.into())
}
fn workers(self, count: NonZeroUsize) -> <Self::Settings as WorkersStep>::Out
where
Self::Settings: WorkersStep,
{
self.declare().apply_workers(Workers::pool(count))
}
fn workers_by_key(self, count: NonZeroUsize) -> <Self::Settings as WorkersStep>::Out
where
Self::Settings: WorkersStep,
{
self.declare().apply_workers(Workers::keyed(count))
}
fn threads(self, count: NonZeroUsize) -> <Self::Settings as WorkersStep>::Out
where
Self::Settings: WorkersStep,
{
self.declare().apply_workers(Workers::threads(count))
}
fn threads_by_key(self, count: NonZeroUsize) -> <Self::Settings as WorkersStep>::Out
where
Self::Settings: WorkersStep,
{
self.declare().apply_workers(Workers::threads_keyed(count))
}
fn on_failure(self, policies: FailurePolicies) -> <Self::Settings as FailureStep>::Out
where
Self::Settings: FailureStep,
{
self.declare().apply_failures(policies)
}
fn start_at<P>(self, position: P) -> <Self::Settings as StartAtStep<P>>::Out
where
Self::Settings: StartAtStep<P>,
{
self.declare().apply_start_at(position)
}
fn batch(self, size: NonZeroUsize) -> <Self::Settings as BatchStep>::Out
where
Self::Settings: BatchStep,
{
self.declare().apply_batch(size)
}
fn map_source<F>(self, f: F) -> <Self::Settings as MapSourceStep<F>>::Out
where
Self::Settings: MapSourceStep<F>,
{
self.declare().apply_map_source(f)
}
}
impl<D: Declared> SubscriberSettings for D {}
#[cfg(test)]
mod tests {
use super::{AllOpen, BatchSized, Declared, SubscriberBuilder, SubscriberSettings};
#[cfg(feature = "memory")]
use crate::SubscriptionSource;
#[cfg(feature = "memory")]
use crate::memory::ConnectedMemoryBroker;
use crate::runtime::dispatch::Workers;
use crate::runtime::failure::{FailurePolicies, FailurePolicy};
use crate::runtime::forms;
use crate::runtime::input::Decoded;
use crate::runtime::router::IncludeDef;
use crate::runtime::subscriber_def::SubscriberDef;
use crate::{Name, Unnamed, nonzero};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Stub;
impl SubscriberDef for Stub {
type Input = Decoded<u32>;
type Context = ();
type Handler = ();
type Source = Name;
fn source(&self) -> Name {
Name::new("stub")
}
fn into_handler(self) {}
}
impl Declared for Stub {
type Form = forms::Subscribing;
type Settings = SubscriberBuilder<Self, Unnamed<Name>, AllOpen>;
fn declare(self) -> Self::Settings {
SubscriberBuilder::new(self, Unnamed::new())
}
}
impl super::CapsBatches for Stub {}
#[test]
fn the_steps_collect_the_settings_the_mount_reads_back() {
let built = Stub
.name("orders")
.workers(nonzero!(4))
.on_failure(FailurePolicies::default().with_decode(FailurePolicy::Skip));
assert_eq!(built.workers, Workers::pool(nonzero!(4)));
assert_eq!(built.failures.decode, FailurePolicy::Skip);
#[cfg(feature = "memory")]
assert_eq!(
SubscriptionSource::<ConnectedMemoryBroker>::name(&built.source),
"orders",
);
assert_eq!(built.def, Stub);
}
#[test]
fn the_batch_size_reaches_the_mount_through_the_fixed_slot() {
let built = Stub.name("orders").batch(nonzero!(16));
assert_eq!(BatchSized::batch_size(&built), nonzero!(16));
}
#[test]
fn keyed_lanes_are_the_same_slot_as_the_pool() {
let built = Stub.name("orders").workers_by_key(nonzero!(2));
assert_eq!(built.workers, Workers::keyed(nonzero!(2)));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Handwritten;
impl IncludeDef for Handwritten {
type Form = forms::RawSubscribing;
}
#[test]
fn a_hand_written_definition_is_its_own_settings() {
assert_eq!(Handwritten.declare(), Handwritten);
}
#[test]
fn the_wrapped_definition_keeps_its_own_surface() {
let built = Stub.name("orders");
let source = SubscriberDef::source(&built.def);
assert!(format!("{source:?}").contains("stub"));
built.def.into_handler();
}
#[test]
fn a_builder_declares_itself_and_reports_its_settings() {
let built = Stub.name("orders").map_source(|source| source);
let same = built.declare();
assert_eq!(SubscriberDef::workers(&same), Workers::sequential());
assert_eq!(
SubscriberDef::failure_policies(&same),
FailurePolicies::default()
);
assert!(format!("{same:?}").contains("SubscriberBuilder"));
}
}