pub struct TrdCache {
pub accounts: DashMap<AccKey, CachedTrdAcc>,
pub account_relations: DashMap<AccKey, Vec<AccKey>>,
pub public_account_ids: DashMap<AccKey, ()>,
pub funds: DashMap<FundsCacheKey, CachedFunds>,
pub positions: DashMap<PositionsCacheKey, Vec<CachedPosition>>,
pub orders: DashMap<AccKey, Vec<CachedOrder>>,
pub ciphers: DashMap<AccKey, Vec<u8>>,
pub order_brokers: DashMap<String, u32>,
pub cipher_state_versions: DashMap<AccKey, Arc<AtomicU64>>,
pub pending_order_confirms: DashMap<OrderConfirmKey, OrderConfirmContext>,
/* private fields */
}Expand description
交易数据缓存
Fields§
§accounts: DashMap<AccKey, CachedTrdAcc>C++ NNData_Trd_AccList::m_mapUserAccList equivalent.
This is the authoritative internal account index used by request
validation, broker routing, and funds/positions/order queries. It may
contain universal parent accounts that are intentionally not exposed by
public Trd_GetAccList.
account_relations: DashMap<AccKey, Vec<AccKey>>C++ NNData_Trd_AccList::m_mapIDRelation equivalent:
universal_or_self_acc_id -> public sub account ids.
Trd_GetAccList uses this relation via get_accounts() to expose the
same public projection as C++ GetAllSubAccList, while lookup_account
and direct accounts.get() still see the full internal map.
public_account_ids: DashMap<AccKey, ()>Public account ids derived from account_relations.
funds: DashMap<FundsCacheKey, CachedFunds>资金: FundsCacheKey { acc_id, asset_category, currency } → funds.
v1.4.106 Finding A: 之前 DashMap<AccKey, CachedFunds> 一 acc 一 snapshot,
Universal/Futures 多币种场景被覆盖 — 改 currency-aware key 对齐 C++
m_mapAccFund: NN_AssetKey -> NN_TrdCurrency -> Ndt_Trd_AccFund.
positions: DashMap<PositionsCacheKey, Vec<CachedPosition>>持仓: PositionsCacheKey { acc_id, asset_category } → Vec
orders: DashMap<AccKey, Vec<CachedOrder>>当日订单: acc_id → Vec
ciphers: DashMap<AccKey, Vec<u8>>交易 cipher: acc_id → cipher bytes (解锁后获得)
order_brokers: DashMap<String, u32>v1.4.48 #1: 订单 broker 映射(order_id_ex → broker_id_used)
起源:v1.4.47 P0.1 修了 PlaceOrder 按 sec_market 选 broker,但 ModifyOrder /
CancelOrder 仍按 account.security_firm 选 broker,导致“在 broker 1007 (US)
下的单,cancel 去 broker 1019 (CA) 拒“ 的 cross-broker 故障。
修法:PlaceOrder 成功后把 (order_id_ex, broker_id_used) 缓存到这里。
ModifyOrder / CancelOrder 拿到 c2s.order_id_ex 后先查 broker_id;
命中 → 路由到同 broker;未命中 → fallback account.firm 路由。
注:cipher 按 sub-account acc_id 存储(ciphers map)。对照 C++
NNData_Trd_AccList::m_mapAccCipher:不同 broker 的账户天然有不同
nAccID,存储已隔离(v1.4.49 清理了 v1.4.48 cipher_brokers workaround,
该字段在 v1.4.48 #11 routing 对齐 C++ 后成 dead code)。
cipher_state_versions: DashMap<AccKey, Arc<AtomicU64>>v1.4.73 A2 BUG-008 fix: per-account cipher state version counter。
外部 tester (v1.4.71) AI 报告 5 步 repro:
Step 1: unlock pwd → cache EXECUTED (idem_key=unlock-xxx)
Step 2: 同 body → cache HIT (正常幂等)
Step 3: EMPTY {} LOCK → v1.4.39 cipher 清
Step 4: 同 body → cache HIT 返 stale 成功! (真 bug)
Step 5: place-order → -401 "交易未解锁"v1.4.72 Option C(空 body 不写 cache)只防 step 3 污染,未修 step 4 stale。
Option A 真修:unlock idem_key 构造时纳入当前 cipher_state_version,
lock 清 cipher 时 fetch_add(1, SeqCst) → version 递增 → step 4 同 body
得 idem_key 不同(version=0 → version=1)→ cache miss → 真执行 unlock
或 backend 校验失败返清晰错误。
为啥 SeqCst:unlock_trade handler 可能并发,确保 version 递增对所有
后续 idem_key 构造 visible(ciphers.remove() + fetch_add() 顺序严格)。
注:version 不持久化 —— daemon restart 重新从 0 开始,等效于“新 cache“, 之前的 idem entries 也被 cache TTL 清光,零冲突。
pending_order_confirms: DashMap<OrderConfirmKey, OrderConfirmContext>v1.4.106 codex 0226 F1+F2: pending OrderConfirm context per
(acc_id, ftapi_order_id).
PlaceOrder ack 响应里若 OrderNewRsp.action.type == ORDER_CONFIRM=5 且
action.order_confirm.is_some(), daemon 必须 capture
CltActionOrderConfirm 字段, 用于后续 Trd_ReconfirmOrder 处理时构造
backend OrderConfirmReq (cmd 4728).
生命周期:
- PlaceOrder ack 路径: capture 后
insert(key, ctx) - ReconfirmOrder handler: lookup → 构造 backend req → 收到
OrderConfirmRspresult==0后remove(key)(一次性消费, 防止重复 confirm) - TTL: 5min (
ORDER_CONFIRM_CONTEXT_TTL_MS),now - inserted_at_ms检查; stale entry handler 拒绝 + GC 清理 - daemon restart 全清 (内存 cache, backend 重新发 PlaceOrder 即可获新 context)
详见 OrderConfirmContext doc.
Implementations§
Source§impl TrdCache
impl TrdCache
Sourcepub const STUB_TTL_MS: u64 = 30_000
pub const STUB_TTL_MS: u64 = 30_000
v1.4.90 S BUG-e4da-009: stub TTL(30s)。
stub 插入超过此 TTL 且 backend 仍不返该 order_id → 视为 backend 永久
拒单(never accepted into authoritative list)→ evict。
pub fn get_cipher(&self, acc_id: u64) -> Option<Vec<u8>>
pub fn set_cipher(&self, acc_id: u64, cipher: Vec<u8>)
Sourcepub fn get_cipher_state_version(&self, acc_id: u64) -> u64
pub fn get_cipher_state_version(&self, acc_id: u64) -> u64
v1.4.73 A2 BUG-008 fix: 读当前账户的 cipher state version(用于 unlock idem_key)。
首次访问 acc_id 会初始化为 0。后续每次 lock 清 cipher 会 fetch_add(1)。
idem_key 构造时把这个 version 纳入 hash → cipher 清后 version 递增 →
同 body 的 idem_key 不同 → cache miss → 真执行 unlock(或 backend 真校验)。
Sourcepub fn bump_cipher_state_version(&self, acc_id: u64) -> u64
pub fn bump_cipher_state_version(&self, acc_id: u64) -> u64
v1.4.73 A2 BUG-008 fix: lock 清 cipher 时调,递增 version → 让下次 unlock 同 body 得 cache miss。
返回 new version(递增后值),便于调用方 log。
Sourcepub fn store_pending_order_confirm(
&self,
acc_id: u64,
ftapi_order_id: u64,
ctx: OrderConfirmContext,
now_ms: u64,
)
pub fn store_pending_order_confirm( &self, acc_id: u64, ftapi_order_id: u64, ctx: OrderConfirmContext, now_ms: u64, )
v1.4.106 codex 0226 F1+F2: PlaceOrder 解析到 OrderNewRsp.action.order_confirm
时调用, 保存上下文用于后续 Trd_ReconfirmOrder 构造 backend OrderConfirmReq.
now_ms 由 caller 传入 (便于单测注入固定时钟); 真实路径用
SystemTime::now().
Sourcepub fn get_pending_order_confirm(
&self,
acc_id: u64,
ftapi_order_id: u64,
now_ms: u64,
) -> Option<OrderConfirmContext>
pub fn get_pending_order_confirm( &self, acc_id: u64, ftapi_order_id: u64, now_ms: u64, ) -> Option<OrderConfirmContext>
v1.4.106 codex 0226 F1+F2: ReconfirmOrder handler 入口 lookup, 取出
(acc_id, ftapi_order_id) 对应 OrderConfirmContext.
返 None: cache miss (PlaceOrder 没存 / TTL 过期 / 已被消费). caller 必须
早 reject loud, 不允许 silent fallback (避免反模式 D / silent-success).
now_ms 检查 TTL: now - ctx.inserted_at_ms > ORDER_CONFIRM_CONTEXT_TTL_MS
视为 stale → return None + remove (proactive GC).
Sourcepub fn remove_pending_order_confirm(
&self,
acc_id: u64,
ftapi_order_id: u64,
) -> bool
pub fn remove_pending_order_confirm( &self, acc_id: u64, ftapi_order_id: u64, ) -> bool
v1.4.106 codex 0226 F1+F2: ReconfirmOrder backend 成功 (OrderConfirmRsp.result==0)
后调用, 从 cache 删除 (一次性消费, 防重复 confirm).
返 true 表示真有删除发生; false = 已被其他路径消费 / 过期 GC.
Sourcepub fn purge_stale_order_confirms(&self, now_ms: u64) -> usize
pub fn purge_stale_order_confirms(&self, now_ms: u64) -> usize
v1.4.106 codex 0226 F1+F2: GC stale OrderConfirmContext entries.
用于定时清理 (push dispatcher 收到 ORDER 类 push 时顺便扫一次), 防止 stale ctx 累积. 返回清理掉的条目数.
Sourcepub fn clear_all_ciphers_and_bump_versions(&self) -> (usize, Vec<(u64, u64)>)
pub fn clear_all_ciphers_and_bump_versions(&self) -> (usize, Vec<(u64, u64)>)
v1.4.106 codex 0554 F1 [P1]: 原子性清空所有 cipher + 同步 bump 各账户的
cipher_state_version。
起源:/api/admin/reload 之前的实现是
bridge.caches.trd_cache.ciphers.clear() 直接动 DashMap,但 没 bump
cipher_state_version。这与 v1.4.73 A2 BUG-008 修复的语义不一致:
lock-trade 路径里 ciphers.remove() 之后必跟 bump_cipher_state_version(),
防止旧 idempotency cache entry(unlock idem_key 含 cipher_state_version
hash)在 cipher 被清后仍命中返 stale “cached success”,导致
step 4 / step 5 silent regression。
admin/reload 漏 bump 的具体后果:
- reload 清光 ciphers
- 客户端再调 unlock-trade 同 body → idem_key 命中(cipher_state_version
未变)→ 返 stale 成功 → cipher cache 仍空 → place-order
-401解锁失败
本 helper 把两步打包,禁止外部直接 cache.ciphers.clear()(那条
路径 silent skip bump,复活 BUG-008)。所有清 cipher 的 control-plane
路径(reload / admin / 未来若加更多)必须走本 helper。
返回 (cleared_count, bumped_versions):
cleared_count:清掉的 cipher 数(即 reload 前已解锁账户数)bumped_versions:每个被清 acc_id 的 (acc_id, new_version) 列表, 便于 log + 客户端调试 idem_key 失效原因
与 lock-trade 路径的 bump 行为一致:仅对实际清掉 cipher 的 acc_id 递增 version;从未解锁的账户 cipher_state_version 保持不变。
并发:DashMap::iter() 期间其他线程的 ciphers.remove() /
ciphers.insert() 可能 race,但本 helper 用 remove(&key) 逐个清,
拿到 Some(_) 才 bump,保证 version 单调递增 + 与 ciphers 实际
状态一致。SeqCst 保证 bump 对所有后续 get_cipher_state_version()
立即可见。
pub fn new() -> Self
pub fn set_accounts(&self, accounts: Vec<CachedTrdAcc>)
Sourcepub fn set_accounts_with_relations(
&self,
accounts: Vec<CachedTrdAcc>,
relations: Vec<(AccKey, Vec<AccKey>)>,
)
pub fn set_accounts_with_relations( &self, accounts: Vec<CachedTrdAcc>, relations: Vec<(AccKey, Vec<AccKey>)>, )
Atomically replace the internal account map and the public projection.
relations mirrors C++ m_mapIDRelation: standalone accounts map to
themselves, while universal parents map to their public sub accounts.
This lets GetAccList expose only C++ GetAllSubAccList output without
losing hidden parent accounts needed by GetAccItem-style request paths.
pub fn get_accounts(&self) -> Vec<CachedTrdAcc>
Sourcepub fn lookup_account(&self, acc_id: u64) -> Option<CachedTrdAcc>
pub fn lookup_account(&self, acc_id: u64) -> Option<CachedTrdAcc>
v1.4.106 codex 0932 F2 [P1]: 单 acc_id O(1) 查询 (DashMap key 直查).
用途: push_builder 构造 Trd_UpdateOrder / Trd_UpdateOrderFill header
之前 resolve trd_env + trd_market. 对齐 C++
INNData_Trd_AllAccList::GetAccEnv(nAccID) / GetAccMkt(nAccID).
返 None = cache miss (账户不在交易 cache 中). caller 必须 loud
return 不 fallback (sentinel 0 让 client filter reject =
silent-success 反模式).
Sourcepub fn find_acc_ids_by_card_num(&self, input: &str) -> Vec<u64>
pub fn find_acc_ids_by_card_num(&self, input: &str) -> Vec<u64>
v1.4.103 (B10): card_num → acc_id resolution helper.
接受输入:
- 16 位完整 card_num (
"1001100100800000"): 完全匹配card_num字段. - 4 位末尾 suffix (
"7680"): 匹配card_num末 4 位 (App 显示格式).
返 Vec<u64> (matching acc_ids):
- 0 个 → cache 中无 match (caller 决定 warn / abort);
- 1 个 → unique resolution;
-
= 2 个 → ambiguous (caller 必须 reject + log 候选, 不能 silent 接受).
空字符串 / 非纯数字 / 长度非 4 / 非 16 → 返 empty Vec (不 panic). 这是为了让 caller 输入校验 + resolution 双责权: 调用方应该已经校验过格式.
Sourcepub fn update_funds(&self, acc_id: u64, funds: CachedFunds)
pub fn update_funds(&self, acc_id: u64, funds: CachedFunds)
v1.4.106 Finding A (legacy compat): 不带 currency 维度的 update.
用 FundsCacheKey::legacy(acc_id) 作 key. 适用于:
- 现有 caller 还没改 signature 的 (背景: backend push 不一定知 currency)
- SingleCurrency / sim / Crypto / Forex 账户 (本来就单币种)
新 caller 应优先用 Self::update_funds_per_currency 显式标
currency 维度, 让 Universal/Futures 账户能存独立 snapshot per currency.
Sourcepub fn update_funds_per_currency(
&self,
acc_id: u64,
currency: Option<i32>,
funds: CachedFunds,
)
pub fn update_funds_per_currency( &self, acc_id: u64, currency: Option<i32>, funds: CachedFunds, )
v1.4.106 Finding A (preferred for Universal/Futures): 带 currency
维度的 update. backend push 时若知 funds 的实际 currency (从 f.currency
字段或 push context 派生), 应该用这个 helper 让多币种 snapshot 不互相覆盖.
对齐 C++ INNData_Trd_Acc::SetAccFund(stKey, enCurrency, ...).
Sourcepub fn update_funds_scoped(
&self,
acc_id: u64,
asset_category: i32,
currency: Option<i32>,
funds: CachedFunds,
)
pub fn update_funds_scoped( &self, acc_id: u64, asset_category: i32, currency: Option<i32>, funds: CachedFunds, )
Currency + asset-category aware funds update.
JP derivative accounts use asset_category as part of the C++ asset key.
Non-JP/legacy callers should pass asset_category=0, which preserves the
existing legacy/per-currency key shape.
Sourcepub fn update_funds_scoped_with_returned_currency(
&self,
acc_id: u64,
asset_category: i32,
requested_currency: Option<i32>,
funds: CachedFunds,
)
pub fn update_funds_scoped_with_returned_currency( &self, acc_id: u64, asset_category: i32, requested_currency: Option<i32>, funds: CachedFunds, )
Update the requested funds bucket and also mirror the returned backend currency bucket when it is known.
C++ stores Ndt_Trd_AccFund under accFund.enCurrency
(INNData_Trd_Acc.cpp::SetAccFund). A Rust caller may request CMD3020
with currency=None because the daemon derived the backend default, but
REST/CLI later read the same account through an explicit effective
currency bucket. Mirroring prevents an older per-currency snapshot from
masking a fresher default refresh.
Sourcepub fn get_funds(
&self,
acc_id: u64,
currency: Option<i32>,
) -> (Option<CachedFunds>, bool)
pub fn get_funds( &self, acc_id: u64, currency: Option<i32>, ) -> (Option<CachedFunds>, bool)
v1.4.106 Finding A: cache lookup with C++-equivalent fallback.
对齐 C++ INNData_Trd_Acc::GetAccFund(stKey, enCurrency, pAccFund):
先试 requested currency, 找不到则 fallback 到 latest/first available
currency, 返 false (caller 应看 boolean 决定是否 trust).
输入 currency:
Some(c): Universal/Futures 路径, 优先 match per-currency snapshotNone: SingleCurrency 路径, 直接 matchlegacy(acc_id)snapshot
输出 (funds, currency_match):
(Some(funds), true): 精确命中 requested currency snapshot(Some(funds), false): 命中 fallback (legacy 或不同 currency 的 snapshot — caller 应不要 silent trust, 至少 log warn 或 surface currency mismatch)(None, _): 完全 cache miss
Sourcepub fn get_funds_scoped(
&self,
acc_id: u64,
asset_category: i32,
currency: Option<i32>,
) -> (Option<CachedFunds>, bool)
pub fn get_funds_scoped( &self, acc_id: u64, asset_category: i32, currency: Option<i32>, ) -> (Option<CachedFunds>, bool)
Funds lookup using the same (acc_id, asset_category, currency) dimensions
as Self::update_funds_scoped.
For asset_category != 0 we require an exact scoped hit. Falling back to a
legacy or another asset-category snapshot would mix JP derivative asset
buckets and silently return the wrong funds.
pub fn update_positions(&self, acc_id: u64, positions: Vec<CachedPosition>)
pub fn update_positions_scoped( &self, acc_id: u64, asset_category: i32, positions: Vec<CachedPosition>, )
pub fn get_positions_scoped( &self, acc_id: u64, asset_category: i32, ) -> Option<Vec<CachedPosition>>
pub fn has_positions_scoped(&self, acc_id: u64, asset_category: i32) -> bool
pub fn update_orders(&self, acc_id: u64, orders: Vec<CachedOrder>)
Sourcepub fn upsert_order(&self, acc_id: u64, order: CachedOrder)
pub fn upsert_order(&self, acc_id: u64, order: CachedOrder)
更新单个订单(推送场景)
Sourcepub fn find_order_for_trade_write(
&self,
acc_id: u64,
order_id: u64,
order_id_ex: Option<&str>,
) -> Result<CachedOrderSnapshot, ResolveOrderError>
pub fn find_order_for_trade_write( &self, acc_id: u64, order_id: u64, order_id_ex: Option<&str>, ) -> Result<CachedOrderSnapshot, ResolveOrderError>
v1.4.106 codex 0219 Finding 1: resolve cached order context for trade-write (modify / cancel) handlers.
对齐 C++ APIServer_Trd_ModifyOrder.cpp:251-256 + :270-271:
- 优先用 client 传的
orderIDEx(= backendszOrderID). - 否则用
(acc_id, order_id_hash)从 cache 找原 order, 取它的szOrderID+version+exchange*字段构造 backend req.
fail-closed 语义: cache miss → 返 Err, caller 把错误透传到
FTAPI client 让用户先刷新 /api/orders 或传 orderIDEx. 不允许 silent
fall-through 到 order_id.to_string() (= 把 hash 当 backend id, 见
pitfall #45 silent-success).
入参:
acc_id: FTAPIc2s.header.acc_id.order_id: FTAPIc2s.order_id(hash).0视为 caller 没传, 仅靠order_id_ex路径生效.order_id_ex: FTAPIc2s.order_id_ex(= backend szOrderID, 优先).
返回:
Ok(snap): 命中 cache, 字段已 populated.Err(ResolveOrderError::CacheMiss): cache 没存这个 (acc_id, order_id), caller 应返清晰提示 “先刷新 /api/orders 或传 orderIDEx”.Err(ResolveOrderError::MissingBackendId): cache 命中但 backend_order_id 字段空 (= cache entry 来自老版本, 没存 szOrderID), caller 应返同样提示.Err(ResolveOrderError::InvalidInput): 同时缺 order_id 和 order_id_ex.
Sourcepub fn merge_preserving_stubs(
&self,
acc_id: u64,
backend_orders: Vec<CachedOrder>,
)
pub fn merge_preserving_stubs( &self, acc_id: u64, backend_orders: Vec<CachedOrder>, )
v1.4.90 S BUG-e4da-009 cache saga 真修:merge backend 权威列表,保留 stub.
历史坑(跨 v1.4.73 → v1.4.89 7 版未真修):
17:36:44.204092 place_order.rs:427 v1.4.82 A2 stub upsert (order_id=X)
17:36:44.204126 place_order.rs:451 PlaceOrder success
17:36:44.204138 futu_audit:511 v1.4.38 idempotency: cached
17:36:44.226531 place_order.rs:488 v1.4.73 A1 orders refreshed count=0 ← 22.4ms 清零根因:v1.4.73 A1 spawn refresh 直接 orders.insert(acc_id, backend_list)
整覆盖,22ms 内把 v1.4.82 A2 刚 upsert 的 stub 抹掉。client 0ms 查
/api/orders 命中 stub OK,但 22ms 后再查就 count=0 —— “单子消失” 假象。
修法(async-safe):refresh 不再 insert 整覆盖,而是 merge:
- backend 返的每个 order: upsert(同
order_id命中 stub → 覆盖且is_stub=false,不在 → push) - cache 里 backend 没返的 stub orders(
is_stub=true):now_ms - stub_inserted_at_ms < STUB_TTL_MS(30s) → 保留- 否则 → evict(backend 永久拒单兜底)
- cache 里 backend 没返的非 stub orders(
is_stub=false): 全清空(backend 是权威,老的非 stub 该被替换)
并发语义:用 DashMap entry api 取写锁,整 merge 在锁内完成 → 多个
merge_preserving_stubs 调用串行化(顺序与到达顺序一致)。
upsert_order 与 merge_preserving_stubs 之间也通过同一 entry lock
排它,不会丢失 stub 插入与 merge 之间的并发更新。
Sourcepub fn merge_preserving_stubs_with_now(
&self,
acc_id: u64,
backend_orders: Vec<CachedOrder>,
now_ms: u64,
)
pub fn merge_preserving_stubs_with_now( &self, acc_id: u64, backend_orders: Vec<CachedOrder>, now_ms: u64, )
v1.4.90 S BUG-e4da-009: merge_preserving_stubs 的可注入时间版(test 用)。
业务代码只调 merge_preserving_stubs;本 fn 暴露便于 unit test 模拟
“stub 已超 TTL” / “stub 仍 fresh” 两种边界。
Sourcepub fn clear_pending_confirm_for_acc(&self, acc_id: u64) -> usize
pub fn clear_pending_confirm_for_acc(&self, acc_id: u64) -> usize
v1.4.105 BUG-v1.4.104-001 (P0): broker async confirm 到达后清 pending 标志.
当 push notice_type=4/5/8/100 (ORDER_UPDATE / ORDER_LIST_UPDATE /
TRADE_STATISTIC / ORDER_NTF) 到达对应 acc_id 时, 调本 fn 把所有
is_pending_broker_confirm=true 的 order 翻成 false.
设计选择: 不按 order_id 精确匹配清 — push notice 通常不带具体 order_id,
只表 “本 acc 有 order 状态变化”. 简化处理: acc 内任何 ORDER 类 push 到
即视为 broker 已开始处理本 acc 的 stub orders.
后续 query_orders refresh 会通过 merge_preserving_stubs 把 enriched
版本写入, 替换 stub.
返被清的 order 数 (caller 用于 audit log).
Sourcepub fn clear_pending_confirm_for_orders(
&self,
acc_id: u64,
order_ids: &[String],
) -> usize
pub fn clear_pending_confirm_for_orders( &self, acc_id: u64, order_ids: &[String], ) -> usize
v1.4.106 codex 0226 F4 (P2): selective clear pending confirm by order_ids.
clear_pending_confirm_for_acc 是 acc-level 全清, 但 ORDER push notify
在 backend 实际带具体 order_ids 时(notice_type=4 ORDER_UPDATE 通常
带), daemon 应只清对应订单的 pending flag, 而不是把同账户其他还没
confirm 的 stub 一并误清.
触发场景 (bridge/dispatcher.rs:251-268):
- notice_type=4/5/9 + 非空
order_ids(backend 真带 → 按订单清) - notice_type=4/5/9 + 空
order_ids→ fall back toclear_pending_confirm_for_acc
match 逻辑: o.order_id_ex (alphanumeric backend szOrderID) 与
order_ids 任一相等. 不 match o.order_id (FTAPI u64 hash) 因为
backend push 带的 order_ids 是 backend 原生 string id.
返被清的 order 数 (caller 用于 audit log).
Sourcepub fn purge_pending_stub_if_still_pending(
&self,
acc_id: u64,
order_id: u64,
) -> Option<String>
pub fn purge_pending_stub_if_still_pending( &self, acc_id: u64, order_id: u64, ) -> Option<String>
v1.4.105 BUG-v1.4.104-001 (P0): cleanup task 删超时未 confirm 的 pending stub.
触发: PlaceOrder spawn 一个 30s 延迟 task, 到点检查 (acc_id, order_id_ex)
对应的 stub 是否仍 is_stub=true && is_pending_broker_confirm=true.
若是 → 删 stub + warn (push channel 断 / broker 拒单未 push 的兜底).
不简单调 STUB_TTL_MS evict — 那个是 query_orders merge 时的逻辑, 这里是主动 GC pending stub. 两者互补.
返 (purged: bool, reason: 描述), caller 写 audit log.
Sourcepub fn scan_orphan_orders(
&self,
now_secs: f64,
threshold_secs: f64,
) -> Vec<OrphanOrder>
pub fn scan_orphan_orders( &self, now_secs: f64, threshold_secs: f64, ) -> Vec<OrphanOrder>
v1.4.83 §9 F6: 扫全 cache 查 orphan orders.
Orphan 定义: order_status ∈ {0, 1, 2, 4} (未达到 Submitted=5
之前的 in-flight stub) 且 create_timestamp.is_some() 且
now_secs - create_timestamp > threshold_secs.
含义对应 C++ proto OrderStatus enum (Trd_Common.proto:108):
- 0 = Unsubmitted (未提交) — 极端情况, daemon stub 修后不应该出现 (v1.4.103 P0 hotfix)
- 1 = WaitingSubmit (等待提交) — 条件单 stub 初值, 等触发
- 2 = Submitting (提交中) — 普通单 stub 初值 (v1.4.103 起)
- 4 = TimeOut (处理超时) — 后端回 timeout, 状态未知
为什么需要: v1.4.82 A2 PlaceOrder 成功后直接 upsert stub order
让 /api/orders 立刻可见 (BUG-60b0-002 fix). 后续 push notice_type=
4/5/8 / re-fetch 把 status 推到 5 (Submitted) / 10/11 (Filled).
若 push 通道断流 (§9 CMD3020 chain broken), stub 卡住 5min+ = orphan.
v1.4.103 P0 (BUG-WUZONG-001): stub status 从 0 (proto 定义为 Unsubmitted “未提交”, 触发客户端 retry 多下单) 改成 1/2 (WaitingSubmit/ Submitting, 对齐 C++ NNProto_Trd_OrderOp.cpp:483-510). orphan 检测同步 扩展到 {0, 1, 2, 4} 全 in-flight 状态 — 老 daemon 留下来 status=0 的 卡死 stub 也能被检测到.
返 Vec<OrphanOrder>; caller 决定 log 级别 + metric bump.