use std::collections::VecDeque;
use std::fmt::{Debug, Formatter};
use std::future::{Future, ready};
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use fred::clients::{Client, Pool};
use fred::error::ErrorKind;
use fred::interfaces::{KeysInterface, ListInterface, SortedSetsInterface};
use fred::types::Value;
use fred::types::lists::LMoveDirection;
use futures::Stream;
use futures::stream::unfold;
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::Bindings;
use ruststream::codec::Codec;
use ruststream::{
AckError, AddressedCopies, BatchSubscriber, BufferedSubscriber, HeaderMap, IncomingMessage,
Lend, PairError, Partitioned, PublishPolicy, RedeliveryAddress, RedeliveryAddressed,
RetryDeclaration, SubscriptionSource,
};
use crate::broker::{ConnectedRedisBroker, RedisCore};
use crate::connection::{Connection, ReadConnection};
use crate::envelope::{SharedEnvelope, frame, unframe};
use crate::loopback::{InFlight, Loopback, Tap};
use crate::partition::{RedisPublishOptions, resolved_headers};
use crate::pipeline::{ListForm, RoundMessage, Window};
use crate::publisher::joins_round;
use crate::recovery::{self, RecoveryConfig};
use crate::route::Route;
use crate::{error::RedisError, message::PARTITION_KEY_HEADER};
pub use crate::list::RedisListPublish as Publish;
pub mod prelude {
pub use ruststream::prelude::*;
pub use super::{Publish, RedisList};
pub use crate::context::{PipelineContext, PoolContext, keys};
pub use crate::pipeline::{Bindable, InRound, RedisPipelineSteps};
pub use crate::{
PARTITION_KEY_HEADER, RedisBroker, RedisPublishOptions, RedisPublishSteps,
RedisSubscribeExt,
};
#[cfg(any(
feature = "tls-rustls",
feature = "tls-rustls-ring",
feature = "tls-native-tls"
))]
pub use crate::{TlsConfig, TlsConnector};
}
const DEFAULT_BLOCK: Duration = Duration::from_secs(5);
const PROCESSING_SUFFIX: &str = ".processing";
fn block_secs(block: Duration) -> f64 {
block.as_secs_f64()
}
fn empty_on_timeout<T>(
result: Result<Option<T>, fred::error::Error>,
) -> Result<Option<T>, RedisError> {
match result {
Ok(value) => Ok(value),
Err(err) if matches!(err.kind(), ErrorKind::Timeout) => Ok(None),
Err(err) => Err(RedisError::stream(err)),
}
}
#[derive(Clone)]
#[must_use]
pub struct RedisList {
key: String,
reliable: bool,
processing: Option<String>,
block: Option<Duration>,
codec: Option<SharedEnvelope>,
min_idle: Option<Duration>,
recovery_zset: Option<String>,
recovery_ttl: Option<Duration>,
dead_letter: Option<String>,
}
impl Debug for RedisList {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisList")
.field("key", &self.key)
.field("reliable", &self.reliable)
.field("processing", &self.processing)
.field("codec", &self.codec.is_some())
.field("recovery_zset", &self.recovery_zset)
.field("recovery_ttl", &self.recovery_ttl)
.field("dead_letter", &self.dead_letter)
.finish_non_exhaustive()
}
}
impl RedisList {
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
reliable: false,
processing: None,
block: None,
codec: None,
min_idle: None,
recovery_zset: None,
recovery_ttl: None,
dead_letter: None,
}
}
pub const fn reliable(mut self) -> Self {
self.reliable = true;
self
}
pub fn processing(mut self, key: impl Into<String>) -> Self {
self.processing = Some(key.into());
self
}
pub const fn block(mut self, block: Duration) -> Self {
self.block = Some(block);
self
}
pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
self.codec = Some(Arc::new(codec));
self
}
pub const fn min_idle(mut self, min_idle: Duration) -> Self {
self.min_idle = Some(min_idle);
self
}
pub fn recovery_zset(mut self, key: impl Into<String>) -> Self {
self.recovery_zset = Some(key.into());
self.reliable = true;
self
}
pub const fn recovery_ttl(mut self, ttl: Duration) -> Self {
self.recovery_ttl = Some(ttl);
self
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
pub(crate) fn into_key(self) -> String {
self.key
}
pub(crate) const fn is_reliable(&self) -> bool {
self.reliable
}
pub(crate) fn processing_or_default(&self) -> String {
self.processing
.clone()
.unwrap_or_else(|| format!("{}{PROCESSING_SUFFIX}", self.key))
}
pub(crate) fn block_or_default(&self) -> Duration {
self.block.unwrap_or(DEFAULT_BLOCK)
}
pub(crate) fn codec_handle(&self) -> Option<SharedEnvelope> {
self.codec.clone()
}
pub(crate) fn route(&self) -> Route {
Route::List {
envelope: self.codec.clone(),
}
}
pub(crate) fn dead_letter(&self) -> Option<&str> {
self.dead_letter.as_deref()
}
fn with_declaration(mut self, declaration: &RetryDeclaration) -> Self {
self.dead_letter = declaration.dead_letter().map(str::to_owned);
self
}
#[cfg(feature = "asyncapi")]
fn describe(&self) -> Bindings {
let processing = self.reliable.then(|| self.processing_or_default());
crate::asyncapi::channel(&crate::asyncapi::ListSubscription::new(
self.reliable,
processing.as_deref(),
crate::asyncapi::Envelope::of(self.codec.as_ref()),
))
}
pub(crate) fn recovery_config(&self) -> Result<Option<RecoveryConfig>, RedisError> {
let Some(zset_key) = self.recovery_zset.clone() else {
return Ok(None);
};
let min_idle = self.min_idle.ok_or_else(|| {
RedisError::InvalidOptions(format!(
"reliable list recovery on `{}` needs a min_idle: call .min_idle(duration) \
alongside .recovery_zset(key)",
self.key
))
})?;
Ok(Some(RecoveryConfig {
zset_key,
min_idle,
ttl: self.recovery_ttl,
loopback: Loopback::default(),
}))
}
}
impl SubscriptionSource<ConnectedRedisBroker> for RedisList {
type Subscriber = RedisListSubscriber;
type Copies = AddressedCopies;
fn name(&self) -> &str {
self.key()
}
fn declare_retry(self, declaration: &RetryDeclaration) -> Self {
self.with_declaration(declaration)
}
async fn subscribe(
self,
connected: &ConnectedRedisBroker,
) -> Result<Self::Subscriber, RedisError> {
connected.subscribe_list(self).await
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self) -> Bindings {
self.describe()
}
}
impl RedeliveryAddressed<ConnectedRedisBroker> for RedisList {
fn redelivery_address(
&self,
_connected: &ConnectedRedisBroker,
) -> impl Future<Output = Result<RedeliveryAddress, RedisError>> {
ready(Ok(RedeliveryAddress::new(self.key.clone())))
}
}
pub struct RedisListSubscriber(BufferedSubscriber<ListWire>);
impl Debug for RedisListSubscriber {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("RedisListSubscriber").field(&self.0).finish()
}
}
impl RedisListSubscriber {
pub(crate) fn new(wire: ListWire) -> Self {
Self(BufferedSubscriber::new(wire))
}
}
impl ruststream::Subscriber for RedisListSubscriber {
type Message = RedisListMessage;
type Error = RedisError;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
self.0.stream()
}
}
impl BatchSubscriber for RedisListSubscriber {
type Batch = Vec<RedisListMessage>;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
self.0.batches(size)
}
}
pub(crate) struct ListWire {
connection: Arc<Connection>,
reader: ReadConnection,
key: String,
reliable: bool,
processing: String,
block: Duration,
codec: Option<SharedEnvelope>,
recovery: Option<RecoveryConfig>,
tap: Tap,
}
impl Debug for ListWire {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListWire")
.field("key", &self.key)
.field("reliable", &self.reliable)
.field("recovery", &self.recovery.is_some())
.finish_non_exhaustive()
}
}
impl ListWire {
#[allow(
clippy::too_many_arguments,
reason = "internal constructor mirroring the descriptor"
)]
pub(crate) fn new(
connection: Arc<Connection>,
reader: ReadConnection,
key: String,
reliable: bool,
processing: String,
block: Duration,
codec: Option<SharedEnvelope>,
recovery: Option<RecoveryConfig>,
tap: Tap,
) -> Self {
Self {
connection,
reader,
key,
reliable,
processing,
block,
codec,
recovery,
tap,
}
}
fn simple_message(&self, raw: &[u8]) -> RedisListMessage {
let (payload, headers) = unframe(self.codec.as_ref(), raw);
RedisListMessage {
payload,
headers,
ack: None,
connection: Arc::clone(&self.connection),
flight: self.tap.delivered(),
}
}
fn reliable_message(&self, raw: Vec<u8>, recovery: Option<RecoveryHandle>) -> RedisListMessage {
let (payload, headers) = unframe(self.codec.as_ref(), &raw);
RedisListMessage {
payload,
headers,
connection: Arc::clone(&self.connection),
ack: Some(ListAck {
main_key: self.key.clone(),
processing_key: self.processing.clone(),
value: raw,
recovery,
}),
flight: self.tap.delivered(),
}
}
async fn next_entry(&self) -> Result<Option<RedisListMessage>, RedisError> {
#[cfg(feature = "testing")]
self.tap.readable().await;
self.connection.ensure_open()?;
let secs = block_secs(self.block);
if self.reliable {
if let Some(cfg) = &self.recovery {
recovery::sweep_orphans(self.connection.pool(), cfg, &self.key, &self.processing)
.await?;
}
let value: Option<Vec<u8>> = empty_on_timeout(
self.reader
.client()
.blmove(
self.key.as_str(),
self.processing.as_str(),
LMoveDirection::Right,
LMoveDirection::Left,
secs,
)
.await,
)?;
let Some(value) = value else {
return Ok(None);
};
let handle = match &self.recovery {
Some(cfg) => {
let member =
recovery::record_claim(self.connection.pool(), cfg, &value).await?;
Some(RecoveryHandle {
zset_key: cfg.zset_key.clone(),
member,
})
}
None => None,
};
Ok(Some(self.reliable_message(value, handle)))
} else {
let popped: Option<(String, Vec<u8>)> =
empty_on_timeout(self.reader.client().brpop(self.key.as_str(), secs).await)?;
Ok(popped.map(|(_, v)| self.simple_message(&v)))
}
}
}
type Claimed = (Vec<u8>, Option<RecoveryHandle>);
impl ListWire {
pub(crate) fn round_form(&self) -> ListForm {
ListForm::new(
self.key.as_str(),
self.processing.as_str(),
self.recovery.as_ref().map(|cfg| cfg.zset_key.as_str()),
)
}
pub(crate) fn round_client(&self) -> Client {
self.connection.pool().next().clone()
}
pub(crate) fn key(&self) -> &str {
&self.key
}
async fn claim(&self, count: usize, into: &mut VecDeque<Claimed>) -> Result<(), RedisError> {
self.connection.ensure_open()?;
let before = into.len();
if self.reliable {
if let Some(cfg) = &self.recovery {
recovery::sweep_orphans(self.connection.pool(), cfg, &self.key, &self.processing)
.await?;
}
let claims = self.reader.client().pipeline();
for _ in 0..count {
claims
.lmove::<(), _, _>(
self.key.as_str(),
self.processing.as_str(),
LMoveDirection::Right,
LMoveDirection::Left,
)
.await
.map_err(RedisError::stream)?;
}
for reply in claims.try_all::<Value>().await {
match reply.map_err(RedisError::stream)? {
Value::Null => {}
value => into.push_back((value_bytes(value)?, None)),
}
}
} else {
let popped: Value = self
.reader
.client()
.rpop(self.key.as_str(), Some(count))
.await
.map_err(RedisError::stream)?;
if let Value::Array(values) = popped {
for value in values {
into.push_back((value_bytes(value)?, None));
}
}
}
if into.len() == before
&& let Some(value) = self.wait_one().await?
{
into.push_back((value, None));
}
self.track(into.range_mut(before..)).await
}
async fn wait_one(&self) -> Result<Option<Vec<u8>>, RedisError> {
#[cfg(feature = "testing")]
self.tap.readable().await;
let secs = block_secs(self.block);
if self.reliable {
empty_on_timeout(
self.reader
.client()
.blmove(
self.key.as_str(),
self.processing.as_str(),
LMoveDirection::Right,
LMoveDirection::Left,
secs,
)
.await,
)
} else {
let popped: Option<(String, Vec<u8>)> =
empty_on_timeout(self.reader.client().brpop(self.key.as_str(), secs).await)?;
Ok(popped.map(|(_, value)| value))
}
}
async fn track<'a>(
&self,
claimed: impl Iterator<Item = &'a mut Claimed>,
) -> Result<(), RedisError> {
let Some(cfg) = &self.recovery else {
return Ok(());
};
let tracking = self.connection.pool().next().pipeline();
let mut any = false;
for (value, handle) in claimed {
let (score, member) = cfg.tracked(value);
tracking
.zadd::<(), _, _>(
cfg.zset_key.as_str(),
None,
None,
false,
false,
(score, member.clone()),
)
.await
.map_err(RedisError::stream)?;
*handle = Some(RecoveryHandle {
zset_key: cfg.zset_key.clone(),
member,
});
any = true;
}
if !any {
return Ok(());
}
if let Some(ttl) = cfg.ttl_millis() {
tracking
.pexpire::<(), _>(cfg.zset_key.as_str(), ttl, None)
.await
.map_err(RedisError::stream)?;
}
for reply in tracking.try_all::<Value>().await {
reply.map_err(RedisError::stream)?;
}
Ok(())
}
fn message_of(&self, (value, recovery): Claimed) -> RedisListMessage {
if self.reliable {
self.reliable_message(value, recovery)
} else {
self.simple_message(&value)
}
}
}
fn value_bytes(value: Value) -> Result<Vec<u8>, RedisError> {
match value {
Value::Bytes(bytes) => Ok(bytes.to_vec()),
Value::String(text) => Ok(text.as_bytes().to_vec()),
other => Err(RedisError::Stream(
format!("a list read answered with {other:?} where an entry was expected").into(),
)),
}
}
pub struct ListReader {
wire: ListWire,
buffer: VecDeque<Claimed>,
}
impl Debug for ListReader {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListReader")
.field("wire", &self.wire)
.field("buffered", &self.buffer.len())
.finish()
}
}
impl ListReader {
pub(crate) const fn new(wire: ListWire) -> Self {
Self {
wire,
buffer: VecDeque::new(),
}
}
pub(crate) fn round_stream<'a>(
&'a mut self,
window: &'a Arc<Window<ListForm>>,
count: usize,
) -> impl Stream<Item = Result<RoundMessage<ListForm>, RedisError>> + Send + 'a {
unfold(self, move |reader| async move {
loop {
if let Some(failure) = window.take_failure() {
return Some((Err(failure), reader));
}
if let Some(claimed) = reader.buffer.pop_front() {
let delivery = reader.wire.message_of(claimed);
let round = window.open();
return Some((
Ok(RoundMessage::new(delivery, Arc::clone(window), round)),
reader,
));
}
if let Err(err) = reader.wire.claim(count, &mut reader.buffer).await {
return Some((Err(err), reader));
}
window.fetched(reader.buffer.len());
}
})
}
}
impl ListReader {
pub(crate) fn round_batches<'a>(
&'a mut self,
window: &'a Arc<Window<ListForm>>,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Vec<RoundMessage<ListForm>>, RedisError>> + Send + 'a {
window.fill_at(size.get());
unfold(self, move |reader| async move {
loop {
if let Some(failure) = window.take_failure() {
return Some((Err(failure), reader));
}
if !reader.buffer.is_empty() {
let take = size.get().min(reader.buffer.len());
let members = u32::try_from(take).unwrap_or(u32::MAX);
let round = window.open_batch(members);
let batch = reader
.buffer
.drain(..take)
.collect::<Vec<_>>()
.into_iter()
.map(|claimed| {
RoundMessage::new(
reader.wire.message_of(claimed),
Arc::clone(window),
round.clone(),
)
})
.collect();
return Some((Ok(batch), reader));
}
if let Err(err) = reader.wire.claim(size.get(), &mut reader.buffer).await {
return Some((Err(err), reader));
}
window.fetched(reader.buffer.len());
}
})
}
}
impl ruststream::Subscriber for ListWire {
type Message = RedisListMessage;
type Error = RedisError;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
unfold(&*self, |s| async move {
loop {
match s.next_entry().await {
Ok(Some(msg)) => return Some((Ok(msg), s)),
Ok(None) => {}
Err(err) => return Some((Err(err), s)),
}
}
})
}
}
struct ListAck {
main_key: String,
processing_key: String,
value: Vec<u8>,
recovery: Option<RecoveryHandle>,
}
struct RecoveryHandle {
zset_key: String,
member: Vec<u8>,
}
pub struct RedisListMessage {
payload: Bytes,
headers: HeaderMap,
ack: Option<ListAck>,
connection: Arc<Connection>,
#[cfg_attr(
not(feature = "testing"),
allow(
dead_code,
reason = "held for its release on drop, which only `testing` gives"
)
)]
flight: InFlight,
}
impl RedisListMessage {
pub(crate) fn pool(&self) -> &Pool {
self.connection.pool()
}
#[cfg(feature = "testing")]
pub(crate) fn take_flight(&mut self) -> InFlight {
std::mem::take(&mut self.flight)
}
pub(crate) fn into_settle(self) -> Option<(Vec<u8>, Option<Vec<u8>>)> {
self.ack
.map(|handle| (handle.value, handle.recovery.map(|rec| rec.member)))
}
}
impl Debug for RedisListMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisListMessage")
.field("payload_len", &self.payload.len())
.field("reliable", &self.ack.is_some())
.finish_non_exhaustive()
}
}
impl IncomingMessage for RedisListMessage {
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(self) -> Result<(), AckError> {
let Some(handle) = self.ack else {
return Err(AckError::Unsupported);
};
self.connection.ensure_open_to_settle()?;
settle(self.connection.pool(), &handle).await
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
let Some(handle) = self.ack else {
return Err(AckError::Unsupported);
};
self.connection.ensure_open_to_settle()?;
let pool = self.connection.pool();
if requeue {
lpush(pool, handle.main_key.as_str(), handle.value.clone()).await?;
}
settle(pool, &handle).await
}
}
fn ack_broker(err: fred::error::Error) -> AckError {
AckError::Broker(Box::new(err))
}
async fn lpush(pool: &Pool, key: &str, body: Vec<u8>) -> Result<(), AckError> {
let _: i64 = pool.lpush(key, body).await.map_err(ack_broker)?;
Ok(())
}
async fn settle(pool: &Pool, handle: &ListAck) -> Result<(), AckError> {
let _: i64 = pool
.lrem(handle.processing_key.as_str(), 1, handle.value.clone())
.await
.map_err(ack_broker)?;
if let Some(rec) = &handle.recovery {
recovery::forget(pool, &rec.zset_key, &rec.member).await?;
}
Ok(())
}
impl Partitioned for RedisListMessage {
fn partition_key(&self) -> Option<&[u8]> {
self.headers().get(PARTITION_KEY_HEADER)
}
}
#[derive(Clone, Default)]
#[must_use]
pub struct RedisListPublish {
codec: Option<SharedEnvelope>,
ttl: Option<Duration>,
}
impl Debug for RedisListPublish {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisListPublish")
.field("codec", &self.codec.is_some())
.field("ttl", &self.ttl)
.finish()
}
}
impl RedisListPublish {
pub fn new() -> Self {
Self::default()
}
pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
self.codec = Some(Arc::new(codec));
self
}
pub const fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
}
impl PublishPolicy<ConnectedRedisBroker> for RedisListPublish {
type Live = RedisListPublisher;
fn pair(
self,
connected: &ConnectedRedisBroker,
) -> impl Future<Output = Result<Self::Live, PairError>> {
ready(Ok(connected.list_publisher(self)))
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self, channel: &str) -> Bindings {
crate::asyncapi::channel(&crate::asyncapi::Publish::list(
channel,
self.ttl
.map(|ttl| u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX)),
crate::asyncapi::Envelope::of(self.codec.as_ref()),
))
}
}
#[derive(Clone)]
pub struct RedisListPublisher {
core: Arc<RedisCore>,
codec: Option<SharedEnvelope>,
ttl: Option<Duration>,
round_name: u64,
}
impl Debug for RedisListPublisher {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisListPublisher")
.field("codec", &self.codec.is_some())
.field("ttl", &self.ttl)
.finish_non_exhaustive()
}
}
impl RedisListPublisher {
pub(crate) const fn round_name(&self) -> u64 {
self.round_name
}
pub(crate) fn new(core: Arc<RedisCore>, publish: RedisListPublish) -> Self {
Self {
round_name: core.rounds().publisher(),
core,
codec: publish.codec,
ttl: publish.ttl,
}
}
}
fn ttl_millis(ttl: Duration) -> i64 {
i64::try_from(ttl.as_millis()).unwrap_or(i64::MAX).max(1)
}
impl ruststream::Publisher for RedisListPublisher {
type Payload = Lend;
type Error = RedisError;
type Options = RedisPublishOptions;
async fn publish(
&self,
msg: ruststream::OutgoingMessage<'_>,
options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
let (key, payload, headers) = msg.into_parts();
if let Some(round) = self
.core
.rounds()
.joined(self.round_name, joins_round(options))
{
let body = frame(
self.codec.as_ref(),
payload,
&resolved_headers(headers, options),
);
return round
.lpush(key, body, self.ttl.map(ttl_millis))
.await
.map_err(RedisError::publish);
}
push(
&self.core,
self.codec.as_ref(),
self.ttl,
key,
payload,
resolved_headers(headers, options),
)
.await
}
}
pub(crate) async fn push(
core: &RedisCore,
codec: Option<&SharedEnvelope>,
ttl: Option<Duration>,
key: &str,
payload: &[u8],
headers: HeaderMap,
) -> Result<(), RedisError> {
let pool = core.pool()?;
let body = frame(codec, payload, &headers);
let Some(ttl) = ttl else {
let _: i64 = pool.lpush(key, body).await.map_err(RedisError::publish)?;
return Ok(());
};
let pipeline = pool.next().pipeline();
let _: () = pipeline
.lpush(key, body)
.await
.map_err(RedisError::publish)?;
let _: () = pipeline
.pexpire(key, ttl_millis(ttl), None)
.await
.map_err(RedisError::publish)?;
let _: Vec<Value> = pipeline.all().await.map_err(RedisError::publish)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ttl_millis_converts_and_clamps() {
assert_eq!(ttl_millis(Duration::from_mins(1)), 60_000);
assert_eq!(ttl_millis(Duration::from_millis(1)), 1);
assert_eq!(ttl_millis(Duration::from_nanos(1)), 1);
assert_eq!(ttl_millis(Duration::ZERO), 1);
}
}