Skip to main content

ecat_lock/
lib.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2use async_trait::async_trait;
3use std::time::Duration;
4
5/// Distributed lock abstraction.
6///
7/// `acquire` returns an ownership token that must be passed to `release`,
8/// so a lock can only be released by the process that holds it.
9#[async_trait]
10pub trait DistributedLock: Send + Sync {
11    /// Try to acquire the lock for `key` with the given `ttl`.
12    /// Returns `Some(token)` on success, `None` if the lock is held by someone else.
13    async fn acquire(&self, key: &str, ttl: Duration) -> Result<Option<String>, LockError>;
14
15    /// Release the lock for `key`, but only if `token` still matches the holder.
16    async fn release(&self, key: &str, token: &str) -> Result<(), LockError>;
17
18    /// Extend the lock's TTL to `ttl`, but only if `token` still matches the holder.
19    ///
20    /// 默认实现返回错误而非静默成功:不支持续租的后端必须让调用方知道
21    /// 「我以为续上了,其实没有」是危险的(锁会在预期之前过期)。
22    async fn renew(&self, _key: &str, _token: &str, _ttl: Duration) -> Result<(), LockError> {
23        Err(LockError::Other(
24            "renew not supported by this backend".into(),
25        ))
26    }
27}
28
29/// 带单调 fencing token 的锁。
30///
31/// `fence` 每次成功/失败的获取都会递增;资源侧只接受不低于已见 fence 的写入,
32/// 从而拒绝租约过期后仍在写的陈旧持有者。
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct FenceGuard {
35    pub token: String,
36    pub fence: u64,
37}
38
39#[async_trait]
40pub trait FencedLock: DistributedLock {
41    /// 获取锁并返回单调 fence;锁已被别人持有时返回 `None`
42    /// (fence 仍会递增,单调性不受影响)。
43    async fn acquire_fenced(
44        &self,
45        key: &str,
46        ttl: Duration,
47    ) -> Result<Option<FenceGuard>, LockError>;
48
49    /// 当前 fence 计数是否仍等于 `fence`。
50    ///
51    /// fence 在每次获取尝试(成功或失败)时递增,release 不会删除或减少它,
52    /// 因此旧持有者只有在有人重新获取过之后才会被拒绝;干净释放且无人
53    /// 重取时旧 fence 仍然相等。资源侧应记录已见的最大 fence 并拒绝更小的值。
54    async fn validate_fence(&self, key: &str, fence: u64) -> Result<bool, LockError>;
55}
56
57#[derive(Debug, thiserror::Error)]
58pub enum LockError {
59    #[error("lock error: {0}")]
60    Other(String),
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use std::collections::HashMap;
67    use std::sync::Arc;
68    use std::time::Instant;
69    use tokio::sync::Mutex;
70
71    #[test]
72    fn fence_guard_exposes_fields() {
73        let guard = FenceGuard {
74            token: "tok".into(),
75            fence: 7,
76        };
77        assert_eq!(guard.token, "tok");
78        assert_eq!(guard.fence, 7);
79    }
80
81    /// 内存版参考实现:锁定 trait 契约——token 匹配才能 release、
82    /// TTL 过期后可重新获取、同一 key 同时只允许一个持有者。
83    struct MemoryLock {
84        held: Mutex<HashMap<String, (String, Instant)>>,
85    }
86
87    impl MemoryLock {
88        fn new() -> Arc<Self> {
89            Arc::new(Self {
90                held: Mutex::new(HashMap::new()),
91            })
92        }
93    }
94
95    #[async_trait]
96    impl DistributedLock for MemoryLock {
97        async fn acquire(&self, key: &str, ttl: Duration) -> Result<Option<String>, LockError> {
98            let mut held = self.held.lock().await;
99            if let Some((_, expires)) = held.get(key)
100                && *expires > Instant::now()
101            {
102                return Ok(None);
103            }
104            let token = format!("tok-{key}");
105            held.insert(key.into(), (token.clone(), Instant::now() + ttl));
106            Ok(Some(token))
107        }
108
109        async fn release(&self, key: &str, token: &str) -> Result<(), LockError> {
110            let mut held = self.held.lock().await;
111            match held.get(key) {
112                Some((t, _)) if t == token => {
113                    held.remove(key);
114                    Ok(())
115                }
116                Some(_) => Err(LockError::Other("token mismatch".into())),
117                None => Err(LockError::Other("lock not held".into())),
118            }
119        }
120    }
121
122    #[tokio::test]
123    async fn acquire_free_key_returns_token() {
124        let lock = MemoryLock::new();
125        let token = lock
126            .acquire("job-a", Duration::from_secs(30))
127            .await
128            .unwrap()
129            .expect("free key must be acquirable");
130        assert_eq!(token, "tok-job-a");
131    }
132
133    #[tokio::test]
134    async fn acquire_while_held_returns_none() {
135        let lock = MemoryLock::new();
136        lock.acquire("job-a", Duration::from_secs(30))
137            .await
138            .unwrap();
139        assert_eq!(
140            lock.acquire("job-a", Duration::from_secs(30))
141                .await
142                .unwrap(),
143            None,
144            "lock held by someone else must not be acquirable"
145        );
146    }
147
148    #[tokio::test]
149    async fn release_matching_token_frees_the_lock() {
150        let lock = MemoryLock::new();
151        let token = lock
152            .acquire("job-a", Duration::from_secs(30))
153            .await
154            .unwrap()
155            .unwrap();
156        lock.release("job-a", &token).await.unwrap();
157        // 释放后原持有者可再次获取。
158        lock.acquire("job-a", Duration::from_secs(30))
159            .await
160            .unwrap()
161            .expect("released lock must be acquirable again");
162    }
163
164    #[tokio::test]
165    async fn release_wrong_token_fails() {
166        let lock = MemoryLock::new();
167        lock.acquire("job-a", Duration::from_secs(30))
168            .await
169            .unwrap();
170        let err = lock.release("job-a", "tok-forgery").await.unwrap_err();
171        assert_eq!(err.to_string(), "lock error: token mismatch");
172    }
173
174    #[tokio::test]
175    async fn ttl_expiry_allows_reacquire() {
176        let lock = MemoryLock::new();
177        lock.acquire("job-a", Duration::from_millis(50))
178            .await
179            .unwrap();
180        tokio::time::sleep(Duration::from_millis(150)).await;
181        lock.acquire("job-a", Duration::from_secs(30))
182            .await
183            .unwrap()
184            .expect("expired lock must be acquirable again");
185    }
186
187    #[tokio::test]
188    async fn concurrent_acquire_yields_single_holder() {
189        let lock = MemoryLock::new();
190        let mut tasks = Vec::new();
191        for _ in 0..8 {
192            let lock = lock.clone();
193            tasks.push(tokio::spawn(async move {
194                lock.acquire("shared", Duration::from_secs(30))
195                    .await
196                    .unwrap()
197            }));
198        }
199        let mut winners = 0;
200        for task in tasks {
201            if task.await.unwrap().is_some() {
202                winners += 1;
203            }
204        }
205        assert_eq!(winners, 1, "exactly one acquirer must win the lock");
206    }
207
208    #[test]
209    fn lock_error_displays() {
210        assert_eq!(
211            LockError::Other("boom".into()).to_string(),
212            "lock error: boom"
213        );
214    }
215
216    #[tokio::test]
217    async fn release_unheld_lock_fails() {
218        let lock = MemoryLock::new();
219        let err = lock.release("job-a", "tok-job-a").await.unwrap_err();
220        assert_eq!(err.to_string(), "lock error: lock not held");
221    }
222
223    #[tokio::test]
224    async fn default_renew_is_not_supported() {
225        let lock = MemoryLock::new();
226        let err = lock
227            .renew("job-a", "tok-job-a", Duration::from_secs(30))
228            .await
229            .unwrap_err();
230        assert!(
231            err.to_string().contains("renew not supported"),
232            "got: {err}"
233        );
234    }
235
236    #[tokio::test]
237    async fn acquire_empty_key_works() {
238        let lock = MemoryLock::new();
239        let token = lock
240            .acquire("", Duration::from_secs(30))
241            .await
242            .unwrap()
243            .expect("empty key must be acquirable");
244        lock.release("", &token).await.unwrap();
245    }
246}