futu_auth/limits/
runtime.rs1use super::*;
6use chrono::NaiveDate;
7use dashmap::DashMap;
8use parking_lot::Mutex;
9use std::collections::HashMap;
10
11mod accepted_quota;
14mod attempted_quota;
15mod batch;
16
17#[derive(Debug, Default)]
18pub struct RuntimeCounters {
19 pub(super) counters: DashMap<String, DailyCounter>,
20 rates: DashMap<String, RateWindow>,
21 daily_receipts: Mutex<HashMap<String, DailyReceipt>>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25struct DailyReceipt {
26 fingerprint: [u8; 32],
27 day: NaiveDate,
28 deltas: Vec<(Option<String>, f64)>,
29}
30
31#[must_use = "LimitGuard 必须显式 commit_daily() 或 drop(); drop 时 daily counter 不写入 (accepted-quota 语义)"]
54#[derive(Debug)]
55pub struct LimitGuard<'a> {
56 counters: &'a RuntimeCounters,
57 key_id: String,
58 pending_daily: Option<(f64, NaiveDate, Option<String>)>,
63 daily_cap: Option<f64>,
65}
66
67#[must_use = "BatchLimitGuard must be committed after durable writer admission or dropped"]
71#[derive(Debug)]
72pub struct BatchLimitGuard<'a> {
73 counters: &'a RuntimeCounters,
74 key_id: String,
75 receipt_key: String,
76 fingerprint: [u8; 32],
77 pending_daily: Vec<(Option<String>, f64)>,
78 daily_cap: Option<f64>,
79 today: NaiveDate,
80 already_committed: bool,
81}
82
83fn batch_daily_error(error: DailyAddError) -> LimitOutcome {
84 match error {
85 DailyAddError::OverCap(message) => LimitOutcome::ThroughputReject(message),
86 DailyAddError::Invalid(reason) => LimitOutcome::ValueReject(format!(
87 "order value invalid ({reason:?}) in batch daily admission"
88 )),
89 DailyAddError::StaleDay {
90 guard_day,
91 current_day,
92 } => stale_day_outcome(guard_day, current_day),
93 DailyAddError::Underflow {
94 currency,
95 current,
96 release,
97 } => LimitOutcome::ValueReject(format!(
98 "daily receipt release underflow for {currency}: current={current:.2}, release={release:.2}"
99 )),
100 }
101}
102
103fn stale_day_outcome(guard_day: NaiveDate, current_day: NaiveDate) -> LimitOutcome {
104 LimitOutcome::ThroughputReject(format!(
105 "stale daily quota guard date {guard_day}; counter already advanced to {current_day}"
106 ))
107}
108
109impl RuntimeCounters {
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 #[cfg(test)]
115 pub(super) fn peek_total(&self, key_id: &str) -> f64 {
116 self.counters
117 .get(key_id)
118 .map(|c| c.peek_total())
119 .unwrap_or(0.0)
120 }
121
122 #[cfg(test)]
123 pub(super) fn peek_total_for_currency(&self, key_id: &str, currency: &str) -> f64 {
124 self.counters
125 .get(key_id)
126 .map(|c| c.peek_total_for_currency(currency))
127 .unwrap_or(0.0)
128 }
129}