1use async_trait::async_trait;
7use dashmap::DashMap;
8use std::collections::{HashMap, VecDeque};
9use std::sync::Arc;
10use std::time::Duration;
11use tokio::sync::Notify;
12use tracing::warn;
13
14use crate::error::HubError;
15use crate::ilink::types::WeixinMessage;
16
17pub const DEFAULT_MAX_QUEUE_SIZE: usize = 200;
19
20#[async_trait]
64pub trait MessageQueue: Send + Sync {
65 async fn push(&self, vtoken: &str, msg: WeixinMessage) -> Result<bool, HubError>;
66 async fn push_shared(
75 &self,
76 vtoken: &str,
77 base: Arc<WeixinMessage>,
78 context_token: Option<String>,
79 hub_ext: Option<crate::ilink::types::HubExt>,
80 ) -> Result<bool, HubError> {
81 let mut msg = (*base).clone();
82 msg.context_token = context_token;
83 msg.ilink_hub_ext = hub_ext;
84 self.push(vtoken, msg).await
85 }
86 async fn drain(&self, vtoken: &str) -> Result<Vec<WeixinMessage>, HubError>;
87 async fn wait_notify(&self, vtoken: &str, timeout_secs: u64) -> Result<bool, HubError>;
88 async fn remove_client(&self, vtoken: &str) -> Result<(), HubError>;
89 async fn queue_sizes(&self) -> Result<HashMap<String, usize>, HubError>;
90}
91
92struct PerClientSlot {
102 messages: std::sync::Mutex<VecDeque<WeixinMessage>>,
103 notify: Arc<Notify>,
104 max_queue_size: usize,
105}
106
107impl PerClientSlot {
108 fn new(max_queue_size: usize) -> Arc<Self> {
109 Arc::new(Self {
110 messages: std::sync::Mutex::new(VecDeque::new()),
111 notify: Arc::new(Notify::new()),
112 max_queue_size,
113 })
114 }
115
116 fn push(&self, msg: WeixinMessage) -> bool {
117 let mut q = self.messages.lock().unwrap_or_else(|e| e.into_inner());
118 let dropped = if q.len() >= self.max_queue_size {
119 q.pop_front();
120 warn!(
121 max = self.max_queue_size,
122 "client queue full, dropping oldest message"
123 );
124 true
125 } else {
126 false
127 };
128 q.push_back(msg);
129 self.notify.notify_one();
130 dropped
131 }
132
133 fn drain(&self) -> Vec<WeixinMessage> {
134 self.messages
135 .lock()
136 .unwrap_or_else(|e| e.into_inner())
137 .drain(..)
138 .collect()
139 }
140
141 fn len(&self) -> usize {
142 self.messages
143 .lock()
144 .unwrap_or_else(|e| e.into_inner())
145 .len()
146 }
147}
148
149pub struct InMemoryQueue {
150 slots: DashMap<String, Arc<PerClientSlot>>,
151 max_queue_size: usize,
152}
153
154impl InMemoryQueue {
155 pub fn new() -> Self {
156 Self::with_limit(DEFAULT_MAX_QUEUE_SIZE)
157 }
158
159 pub fn with_limit(max_queue_size: usize) -> Self {
160 Self {
161 slots: DashMap::new(),
162 max_queue_size,
163 }
164 }
165
166 fn get_or_create(&self, vtoken: &str) -> Arc<PerClientSlot> {
167 self.slots
168 .entry(vtoken.to_string())
169 .or_insert_with(|| PerClientSlot::new(self.max_queue_size))
170 .clone()
171 }
172}
173
174impl Default for InMemoryQueue {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180#[async_trait]
181impl MessageQueue for InMemoryQueue {
182 async fn push(&self, vtoken: &str, msg: WeixinMessage) -> Result<bool, HubError> {
183 Ok(self.get_or_create(vtoken).push(msg))
184 }
185
186 async fn push_shared(
187 &self,
188 vtoken: &str,
189 base: Arc<WeixinMessage>,
190 context_token: Option<String>,
191 hub_ext: Option<crate::ilink::types::HubExt>,
192 ) -> Result<bool, HubError> {
193 let mut msg = (*base).clone();
199 msg.context_token = context_token;
200 msg.ilink_hub_ext = hub_ext;
201 Ok(self.get_or_create(vtoken).push(msg))
202 }
203
204 async fn drain(&self, vtoken: &str) -> Result<Vec<WeixinMessage>, HubError> {
205 Ok(self
206 .slots
207 .get(vtoken)
208 .map(|s| s.drain())
209 .unwrap_or_default())
210 }
211
212 async fn wait_notify(&self, vtoken: &str, timeout_secs: u64) -> Result<bool, HubError> {
213 let notify = self.get_or_create(vtoken).notify.clone();
215 let result =
216 tokio::time::timeout(Duration::from_secs(timeout_secs), notify.notified()).await;
217 Ok(result.is_ok())
218 }
219
220 async fn remove_client(&self, vtoken: &str) -> Result<(), HubError> {
221 self.slots.remove(vtoken);
222 Ok(())
223 }
224
225 async fn queue_sizes(&self) -> Result<HashMap<String, usize>, HubError> {
226 Ok(self
227 .slots
228 .iter()
229 .map(|e| (e.key().clone(), e.value().len()))
230 .collect())
231 }
232}
233
234#[cfg(test)]
235mod queue_config_tests {
236 use super::*;
237 use std::sync::Arc;
238
239 #[tokio::test]
240 async fn test_in_memory_queue_with_limit() {
241 let q = InMemoryQueue::with_limit(10);
242 let vtoken = "v1";
243
244 for i in 0..10 {
246 let msg = WeixinMessage {
247 message_id: Some(i),
248 ..Default::default()
249 };
250 let dropped = q.push(vtoken, msg).await.unwrap();
251 assert!(!dropped);
252 }
253
254 let msg = WeixinMessage {
256 message_id: Some(10),
257 ..Default::default()
258 };
259 let dropped = q.push(vtoken, msg).await.unwrap();
260 assert!(dropped);
261
262 let drained = q.drain(vtoken).await.unwrap();
263 assert_eq!(drained.len(), 10);
264 assert_eq!(drained[0].message_id, Some(1));
265 assert_eq!(drained[9].message_id, Some(10));
266 }
267
268 #[tokio::test]
269 async fn test_push_shared_overrides_context_and_hub_ext_per_recipient() {
270 use crate::ilink::types::HubExt;
271 let q = InMemoryQueue::new();
272 let base = Arc::new(WeixinMessage {
273 from_user_id: Some("user-1".into()),
274 context_token: Some("shared".into()),
275 ..Default::default()
276 });
277
278 q.push_shared(
281 "v1",
282 Arc::clone(&base),
283 Some("vctx-v1".into()),
284 Some(HubExt {
285 session_id: Some("sid-v1".into()),
286 ..Default::default()
287 }),
288 )
289 .await
290 .unwrap();
291 q.push_shared(
292 "v2",
293 Arc::clone(&base),
294 Some("vctx-v2".into()),
295 Some(HubExt {
296 session_id: Some("sid-v2".into()),
297 ..Default::default()
298 }),
299 )
300 .await
301 .unwrap();
302
303 let v1 = q.drain("v1").await.unwrap();
304 let v2 = q.drain("v2").await.unwrap();
305 assert_eq!(v1.len(), 1);
306 assert_eq!(v2.len(), 1);
307 assert_eq!(v1[0].context_token.as_deref(), Some("vctx-v1"));
308 assert_eq!(v2[0].context_token.as_deref(), Some("vctx-v2"));
309 assert_eq!(v1[0].from_user_id.as_deref(), Some("user-1"));
311 assert_eq!(v2[0].from_user_id.as_deref(), Some("user-1"));
312 assert_eq!(
314 v1[0]
315 .ilink_hub_ext
316 .as_ref()
317 .and_then(|e| e.session_id.as_deref()),
318 Some("sid-v1")
319 );
320 assert_eq!(
321 v2[0]
322 .ilink_hub_ext
323 .as_ref()
324 .and_then(|e| e.session_id.as_deref()),
325 Some("sid-v2")
326 );
327 }
328
329 #[test]
330 fn test_mutex_poison_safe() {
331 use std::thread;
332
333 let slot = Arc::new(PerClientSlot::new(10));
335 let slot_clone = slot.clone();
336 let handle3 = thread::spawn(move || {
337 let _lock = slot_clone.messages.lock().unwrap();
338 panic!("force panic to poison PerClientSlot Mutex");
339 });
340 let _ = handle3.join();
341
342 assert!(!slot.push(WeixinMessage::default()));
344 assert_eq!(slot.len(), 1);
345 assert_eq!(slot.drain().len(), 1);
346 assert_eq!(slot.len(), 0);
347
348 for i in 0..5 {
350 let msg = WeixinMessage {
351 message_id: Some(i),
352 ..Default::default()
353 };
354 slot.push(msg);
355 }
356 assert_eq!(slot.len(), 5);
357 let drained = slot.drain();
358 assert_eq!(drained.len(), 5);
359 assert_eq!(drained[0].message_id, Some(0));
360 assert_eq!(slot.len(), 0);
361
362 let mut slot_handles = vec![];
364 for thread_idx in 0..10 {
365 let slot_thread = slot.clone();
366 slot_handles.push(thread::spawn(move || {
367 for i in 0..50 {
368 let msg = WeixinMessage {
369 message_id: Some(thread_idx * 100 + i),
370 ..Default::default()
371 };
372 slot_thread.push(msg);
373 let drained = slot_thread.drain();
374 for m in drained {
375 assert!(m.message_id.is_some());
376 }
377 }
378 }));
379 }
380 for h in slot_handles {
381 h.join().unwrap();
382 }
383 }
384
385 struct AlwaysFalseQueue;
386
387 #[async_trait::async_trait]
388 impl crate::MessageQueue for AlwaysFalseQueue {
389 async fn push(
390 &self,
391 _vtoken: &str,
392 _msg: crate::ilink::types::WeixinMessage,
393 ) -> Result<bool, crate::error::HubError> {
394 Ok(false)
395 }
396 async fn drain(
397 &self,
398 _vtoken: &str,
399 ) -> Result<Vec<crate::ilink::types::WeixinMessage>, crate::error::HubError> {
400 Ok(vec![])
401 }
402 async fn wait_notify(
403 &self,
404 _vtoken: &str,
405 _timeout_secs: u64,
406 ) -> Result<bool, crate::error::HubError> {
407 Ok(false)
408 }
409 async fn remove_client(&self, _vtoken: &str) -> Result<(), crate::error::HubError> {
410 Ok(())
411 }
412 async fn queue_sizes(
413 &self,
414 ) -> Result<std::collections::HashMap<String, usize>, crate::error::HubError> {
415 Ok(std::collections::HashMap::new())
416 }
417 }
418
419 struct AlwaysTrueQueue;
420
421 #[async_trait::async_trait]
422 impl crate::MessageQueue for AlwaysTrueQueue {
423 async fn push(
424 &self,
425 _vtoken: &str,
426 _msg: crate::ilink::types::WeixinMessage,
427 ) -> Result<bool, crate::error::HubError> {
428 Ok(true)
429 }
430 async fn drain(
431 &self,
432 _vtoken: &str,
433 ) -> Result<Vec<crate::ilink::types::WeixinMessage>, crate::error::HubError> {
434 Ok(vec![])
435 }
436 async fn wait_notify(
437 &self,
438 _vtoken: &str,
439 _timeout_secs: u64,
440 ) -> Result<bool, crate::error::HubError> {
441 Ok(false)
442 }
443 async fn remove_client(&self, _vtoken: &str) -> Result<(), crate::error::HubError> {
444 Ok(())
445 }
446 async fn queue_sizes(
447 &self,
448 ) -> Result<std::collections::HashMap<String, usize>, crate::error::HubError> {
449 Ok(std::collections::HashMap::new())
450 }
451 }
452
453 #[tokio::test]
454 async fn push_shared_default_propagates_false_from_push() {
455 let queue = AlwaysFalseQueue;
456 let base = Arc::new(WeixinMessage::default());
457 let result = queue.push_shared("v1", base, None, None).await.unwrap();
458 assert!(
459 !result,
460 "push_shared default impl must propagate Ok(false) from push()"
461 );
462 }
463
464 #[tokio::test]
465 async fn push_shared_default_propagates_true_from_push() {
466 let queue = AlwaysTrueQueue;
467 let base = Arc::new(WeixinMessage::default());
468 let result = queue.push_shared("v1", base, None, None).await.unwrap();
469 assert!(
470 result,
471 "push_shared default impl must propagate Ok(true) from push()"
472 );
473 }
474}