linkrs_server/client/
cursor_context.rs1use parking_lot::RwLock;
12use std::collections::{HashMap, VecDeque};
13use std::fmt;
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::time::{Duration, Instant};
16
17use crate::query::executor::streaming::StreamingQueryResult;
18use linkrs_core::Value;
19
20pub const MAX_CURSORS_PER_SESSION: usize = 16;
22pub const CURSOR_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
24pub const MAX_CURSOR_PAGE_SIZE: usize = 10_000;
26pub const MIN_CURSOR_PAGE_SIZE: usize = 1;
28
29pub struct CursorState {
31 query: String,
32 columns: Vec<String>,
33 result: StreamingQueryResult,
34 buffered: VecDeque<Vec<Value>>,
35 exhausted: bool,
36 last_active: Instant,
37}
38
39impl fmt::Debug for CursorState {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.debug_struct("CursorState")
42 .field("query", &self.query)
43 .field("columns", &self.columns)
44 .field("buffered", &self.buffered.len())
45 .field("exhausted", &self.exhausted)
46 .finish()
47 }
48}
49
50pub struct CursorPage {
52 pub columns: Vec<String>,
53 pub rows: Vec<Vec<Value>>,
54 pub has_more: bool,
55}
56
57#[derive(Debug)]
59pub struct CursorContext {
60 cursors: RwLock<HashMap<u64, CursorState>>,
61 next_id: AtomicU64,
62}
63
64impl Default for CursorContext {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl CursorContext {
71 pub fn new() -> Self {
72 Self {
73 cursors: RwLock::new(HashMap::new()),
74 next_id: AtomicU64::new(1),
75 }
76 }
77
78 pub fn open(
80 &self,
81 query: String,
82 result: StreamingQueryResult,
83 columns: Vec<String>,
84 ) -> Result<u64, String> {
85 self.sweep_expired();
86 let mut cursors = self.cursors.write();
87 if cursors.len() >= MAX_CURSORS_PER_SESSION {
88 return Err(format!(
89 "Too many open cursors (limit {MAX_CURSORS_PER_SESSION}); close idle ones first"
90 ));
91 }
92 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
93 cursors.insert(
94 id,
95 CursorState {
96 query,
97 columns,
98 result,
99 buffered: VecDeque::new(),
100 exhausted: false,
101 last_active: Instant::now(),
102 },
103 );
104 Ok(id)
105 }
106
107 pub fn fetch(&self, cursor_id: u64, page_size: usize) -> Result<CursorPage, String> {
110 self.sweep_expired();
111 let page_size = page_size.clamp(MIN_CURSOR_PAGE_SIZE, MAX_CURSOR_PAGE_SIZE);
112 let (result, must_pull) = {
114 let cursors = self.cursors.read();
115 let cursor = cursors
116 .get(&cursor_id)
117 .ok_or_else(|| format!("Unknown cursor: {cursor_id}"))?;
118 (
119 cursor.result.clone(),
120 !cursor.exhausted && cursor.buffered.len() < page_size,
121 )
122 };
123
124 let mut pulled: Vec<(Vec<String>, Vec<Vec<Value>>)> = Vec::new();
125 let mut pulled_rows: usize = 0;
126 let mut now_exhausted = false;
127 if must_pull {
128 loop {
132 match result.next_chunk() {
133 Ok(Some(chunk)) => {
134 pulled_rows += chunk.rows.len();
135 pulled.push((chunk.col_names(), chunk.rows));
136 if pulled_rows >= page_size {
137 break;
138 }
139 }
140 Ok(None) => {
141 now_exhausted = true;
142 break;
143 }
144 Err(e) => {
145 self.cursors.write().remove(&cursor_id);
146 return Err(format!("Cursor query failed: {e}"));
147 }
148 }
149 }
150 }
151
152 let mut cursors = self.cursors.write();
154 let cursor = cursors
155 .get_mut(&cursor_id)
156 .ok_or_else(|| format!("Unknown cursor: {cursor_id}"))?;
157 for (names, rows) in pulled {
158 if cursor.columns.is_empty() {
159 cursor.columns = names;
160 }
161 cursor.buffered.extend(rows);
162 }
163 cursor.exhausted = cursor.exhausted || now_exhausted;
164 let mut rows = Vec::with_capacity(page_size.min(cursor.buffered.len()));
165 while rows.len() < page_size {
166 match cursor.buffered.pop_front() {
167 Some(row) => rows.push(row),
168 None => break,
169 }
170 }
171 cursor.last_active = Instant::now();
172 let has_more = !(cursor.exhausted && cursor.buffered.is_empty());
173 Ok(CursorPage {
174 columns: cursor.columns.clone(),
175 rows,
176 has_more,
177 })
178 }
179
180 pub fn close(&self, cursor_id: u64) -> bool {
182 self.sweep_expired();
183 self.cursors.write().remove(&cursor_id).is_some()
184 }
185
186 pub fn close_all(&self) {
188 self.cursors.write().clear();
189 }
190
191 pub fn active_count(&self) -> usize {
193 self.cursors.read().len()
194 }
195
196 fn sweep_expired(&self) {
198 let mut cursors = self.cursors.write();
199 cursors.retain(|_, cursor| cursor.last_active.elapsed() < CURSOR_IDLE_TIMEOUT);
200 }
201}