Skip to main content

teaql_runtime/context/
pagination.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex, OnceLock};
3use std::time::SystemTime;
4
5use teaql_core::Value;
6
7use super::UserContext;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct ContinuousPageCursor {
11    pub cursor_id: String,
12    pub query_key: String,
13    pub entity: String,
14    pub direction: teaql_core::SortDirection,
15    pub boundary: Value,
16    pub page_size: u64,
17    pub next_offset: u64,
18    pub expires_at: SystemTime,
19}
20
21#[async_trait::async_trait]
22pub trait ContinuousPageCursorStore: Send + Sync + 'static {
23    async fn get(
24        &self,
25        query_key: &str,
26        target_offset: u64,
27    ) -> Result<Option<ContinuousPageCursor>, String>;
28    async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String>;
29    async fn invalidate(&self, query_key: &str) -> Result<(), String>;
30}
31
32pub struct InMemoryContinuousPageCursorStore {
33    cursors: Mutex<HashMap<String, ContinuousPageCursor>>,
34    max_entries: usize,
35}
36
37impl Default for InMemoryContinuousPageCursorStore {
38    fn default() -> Self {
39        Self {
40            cursors: Mutex::new(HashMap::new()),
41            max_entries: 4096,
42        }
43    }
44}
45
46#[async_trait::async_trait]
47impl ContinuousPageCursorStore for InMemoryContinuousPageCursorStore {
48    async fn get(
49        &self,
50        query_key: &str,
51        target_offset: u64,
52    ) -> Result<Option<ContinuousPageCursor>, String> {
53        let key = format!("{query_key}:{target_offset}");
54        let mut cursors = self.cursors.lock().map_err(|error| error.to_string())?;
55        if cursors
56            .get(&key)
57            .is_some_and(|cursor| cursor.expires_at <= SystemTime::now())
58        {
59            cursors.remove(&key);
60        }
61        Ok(cursors.get(&key).cloned())
62    }
63
64    async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String> {
65        let key = format!("{}:{}", cursor.query_key, cursor.next_offset);
66        let mut cursors = self.cursors.lock().map_err(|error| error.to_string())?;
67        if cursors.len() >= self.max_entries
68            && let Some(oldest) = cursors
69                .iter()
70                .min_by_key(|(_, value)| value.expires_at)
71                .map(|(key, _)| key.clone())
72        {
73            cursors.remove(&oldest);
74        }
75        cursors.insert(key, cursor);
76        Ok(())
77    }
78
79    async fn invalidate(&self, query_key: &str) -> Result<(), String> {
80        let prefix = format!("{query_key}:");
81        self.cursors
82            .lock()
83            .map_err(|error| error.to_string())?
84            .retain(|key, _| !key.starts_with(&prefix));
85        Ok(())
86    }
87}
88
89#[derive(Debug, Clone)]
90pub struct RetainedIdSet {
91    pub query_key: String,
92    pub ids: Arc<Vec<u64>>,
93    pub expires_at: SystemTime,
94}
95
96#[async_trait::async_trait]
97pub trait IdSetStore: Send + Sync + 'static {
98    async fn get(&self, query_key: &str) -> Result<Option<RetainedIdSet>, String>;
99    async fn put(&self, id_set: RetainedIdSet) -> Result<(), String>;
100    async fn invalidate(&self, query_key: &str) -> Result<(), String>;
101}
102
103pub struct InMemoryIdSetStore {
104    sets: Mutex<HashMap<String, RetainedIdSet>>,
105    max_entries: usize,
106    max_bytes: usize,
107}
108
109impl Default for InMemoryIdSetStore {
110    fn default() -> Self {
111        Self {
112            sets: Mutex::new(HashMap::new()),
113            max_entries: 64,
114            max_bytes: 256 * 1024 * 1024,
115        }
116    }
117}
118
119impl InMemoryIdSetStore {
120    fn retained_bytes(sets: &HashMap<String, RetainedIdSet>) -> usize {
121        sets.values()
122            .map(|value| value.ids.len().saturating_mul(std::mem::size_of::<u64>()))
123            .sum()
124    }
125}
126
127#[async_trait::async_trait]
128impl IdSetStore for InMemoryIdSetStore {
129    async fn get(&self, query_key: &str) -> Result<Option<RetainedIdSet>, String> {
130        let mut sets = self.sets.lock().map_err(|error| error.to_string())?;
131        if sets
132            .get(query_key)
133            .is_some_and(|value| value.expires_at <= SystemTime::now())
134        {
135            sets.remove(query_key);
136        }
137        Ok(sets.get(query_key).cloned())
138    }
139
140    async fn put(&self, id_set: RetainedIdSet) -> Result<(), String> {
141        let incoming_bytes = id_set.ids.len().saturating_mul(std::mem::size_of::<u64>());
142        if incoming_bytes > self.max_bytes {
143            return Err("ID set exceeds the process-local store memory ceiling".to_owned());
144        }
145        let mut sets = self.sets.lock().map_err(|error| error.to_string())?;
146        sets.retain(|_, value| value.expires_at > SystemTime::now());
147        while sets.len() >= self.max_entries
148            || Self::retained_bytes(&sets).saturating_add(incoming_bytes) > self.max_bytes
149        {
150            let Some(oldest) = sets
151                .iter()
152                .min_by_key(|(_, value)| value.expires_at)
153                .map(|(key, _)| key.clone())
154            else {
155                break;
156            };
157            sets.remove(&oldest);
158        }
159        sets.insert(id_set.query_key.clone(), id_set);
160        Ok(())
161    }
162
163    async fn invalidate(&self, query_key: &str) -> Result<(), String> {
164        self.sets
165            .lock()
166            .map_err(|error| error.to_string())?
167            .remove(query_key);
168        Ok(())
169    }
170}
171
172pub(super) fn id_set_build_lock(query_key: &str) -> Arc<futures_util::lock::Mutex<()>> {
173    static LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<futures_util::lock::Mutex<()>>>>> =
174        OnceLock::new();
175    let mut locks = LOCKS
176        .get_or_init(|| Mutex::new(HashMap::new()))
177        .lock()
178        .expect("ID set build lock registry poisoned");
179    locks.retain(|_, lock| lock.strong_count() > 0);
180    if let Some(lock) = locks.get(query_key).and_then(std::sync::Weak::upgrade) {
181        return lock;
182    }
183    let lock = Arc::new(futures_util::lock::Mutex::new(()));
184    locks.insert(query_key.to_owned(), Arc::downgrade(&lock));
185    lock
186}
187
188impl UserContext {
189    pub fn set_continuous_page_cursor_store(&mut self, store: Arc<dyn ContinuousPageCursorStore>) {
190        self.continuous_page_cursor_store = store;
191    }
192
193    pub fn continuous_page_plan(&self) -> Option<String> {
194        self.continuous_page_observation
195            .lock()
196            .ok()
197            .map(|value| value.0.clone())
198    }
199
200    pub fn continuous_page_cursor_id(&self) -> Option<String> {
201        self.continuous_page_observation
202            .lock()
203            .ok()
204            .and_then(|value| value.1.clone())
205    }
206
207    pub(crate) fn observe_continuous_page(
208        &self,
209        plan: impl Into<String>,
210        cursor_id: Option<String>,
211    ) {
212        if let Ok(mut observation) = self.continuous_page_observation.lock() {
213            *observation = (plan.into(), cursor_id);
214        }
215    }
216
217    pub(crate) fn continuous_page_cursor_store(&self) -> &dyn ContinuousPageCursorStore {
218        self.continuous_page_cursor_store.as_ref()
219    }
220
221    pub fn set_id_set_store(&mut self, store: Arc<dyn IdSetStore>) {
222        self.id_set_store = store;
223    }
224
225    pub fn id_set_plan(&self) -> Option<String> {
226        self.id_set_observation
227            .lock()
228            .ok()
229            .map(|observation| observation.0.clone())
230    }
231
232    pub fn id_set_count(&self) -> Option<u64> {
233        self.id_set_observation
234            .lock()
235            .ok()
236            .and_then(|observation| observation.1)
237    }
238
239    pub(crate) fn observe_id_set(&self, plan: impl Into<String>, count: Option<u64>) {
240        if let Ok(mut observation) = self.id_set_observation.lock() {
241            *observation = (plan.into(), count);
242        }
243    }
244
245    pub(crate) fn id_set_store(&self) -> &dyn IdSetStore {
246        self.id_set_store.as_ref()
247    }
248
249    pub(crate) fn id_set_build_lock(&self, query_key: &str) -> Arc<futures_util::lock::Mutex<()>> {
250        id_set_build_lock(query_key)
251    }
252}