后端:游玩计数去重与限流改用元组键
- game_play_counter.rs:seen/rate 的键由 U+001F 拼接字符串改为 (identity, game_id)/(ip, game_id) 元组,clientId 携带分隔符时不再发生键碰撞 - 补分隔符碰撞回归测试
This commit is contained in:
@@ -81,10 +81,10 @@ pub struct GamePlayCounter {
|
||||
struct GamePlayCounterState {
|
||||
/// `game_id -> 待落库增量`;只包含通过校验的上报,键空间由公开游戏目录界定。
|
||||
pending: HashMap<String, u64>,
|
||||
/// `identity + game_id -> 最近一次计数时间`。
|
||||
seen: HashMap<String, Instant>,
|
||||
/// `IP + game_id -> 当前固定窗口`。
|
||||
rate: HashMap<String, RateWindow>,
|
||||
/// `(identity, game_id) -> 最近一次计数时间`。
|
||||
seen: HashMap<(String, String), Instant>,
|
||||
/// `(IP, game_id) -> 当前固定窗口`。
|
||||
rate: HashMap<(String, String), RateWindow>,
|
||||
/// 最近一次真正取走增量的时间,用于判断是否到达 flush 间隔。
|
||||
last_flush_at: Instant,
|
||||
}
|
||||
@@ -115,8 +115,10 @@ impl GamePlayCounter {
|
||||
///
|
||||
/// 被去重命中的上报不消耗限流额度;限流只挡同一 IP 对同一游戏的超额上报。
|
||||
pub fn record(&self, report: GamePlayReport<'_>, now: Instant) -> GamePlayOutcome {
|
||||
let dedup_key = format!("{}\u{1f}{}", report.identity, report.game_id);
|
||||
let rate_key = format!("{}\u{1f}{}", report.client_ip, report.game_id);
|
||||
// 键用元组而不是拼接字符串:clientId 来自 JSON,可能包含任意字节(含 U+001F),
|
||||
// 拼接会产生本不存在的键碰撞。
|
||||
let dedup_key = (report.identity.to_string(), report.game_id.to_string());
|
||||
let rate_key = (report.client_ip.to_string(), report.game_id.to_string());
|
||||
let mut state = self.lock();
|
||||
|
||||
if let Some(seen_at) = state.seen.get(&dedup_key)
|
||||
@@ -413,4 +415,21 @@ mod tests {
|
||||
GamePlayOutcome::Counted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separator_like_bytes_in_key_parts_do_not_collide() {
|
||||
let start = Instant::now();
|
||||
let counter = GamePlayCounter::new(settings(), start);
|
||||
|
||||
// 旧实现用 U+001F 拼接 identity/game_id,下面两个不同元组会被拼成同一个键。
|
||||
assert_eq!(
|
||||
counter.record(report("b\u{1f}c", "a", "1.1.1.1"), start),
|
||||
GamePlayOutcome::Counted
|
||||
);
|
||||
assert_eq!(
|
||||
counter.record(report("c", "a\u{1f}b", "1.1.1.1"), start),
|
||||
GamePlayOutcome::Counted
|
||||
);
|
||||
assert_eq!(counter.pending_total(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user