use std::collections::{HashMap, VecDeque};
use std::fmt::{Debug, Formatter};
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use fred::clients::Client;
use fred::error::Error as FredError;
use fred::interfaces::{ClientLike, StreamsInterface};
use fred::types::streams::XReadValue;
use fred::types::{CustomCommand, Value};
use futures::Stream;
use futures::stream::unfold;
use ruststream::{BatchSubscriber, Seekable, Subscriber};
use tracing::warn;
use crate::broker::ensure_group;
use crate::claim::{self, ClaimedEntry};
use crate::connection::{Connection, ReadConnection};
use crate::convert::{HEADER_PREFIX, parts_from_fields};
use crate::delay::DelayConfig;
use crate::loopback::Tap;
use crate::message::{DELIVERY_COUNT_HEADER, IDLE_MS_HEADER, RedisMessage};
use crate::pipeline::{RoundMessage, StreamForm, Window};
use crate::seek::{EntryId, RedisGroupSeeker};
use crate::{error::RedisError, stream::ReadMode};
struct Entry {
id: String,
fields: HashMap<String, Vec<u8>>,
delivered: Option<u64>,
}
type RawStreams = Vec<(String, Vec<(String, Vec<(String, Vec<u8>)>)>)>;
const RECLAIM_START: &str = "0-0";
const XREADGROUP: &str = "XREADGROUP";
pub(crate) const PREFETCH: u64 = 64;
const RECREATED_GROUP_START: &str = "0";
fn lost_group(err: &RedisError) -> bool {
matches!(err, RedisError::Stream(source)
if source
.downcast_ref::<FredError>()
.is_some_and(|err| err.details().starts_with("NOGROUP")))
}
fn duration_to_millis(d: Duration) -> u64 {
u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
}
pub struct RedisSubscriber {
connection: Arc<Connection>,
reader: ReadConnection,
key: String,
group: String,
consumer: String,
block: Duration,
mode: ReadMode,
delay: Option<DelayConfig>,
cursor: String,
buffer: VecDeque<Entry>,
generation: Arc<AtomicU64>,
buffer_generation: u64,
seeker: Arc<RedisGroupSeeker>,
tap: Tap,
}
impl Debug for RedisSubscriber {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisSubscriber")
.field("key", &self.key)
.field("group", &self.group)
.field("consumer", &self.consumer)
.field("mode", &self.mode)
.finish_non_exhaustive()
}
}
impl RedisSubscriber {
#[allow(
clippy::too_many_arguments,
reason = "internal constructor mirroring the descriptor"
)]
pub(crate) fn new(
connection: Arc<Connection>,
reader: ReadConnection,
key: String,
group: String,
consumer: String,
block: Duration,
mode: ReadMode,
delay: Option<DelayConfig>,
tap: Tap,
) -> Self {
let generation = Arc::new(AtomicU64::new(0));
let seeker = Arc::new(RedisGroupSeeker::new(
Arc::clone(&connection),
key.as_str(),
group.as_str(),
Arc::clone(&generation),
));
Self {
connection,
reader,
key,
group,
consumer,
block,
mode,
delay,
cursor: RECLAIM_START.to_owned(),
buffer: VecDeque::new(),
generation,
buffer_generation: 0,
seeker,
tap,
}
}
fn message(&self, entry: Entry) -> Result<RedisMessage, RedisError> {
let id = entry.id;
let parsed: EntryId = id.parse()?;
let (payload, headers) = parts_from_fields(entry.fields);
Ok(RedisMessage::new(
self.connection.pool().clone(),
self.key.clone(),
self.group.clone(),
id,
parsed,
payload,
headers,
self.delay.clone(),
entry.delivered,
Arc::clone(&self.seeker),
self.mode.requeue(),
self.tap.delivered(),
))
}
fn discard_stale(&mut self) -> usize {
let current = self.generation.load(Ordering::Acquire);
if self.buffer_generation == current {
return 0;
}
let dropped = self.buffer.len();
self.buffer.clear();
self.buffer_generation = current;
#[cfg(feature = "testing")]
self.tap.discard(dropped);
dropped
}
pub(crate) fn key(&self) -> &str {
&self.key
}
pub(crate) fn round_form(&self) -> StreamForm {
StreamForm::new(
self.key.as_str(),
self.group.as_str(),
self.delay.clone(),
self.mode.requeue(),
)
}
pub(crate) fn round_client(&self) -> Client {
self.connection.pool().next().clone()
}
pub(crate) fn round_stream<'a>(
&'a mut self,
window: &'a Arc<Window<StreamForm>>,
) -> impl Stream<Item = Result<RoundMessage<StreamForm>, RedisError>> + Send + 'a {
unfold(self, move |s| async move {
loop {
if let Some(failure) = window.take_failure() {
return Some((Err(failure), s));
}
window.discarded(s.discard_stale());
if let Some(entry) = s.buffer.pop_front() {
let delivery = match s.message(entry) {
Ok(delivery) => delivery,
Err(err) => {
window.discarded(1);
return Some((Err(err), s));
}
};
let round = window.open();
return Some((
Ok(RoundMessage::new(delivery, Arc::clone(window), round)),
s,
));
}
if let Err(err) = s.fetch(PREFETCH).await {
return Some((Err(err), s));
}
window.fetched(s.buffer.len());
}
})
}
async fn fetch(&mut self, count: u64) -> Result<(), RedisError> {
#[cfg(feature = "testing")]
self.tap.readable().await;
self.connection.ensure_open()?;
if let Some(cfg) = &self.delay {
cfg.sweep_due(self.connection.pool(), &self.key).await?;
}
let selected_at = self.generation.load(Ordering::Acquire);
let read = match self.mode.clone() {
ReadMode::Fresh => self.fetch_fresh(count).await,
ReadMode::Reclaim { min_idle } => self.fetch_reclaim(min_idle, count).await,
ReadMode::Claiming { min_idle } => self.fetch_claiming(min_idle, count).await,
};
let entries = match read {
Ok(entries) => entries,
Err(err) if lost_group(&err) => return self.recreate_group().await,
Err(err) => return Err(err),
};
if selected_at != self.generation.load(Ordering::Acquire) {
#[cfg(feature = "testing")]
self.tap.discard(entries.len());
return Ok(());
}
self.buffer_generation = selected_at;
self.buffer.extend(entries);
Ok(())
}
#[cold]
async fn recreate_group(&mut self) -> Result<(), RedisError> {
warn!(
subscription = self.key.as_str(),
group = self.group.as_str(),
"the Redis server lost this stream's consumer group, as after a restart that kept no \
data; creating it again and reading the stream from its start"
);
ensure_group(
self.connection.pool(),
&self.key,
&self.group,
RECREATED_GROUP_START,
)
.await?;
RECLAIM_START.clone_into(&mut self.cursor);
Ok(())
}
async fn fetch_fresh(&self, count: u64) -> Result<Vec<Entry>, RedisError> {
let resp: RawStreams = self
.reader
.client()
.xreadgroup(
self.group.as_str(),
self.consumer.as_str(),
Some(count),
Some(duration_to_millis(self.block)),
false,
self.key.as_str(),
">",
)
.await
.map_err(RedisError::stream)?;
let entries = resp
.into_iter()
.find(|(key, _)| key == &self.key)
.map(|(_, entries)| entries)
.unwrap_or_default();
Ok(entries
.into_iter()
.map(|(id, fields)| Entry {
id,
fields: fields.into_iter().collect(),
delivered: None,
})
.collect())
}
async fn fetch_claiming(
&self,
min_idle: Duration,
count: u64,
) -> Result<Vec<Entry>, RedisError> {
let command = CustomCommand::new_static(XREADGROUP, self.key.as_str(), true);
let args: Vec<Value> = [
"GROUP",
self.group.as_str(),
self.consumer.as_str(),
"COUNT",
&count.to_string(),
"BLOCK",
&duration_to_millis(self.block).to_string(),
"CLAIM",
&duration_to_millis(min_idle).to_string(),
"STREAMS",
self.key.as_str(),
">",
]
.into_iter()
.map(Value::from)
.collect();
let frame = self
.reader
.client()
.custom_raw(command, args)
.await
.map_err(RedisError::stream)?;
let claimed = claim::decode_reply(&frame, &self.key)?;
Ok(Self::annotate_claimed(claimed))
}
fn annotate_claimed(claimed: Vec<ClaimedEntry>) -> Vec<Entry> {
claimed
.into_iter()
.map(|entry| {
let mut fields = entry.fields;
insert_meta_header(&mut fields, DELIVERY_COUNT_HEADER, entry.delivery_count);
insert_meta_header(&mut fields, IDLE_MS_HEADER, entry.idle_ms);
Entry {
id: entry.id,
fields,
delivered: Some(entry.delivery_count + 1),
}
})
.collect()
}
async fn fetch_reclaim(
&mut self,
min_idle: Duration,
count: u64,
) -> Result<Vec<Entry>, RedisError> {
let (cursor, entries): (String, Vec<XReadValue<String, String, Vec<u8>>>) = self
.reader
.client()
.xautoclaim_values(
self.key.as_str(),
self.group.as_str(),
self.consumer.as_str(),
duration_to_millis(min_idle),
self.cursor.as_str(),
Some(count),
false,
)
.await
.map_err(RedisError::stream)?;
self.cursor = cursor;
if entries.is_empty() {
#[cfg(feature = "testing")]
if self.tap.in_process() {
return Ok(Vec::new());
}
tokio::time::sleep(self.block).await;
return Ok(Vec::new());
}
self.enrich_reclaimed(entries, count).await
}
async fn enrich_reclaimed(
&self,
entries: Vec<XReadValue<String, String, Vec<u8>>>,
limit: u64,
) -> Result<Vec<Entry>, RedisError> {
let meta = self.pending_meta(limit).await?;
let mut out = Vec::with_capacity(entries.len());
for (id, mut fields) in entries {
let (idle, count) = meta.get(&id).copied().unwrap_or((0, 0));
insert_meta_header(&mut fields, DELIVERY_COUNT_HEADER, count);
insert_meta_header(&mut fields, IDLE_MS_HEADER, idle);
out.push(Entry {
id,
fields,
delivered: Some(count),
});
}
Ok(out)
}
async fn pending_meta(&self, limit: u64) -> Result<HashMap<String, (u64, u64)>, RedisError> {
let rows: Vec<(String, String, u64, u64)> = self
.reader
.client()
.xpending(
self.key.as_str(),
self.group.as_str(),
(0_u64, "-", "+", limit, self.consumer.as_str()),
)
.await
.map_err(RedisError::stream)?;
Ok(rows
.into_iter()
.map(|(id, _consumer, idle, count)| (id, (idle, count)))
.collect())
}
}
impl RedisSubscriber {
pub(crate) fn round_batches<'a>(
&'a mut self,
window: &'a Arc<Window<StreamForm>>,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Vec<RoundMessage<StreamForm>>, RedisError>> + Send + 'a {
let count = u64::try_from(size.get()).unwrap_or(u64::MAX);
window.fill_at(size.get());
unfold(self, move |s| async move {
loop {
if let Some(failure) = window.take_failure() {
return Some((Err(failure), s));
}
window.discarded(s.discard_stale());
if !s.buffer.is_empty() {
let tail = s.buffer.split_off(size.get().min(s.buffer.len()));
let entries = std::mem::replace(&mut s.buffer, tail);
let members = u32::try_from(entries.len()).unwrap_or(u32::MAX);
let round = window.open_batch(members);
let mut batch = Vec::with_capacity(entries.len());
for entry in entries {
match s.message(entry) {
Ok(delivery) => batch.push(RoundMessage::new(
delivery,
Arc::clone(window),
round.clone(),
)),
Err(err) => return Some((Err(err), s)),
}
}
return Some((Ok(batch), s));
}
if let Err(err) = s.fetch(count).await {
return Some((Err(err), s));
}
window.fetched(s.buffer.len());
}
})
}
}
fn insert_meta_header(fields: &mut HashMap<String, Vec<u8>>, name: &str, value: u64) {
fields.insert(
format!("{HEADER_PREFIX}{name}"),
value.to_string().into_bytes(),
);
}
impl Subscriber for RedisSubscriber {
type Message = RedisMessage;
type Error = RedisError;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
unfold(self, |s| async move {
loop {
let _ = s.discard_stale();
if let Some(entry) = s.buffer.pop_front() {
return Some((s.message(entry), s));
}
if let Err(err) = s.fetch(PREFETCH).await {
return Some((Err(err), s));
}
}
})
}
}
impl Seekable for RedisSubscriber {
type Seeker = RedisGroupSeeker;
fn seeker(&self) -> RedisGroupSeeker {
(*self.seeker).clone()
}
}
impl BatchSubscriber for RedisSubscriber {
type Batch = Vec<RedisMessage>;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
let count = u64::try_from(size.get()).unwrap_or(u64::MAX);
unfold(self, move |s| async move {
loop {
let _ = s.discard_stale();
if !s.buffer.is_empty() {
let tail = s.buffer.split_off(size.get().min(s.buffer.len()));
let entries = std::mem::replace(&mut s.buffer, tail);
let batch = entries
.into_iter()
.map(|entry| s.message(entry))
.collect::<Result<Vec<_>, _>>();
return Some((batch, s));
}
if let Err(err) = s.fetch(count).await {
return Some((Err(err), s));
}
}
})
}
}