1use std::collections::HashMap;
10use std::error::Error as StdError;
11use std::sync::Mutex;
12use std::time::Duration;
13
14use futures::future::BoxFuture;
15
16pub const SHARD_END: &str = "SHARD_END";
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
21#[non_exhaustive]
22pub struct LeaseState {
23 pub checkpoint: Option<String>,
26}
27
28pub type LeaseError = Box<dyn StdError + Send + Sync>;
30
31pub trait LeaseStore: Send + Sync + 'static {
38 fn acquire<'a>(
41 &'a self,
42 shard: &'a str,
43 owner: &'a str,
44 ttl: Duration,
45 ) -> BoxFuture<'a, Result<bool, LeaseError>>;
46
47 fn renew<'a>(
49 &'a self,
50 shard: &'a str,
51 owner: &'a str,
52 ttl: Duration,
53 ) -> BoxFuture<'a, Result<bool, LeaseError>>;
54
55 fn checkpoint<'a>(
58 &'a self,
59 shard: &'a str,
60 owner: &'a str,
61 sequence: &'a str,
62 ) -> BoxFuture<'a, Result<bool, LeaseError>>;
63
64 fn read<'a>(&'a self, shard: &'a str) -> BoxFuture<'a, Result<LeaseState, LeaseError>>;
66
67 fn release<'a>(
69 &'a self,
70 shard: &'a str,
71 owner: &'a str,
72 ) -> BoxFuture<'a, Result<(), LeaseError>>;
73}
74
75#[derive(Debug, Default)]
76struct MemoryLease {
77 owner: Option<String>,
78 expires: Option<std::time::Instant>,
79 checkpoint: Option<String>,
80}
81
82#[derive(Debug, Default)]
87pub struct MemoryLeaseStore {
88 leases: Mutex<HashMap<String, MemoryLease>>,
89}
90
91impl MemoryLeaseStore {
92 #[must_use]
94 pub fn new() -> Self {
95 Self::default()
96 }
97
98 fn with_lease<R>(&self, shard: &str, f: impl FnOnce(&mut MemoryLease) -> R) -> R {
99 let mut leases = self.leases.lock().expect("lease store mutex poisoned");
100 f(leases.entry(shard.to_owned()).or_default())
101 }
102}
103
104impl LeaseStore for MemoryLeaseStore {
105 fn acquire<'a>(
106 &'a self,
107 shard: &'a str,
108 owner: &'a str,
109 ttl: Duration,
110 ) -> BoxFuture<'a, Result<bool, LeaseError>> {
111 Box::pin(async move {
112 Ok(self.with_lease(shard, |lease| {
113 let now = std::time::Instant::now();
114 let held = lease.owner.as_deref().is_some_and(|current| {
115 current != owner && lease.expires.is_some_and(|expiry| expiry > now)
116 });
117 if held {
118 false
119 } else {
120 lease.owner = Some(owner.to_owned());
121 lease.expires = Some(now + ttl);
122 true
123 }
124 }))
125 })
126 }
127
128 fn renew<'a>(
129 &'a self,
130 shard: &'a str,
131 owner: &'a str,
132 ttl: Duration,
133 ) -> BoxFuture<'a, Result<bool, LeaseError>> {
134 Box::pin(async move {
135 Ok(self.with_lease(shard, |lease| {
136 if lease.owner.as_deref() == Some(owner) {
137 lease.expires = Some(std::time::Instant::now() + ttl);
138 true
139 } else {
140 false
141 }
142 }))
143 })
144 }
145
146 fn checkpoint<'a>(
147 &'a self,
148 shard: &'a str,
149 owner: &'a str,
150 sequence: &'a str,
151 ) -> BoxFuture<'a, Result<bool, LeaseError>> {
152 Box::pin(async move {
153 Ok(self.with_lease(shard, |lease| {
154 if lease.owner.as_deref() == Some(owner) {
155 lease.checkpoint = Some(sequence.to_owned());
156 true
157 } else {
158 false
159 }
160 }))
161 })
162 }
163
164 fn read<'a>(&'a self, shard: &'a str) -> BoxFuture<'a, Result<LeaseState, LeaseError>> {
165 Box::pin(async move {
166 Ok(self.with_lease(shard, |lease| LeaseState {
167 checkpoint: lease.checkpoint.clone(),
168 }))
169 })
170 }
171
172 fn release<'a>(
173 &'a self,
174 shard: &'a str,
175 owner: &'a str,
176 ) -> BoxFuture<'a, Result<(), LeaseError>> {
177 Box::pin(async move {
178 self.with_lease(shard, |lease| {
179 if lease.owner.as_deref() == Some(owner) {
180 lease.owner = None;
181 lease.expires = None;
182 }
183 });
184 Ok(())
185 })
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[tokio::test]
194 async fn a_live_lease_is_exclusive_and_an_expired_one_is_stealable() {
195 let store = MemoryLeaseStore::new();
196 let ttl = Duration::from_mins(1);
197 assert!(store.acquire("s1", "a", ttl).await.expect("acquire"));
198 assert!(!store.acquire("s1", "b", ttl).await.expect("acquire"));
199
200 let store = MemoryLeaseStore::new();
202 assert!(
203 store
204 .acquire("s1", "a", Duration::ZERO)
205 .await
206 .expect("acquire")
207 );
208 assert!(store.acquire("s1", "b", ttl).await.expect("steal"));
209 assert!(!store.renew("s1", "a", ttl).await.expect("renew"));
211 assert!(!store.checkpoint("s1", "a", "42").await.expect("checkpoint"));
212 }
213
214 #[tokio::test]
215 async fn checkpoints_survive_release() {
216 let store = MemoryLeaseStore::new();
217 let ttl = Duration::from_mins(1);
218 assert!(store.acquire("s1", "a", ttl).await.expect("acquire"));
219 assert!(store.checkpoint("s1", "a", "41").await.expect("checkpoint"));
220 store.release("s1", "a").await.expect("release");
221 assert_eq!(
222 store.read("s1").await.expect("read").checkpoint.as_deref(),
223 Some("41")
224 );
225 }
226}