1mod connection_lifecycle;
29mod disconnected_cleanup;
30mod push_regs;
31mod qot_commit;
32mod session_detail;
33mod unsubscribe_all_commit;
34mod views;
35
36use std::collections::{HashMap, HashSet};
37use std::sync::Arc;
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::time::{Duration, Instant};
40
41use dashmap::DashMap;
42use futu_core::qot_stock_key::QotSecurityKey;
43pub use futu_domain_qot_subscription::QOT_MIN_UNSUB_ELAPSED_SECS;
44use futu_domain_qot_subscription::{
45 CryptoSubscriptionProbe, UnsubscribeAllGlobalEmptyProbe,
46 is_crypto_stock_broker_globally_unsubscribed, is_crypto_stock_globally_unsubscribed,
47 plan_unsubscribe_all_global_empty_keys, qot_min_unsub_freshness_from_elapsed_secs,
48};
49use parking_lot::RwLock;
50
51use crate::conn::ClientCloseControl;
52
53pub type ConnectionDisconnectObserver = Arc<dyn Fn(u64) + Send + Sync>;
54pub type ConnectionOpenObserver = Arc<dyn Fn(u64, u64) + Send + Sync>;
55
56pub struct SubscriptionManager {
58 connection_open_observers: RwLock<Vec<ConnectionOpenObserver>>,
59 disconnect_observers: RwLock<Vec<ConnectionDisconnectObserver>>,
63
64 client_close_controls: DashMap<u64, ClientCloseControl>,
70 connection_generations: DashMap<u64, u64>,
72
73 notify_subs: RwLock<HashSet<u64>>,
75
76 trd_acc_subs: RwLock<HashMap<u64, HashSet<u64>>>,
78
79 api_page_req_keys: RwLock<HashMap<u64, HashSet<[u8; 16]>>>,
82
83 qot_subs: RwLock<HashMap<(QotSecurityKey, i32), HashSet<u64>>>,
86
87 qot_push_regs: RwLock<QotPushRegistrations>,
91
92 qot_sub_sessions: RwLock<QotSessionState>,
97
98 qot_orderbook_detail: RwLock<HashMap<QotSecurityKey, HashMap<u64, bool>>>,
101
102 qot_broker_detail: RwLock<HashMap<QotSecurityKey, HashMap<u64, bool>>>,
105
106 qot_sub_times: RwLock<HashMap<(QotSecurityKey, i32), Instant>>,
110
111 qot_disconnected_conns: RwLock<HashSet<u64>>,
117
118 qot_disconnect_sync_generation: AtomicU64,
124 qot_owner_token_high_water: AtomicU64,
125 qot_owner_tokens: RwLock<HashMap<(QotSecurityKey, i32, u64), u64>>,
126 qot_push_intent_high_water: AtomicU64,
127}
128
129#[derive(Default)]
130struct QotPushRegistrations {
131 by_tuple: HashMap<(QotSecurityKey, i32, i32), HashSet<u64>>,
132 qot_push_regs_by_cache_key: QotPushIntentIndex,
133 intent_epochs: HashMap<(QotSecurityKey, i32, i32, u64), u64>,
134}
135
136type QotPushIntentOwners = HashMap<u64, u64>;
137type QotPushIntentRoutes = HashMap<(i32, i32), QotPushIntentOwners>;
138type QotPushIntentIndex = HashMap<String, QotPushIntentRoutes>;
139
140#[derive(Clone, Debug, PartialEq, Eq)]
146pub struct QotPushRegistrationLease {
147 key: QotSecurityKey,
148 sub_type: i32,
149 rehab_type: i32,
150 conn_id: u64,
151 intent_epoch: u64,
152 connection_generation: u64,
153 conn_session: i32,
154}
155
156impl QotPushRegistrationLease {
157 #[must_use]
158 pub fn connection_generation(&self) -> u64 {
159 self.connection_generation
160 }
161
162 #[must_use]
163 pub(crate) fn intent_epoch(&self) -> u64 {
164 self.intent_epoch
165 }
166
167 #[must_use]
168 pub fn matches_delivery(
169 &self,
170 conn_id: u64,
171 connection_generation: u64,
172 sec_key: &str,
173 sub_type: i32,
174 rehab_type: i32,
175 ) -> bool {
176 self.conn_id == conn_id
177 && self.connection_generation == connection_generation
178 && self.key.cache_key() == sec_key
179 && self.sub_type == sub_type
180 && self.rehab_type == rehab_type
181 }
182}
183
184#[derive(Default)]
185struct QotSessionState {
186 by_key: HashMap<(QotSecurityKey, i32), HashMap<u64, i32>>,
187 by_cache_key: HashMap<(String, i32), HashMap<u64, i32>>,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum SubResult {
199 NewGlobal,
201 AlreadyGlobal,
203 NoChange,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum UnsubResult {
210 LastSubscriber,
213 StillSubscribed,
215 NotSubscribed,
218}
219
220impl SubscriptionManager {
221 pub fn new() -> Self {
222 Self {
223 connection_open_observers: RwLock::new(Vec::new()),
224 disconnect_observers: RwLock::new(Vec::new()),
225 client_close_controls: DashMap::new(),
226 connection_generations: DashMap::new(),
227 notify_subs: RwLock::new(HashSet::new()),
228 trd_acc_subs: RwLock::new(HashMap::new()),
229 api_page_req_keys: RwLock::new(HashMap::new()),
230 qot_subs: RwLock::new(HashMap::new()),
231 qot_push_regs: RwLock::new(QotPushRegistrations::default()),
232 qot_sub_sessions: RwLock::new(QotSessionState::default()),
233 qot_orderbook_detail: RwLock::new(HashMap::new()),
234 qot_broker_detail: RwLock::new(HashMap::new()),
235 qot_sub_times: RwLock::new(HashMap::new()),
236 qot_disconnected_conns: RwLock::new(HashSet::new()),
237 qot_disconnect_sync_generation: AtomicU64::new(0),
238 qot_owner_token_high_water: AtomicU64::new(0),
239 qot_owner_tokens: RwLock::new(HashMap::new()),
240 qot_push_intent_high_water: AtomicU64::new(0),
241 }
242 }
243
244 pub fn subscribe_notify(&self, conn_id: u64) {
247 self.notify_subs.write().insert(conn_id);
248 }
249
250 pub fn unsubscribe_notify(&self, conn_id: u64) {
251 self.notify_subs.write().remove(&conn_id);
252 }
253
254 pub fn is_subscribed_notify(&self, conn_id: u64) -> bool {
255 self.notify_subs.read().contains(&conn_id)
256 }
257
258 pub fn register_api_page_req_key(&self, conn_id: u64, key: &[u8]) -> bool {
259 let Ok(key) = <[u8; 16]>::try_from(key) else {
260 return false;
261 };
262 self.api_page_req_keys
263 .write()
264 .entry(conn_id)
265 .or_default()
266 .insert(key);
267 true
268 }
269
270 #[must_use]
271 pub fn is_api_page_req_key_registered(&self, conn_id: u64, key: &[u8]) -> bool {
272 let Ok(key) = <[u8; 16]>::try_from(key) else {
273 return false;
274 };
275 self.api_page_req_keys
276 .read()
277 .get(&conn_id)
278 .is_some_and(|keys| keys.contains(&key))
279 }
280
281 pub fn subscribe_trd_acc(&self, conn_id: u64, acc_id: u64) {
284 self.trd_acc_subs
285 .write()
286 .entry(acc_id)
287 .or_default()
288 .insert(conn_id);
289 }
290
291 pub fn unsubscribe_trd_acc(&self, conn_id: u64, acc_id: u64) {
292 if let Some(subs) = self.trd_acc_subs.write().get_mut(&acc_id) {
293 subs.remove(&conn_id);
294 }
295 }
296
297 pub fn get_acc_subscribers(&self, acc_id: u64) -> Vec<u64> {
298 match self.trd_acc_subs.read().get(&acc_id) {
299 Some(subscribers) => subscribers.iter().copied().collect(),
300 None => Vec::new(),
301 }
302 }
303
304 pub fn make_qot_key(market: i32, code: &str, sub_type: i32) -> String {
308 format!("{market}_{code}:{sub_type}")
309 }
310
311 #[inline]
312 fn broker_key(sec_key: &QotSecurityKey) -> QotSecurityKey {
313 sec_key.clone()
314 }
315
316 pub fn subscribe_qot_broker(
322 &self,
323 conn_id: u64,
324 sec_key: &QotSecurityKey,
325 sub_type: i32,
326 ) -> SubResult {
327 qot_commit::subscribe_broker(self, conn_id, Self::broker_key(sec_key), sub_type)
328 }
329
330 pub fn unsubscribe_qot_broker(
333 &self,
334 conn_id: u64,
335 sec_key: &QotSecurityKey,
336 sub_type: i32,
337 ) -> UnsubResult {
338 qot_commit::unsubscribe_broker(self, conn_id, Self::broker_key(sec_key), sub_type)
339 }
340
341 pub fn is_qot_subscribed_broker(
343 &self,
344 conn_id: u64,
345 sec_key: &QotSecurityKey,
346 sub_type: i32,
347 ) -> bool {
348 self.qot_subs
349 .read()
350 .get(&(Self::broker_key(sec_key), sub_type))
351 .is_some_and(|subs| subs.contains(&conn_id))
352 }
353
354 pub fn is_globally_subscribed_broker(&self, sec_key: &QotSecurityKey, sub_type: i32) -> bool {
357 self.qot_subs
358 .read()
359 .get(&(Self::broker_key(sec_key), sub_type))
360 .is_some_and(|subs| !subs.is_empty())
361 }
362
363 pub fn qot_min_unsub_elapsed_broker(&self, sec_key: &QotSecurityKey, sub_type: i32) -> bool {
365 self.qot_sub_times
366 .read()
367 .get(&(Self::broker_key(sec_key), sub_type))
368 .map(|instant| {
369 qot_min_unsub_freshness_from_elapsed_secs(instant.elapsed().as_secs()).min_elapsed
370 })
371 .unwrap_or(true)
372 }
373
374 pub fn qot_min_unsub_remaining_secs_broker(
376 &self,
377 sec_key: &QotSecurityKey,
378 sub_type: i32,
379 ) -> u64 {
380 self.qot_sub_times
381 .read()
382 .get(&(Self::broker_key(sec_key), sub_type))
383 .map(|instant| {
384 qot_min_unsub_freshness_from_elapsed_secs(instant.elapsed().as_secs())
385 .remaining_secs
386 })
387 .unwrap_or(0)
388 }
389
390 pub fn qot_disconnect_sync_generation(&self) -> u64 {
392 self.qot_disconnect_sync_generation.load(Ordering::SeqCst)
393 }
394
395 #[doc(hidden)]
396 pub fn backdate_qot_sub_time_broker_for_test(
397 &self,
398 sec_key: &QotSecurityKey,
399 sub_type: i32,
400 elapsed: Duration,
401 ) {
402 let map_key = (Self::broker_key(sec_key), sub_type);
403 let instant = Instant::now()
404 .checked_sub(elapsed)
405 .unwrap_or_else(Instant::now);
406 self.qot_sub_times.write().insert(map_key, instant);
407 }
408
409 pub fn unsubscribe_all_qot_collect_global_empty(&self, conn_id: u64) -> Vec<(String, i32)> {
414 unsubscribe_all_commit::collect_global_empty(self, conn_id)
415 }
416
417 pub fn cleanup_due_disconnected_qot(&self) -> Vec<(String, i32)> {
425 disconnected_cleanup::cleanup_due(self)
426 }
427
428 pub fn unsubscribe_all_qot_dry_run(&self, conn_id: u64) -> Vec<(String, i32)> {
440 let qot = self.qot_subs.read();
441 let probes = qot
442 .iter()
443 .map(|((key, sub_type), set)| UnsubscribeAllGlobalEmptyProbe {
444 key: key.cache_key(),
445 sub_type: *sub_type,
446 conn_is_subscribed: set.contains(&conn_id),
447 subscriber_count: set.len(),
448 });
449 plan_unsubscribe_all_global_empty_keys(probes)
450 }
451
452 pub fn unsubscribe_all_qot_commit(&self, conn_id: u64) -> Vec<(String, i32)> {
456 self.unsubscribe_all_qot_collect_global_empty(conn_id)
457 }
458
459 pub fn get_qot_subscribers_broker(&self, sec_key: &QotSecurityKey, sub_type: i32) -> Vec<u64> {
463 match self
464 .qot_subs
465 .read()
466 .get(&(Self::broker_key(sec_key), sub_type))
467 {
468 Some(subscribers) => subscribers.iter().copied().collect(),
469 None => Vec::new(),
470 }
471 }
472
473 pub fn qot_owner_lease_broker(
475 &self,
476 sec_key: &QotSecurityKey,
477 sub_type: i32,
478 ) -> Vec<(u64, u64, i32, u64)> {
479 let disconnected = self.qot_disconnected_conns.read();
480 let mut owners = self
481 .get_qot_subscribers_broker(sec_key, sub_type)
482 .into_iter()
483 .filter(|conn_id| !disconnected.contains(conn_id))
484 .filter_map(|conn_id| {
485 let connection_generation = self
486 .connection_generations
487 .get(&conn_id)
488 .map(|generation| *generation)
489 .unwrap_or(0);
490 let owner_token = self
491 .qot_owner_tokens
492 .read()
493 .get(&(Self::broker_key(sec_key), sub_type, conn_id))
494 .copied()?;
495 Some((
496 conn_id,
497 connection_generation,
498 self.get_conn_session_broker(conn_id, sec_key, sub_type),
499 owner_token,
500 ))
501 })
502 .collect::<Vec<_>>();
503 owners.sort_unstable();
504 owners
505 }
506
507 pub fn qot_owner_lease_is_current(
508 &self,
509 sec_key: &QotSecurityKey,
510 sub_type: i32,
511 captured: &[(u64, u64, i32, u64)],
512 request_section: Option<i32>,
513 ) -> bool {
514 self.with_current_qot_owner_lease(sec_key, sub_type, captured, request_section, || ())
515 .is_some()
516 }
517
518 pub fn with_current_qot_owner_lease<R>(
519 &self,
520 sec_key: &QotSecurityKey,
521 sub_type: i32,
522 captured: &[(u64, u64, i32, u64)],
523 request_section: Option<i32>,
524 publish: impl FnOnce() -> R,
525 ) -> Option<R> {
526 let key = Self::broker_key(sec_key);
527 let qot = self.qot_subs.read();
528 let subscribers = qot.get(&(key.clone(), sub_type))?;
529 let disconnected = self.qot_disconnected_conns.read();
530 let tokens = self.qot_owner_tokens.read();
531 let sessions = self.qot_sub_sessions.read();
532 let session_map = sessions.by_key.get(&(key.clone(), sub_type));
533 let current = captured.iter().any(
534 |(conn_id, connection_generation, captured_session, owner_token)| {
535 if !subscribers.contains(conn_id) || disconnected.contains(conn_id) {
536 return false;
537 }
538 let current_connection_generation = self
539 .connection_generations
540 .get(conn_id)
541 .map(|generation| *generation)
542 .unwrap_or(0);
543 let same_connection = current_connection_generation == *connection_generation;
544 let same_owner = tokens
545 .get(&(key.clone(), sub_type, *conn_id))
546 .is_some_and(|token| *token == *owner_token);
547 let session = session_map
548 .and_then(|map| map.get(conn_id))
549 .copied()
550 .unwrap_or(1);
551 same_connection
552 && same_owner
553 && session == *captured_session
554 && request_section.is_none_or(|section| match section {
555 2 | 3 => matches!(session, 2 | 3),
556 5 => session == 3,
557 _ => matches!(session, 0..=3),
558 })
559 },
560 );
561 current.then(publish)
562 }
563
564 fn next_qot_owner_token(&self) -> u64 {
565 let mut current = self.qot_owner_token_high_water.load(Ordering::SeqCst);
566 loop {
567 let next = if current == u64::MAX { 1 } else { current + 1 };
568 match self.qot_owner_token_high_water.compare_exchange(
569 current,
570 next,
571 Ordering::SeqCst,
572 Ordering::SeqCst,
573 ) {
574 Ok(_) => return next,
575 Err(observed) => current = observed,
576 }
577 }
578 }
579
580 pub(super) fn next_qot_push_intent_epoch(&self) -> u64 {
581 let mut current = self.qot_push_intent_high_water.load(Ordering::SeqCst);
582 loop {
583 let next = if current == u64::MAX { 1 } else { current + 1 };
584 match self.qot_push_intent_high_water.compare_exchange(
585 current,
586 next,
587 Ordering::SeqCst,
588 Ordering::SeqCst,
589 ) {
590 Ok(_) => return next,
591 Err(observed) => current = observed,
592 }
593 }
594 }
595
596 pub(super) fn assign_qot_owner_token(&self, key: &QotSecurityKey, sub_type: i32, conn_id: u64) {
597 let token = self.next_qot_owner_token();
598 self.qot_owner_tokens
599 .write()
600 .insert((Self::broker_key(key), sub_type, conn_id), token);
601 }
602
603 pub(super) fn remove_qot_owner_token(&self, key: &QotSecurityKey, sub_type: i32, conn_id: u64) {
604 self.qot_owner_tokens
605 .write()
606 .remove(&(Self::broker_key(key), sub_type, conn_id));
607 }
608
609 pub(super) fn remove_all_qot_owner_tokens(&self, conn_id: u64) {
610 self.qot_owner_tokens
611 .write()
612 .retain(|(_, _, owner), _| *owner != conn_id);
613 }
614
615 pub fn crypto_stock_globally_unsubscribed(&self, stock_id: u64) -> bool {
625 let qot = self.qot_subs.read();
626 let probes = qot.iter().map(|((key, _sub_type), subs)| {
627 CryptoSubscriptionProbe::from_runtime_facts(
628 key.stock_key.stock_id,
629 key.stock_key.broker_id,
630 subs.len(),
631 )
632 });
633 is_crypto_stock_globally_unsubscribed(stock_id, probes)
634 }
635
636 pub fn crypto_stock_broker_globally_unsubscribed(&self, stock_id: u64, broker_id: u32) -> bool {
645 let target_broker = std::num::NonZeroU32::new(broker_id);
646 let qot = self.qot_subs.read();
647 let probes = qot.iter().map(|((key, _sub_type), subs)| {
648 CryptoSubscriptionProbe::from_runtime_facts(
649 key.stock_key.stock_id,
650 key.stock_key.broker_id,
651 subs.len(),
652 )
653 });
654 is_crypto_stock_broker_globally_unsubscribed(stock_id, target_broker, probes)
655 }
656
657 pub fn set_conn_session_broker(
660 &self,
661 conn_id: u64,
662 sec_key: &QotSecurityKey,
663 sub_type: i32,
664 session: i32,
665 ) {
666 let previous = self.get_conn_session_broker(conn_id, sec_key, sub_type);
667 session_detail::set_conn_session(
668 self,
669 conn_id,
670 Self::broker_key(sec_key),
671 sub_type,
672 session,
673 );
674 if previous != session && self.is_qot_subscribed_broker(conn_id, sec_key, sub_type) {
675 self.assign_qot_owner_token(sec_key, sub_type, conn_id);
676 }
677 }
678
679 pub fn get_global_session_broker(&self, sec_key: &QotSecurityKey, sub_type: i32) -> i32 {
680 session_detail::global_session(self, Self::broker_key(sec_key), sub_type)
681 }
682
683 pub fn get_conn_session_broker(
685 &self,
686 conn_id: u64,
687 sec_key: &QotSecurityKey,
688 sub_type: i32,
689 ) -> i32 {
690 session_detail::conn_session(self, conn_id, Self::broker_key(sec_key), sub_type)
691 }
692
693 pub fn get_conn_session_by_cache_key(
696 &self,
697 conn_id: u64,
698 cache_key: &str,
699 sub_type: i32,
700 ) -> i32 {
701 self.qot_sub_sessions
702 .read()
703 .by_cache_key
704 .get(&(cache_key.to_owned(), sub_type))
705 .and_then(|sessions| sessions.get(&conn_id))
706 .copied()
707 .unwrap_or(1)
708 }
709
710 pub fn set_conn_orderbook_detail_broker(
711 &self,
712 conn_id: u64,
713 sec_key: &QotSecurityKey,
714 detail: bool,
715 ) {
716 session_detail::set_conn_orderbook_detail(self, conn_id, Self::broker_key(sec_key), detail);
717 }
718
719 pub fn is_global_orderbook_detail_broker(&self, sec_key: &QotSecurityKey) -> bool {
720 session_detail::global_orderbook_detail(self, Self::broker_key(sec_key))
721 }
722
723 pub fn set_conn_broker_detail_broker(
724 &self,
725 conn_id: u64,
726 sec_key: &QotSecurityKey,
727 detail: bool,
728 ) {
729 session_detail::set_conn_broker_detail(self, conn_id, Self::broker_key(sec_key), detail);
730 }
731
732 pub fn is_global_broker_detail_broker(&self, sec_key: &QotSecurityKey) -> bool {
733 session_detail::global_broker_detail(self, Self::broker_key(sec_key))
734 }
735
736 pub fn register_connection_open_observer(&self, observer: ConnectionOpenObserver) {
739 self.connection_open_observers.write().push(observer);
740 }
741
742 pub(crate) fn on_connect(&self, conn_id: u64, session_generation: u64) {
743 self.connection_generations
744 .insert(conn_id, session_generation);
745 let observers = self.connection_open_observers.read().clone();
746 for observer in observers {
747 observer(conn_id, session_generation);
748 }
749 }
750
751 pub fn register_disconnect_observer(&self, observer: ConnectionDisconnectObserver) {
752 self.disconnect_observers.write().push(observer);
753 }
754
755 pub(crate) fn register_client_close_control(
756 &self,
757 conn_id: u64,
758 close_control: ClientCloseControl,
759 ) {
760 self.client_close_controls.insert(conn_id, close_control);
761 }
762
763 pub(crate) fn request_client_close(&self, conn_id: u64) -> Option<bool> {
767 self.client_close_controls
768 .get(&conn_id)
769 .map(|control| control.request_close())
770 }
771
772 pub(crate) fn remove_client_close_control(&self, conn_id: u64) {
773 self.client_close_controls.remove(&conn_id);
774 }
775
776 pub fn on_disconnect(&self, conn_id: u64) -> Vec<(String, i32)> {
777 connection_lifecycle::on_disconnect(self, conn_id)
778 }
779}
780
781impl Default for SubscriptionManager {
782 fn default() -> Self {
783 Self::new()
784 }
785}
786
787#[inline]
788fn sub_type_orderbook() -> i32 {
789 2
790}
791
792#[inline]
793fn sub_type_broker() -> i32 {
794 14
795}
796
797#[cfg(test)]
798mod tests;