1use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10use tokio::sync::Mutex;
11use tracing::{debug, info, warn};
12
13type ChatKey = (String, i64);
15
16#[derive(Debug, Clone)]
18pub struct PendingMessage {
19 pub sender_name: String,
20 pub content: String,
21 pub message_id: String,
22 pub timestamp: String,
23}
24
25struct ChatSlot {
27 turn_lock: Arc<Mutex<()>>,
29 pending_messages: Vec<PendingMessage>,
31 last_active: Instant,
33}
34
35impl ChatSlot {
36 fn new() -> Self {
37 Self {
38 turn_lock: Arc::new(Mutex::new(())),
39 pending_messages: Vec::new(),
40 last_active: Instant::now(),
41 }
42 }
43}
44
45pub struct TurnGuard {
47 _guard: tokio::sync::OwnedMutexGuard<()>,
48 key: ChatKey,
49}
50
51impl Drop for TurnGuard {
52 fn drop(&mut self) {
53 debug!(
54 channel = %self.key.0,
55 chat_id = self.key.1,
56 "Chat turn released"
57 );
58 }
59}
60
61pub struct ChatTurnQueue {
67 slots: Mutex<HashMap<ChatKey, Arc<Mutex<ChatSlot>>>>,
68 max_pending: usize,
69 idle_ttl: Duration,
70}
71
72impl ChatTurnQueue {
73 pub fn new(max_pending: usize) -> Self {
74 Self {
75 slots: Mutex::new(HashMap::new()),
76 max_pending,
77 idle_ttl: Duration::from_secs(600),
78 }
79 }
80
81 async fn get_slot(&self, key: &ChatKey) -> Arc<Mutex<ChatSlot>> {
84 let mut slots = self.slots.lock().await;
85 if slots.len() > 100 {
87 let now = Instant::now();
88 let ttl = self.idle_ttl;
89 slots.retain(|_, slot_arc| {
90 if let Ok(slot) = slot_arc.try_lock() {
92 if slot.pending_messages.is_empty()
93 && now.duration_since(slot.last_active) > ttl
94 {
95 return false; }
97 }
98 true
99 });
100 }
101 slots
102 .entry(key.clone())
103 .or_insert_with(|| Arc::new(Mutex::new(ChatSlot::new())))
104 .clone()
105 }
106
107 pub async fn acquire(self: &Arc<Self>, channel: &str, chat_id: i64) -> Option<TurnGuard> {
110 let key: ChatKey = (channel.to_string(), chat_id);
111 let slot_arc = self.get_slot(&key).await;
112
113 let turn_lock = {
114 let slot = slot_arc.lock().await;
115 slot.turn_lock.clone()
116 };
117
118 let guard = match tokio::time::timeout(Duration::from_secs(60), turn_lock.lock_owned())
120 .await
121 {
122 Ok(guard) => guard,
123 Err(_) => {
124 warn!(
125 channel = %key.0,
126 chat_id = key.1,
127 "ChatTurnQueue: timeout waiting for turn lock (60s); proceeding without lock"
128 );
129 return None;
130 }
131 };
132
133 {
134 let mut slot = slot_arc.lock().await;
135 slot.last_active = Instant::now();
136 }
137
138 debug!(channel, chat_id, "Chat turn acquired");
139
140 Some(TurnGuard { _guard: guard, key })
141 }
142
143 pub async fn enqueue_if_busy(&self, channel: &str, chat_id: i64, msg: PendingMessage) -> bool {
148 let key: ChatKey = (channel.to_string(), chat_id);
149 let slot_arc = {
150 let slots = self.slots.lock().await;
151 match slots.get(&key) {
152 Some(arc) => arc.clone(),
153 None => return false, }
155 };
156
157 let mut slot = slot_arc.lock().await;
158 if slot.turn_lock.try_lock().is_ok() {
160 return false;
162 }
163
164 if slot.pending_messages.len() >= self.max_pending {
166 slot.pending_messages.remove(0);
168 warn!(
169 channel,
170 chat_id,
171 max_pending = self.max_pending,
172 "ChatTurnQueue: pending messages at capacity; dropped oldest"
173 );
174 }
175
176 info!(
177 channel,
178 chat_id,
179 sender = %msg.sender_name,
180 pending_count = slot.pending_messages.len() + 1,
181 "Message queued while chat turn is active"
182 );
183 slot.pending_messages.push(msg);
184 true
185 }
186
187 pub async fn try_start_or_enqueue(
195 self: &Arc<Self>,
196 channel: &str,
197 chat_id: i64,
198 msg: PendingMessage,
199 ) -> Option<TurnGuard> {
200 let key: ChatKey = (channel.to_string(), chat_id);
201 let slot_arc = self.get_slot(&key).await;
202
203 let turn_lock = {
204 let slot = slot_arc.lock().await;
205 slot.turn_lock.clone()
206 };
207
208 match turn_lock.try_lock_owned() {
209 Ok(guard) => {
210 let mut slot = slot_arc.lock().await;
212 slot.last_active = Instant::now();
213 debug!(channel, chat_id, "Chat turn acquired");
214 Some(TurnGuard { _guard: guard, key })
215 }
216 Err(_) => {
217 let mut slot = slot_arc.lock().await;
219 if slot.pending_messages.len() >= self.max_pending {
220 slot.pending_messages.remove(0);
221 warn!(
222 channel,
223 chat_id,
224 max_pending = self.max_pending,
225 "ChatTurnQueue: pending messages at capacity; dropped oldest"
226 );
227 }
228 info!(
229 channel,
230 chat_id,
231 sender = %msg.sender_name,
232 pending_count = slot.pending_messages.len() + 1,
233 "Message queued while chat turn is active"
234 );
235 slot.pending_messages.push(msg);
236 None
237 }
238 }
239 }
240
241 pub async fn drain_pending(&self, channel: &str, chat_id: i64) -> Vec<PendingMessage> {
244 let key: ChatKey = (channel.to_string(), chat_id);
245 let slot_arc = {
246 let slots = self.slots.lock().await;
247 match slots.get(&key) {
248 Some(arc) => arc.clone(),
249 None => return Vec::new(),
250 }
251 };
252
253 let mut slot = slot_arc.lock().await;
254 std::mem::take(&mut slot.pending_messages)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use std::sync::atomic::{AtomicUsize, Ordering};
262
263 fn make_queue() -> Arc<ChatTurnQueue> {
264 Arc::new(ChatTurnQueue::new(20))
265 }
266
267 fn make_msg(content: &str) -> PendingMessage {
268 PendingMessage {
269 sender_name: "user".to_string(),
270 content: content.to_string(),
271 message_id: format!("msg_{content}"),
272 timestamp: "2026-04-01T00:00:00Z".to_string(),
273 }
274 }
275
276 #[tokio::test]
277 async fn test_acquire_release_basic() {
278 let q = make_queue();
279 let guard = q.acquire("telegram", 1).await;
280 assert!(guard.is_some());
281 drop(guard);
282 let guard2 = q.acquire("telegram", 1).await;
284 assert!(guard2.is_some());
285 }
286
287 #[tokio::test]
288 async fn test_acquire_blocks_concurrent() {
289 let q = make_queue();
290 let counter = Arc::new(AtomicUsize::new(0));
291
292 let guard = q.acquire("tg", 1).await.unwrap();
293 let q2 = q.clone();
294 let c2 = counter.clone();
295
296 let handle = tokio::spawn(async move {
298 let _g = q2.acquire("tg", 1).await;
299 c2.fetch_add(1, Ordering::SeqCst);
300 });
301
302 tokio::time::sleep(Duration::from_millis(50)).await;
304 assert_eq!(counter.load(Ordering::SeqCst), 0);
306
307 drop(guard);
309 handle.await.unwrap();
311 assert_eq!(counter.load(Ordering::SeqCst), 1);
312 }
313
314 #[tokio::test]
315 async fn test_enqueue_if_busy_returns_true_when_active() {
316 let q = make_queue();
317 let _guard = q.acquire("tg", 1).await.unwrap();
318
319 let queued = q.enqueue_if_busy("tg", 1, make_msg("hello")).await;
320 assert!(queued);
321
322 let queued2 = q.enqueue_if_busy("tg", 1, make_msg("world")).await;
323 assert!(queued2);
324 }
325
326 #[tokio::test]
327 async fn test_enqueue_if_busy_returns_false_when_idle() {
328 let q = make_queue();
329 let queued = q.enqueue_if_busy("tg", 1, make_msg("hello")).await;
331 assert!(!queued);
332 }
333
334 #[tokio::test]
335 async fn test_drain_pending_returns_all() {
336 let q = make_queue();
337 let _guard = q.acquire("tg", 1).await.unwrap();
338
339 q.enqueue_if_busy("tg", 1, make_msg("a")).await;
340 q.enqueue_if_busy("tg", 1, make_msg("b")).await;
341 q.enqueue_if_busy("tg", 1, make_msg("c")).await;
342
343 let pending = q.drain_pending("tg", 1).await;
344 assert_eq!(pending.len(), 3);
345 assert_eq!(pending[0].content, "a");
346 assert_eq!(pending[1].content, "b");
347 assert_eq!(pending[2].content, "c");
348 }
349
350 #[tokio::test]
351 async fn test_drain_clears_queue() {
352 let q = make_queue();
353 let _guard = q.acquire("tg", 1).await.unwrap();
354
355 q.enqueue_if_busy("tg", 1, make_msg("a")).await;
356 let _ = q.drain_pending("tg", 1).await;
357
358 let pending = q.drain_pending("tg", 1).await;
359 assert!(pending.is_empty());
360 }
361
362 #[tokio::test]
363 async fn test_different_chats_independent() {
364 let q = make_queue();
365 let counter = Arc::new(AtomicUsize::new(0));
366
367 let _guard_chat1 = q.acquire("tg", 1).await.unwrap();
368 let q2 = q.clone();
369 let c2 = counter.clone();
370
371 let handle = tokio::spawn(async move {
373 let _g = q2.acquire("tg", 2).await;
374 c2.fetch_add(1, Ordering::SeqCst);
375 });
376
377 handle.await.unwrap();
378 assert_eq!(counter.load(Ordering::SeqCst), 1);
379 }
380
381 #[tokio::test]
382 async fn test_max_pending_drops_oldest() {
383 let q = Arc::new(ChatTurnQueue::new(3));
384 let _guard = q.acquire("tg", 1).await.unwrap();
385
386 q.enqueue_if_busy("tg", 1, make_msg("a")).await;
387 q.enqueue_if_busy("tg", 1, make_msg("b")).await;
388 q.enqueue_if_busy("tg", 1, make_msg("c")).await;
389 q.enqueue_if_busy("tg", 1, make_msg("d")).await;
391
392 let pending = q.drain_pending("tg", 1).await;
393 assert_eq!(pending.len(), 3);
394 assert_eq!(pending[0].content, "b");
395 assert_eq!(pending[1].content, "c");
396 assert_eq!(pending[2].content, "d");
397 }
398}