use std::collections::HashMap;
use std::error::Error as StdError;
use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use futures::future::BoxFuture;
pub const SHARD_END: &str = "SHARD_END";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LeaseKey {
stream: Arc<str>,
shard: Arc<str>,
}
impl LeaseKey {
#[must_use]
pub fn new(stream: impl Into<Arc<str>>, shard: impl Into<Arc<str>>) -> Self {
Self {
stream: stream.into(),
shard: shard.into(),
}
}
#[must_use]
pub fn stream(&self) -> &str {
&self.stream
}
#[must_use]
pub fn shard(&self) -> &str {
&self.shard
}
pub(crate) const fn shard_shared(&self) -> &Arc<str> {
&self.shard
}
}
impl fmt::Display for LeaseKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.stream, self.shard)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct LeaseState {
pub checkpoint: Option<String>,
}
pub type LeaseError = Box<dyn StdError + Send + Sync>;
pub trait LeaseStore: Send + Sync + 'static {
fn acquire<'a>(
&'a self,
key: &'a LeaseKey,
owner: &'a str,
ttl: Duration,
) -> BoxFuture<'a, Result<bool, LeaseError>>;
fn renew<'a>(
&'a self,
key: &'a LeaseKey,
owner: &'a str,
ttl: Duration,
) -> BoxFuture<'a, Result<bool, LeaseError>>;
fn checkpoint<'a>(
&'a self,
key: &'a LeaseKey,
owner: &'a str,
sequence: &'a str,
) -> BoxFuture<'a, Result<bool, LeaseError>>;
fn read<'a>(&'a self, key: &'a LeaseKey) -> BoxFuture<'a, Result<LeaseState, LeaseError>>;
fn release<'a>(
&'a self,
key: &'a LeaseKey,
owner: &'a str,
) -> BoxFuture<'a, Result<(), LeaseError>>;
}
#[derive(Debug, Default)]
struct MemoryLease {
owner: Option<String>,
expires: Option<Instant>,
checkpoint: Option<String>,
}
#[derive(Debug, Default)]
pub struct MemoryLeaseStore {
leases: Mutex<HashMap<LeaseKey, MemoryLease>>,
}
impl MemoryLeaseStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn with_lease<R>(&self, key: &LeaseKey, f: impl FnOnce(&mut MemoryLease) -> R) -> R {
let mut leases = self.leases.lock().expect("lease store mutex poisoned");
if let Some(lease) = leases.get_mut(key) {
return f(lease);
}
f(leases.entry(key.clone()).or_default())
}
}
impl LeaseStore for MemoryLeaseStore {
fn acquire<'a>(
&'a self,
key: &'a LeaseKey,
owner: &'a str,
ttl: Duration,
) -> BoxFuture<'a, Result<bool, LeaseError>> {
Box::pin(async move {
Ok(self.with_lease(key, |lease| {
let now = Instant::now();
let held = lease.owner.as_deref().is_some_and(|current| {
current != owner && lease.expires.is_some_and(|expiry| expiry > now)
});
if held {
false
} else {
lease.owner = Some(owner.to_owned());
lease.expires = Some(now + ttl);
true
}
}))
})
}
fn renew<'a>(
&'a self,
key: &'a LeaseKey,
owner: &'a str,
ttl: Duration,
) -> BoxFuture<'a, Result<bool, LeaseError>> {
Box::pin(async move {
Ok(self.with_lease(key, |lease| {
if lease.owner.as_deref() == Some(owner) {
lease.expires = Some(Instant::now() + ttl);
true
} else {
false
}
}))
})
}
fn checkpoint<'a>(
&'a self,
key: &'a LeaseKey,
owner: &'a str,
sequence: &'a str,
) -> BoxFuture<'a, Result<bool, LeaseError>> {
Box::pin(async move {
Ok(self.with_lease(key, |lease| {
if lease.owner.as_deref() == Some(owner) {
lease.checkpoint = Some(sequence.to_owned());
true
} else {
false
}
}))
})
}
fn read<'a>(&'a self, key: &'a LeaseKey) -> BoxFuture<'a, Result<LeaseState, LeaseError>> {
Box::pin(async move {
Ok(self.with_lease(key, |lease| LeaseState {
checkpoint: lease.checkpoint.clone(),
}))
})
}
fn release<'a>(
&'a self,
key: &'a LeaseKey,
owner: &'a str,
) -> BoxFuture<'a, Result<(), LeaseError>> {
Box::pin(async move {
self.with_lease(key, |lease| {
if lease.owner.as_deref() == Some(owner) {
lease.owner = None;
lease.expires = None;
}
});
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const TTL: Duration = Duration::from_mins(1);
fn key(shard: &str) -> LeaseKey {
LeaseKey::new("orders", shard)
}
#[tokio::test]
async fn a_live_lease_is_exclusive_and_an_expired_one_is_stealable() {
let store = MemoryLeaseStore::new();
let s1 = key("s1");
assert!(store.acquire(&s1, "a", TTL).await.expect("acquire"));
assert!(!store.acquire(&s1, "b", TTL).await.expect("acquire"));
let store = MemoryLeaseStore::new();
assert!(
store
.acquire(&s1, "a", Duration::ZERO)
.await
.expect("acquire")
);
assert!(store.acquire(&s1, "b", TTL).await.expect("steal"));
assert!(!store.renew(&s1, "a", TTL).await.expect("renew"));
assert!(!store.checkpoint(&s1, "a", "42").await.expect("checkpoint"));
}
#[tokio::test]
async fn checkpoints_survive_release() {
let store = MemoryLeaseStore::new();
let s1 = key("s1");
assert!(store.acquire(&s1, "a", TTL).await.expect("acquire"));
assert!(store.checkpoint(&s1, "a", "41").await.expect("checkpoint"));
store.release(&s1, "a").await.expect("release");
assert_eq!(
store.read(&s1).await.expect("read").checkpoint.as_deref(),
Some("41")
);
}
#[tokio::test]
async fn the_same_shard_id_on_two_streams_is_two_leases() {
let store = MemoryLeaseStore::new();
let orders = LeaseKey::new("orders", "shardId-000000000000");
let refunds = LeaseKey::new("refunds", "shardId-000000000000");
assert!(store.acquire(&orders, "a", TTL).await.expect("acquire"));
assert!(
store.acquire(&refunds, "b", TTL).await.expect("acquire"),
"a lease on one stream must not hold the same shard id on another",
);
assert!(
store
.checkpoint(&orders, "a", "41")
.await
.expect("checkpoint")
);
assert_eq!(
store.read(&refunds).await.expect("read").checkpoint,
None,
"one stream's progress must not become another's",
);
assert!(
store
.checkpoint(&refunds, "b", SHARD_END)
.await
.expect("checkpoint")
);
assert_eq!(
store
.read(&orders)
.await
.expect("read")
.checkpoint
.as_deref(),
Some("41"),
"a shard finished on one stream must stay open on another",
);
}
}