Skip to main content

RedisStream

Struct RedisStream 

Source
pub struct RedisStream { /* private fields */ }
Expand description

Describes one Redis Streams subscription against a ConnectedRedisBroker.

§Examples

use std::time::Duration;

use ruststream_fred::stream::prelude::*;
use serde::Deserialize;

#[derive(Deserialize)]
struct Order {
    id: u64,
}

// Fresh tail: the normal worker, reading new entries.
#[subscriber(RedisStream::new("orders").group("workers"))]
async fn handle(order: &Order) -> HandlerOutcome {
    tracing::info!(order.id, "order");
    HandlerOutcome::ack()
}

// Recovery on the same group: entries a crashed worker left pending for over 30s.
#[subscriber(RedisStream::reclaim("orders", Duration::from_secs(30)).group("workers"))]
async fn recover(order: &Order) -> HandlerOutcome {
    tracing::info!(order.id, "recovered");
    HandlerOutcome::ack()
}

#[ruststream::app]
fn app() -> impl App {
    RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
        RedisBroker::standalone("redis://localhost:6379"),
        |b| {
            b.include(handle);
            b.include(recover);
        },
    )
}

Implementations§

Source§

impl RedisStream

Source

pub fn new(key: impl Into<String>) -> Self

A fresh-tail subscription on key: reads new entries via XREADGROUP >.

A consumer group is required; set it with group.

§Examples

A plain stream has no delivery count of its own, so a retry cap reads the framework’s retry-count header on the copies the runtime publishes:

use ruststream_fred::stream::prelude::*;
use serde::Deserialize;

#[derive(Deserialize)]
struct Order {
    id: u64,
}

// The handler counts nothing itself.
#[subscriber(RedisStream::new("orders").group("workers"))]
async fn handle(order: &Order) -> HandlerOutcome {
    if order.id == 0 {
        return HandlerOutcome::retry();
    }
    HandlerOutcome::ack()
}

#[ruststream::app]
fn app() -> impl App {
    RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
        RedisBroker::standalone("redis://localhost:6379"),
        |b| {
            // Five deliveries per message, counting the first. The fifth failure sends it to
            // "orders.dlq" instead of back to "orders".
            b.include(handle)
                .max_attempts(nonzero!(5u32))
                .dead_letter("orders.dlq");
        },
    )
}
Source

pub fn reclaim(key: impl Into<String>, min_idle: Duration) -> Self

A recovery subscription on key: reclaims pending entries idle at least min_idle via XAUTOCLAIM. Run it alongside a new subscriber on the same group to pick up messages a consumer fetched but died before acking.

min_idle has no default and must exceed the longest legitimate handler runtime: set it too low and a healthy consumer’s in-flight message gets reclaimed and processed twice.

§Examples
use std::time::Duration;

use ruststream_fred::stream::prelude::*;
use serde::Deserialize;

#[derive(Deserialize)]
struct Order {
    id: u64,
}

#[subscriber(RedisStream::new("orders").group("workers"))]
async fn handle(order: &Order) -> HandlerOutcome {
    tracing::info!(order.id, "processing");
    HandlerOutcome::ack()
}

// Recovery on a server older than 8.4: a reclaim subscription beside the fresh-tail one, on the
// same group, takes the orders a dead worker fetched and never finished.
#[subscriber(RedisStream::reclaim("orders", Duration::from_secs(30)).group("workers"))]
async fn recover(order: &Order) -> HandlerOutcome {
    tracing::info!(order.id, "recovered");
    HandlerOutcome::ack()
}

#[ruststream::app]
fn app() -> impl App {
    RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
        RedisBroker::standalone("redis://localhost:6379"),
        |b| {
            b.include(handle);
            b.include(recover);
        },
    )
}
Source

pub fn claiming(key: impl Into<String>, min_idle: Duration) -> Self

A subscription on key that claims and reads in one call: XREADGROUP ... CLAIM first takes over the group’s entries idle at least min_idle, longest idle first, then fills the rest of the read with fresh ones. One subscription, one consumer, one handler.

Every delivery carries the server’s own IDLE_MS_HEADER and DELIVERY_COUNT_HEADER, both as of the delivery before this one: a fresh entry reports zero for both, and the first claim back reports one earlier delivery. The count is therefore the number of attempts that did not finish, which is what a handler reads to give up on a message itself.

This is Redis’s own redelivery, so this mode is the one that settles a retry natively. retry() leaves the entry in the pending entries list rather than appending a copy, so the next read brings it back under its own id with the count one higher, no sooner than min_idle from now. retry_after(delay) takes the same path and the entry comes back after min_idle, so a delay shorter than that rounds up to it; name a delayed_retry queue to have the delay honoured exactly instead. The delivery reports its count to the runtime, which is what a max_attempts declaration at the mount site caps.

min_idle has no default and must exceed the longest legitimate handler runtime: below it, a message a healthy consumer is still working on is taken away and processed twice.

Redis 8.4.0 added the option. A registration of this mode against an older server refuses to start, naming both versions; on such a server use new with a reclaim subscription beside it.

§Examples
use std::time::Duration;

use ruststream_fred::stream::prelude::*;
use serde::Deserialize;

#[derive(Deserialize)]
struct Order {
    id: u64,
    in_stock: bool,
}

// New orders and the ones a dead worker took and never finished, in one subscription. A
// retry stays pending and comes back after 30s under its own id.
#[subscriber(RedisStream::claiming("orders", Duration::from_secs(30)).group("workers"))]
async fn handle(order: &Order) -> HandlerOutcome {
    if !order.in_stock {
        return HandlerOutcome::retry();
    }
    tracing::info!(order.id, "order");
    HandlerOutcome::ack()
}

#[ruststream::app]
fn app() -> impl App {
    RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
        RedisBroker::standalone("redis://localhost:6379"),
        |b| {
            // The server's delivery count feeds the cap.
            b.include(handle)
                .max_attempts(nonzero!(5u32))
                .dead_letter("orders.dlq");
        },
    )
}
Source

pub fn group(self, group: impl Into<String>) -> Self

Sets the consumer group. Required for every subscription.

Source

pub fn consumer(self, consumer: impl Into<String>) -> Self

Sets this consumer’s name within the group. Defaults to an auto-generated unique name.

Source

pub const fn block(self, block: Duration) -> Self

How long one read blocks waiting for entries. Defaults to 5 seconds. In the fresh-tail and claiming modes this is the XREADGROUP server-side block, which wakes on a new entry and, under CLAIM, on an entry ageing past the threshold; in reclaim mode XAUTOCLAIM does not block, so this is the poll interval slept between scans that find nothing to reclaim.

Also reachable at the mount site, after the batch size, through RedisSubscribeExt.

Source

pub fn start_id(self, start: StreamStart) -> Self

Where a newly created group starts reading. Ignored if the group already exists. Only meaningful for the modes that read the tail, new and claiming.

Source

pub fn delayed_retry(self, retry: DelayedRetry) -> Self

Opts this subscription into durable, crash-safe delayed retry backed by a ZSET delay queue.

Off by default: without it, retry_after(delay) / nack_after(delay) degrade to the runtime’s broker-agnostic deferred re-publish, which the mount site binds with out_retry and which is at-most-once over the delay window. With it, a delayed delivery is ZADDed to the named ZSET and replayed from there once due, so the retry survives a process crash. See DelayedRetry for the key and TTL requirements.

The sweeper that replays due entries runs inside this subscription’s read loop, so its granularity is the read block interval.

Source

pub fn key(&self) -> &str

The stream key this subscription reads.

Trait Implementations§

Source§

impl Clone for RedisStream

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for RedisStream

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl IntoSource for RedisStream

Source§

type Source = RedisStream

The source the constructor stores.
Source§

fn into_source(self) -> Self

Builds the source.
Source§

impl RedeliveryAddressed<ConnectedRedisBroker> for RedisStream

The stream key, in every read mode: an XADD there is read by the consumer group this subscription belongs to.

Under reclaim the copy is read by the group’s tail reader rather than by this subscription, which only ever takes entries already pending on another consumer. That is the topology the mode is written for - a reclaim subscriber runs beside a new one on the same group - so the copy reaches the handler either way.

Source§

fn redelivery_address( &self, _connected: &ConnectedRedisBroker, ) -> impl Future<Output = Result<RedeliveryAddress, RedisError>>

Where a publish reaches this subscription again. Read more
Source§

impl SubscriptionSource<ConnectedRedisBroker> for RedisStream

Source§

type Copies = AddressedCopies

A retry copy is an XADD this process makes, and the stream key is where it goes.

Source§

type Subscriber = RedisSubscriber

The subscriber type this source opens.
Source§

fn name(&self) -> &str

The name (subject / channel) this subscription binds to. Read more
Source§

fn declare_retry(self, declaration: &RetryDeclaration) -> Self

Takes the registration’s retry declaration into the descriptor, before the subscription opens. Read more
Source§

async fn subscribe( self, connected: &ConnectedRedisBroker, ) -> Result<Self::Subscriber, RedisError>

Opens the subscription against the connected broker. Called once at startup. Read more
Source§

fn channel_bindings(&self) -> Bindings

What this subscription adds to its channel in the generated AsyncAPI document. Read more
Source§

fn declare_retry_on( &self, connected: &C, declaration: &RetryDeclaration, ) -> Result<(), DeclareRetryError>

Carries the registration’s declaration to the broker, at the same point and before the subscription opens. Read more
Source§

fn operation_bindings(&self) -> Bindings

What this subscription adds to its receive operation in the document. Read more
Source§

fn message_bindings(&self) -> Bindings

What this subscription adds to the messages that arrive on it. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<Descriptor> OpensWindow for Descriptor
where Descriptor: Base,

Source§

type Pipelined = Pipelined<Descriptor>

The same source with a window.
Source§

fn open_window(self) -> <Descriptor as OpensWindow>::Pipelined

Opens the window.
Source§

impl<T> PublishedThrough<DefaultSlot> for T

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more