teaql_runtime/context/
locking.rs1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::{Arc, Condvar, Mutex, OnceLock};
4use std::time::{Duration, Instant};
5
6use super::UserContext;
7
8#[derive(Clone, Copy)]
9struct LocalLockEntry {
10 owner: u64,
11 expires_at: Option<Instant>,
12}
13
14#[derive(Default)]
15struct ProcessLocalLocks {
16 entries: Mutex<HashMap<String, LocalLockEntry>>,
17 changed: Condvar,
18}
19
20static PROCESS_LOCAL_LOCKS: OnceLock<ProcessLocalLocks> = OnceLock::new();
21static NEXT_LOCAL_LOCK_OWNER: AtomicU64 = AtomicU64::new(1);
22
23pub(super) fn next_local_lock_owner() -> u64 {
24 NEXT_LOCAL_LOCK_OWNER.fetch_add(1, Ordering::Relaxed)
25}
26
27#[async_trait::async_trait]
33pub trait RemoteLockProvider: Send + Sync + 'static {
34 async fn try_remote_lock(
35 &self,
36 key: &str,
37 owner_token: &str,
38 timeout_millis: u64,
39 expire_millis: u64,
40 ) -> bool;
41
42 async fn unlock_remote(&self, key: &str, owner_token: &str) -> bool;
43}
44
45impl UserContext {
46 pub fn try_local_lock(&self, key: &str, timeout_millis: u64, expire_millis: u64) -> bool {
47 let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
48 let deadline = Instant::now() + Duration::from_millis(timeout_millis);
49 let mut entries = locks.entries.lock().expect("local lock state poisoned");
50 loop {
51 let now = Instant::now();
52 match entries.get(key).copied() {
53 None => {
54 entries.insert(
55 key.to_owned(),
56 LocalLockEntry {
57 owner: self.local_lock_owner,
58 expires_at: (expire_millis > 0)
59 .then(|| now + Duration::from_millis(expire_millis)),
60 },
61 );
62 return true;
63 }
64 Some(current)
65 if current.owner == self.local_lock_owner
66 || current.expires_at.is_some_and(|expiry| now >= expiry) =>
67 {
68 entries.insert(
69 key.to_owned(),
70 LocalLockEntry {
71 owner: self.local_lock_owner,
72 expires_at: (expire_millis > 0)
73 .then(|| now + Duration::from_millis(expire_millis)),
74 },
75 );
76 return true;
77 }
78 Some(current) => {
79 if timeout_millis == 0 || now >= deadline {
80 return false;
81 }
82 let wake_after = current
83 .expires_at
84 .map(|expiry| expiry.saturating_duration_since(now))
85 .unwrap_or_else(|| deadline.saturating_duration_since(now))
86 .min(deadline.saturating_duration_since(now));
87 let waited = locks
88 .changed
89 .wait_timeout(entries, wake_after)
90 .expect("local lock state poisoned");
91 entries = waited.0;
92 }
93 }
94 }
95 }
96
97 pub fn unlock_local(&self, key: &str) {
98 let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
99 let mut entries = locks.entries.lock().expect("local lock state poisoned");
100 if entries
101 .get(key)
102 .is_some_and(|entry| entry.owner == self.local_lock_owner)
103 {
104 entries.remove(key);
105 locks.changed.notify_all();
106 }
107 }
108
109 pub async fn try_remote_lock(
115 &self,
116 key: &str,
117 timeout_millis: u64,
118 expire_millis: u64,
119 ) -> bool {
120 match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
121 Some(provider) => {
122 provider
123 .try_remote_lock(key, &self.remote_lock_owner, timeout_millis, expire_millis)
124 .await
125 }
126 None => true,
127 }
128 }
129
130 pub async fn unlock_remote(&self, key: &str) -> bool {
132 match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
133 Some(provider) => provider.unlock_remote(key, &self.remote_lock_owner).await,
134 None => true,
135 }
136 }
137}