#![cfg(feature = "cache")]
#![cfg_attr(docsrs, doc(cfg(feature = "cache")))]
use std::fmt::{self, Debug, Formatter};
use std::time::Duration;
use moka::sync::Cache;
#[must_use]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Duplicate {
Yes,
No,
}
#[derive(Clone)]
pub struct TtlSet {
inner: Cache<String, ()>,
}
impl TtlSet {
#[must_use]
pub fn new(ttl: Duration, max_capacity: u64) -> Self {
let inner = Cache::builder()
.time_to_live(ttl)
.max_capacity(max_capacity)
.build();
Self { inner }
}
pub fn reserve(&self, key: impl Into<String>) -> Duplicate {
let key = key.into();
if self.inner.contains_key(&key) {
return Duplicate::Yes;
}
let mut was_new = false;
let () = self.inner.get_with(key, || {
was_new = true;
});
if was_new {
Duplicate::No
} else {
Duplicate::Yes
}
}
#[must_use]
pub fn contains(&self, key: &str) -> bool {
self.inner.contains_key(key)
}
#[must_use]
pub fn entry_count(&self) -> u64 {
self.inner.entry_count()
}
}
impl Debug for TtlSet {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("TtlSet")
.field("entries", &self.inner.entry_count())
.finish()
}
}
pub const DEFAULT_SETTLEMENT_TTL: Duration = Duration::from_mins(2);
pub const DEFAULT_SETTLEMENT_CAPACITY: u64 = 10_000;
#[derive(Debug, Clone)]
pub struct SettlementCache {
inner: TtlSet,
}
impl SettlementCache {
#[must_use]
pub fn new() -> Self {
Self {
inner: TtlSet::new(DEFAULT_SETTLEMENT_TTL, DEFAULT_SETTLEMENT_CAPACITY),
}
}
#[must_use]
pub fn with_params(ttl: Duration, capacity: u64) -> Self {
Self {
inner: TtlSet::new(ttl, capacity),
}
}
#[must_use = "callers MUST honour the Duplicate outcome to enforce idempotency"]
pub fn reserve(&self, key: impl Into<String>) -> Duplicate {
let outcome = self.inner.reserve(key);
#[cfg(feature = "metrics")]
{
let label = match outcome {
Duplicate::No => "inserted",
Duplicate::Yes => "duplicate",
};
::metrics::counter!(
crate::metrics::SETTLEMENT_CACHE_RESERVE_TOTAL,
"outcome" => label,
)
.increment(1);
}
outcome
}
#[must_use]
pub fn entry_count(&self) -> u64 {
self.inner.entry_count()
}
}
impl Default for SettlementCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cache() -> TtlSet {
TtlSet::new(Duration::from_mins(1), 1024)
}
#[test]
fn reserve_fresh_key_is_new() {
let cache = cache();
assert_eq!(cache.reserve("a"), Duplicate::No);
}
#[test]
fn reserve_same_key_twice_is_duplicate() {
let cache = cache();
assert_eq!(cache.reserve("a"), Duplicate::No);
assert_eq!(cache.reserve("a"), Duplicate::Yes);
}
#[test]
fn distinct_keys_are_independent() {
let cache = cache();
assert_eq!(cache.reserve("a"), Duplicate::No);
assert_eq!(cache.reserve("b"), Duplicate::No);
assert_eq!(cache.reserve("a"), Duplicate::Yes);
assert_eq!(cache.reserve("b"), Duplicate::Yes);
}
#[test]
fn settlement_cache_default_is_2_minute_ttl() {
assert_eq!(DEFAULT_SETTLEMENT_TTL, Duration::from_mins(2));
}
#[test]
fn settlement_cache_reserves_then_dedups() {
let cache = SettlementCache::new();
assert_eq!(cache.reserve("0xabc"), Duplicate::No);
assert_eq!(cache.reserve("0xabc"), Duplicate::Yes);
}
#[test]
fn settlement_cache_independent_keys() {
let cache = SettlementCache::new();
assert_eq!(cache.reserve("eip155:8453:0xnonce_a"), Duplicate::No);
assert_eq!(cache.reserve("eip155:8453:0xnonce_b"), Duplicate::No);
assert_eq!(cache.reserve("eip155:8453:0xnonce_a"), Duplicate::Yes);
assert_eq!(cache.reserve("eip155:8453:0xnonce_b"), Duplicate::Yes);
}
}