use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use fred::clients::Pool;
use fred::interfaces::StreamsInterface;
use ruststream::{AckError, HeaderMap, IncomingMessage, Partitioned, Positioned};
use crate::convert::fields_for_publish;
use crate::delay::DelayConfig;
use crate::loopback::InFlight;
use crate::seek::{EntryId, RedisGroupPosition, RedisGroupSeeker};
use crate::stream::RequeueMode;
pub const PARTITION_KEY_HEADER: &str = "redis-partition-key";
pub const DELIVERY_COUNT_HEADER: &str = "redis-delivery-count";
pub const IDLE_MS_HEADER: &str = "redis-idle-ms";
struct AckHandle {
pool: Pool,
key: String,
group: String,
id: String,
}
pub struct RedisMessage {
payload: Bytes,
headers: HeaderMap,
ack: Option<AckHandle>,
entry: EntryId,
delay: Option<DelayConfig>,
delivered: Option<u64>,
seeker: Arc<RedisGroupSeeker>,
requeue: RequeueMode,
#[cfg_attr(
not(feature = "testing"),
allow(
dead_code,
reason = "held for its release on drop, which only `testing` gives"
)
)]
flight: InFlight,
}
impl Debug for RedisMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("RedisMessage");
s.field("payload_len", &self.payload.len());
if let Some(ack) = &self.ack {
s.field("key", &ack.key).field("id", &ack.id);
}
s.finish_non_exhaustive()
}
}
impl RedisMessage {
#[allow(
clippy::too_many_arguments,
reason = "internal constructor mirroring the descriptor"
)]
pub(crate) fn new(
pool: Pool,
key: String,
group: String,
id: String,
entry: EntryId,
payload: Bytes,
headers: HeaderMap,
delay: Option<DelayConfig>,
delivered: Option<u64>,
seeker: Arc<RedisGroupSeeker>,
requeue: RequeueMode,
flight: InFlight,
) -> Self {
Self {
payload,
headers,
ack: Some(AckHandle {
pool,
key,
group,
id,
}),
entry,
delay,
delivered,
seeker,
requeue,
flight,
}
}
#[must_use]
pub fn id(&self) -> Option<&str> {
self.ack.as_ref().map(|a| a.id.as_str())
}
#[must_use]
pub const fn entry_id(&self) -> EntryId {
self.entry
}
#[must_use]
pub fn group(&self) -> Option<&str> {
self.ack.as_ref().map(|a| a.group.as_str())
}
pub(crate) fn seeker(&self) -> &RedisGroupSeeker {
&self.seeker
}
#[cfg(feature = "testing")]
pub(crate) fn take_flight(&mut self) -> InFlight {
std::mem::take(&mut self.flight)
}
pub(crate) fn into_settle(mut self) -> (String, Bytes, HeaderMap) {
let handle = self.ack.take().expect("RedisMessage settled twice");
(
handle.id,
std::mem::take(&mut self.payload),
std::mem::take(&mut self.headers),
)
}
}
impl Partitioned for RedisMessage {
fn partition_key(&self) -> Option<&[u8]> {
self.headers().get(PARTITION_KEY_HEADER)
}
}
impl Positioned for RedisMessage {
type Position = RedisGroupPosition;
fn position(&self) -> RedisGroupPosition {
RedisGroupPosition::after(self.entry.previous())
}
}
impl IncomingMessage for RedisMessage {
fn payload(&self) -> &[u8] {
&self.payload
}
fn headers(&self) -> &HeaderMap {
&self.headers
}
fn partition_key(&self) -> Option<&[u8]> {
Partitioned::partition_key(self)
}
async fn ack(mut self) -> Result<(), AckError> {
let handle = self.ack.take().expect("RedisMessage settled twice");
self.seeker.ensure_open_to_settle()?;
xack(&handle).await
}
fn redelivery_count(&self) -> Option<u64> {
self.delivered
}
async fn nack(mut self, requeue: bool) -> Result<(), AckError> {
let handle = self.ack.take().expect("RedisMessage settled twice");
self.seeker.ensure_open_to_settle()?;
if requeue && let RequeueMode::LeavePending { .. } = self.requeue {
drop(handle);
return Ok(());
}
if requeue {
republish(&handle, &self.payload, &self.headers).await?;
}
xack(&handle).await
}
fn supports_nack_after(&self) -> bool {
self.delay.is_some() || matches!(self.requeue, RequeueMode::LeavePending { .. })
}
async fn nack_after(mut self, delay: Duration) -> Result<(), AckError> {
let handle = self.ack.take().expect("RedisMessage settled twice");
self.seeker.ensure_open_to_settle()?;
let Some(cfg) = self.delay.as_ref() else {
if let RequeueMode::LeavePending { .. } = self.requeue {
drop(handle);
return Ok(());
}
return Err(AckError::Unsupported);
};
cfg.schedule(
&handle.pool,
&handle.id,
&self.payload,
&self.headers,
delay,
)
.await?;
xack(&handle).await
}
}
fn broker_err(err: fred::error::Error) -> AckError {
AckError::Broker(Box::new(err))
}
async fn republish(
handle: &AckHandle,
payload: &[u8],
headers: &HeaderMap,
) -> Result<(), AckError> {
let fields = fields_for_publish(payload.to_vec(), headers);
let _: String = handle
.pool
.xadd(handle.key.as_str(), false, None::<()>, "*", fields)
.await
.map_err(broker_err)?;
Ok(())
}
async fn xack(handle: &AckHandle) -> Result<(), AckError> {
let _: i64 = handle
.pool
.xack(
handle.key.as_str(),
handle.group.as_str(),
handle.id.as_str(),
)
.await
.map_err(broker_err)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicU64;
use super::*;
use crate::connection::Connection;
use crate::context::keys::{ConsumerGroup, EntryId as EntryIdKey, Position, SeekHandle};
use crate::context::{StreamBatchContext, StreamContext};
use fred::clients::Pool;
use fred::types::config::Config;
use ruststream::{BuildBatchContext, BuildContext, Field};
fn offline_pool() -> Pool {
Pool::new(Config::default(), None, None, None, 1).expect("offline pool")
}
fn delivery(id: &str) -> RedisMessage {
let pool = offline_pool();
let seeker = Arc::new(RedisGroupSeeker::new(
Connection::new(pool.clone()),
"orders",
"workers",
Arc::new(AtomicU64::new(0)),
));
RedisMessage::new(
pool,
"orders".to_owned(),
"workers".to_owned(),
id.to_owned(),
id.parse().expect("valid entry id"),
Bytes::from_static(b"{}"),
HeaderMap::new(),
None,
None,
seeker,
RequeueMode::Republish,
InFlight::default(),
)
}
#[test]
fn build_context_reads_the_native_fields() {
let cx = StreamContext::build(&delivery("1700000000000-0"));
assert_eq!(EntryIdKey.get(&cx), EntryId::new(1_700_000_000_000, 0));
assert_eq!(ConsumerGroup.get(&cx), "workers");
assert_eq!(SeekHandle.get(&cx).key(), "orders");
}
#[test]
fn build_context_carries_the_position_that_redelivers() {
let cx = StreamContext::build(&delivery("1700000000000-4"));
assert_eq!(
Position.get(&cx),
RedisGroupPosition::after(EntryId::new(1_700_000_000_000, 3))
);
}
#[test]
fn build_batch_context_carries_only_subscription_scoped_fields() {
let cx = StreamBatchContext::build(&delivery("1700000000000-0"));
assert_eq!(ConsumerGroup.get(&cx), "workers");
assert_eq!(SeekHandle.get(&cx).group(), "workers");
}
#[test]
fn position_pins_the_delivery_for_redelivery() {
assert_eq!(
delivery("1700000000000-4").position(),
RedisGroupPosition::after(EntryId::new(1_700_000_000_000, 3))
);
assert_eq!(
delivery("1700000000000-0").position(),
RedisGroupPosition::after(EntryId::new(1_699_999_999_999, u64::MAX))
);
}
}