1use std::collections::HashMap;
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU32, Ordering};
8
9use bytes::Bytes;
10use dashmap::DashMap;
11use futu_auth::Scope;
12use futu_codec::frame::FutuFrame;
13use tokio::sync::mpsc::error::TrySendError;
14
15use crate::conn::ClientConn;
16use crate::metrics::GatewayMetrics;
17use crate::subscription::SubscriptionManager;
18
19mod kline_delivery;
20use kline_delivery::{KlineCursorKey, KlinePushCursor, RegisteredKlinePushCursor};
21
22fn should_push_to(conn: &ClientConn, needed: Scope, event_label: &str) -> bool {
29 if conn.scopes.is_empty() {
30 return true; }
32 if conn.scopes.contains(&needed) {
33 return true;
34 }
35 let key_id = conn.key_id.as_deref().unwrap_or("<none>");
37 futu_auth::metrics::bump_ws_filtered(event_label, key_id);
38 false
39}
40
41pub trait ExternalPushSink: Send + Sync {
52 fn on_quote_push(
54 &self,
55 sec_key: &str,
56 sub_type: i32,
57 rehab_type: i32,
58 proto_id: u32,
59 body: &[u8],
60 );
61 fn on_broadcast_push(&self, proto_id: u32, body: &[u8]);
63 fn on_private_user_push(&self, _proto_id: u32, _body: &[u8]) {}
65 fn on_trade_push(&self, acc_id: u64, proto_id: u32, body: &[u8], trd_market: Option<&str>);
77}
78
79#[must_use]
95pub fn extract_trd_market_from_trade_body(proto_id: u32, body: &[u8]) -> Option<&'static str> {
96 use prost::Message;
97 let market_int = match proto_id {
98 2208 => {
100 let resp = match futu_proto::trd_update_order::Response::decode(body) {
101 Ok(resp) => resp,
102 Err(e) => {
103 tracing::debug!(
104 proto_id,
105 body_len = body.len(),
106 error = %e,
107 "trade push body decode failed while extracting trd_market"
108 );
109 return None;
110 }
111 };
112 resp.s2c?.header.trd_market
113 }
114 2218 => {
116 let resp = match futu_proto::trd_update_order_fill::Response::decode(body) {
117 Ok(resp) => resp,
118 Err(e) => {
119 tracing::debug!(
120 proto_id,
121 body_len = body.len(),
122 error = %e,
123 "trade push body decode failed while extracting trd_market"
124 );
125 return None;
126 }
127 };
128 resp.s2c?.header.trd_market
129 }
130 _ => return None,
132 };
133 match market_int {
136 1 => Some("HK"),
137 2 => Some("US"),
138 3 => Some("CN"),
139 4 => Some("HKCC"),
140 5 => Some("FUTURES"),
141 6 => Some("SG"),
142 7 => Some("CRYPTO"),
143 8 => Some("AU"),
144 10 => Some("FUTURES_SIMULATE_HK"),
145 11 => Some("FUTURES_SIMULATE_US"),
146 12 => Some("FUTURES_SIMULATE_SG"),
147 13 => Some("FUTURES_SIMULATE_JP"),
148 15 => Some("JP"),
149 111 => Some("MY"),
150 112 => Some("CA"),
151 113 => Some("HKFUND"),
152 123 => Some("USFUND"),
153 124 => Some("SGFUND"),
154 125 => Some("MYFUND"),
155 126 => Some("JPFUND"),
156 _ => None,
157 }
158}
159
160pub struct PushDispatcher {
162 connections: Arc<DashMap<u64, ClientConn>>,
163 subscriptions: Arc<SubscriptionManager>,
164 metrics: Option<Arc<GatewayMetrics>>,
165 push_serial_no: AtomicU32,
170 event_contract_cursors: parking_lot::Mutex<EventContractPushCursors>,
177 kline_cursors: Arc<parking_lot::Mutex<HashMap<KlineCursorKey, RegisteredKlinePushCursor>>>,
184 external_kline_cursors: parking_lot::Mutex<HashMap<(String, i32, i32), KlinePushCursor>>,
187 external_sinks: Vec<Arc<dyn ExternalPushSink>>,
189 startup_readiness: crate::identity::StartupReadiness,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
193struct EventContractKlineCursor {
194 time_key: String,
195 fingerprint: u64,
196}
197
198#[derive(Default)]
199struct EventContractPushCursors {
200 order_book_hashes: HashMap<(u64, String), u64>,
201 ticker_sequences: HashMap<(u64, String), u64>,
202 kline_points: HashMap<(u64, String, i32, i32), EventContractKlineCursor>,
203}
204
205type EventContractKlineUpdates = Vec<(i32, EventContractKlineCursor)>;
206
207enum EventContractCursorUpdate {
208 OrderBook(u64),
209 Ticker(u64),
210 Kline(EventContractKlineUpdates),
211}
212
213impl PushDispatcher {
214 pub fn new(
218 connections: Arc<DashMap<u64, ClientConn>>,
219 subscriptions: Arc<SubscriptionManager>,
220 ) -> Self {
221 let kline_cursors = Arc::new(parking_lot::Mutex::new(HashMap::<
222 KlineCursorKey,
223 RegisteredKlinePushCursor,
224 >::new()));
225 let cursor_cleanup = Arc::clone(&kline_cursors);
226 subscriptions.register_disconnect_observer(Arc::new(move |conn_id| {
227 cursor_cleanup
228 .lock()
229 .retain(|key, _| key.conn_id != conn_id);
230 }));
231 Self {
232 connections,
233 subscriptions,
234 metrics: None,
235 push_serial_no: AtomicU32::new(0),
236 event_contract_cursors: parking_lot::Mutex::new(EventContractPushCursors::default()),
237 kline_cursors,
238 external_kline_cursors: parking_lot::Mutex::new(HashMap::new()),
239 external_sinks: Vec::new(),
240 startup_readiness: crate::identity::StartupReadiness::default(),
241 }
242 }
243
244 pub fn with_metrics(mut self, metrics: Arc<GatewayMetrics>) -> Self {
246 self.metrics = Some(metrics);
247 self
248 }
249
250 pub fn with_external_sink(mut self, sink: Arc<dyn ExternalPushSink>) -> Self {
252 self.external_sinks.push(sink);
253 self
254 }
255
256 pub fn with_startup_readiness(
257 mut self,
258 startup_readiness: crate::identity::StartupReadiness,
259 ) -> Self {
260 self.startup_readiness = startup_readiness;
261 self
262 }
263
264 fn delivery_ready(&self) -> bool {
265 self.startup_readiness.snapshot().state == crate::identity::StartupState::Ready
266 }
267
268 fn record_push(&self) {
269 if let Some(ref m) = self.metrics {
270 m.client_pushes_sent
271 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
272 }
273 }
274
275 fn record_push_send_failure(&self) {
276 if let Some(ref m) = self.metrics {
277 m.client_push_send_failures
278 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
279 }
280 }
281
282 fn record_ordinary_client_backpressure_disconnect(&self) {
283 if let Some(ref m) = self.metrics {
284 m.ordinary_client_push_backpressure_disconnects
285 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
286 }
287 }
288
289 fn record_qot_client_backpressure_drop(&self, sub_type: i32) {
290 if let Some(ref m) = self.metrics {
291 m.record_qot_client_push_backpressure_drop(sub_type);
292 }
293 }
294
295 fn next_push_serial_no(&self) -> u32 {
296 self.push_serial_no
297 .fetch_add(1, Ordering::Relaxed)
298 .wrapping_add(1)
299 }
300
301 fn try_send_ordinary_client_frame(
302 &self,
303 conn_id: u64,
304 tx: tokio::sync::mpsc::Sender<FutuFrame>,
305 frame: FutuFrame,
306 push_path: &'static str,
307 ) {
308 match tx.try_send(frame) {
309 Ok(()) => self.record_push(),
310 Err(TrySendError::Full(_frame)) => {
311 match self.subscriptions.request_client_close(conn_id) {
312 Some(true) => {
313 self.record_ordinary_client_backpressure_disconnect();
314 tracing::warn!(
315 conn_id,
316 push_path,
317 "ordinary client push queue is full; closing slow connection"
318 );
319 }
320 Some(false) => {}
321 None => {
322 let removed = self.connections.remove(&conn_id).is_some();
323 self.subscriptions.on_disconnect(conn_id);
324 if removed {
325 self.record_ordinary_client_backpressure_disconnect();
326 }
327 tracing::error!(
328 conn_id,
329 push_path,
330 removed,
331 "ordinary client push queue is full without registered close control; removed connection fail-closed"
332 );
333 }
334 }
335 }
336 Err(TrySendError::Closed(_frame)) => {
337 self.record_push_send_failure();
338 tracing::warn!(
339 conn_id,
340 push_path,
341 "client push send failed because downstream channel is closed"
342 );
343 }
344 }
345 }
346
347 fn try_send_qot_client_frame(
348 &self,
349 tx: tokio::sync::mpsc::Sender<FutuFrame>,
350 frame: FutuFrame,
351 sub_type: i32,
352 push_path: &'static str,
353 ) -> bool {
354 match tx.try_send(frame) {
355 Ok(()) => {
356 self.record_push();
357 true
358 }
359 Err(TrySendError::Full(_frame)) => {
360 self.record_qot_client_backpressure_drop(sub_type);
361 tracing::warn!(
362 push_path,
363 sub_type,
364 "client quote push dropped because downstream channel is full"
365 );
366 false
367 }
368 Err(TrySendError::Closed(_frame)) => {
369 self.record_push_send_failure();
370 tracing::warn!(
371 push_path,
372 "client quote push send failed because downstream channel is closed"
373 );
374 false
375 }
376 }
377 }
378
379 pub async fn push_to_conn(&self, conn_id: u64, proto_id: u32, body: Vec<u8>) {
381 if !self.delivery_ready() {
382 return;
383 }
384 let push = self.connections.get(&conn_id).map(|conn| {
385 let frame = conn.make_frame(proto_id, self.next_push_serial_no(), Bytes::from(body));
386 (conn.tx.clone(), frame)
387 });
388 if let Some((tx, frame)) = push {
389 self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_to_conn");
390 }
391 }
392
393 pub async fn push_qot_to_conn_generation(
395 &self,
396 conn_id: u64,
397 expected_generation: u64,
398 proto_id: u32,
399 body: Vec<u8>,
400 ) {
401 if !self.delivery_ready() {
402 return;
403 }
404 let push = self.connections.get(&conn_id).and_then(|conn| {
405 if conn.session_generation != expected_generation
406 || !should_push_to(&conn, Scope::QotRead, "indicator_direct")
407 {
408 return None;
409 }
410 let frame = conn.make_frame(proto_id, self.next_push_serial_no(), Bytes::from(body));
411 Some((conn.tx.clone(), frame))
412 });
413 if let Some((tx, frame)) = push {
414 self.try_send_qot_client_frame(tx, frame, 0, "push_qot_to_conn_generation");
415 }
416 }
417
418 pub async fn push_notify(&self, proto_id: u32, body: Vec<u8>) {
420 if !self.delivery_ready() {
421 return;
422 }
423 let body = Bytes::from(body);
424 let body_sha1 = FutuFrame::body_sha1(&body);
425 let pushes: Vec<_> = self
426 .connections
427 .iter()
428 .filter_map(|entry| {
429 let conn = entry.value();
430 if !conn.recv_notify {
431 return None;
432 }
433 if !should_push_to(conn, Scope::QotRead, "notify") {
435 return None;
436 }
437 let serial_no = self.next_push_serial_no();
438 let frame = conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
439 Some((conn.conn_id, conn.tx.clone(), frame))
440 })
441 .collect();
442 for (conn_id, tx, frame) in pushes {
443 self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_notify");
444 }
445 }
446
447 pub async fn push_trd_acc(&self, acc_id: u64, proto_id: u32, body: Vec<u8>) {
449 if !self.delivery_ready() {
450 return;
451 }
452 let trd_market = extract_trd_market_from_trade_body(proto_id, &body);
455 for sink in &self.external_sinks {
457 sink.on_trade_push(acc_id, proto_id, &body, trd_market);
458 }
459 let body = Bytes::from(body);
460 let body_sha1 = FutuFrame::body_sha1(&body);
461 let subscribers = self.subscriptions.get_acc_subscribers(acc_id);
462 let pushes: Vec<_> = subscribers
463 .into_iter()
464 .filter_map(|conn_id| {
465 let conn = self.connections.get(&conn_id)?;
466 if !should_push_to(&conn, Scope::AccRead, "trade") {
468 return None;
469 }
470 if let Some(allowed_accs) = conn.allowed_acc_ids.as_ref()
480 && !allowed_accs.is_empty()
481 && !allowed_accs.contains(&acc_id)
482 {
483 let key_id = conn.key_id.as_deref().unwrap_or("<none>");
484 futu_auth::metrics::bump_ws_filtered("trade_acc_id", key_id);
485 return None;
486 }
487 if let (Some(market), Some(allowed_mkts)) =
493 (trd_market, conn.allowed_markets.as_ref())
494 && !allowed_mkts.is_empty()
495 && !allowed_mkts.contains(market)
496 {
497 let key_id = conn.key_id.as_deref().unwrap_or("<none>");
498 futu_auth::metrics::bump_ws_filtered("trade_market", key_id);
499 return None;
500 }
501 let serial_no = self.next_push_serial_no();
502 let frame = conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
503 Some((conn.conn_id, conn.tx.clone(), frame))
504 })
505 .collect();
506 for (conn_id, tx, frame) in pushes {
507 self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_trd_acc");
508 }
509 }
510
511 pub async fn push_broadcast(&self, proto_id: u32, body: Vec<u8>) {
514 if !self.delivery_ready() {
515 return;
516 }
517 for sink in &self.external_sinks {
519 sink.on_broadcast_push(proto_id, &body);
520 }
521 let body = Bytes::from(body);
522 let body_sha1 = FutuFrame::body_sha1(&body);
523 let pushes: Vec<_> = self
524 .connections
525 .iter()
526 .filter_map(|entry| {
527 let conn = entry.value();
528 if !conn.recv_notify {
529 return None;
530 }
531 if !should_push_to(conn, Scope::QotRead, "broadcast") {
532 return None;
533 }
534 let serial_no = self.next_push_serial_no();
535 let frame = conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
536 Some((conn.conn_id, conn.tx.clone(), frame))
537 })
538 .collect();
539 for (conn_id, tx, frame) in pushes {
540 self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_broadcast");
541 }
542 }
543
544 pub fn push_private_user(&self, proto_id: u32, body: Vec<u8>) {
545 if !self.delivery_ready() {
546 return;
547 }
548 for sink in &self.external_sinks {
549 sink.on_private_user_push(proto_id, &body);
550 }
551 let body = Bytes::from(body);
552 let body_sha1 = FutuFrame::body_sha1(&body);
553 let pushes: Vec<_> = self
554 .connections
555 .iter()
556 .filter_map(|entry| {
557 let conn = entry.value();
558 if !conn.recv_notify
559 || conn.key_id.is_none()
560 || !conn.scopes.contains(&Scope::AccRead)
561 {
562 return None;
563 }
564 let serial_no = self.next_push_serial_no();
565 let frame = conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
566 Some((conn.conn_id, conn.tx.clone(), frame))
567 })
568 .collect();
569 for (conn_id, tx, frame) in pushes {
570 self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_private_user");
571 }
572 }
573
574 fn push_event_contract_qot(
575 &self,
576 security_key: &str,
577 sub_type: i32,
578 rehab_type: i32,
579 proto_id: u32,
580 body: &[u8],
581 ) {
582 self.prune_event_contract_cursors();
583 let subscribers = self.subscriptions.get_qot_push_subscribers_by_cache_key(
584 security_key,
585 sub_type,
586 rehab_type,
587 );
588 for conn_id in subscribers {
589 let Some(conn) = self.connections.get(&conn_id) else {
590 continue;
591 };
592 if !should_push_to(&conn, Scope::QotRead, "quote") {
593 continue;
594 }
595 let decision =
596 self.event_contract_body_for_conn(conn_id, security_key, sub_type, proto_id, body);
597 let Some((body, update)) = decision else {
598 continue;
599 };
600 let body = Bytes::from(body);
601 let frame = conn.make_frame_with_sha1(
602 proto_id,
603 self.next_push_serial_no(),
604 body.clone(),
605 FutuFrame::body_sha1(&body),
606 );
607 let tx = conn.tx.clone();
608 drop(conn);
609 if self.try_send_qot_client_frame(tx, frame, sub_type, "push_event_contract_qot") {
610 self.commit_event_contract_cursor(conn_id, security_key, sub_type, update);
611 }
612 }
613 }
614
615 fn prune_event_contract_cursors(&self) {
616 let mut cursors = self.event_contract_cursors.lock();
617 cursors
618 .order_book_hashes
619 .retain(|(conn_id, _), _| self.connections.contains_key(conn_id));
620 cursors
621 .ticker_sequences
622 .retain(|(conn_id, _), _| self.connections.contains_key(conn_id));
623 cursors
624 .kline_points
625 .retain(|(conn_id, _, _, _), _| self.connections.contains_key(conn_id));
626 }
627
628 fn event_contract_body_for_conn(
629 &self,
630 conn_id: u64,
631 security_key: &str,
632 sub_type: i32,
633 proto_id: u32,
634 body: &[u8],
635 ) -> Option<(Vec<u8>, EventContractCursorUpdate)> {
636 match proto_id {
637 futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_ORDER_BOOK => {
638 let hash = push_body_hash(body);
639 let repeated = self
640 .event_contract_cursors
641 .lock()
642 .order_book_hashes
643 .get(&(conn_id, security_key.to_owned()))
644 .is_some_and(|previous| *previous == hash);
645 (!repeated).then(|| (body.to_vec(), EventContractCursorUpdate::OrderBook(hash)))
646 }
647 futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_TICKER => {
648 let previous = self
649 .event_contract_cursors
650 .lock()
651 .ticker_sequences
652 .get(&(conn_id, security_key.to_owned()))
653 .copied()
654 .unwrap_or(0);
655 filter_event_contract_ticker_body(body, previous)
656 .map(|(body, sequence)| (body, EventContractCursorUpdate::Ticker(sequence)))
657 }
658 futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_KLINE => {
659 let cursors = self.event_contract_cursors.lock();
660 filter_event_contract_kline_body(body, |direction| {
661 cursors
662 .kline_points
663 .get(&(conn_id, security_key.to_owned(), sub_type, direction))
664 .cloned()
665 })
666 .map(|(body, updates)| (body, EventContractCursorUpdate::Kline(updates)))
667 }
668 _ => None,
669 }
670 }
671
672 fn commit_event_contract_cursor(
673 &self,
674 conn_id: u64,
675 security_key: &str,
676 sub_type: i32,
677 update: EventContractCursorUpdate,
678 ) {
679 let mut cursors = self.event_contract_cursors.lock();
680 match update {
681 EventContractCursorUpdate::OrderBook(hash) => {
682 cursors
683 .order_book_hashes
684 .insert((conn_id, security_key.to_owned()), hash);
685 }
686 EventContractCursorUpdate::Ticker(sequence) => {
687 cursors
688 .ticker_sequences
689 .entry((conn_id, security_key.to_owned()))
690 .and_modify(|current| *current = (*current).max(sequence))
691 .or_insert(sequence);
692 }
693 EventContractCursorUpdate::Kline(updates) => {
694 for (direction, cursor) in updates {
695 cursors.kline_points.insert(
696 (conn_id, security_key.to_owned(), sub_type, direction),
697 cursor,
698 );
699 }
700 }
701 }
702 }
703}
704
705fn push_body_hash(body: &[u8]) -> u64 {
706 let mut hasher = DefaultHasher::new();
707 body.hash(&mut hasher);
708 hasher.finish()
709}
710
711fn event_contract_ticker_cursor_from_body(proto_id: u32, body: &[u8]) -> Option<(String, u64)> {
712 use prost::Message;
713
714 if proto_id != futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_TICKER {
715 return None;
716 }
717 let response = futu_proto::qot_update_event_contract_ticker::Response::decode(body).ok()?;
718 let item = response.s2c?.ticker_list.into_iter().next()?;
719 let sequence = item
720 .ticker_list
721 .iter()
722 .filter_map(|point| point.sequence.as_deref()?.parse::<u64>().ok())
723 .max()?;
724 Some((format!("{}_{}", item.code.market, item.code.code), sequence))
725}
726
727fn filter_event_contract_ticker_body(body: &[u8], previous: u64) -> Option<(Vec<u8>, u64)> {
728 use prost::Message;
729
730 let mut response = futu_proto::qot_update_event_contract_ticker::Response::decode(body).ok()?;
731 let s2c = response.s2c.as_mut()?;
732 let mut newest = previous;
733 for item in &mut s2c.ticker_list {
734 item.ticker_list.retain(|point| {
735 let Some(sequence) = point
736 .sequence
737 .as_deref()
738 .and_then(|value| value.parse::<u64>().ok())
739 else {
740 return false;
741 };
742 if sequence <= previous {
743 return false;
744 }
745 newest = newest.max(sequence);
746 true
747 });
748 }
749 s2c.ticker_list.retain(|item| !item.ticker_list.is_empty());
750 (!s2c.ticker_list.is_empty()).then(|| (response.encode_to_vec(), newest))
751}
752
753fn filter_event_contract_kline_body(
754 body: &[u8],
755 mut previous_for_direction: impl FnMut(i32) -> Option<EventContractKlineCursor>,
756) -> Option<(Vec<u8>, EventContractKlineUpdates)> {
757 use prost::Message;
758
759 let mut response = futu_proto::qot_update_event_contract_kline::Response::decode(body).ok()?;
760 let s2c = response.s2c.as_mut()?;
761 let mut updates = Vec::new();
762 for item in &mut s2c.kline_list {
763 let direction = item.pre_side.unwrap_or(0);
764 let mut cursor = previous_for_direction(direction);
765 if item.kline_list.is_empty() {
766 let fingerprint = push_body_hash(&item.encode_to_vec());
767 if cursor
768 .as_ref()
769 .is_some_and(|previous| previous.fingerprint == fingerprint)
770 {
771 continue;
772 }
773 updates.push((
774 direction,
775 EventContractKlineCursor {
776 time_key: cursor
777 .as_ref()
778 .map(|previous| previous.time_key.clone())
779 .unwrap_or_default(),
780 fingerprint,
781 },
782 ));
783 continue;
784 }
785 item.kline_list.retain(|point| {
786 let fingerprint = push_body_hash(&point.encode_to_vec());
787 if cursor.as_ref().is_some_and(|previous| {
788 previous.time_key > point.time_key
789 || (previous.time_key == point.time_key && previous.fingerprint == fingerprint)
790 }) {
791 return false;
792 }
793 cursor = Some(EventContractKlineCursor {
794 time_key: point.time_key.clone(),
795 fingerprint,
796 });
797 true
798 });
799 if let Some(cursor) = cursor
800 && !item.kline_list.is_empty()
801 {
802 updates.push((direction, cursor));
803 }
804 }
805 s2c.kline_list.retain(|item| {
806 !item.kline_list.is_empty()
807 || updates
808 .iter()
809 .any(|(direction, _)| *direction == item.pre_side.unwrap_or(0))
810 });
811 (!s2c.kline_list.is_empty() && !updates.is_empty()).then(|| (response.encode_to_vec(), updates))
812}
813
814#[cfg(test)]
815mod tests;