futu_cache/trd_cache/
freshness.rs1use std::hash::Hash;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::time::Instant;
4
5use dashmap::DashMap;
6use dashmap::mapref::entry::Entry;
7use futu_domain_trade_account::TradeSnapshotFreshnessFacts;
8
9use super::{CachedFunds, CachedPosition, PositionsCacheKey, TrdCache};
10
11#[derive(Debug, Clone)]
12pub struct StampedTradeSnapshot<T> {
13 value: T,
14 updated_at: Instant,
15 sequence: u64,
16}
17
18impl<T> StampedTradeSnapshot<T> {
19 #[must_use]
20 pub fn value(&self) -> &T {
21 &self.value
22 }
23
24 #[must_use]
25 pub fn sequence(&self) -> u64 {
26 self.sequence
27 }
28}
29
30#[derive(Debug, Clone)]
31pub struct FundsSnapshotLookup {
32 pub funds: Option<CachedFunds>,
33 pub currency_match: bool,
34 pub freshness: TradeSnapshotFreshnessFacts,
35}
36
37#[derive(Debug, Clone)]
38pub struct PositionsSnapshotLookup {
39 pub positions: Option<Vec<CachedPosition>>,
40 pub freshness: TradeSnapshotFreshnessFacts,
41 pub sequence: Option<u64>,
43}
44
45pub(super) struct TradeSnapshotFreshnessStore {
46 next_sequence: AtomicU64,
47 asset_push_sequences: DashMap<PositionsCacheKey, u64>,
48}
49
50impl TradeSnapshotFreshnessStore {
51 pub(super) fn new() -> Self {
52 Self {
53 next_sequence: AtomicU64::new(1),
54 asset_push_sequences: DashMap::new(),
55 }
56 }
57
58 pub(super) fn stamp<T>(&self, value: T) -> StampedTradeSnapshot<T> {
59 StampedTradeSnapshot {
60 value,
61 updated_at: Instant::now(),
62 sequence: self.take_sequence(),
63 }
64 }
65
66 pub(super) fn record_asset_push(&self, key: PositionsCacheKey) {
67 let sequence = self.take_sequence();
68 match self.asset_push_sequences.entry(key) {
69 Entry::Occupied(mut entry) => {
70 if sequence > *entry.get() {
71 entry.insert(sequence);
72 }
73 }
74 Entry::Vacant(entry) => {
75 entry.insert(sequence);
76 }
77 }
78 }
79
80 pub(super) fn freshness<T>(
81 &self,
82 key: PositionsCacheKey,
83 snapshot: Option<&StampedTradeSnapshot<T>>,
84 ) -> TradeSnapshotFreshnessFacts {
85 let Some(snapshot) = snapshot else {
86 return TradeSnapshotFreshnessFacts {
87 snapshot_age_ms: None,
88 predates_server_push: false,
89 };
90 };
91 let snapshot_age_ms =
92 u64::try_from(snapshot.updated_at.elapsed().as_millis()).unwrap_or(u64::MAX);
93 let predates_server_push = self
94 .asset_push_sequences
95 .get(&key)
96 .is_some_and(|push_sequence| snapshot.sequence <= *push_sequence);
97 TradeSnapshotFreshnessFacts {
98 snapshot_age_ms: Some(snapshot_age_ms),
99 predates_server_push,
100 }
101 }
102
103 fn take_sequence(&self) -> u64 {
104 self.next_sequence.fetch_add(1, Ordering::Relaxed)
107 }
108}
109
110pub(super) fn insert_newer_snapshot<K, T>(
111 map: &DashMap<K, StampedTradeSnapshot<T>>,
112 key: K,
113 snapshot: StampedTradeSnapshot<T>,
114) where
115 K: Eq + Hash,
116{
117 match map.entry(key) {
118 Entry::Occupied(mut entry) => {
119 if snapshot.sequence > entry.get().sequence {
120 entry.insert(snapshot);
121 }
122 }
123 Entry::Vacant(entry) => {
124 entry.insert(snapshot);
125 }
126 }
127}
128
129impl TrdCache {
130 pub fn mark_asset_server_push(&self, acc_id: u64, asset_category: i32) {
135 self.snapshot_freshness
136 .record_asset_push(PositionsCacheKey::scoped(acc_id, asset_category));
137 }
138}