Skip to main content

rustis/client/
client_tracking_invalidation_stream.rs

1use crate::{network::PushReceiver, resp::BulkString};
2use futures_util::{Stream, StreamExt};
3use std::{
4    pin::Pin,
5    task::{Context, Poll},
6};
7use tracing::warn;
8
9pub struct ClientTrackingInvalidationStream {
10    receiver: PushReceiver,
11}
12
13impl ClientTrackingInvalidationStream {
14    pub(crate) fn new(receiver: PushReceiver) -> Self {
15        Self { receiver }
16    }
17
18    /// Number of invalidation messages dropped so far because this stream fell
19    /// behind its memory budget.
20    ///
21    /// **A non-zero value means the reader no longer knows which keys are
22    /// stale.** Unlike a pub/sub message, a lost invalidation is not merely
23    /// missing data: acting on the remaining ones would leave the dropped keys
24    /// cached and served forever. A consumer that observes this counter move
25    /// must discard everything it cached, which is what the `Cache` (feature `client-cache`)
26    /// does — the same response it already has for invalidations lost across a
27    /// reconnection.
28    pub fn dropped_messages(&self) -> usize {
29        self.receiver.dropped_messages()
30    }
31}
32
33impl Stream for ClientTrackingInvalidationStream {
34    /// Redis keys are binary-safe, hence [`BulkString`] rather than `String`:
35    /// a key that is not valid UTF-8 must still reach the consumer.
36    type Item = Vec<BulkString>;
37
38    fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
39        let this = self.get_mut();
40
41        // An undecodable event must not end the stream: the consumer would stop
42        // polling and never learn of another invalidation, leaving every cache
43        // built on top of it serving stale data for good. Skip it and keep
44        // reading instead.
45        loop {
46            let Poll::Ready(response) = this.receiver.poll_next_unpin(cx) else {
47                return Poll::Pending;
48            };
49
50            let Some(response) = response else {
51                return Poll::Ready(None);
52            };
53
54            match response {
55                Ok(response) => match response.to::<((), Vec<BulkString>)>() {
56                    Ok((_invalidate, keys)) => return Poll::Ready(Some(keys)),
57                    Err(e) => warn!("Cannot decode a client tracking invalidation: {e}"),
58                },
59                Err(e) => warn!("Error while receiving a client tracking invalidation: {e}"),
60            }
61        }
62    }
63}