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