Feat/游玩计数 #604

Merged
k88936 merged 23 commits from feat/play-count-with-cache into master 2026-10-04 13:21:04 +08:00
Member

close #539

5s内apiserver内存缓冲
30分钟内按账号/ip去重

close #539 5s内apiserver内存缓冲 30分钟内按账号/ip去重
k88936 added 14 commits 2026-10-03 19:45:00 +08:00
- 新增 docs/adr/【ADR】游戏游玩次数计数-2026-10-03.md:点开始游戏触发、api-server 纯内存 5s flush、30min 去重、IP+game 限流、批量 procedure 自增 play_count、不 bump updated_at、失败少计优于双计
- docs/README.md:登记该 ADR 到当前产品与平台
- spacetime-module/game_distribution.rs:新增 GameDistributionPlayCountIncrement(Input) 与 increment_game_distribution_game_play_counts_and_return,事务内只对 published 且有 active_version_id 的游戏 saturating_add,且不 bump updated_at
- 重新执行 npm run spacetime:generate,新增 3 个绑定并同步 module_bindings.rs 等生成产物
- game_distribution.rs:新增 GameDistributionPlayCountIncrementRecordInput 与 increment_game_distribution_game_play_counts,把批次映射为绑定输入并调用增量 procedure,错误转 procedure_failed
- game_play_counter.rs:新增纯内存计数器(30min 身份去重、IP+game 固定窗口限流、按时/按量取增量、requeue、过期清理)与 9 个单测
- game_play_counter_worker.rs:新增 flush worker,Build 失败放回重试、Timeout/ConnectDropped 丢弃并记录丢失量,关停前强制 flush
- main.rs:HTTP 角色注册 worker,并在 finalize_shutdown 内按 outbox 超时强制落库
- state.rs/config.rs:AppState 持有计数器,新增 GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS(默认 5s)
- request_context.rs:抽出 client_ip_from_headers 并补测试,runtime_profile.rs 改为复用
- modules/game_distribution.rs:新增 POST /api/game-distribution/games/{game_id}/plays,公开可带 bearer,非公开 404、限流 429、成功 200 {recorded}
- 身份组成:登录用 userId、匿名用 clientId、都缺失回退 IP+UA;无效 bearer 按匿名处理不影响计数
- 请求体读取原始 Bytes,空体或畸形体不阻断计数
- gamePlayClientId.ts:新增匿名游玩身份,localStorage 持久并在不可用时退化为会话内存值
- gameDistributionClient.ts:新增 recordGamePlay,携带 clientId 且按后台请求处理,不刷新会话、不改全局登录态
- GamePlayPage.tsx:点击开始游戏即 fire-and-forget 上报,失败静默不阻断进入游戏
- 补 clientId 稳定性用例与点击上报断言,并把新服务文件加入 eslint 白名单
- 玩法链路:路由表新增 POST /games/{gameId}/plays,并新增「游玩计数(已实现)」小节说明触发、落点、缓冲延迟、去重限流与写入语义
- 后端架构:game_distribution_game 节补 play_count 的批量 procedure 写入路径与参数
- decision-log:记录内存去重缓冲 + 批量 procedure 落库的决策、失败语义与影响范围
- modules/game_distribution.rs:新增公开路由在未连接 SpacetimeDB 时可达、无需登录且 no-store 的断言
- 补 clientId 去空白/截断 128 字符与缺失 UA 回退的单测
- gameDistributionClient.ts:删除与 BACKGROUND_AUTH_REQUEST_OPTIONS 逐字重复的 PLAY_REPORT_REQUEST_OPTIONS,改为直接复用该常量,避免两处 local/background 鉴权语义日后漂移
- gameDistributionClient.ts:recordGamePlay 标注 Promise<{ recorded?: boolean }>,空 id 分支也返回同形状对象,消除 Promise<void> 与对象 Promise 的联合类型
- game_play_counter.rs:drain 只负责在锁内取走增量,新增 sort_deltas 在释放锁后排序,避免 O(n log n) 排序阻塞并发 record
- game_play_counter.rs:seen/rate 的键由 U+001F 拼接字符串改为 (identity, game_id)/(ip, game_id) 元组,clientId 携带分隔符时不再发生键碰撞
- 补分隔符碰撞回归测试
- game_play_counter.rs:lock 仍从中毒互斥锁恢复,但先 warn 一次,便于关联留下部分更新状态的 panic
后端:关停 flush 不再假承诺重试
Project CI / Backend tests (pull_request) Failing after 32s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m37s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m46s
Project CI / Repository checks (pull_request) Failing after 34s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m21s
Project CI / Frontend tests (pull_request) Successful in 3m16s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m3s
Project CI / Native shell tests (pull_request) Successful in 6m51s
2122dc9469
- game_play_counter_worker.rs:flush_deltas 增加 requeue_on_build;worker 循环仍把 Build 失败放回下一轮,关停路径改为直接记丢失量,不再把增量放回一个不会再被 flush 的缓冲
k88936 added 7 commits 2026-10-04 11:59:46 +08:00
- game_play_counter.rs:新增只读 is_rate_limited,不消耗额度也不改状态
- modules/game_distribution.rs:record_game_play 先做内存限流预检,超限直接 429,不再让这些请求也打一次 SpacetimeDB;真正计数时 record 仍会重新判定
- 补预检只读与固定窗口作用域测试
- main.rs:finalize_shutdown 删除游戏游玩计数强制 flush,关停不再为它等待网络,内存里剩余增量随进程结束丢弃
- game_play_counter_worker.rs:删除 flush_game_play_counter_for_shutdown,flush_deltas 去掉 requeue_on_build 参数(关停路径已不存在),Build 仍放回等待下一轮
- game_play_counter.rs:take_pending 标记为仅测试使用,模块文档改指 take_pending_if_due
- game_play_counter_worker.rs:任一分片失败即停止本次 flush,不再为后续分片逐个重建连接(连接不通时每片都会各自等一次超时)
- Build(确定未发出)仍放回本批,剩余分片直接丢弃;其余错误连本批一起丢弃,并记录丢弃增量
- 补 total_delta 汇总剩余增量,模块文档写明失败即停止与丢弃口径
- ADR:修订「正常关停必须 force flush」为关停不做强制 flush,并补充 flush 任一分片失败即丢弃剩余分片;追加修订记录
- 玩法链路:缓冲与延迟改为崩溃/被杀/正常关停都允许丢一个窗口;限流节补充先做内存预检;写入语义补充失败丢弃口径
- decision-log:顶部新增 2026-10-04 修订条目,并给 2026-10-03 条目标注失效项
- docs/README:ADR 摘要去掉「关停 flush」
- request_context.rs:client_ip_from_headers 改为优先 nginx 覆盖写入的 x-real-ip,x-forwarded-for 只作回退且取最后一段(nginx 用 $proxy_add_x_forwarded_for 追加的真实对端),不再信任可伪造的首段
- 影响两个调用方:公开游玩上报的匿名身份与 IP+game 限流键、微信支付下单的 payer_client_ip
- 更新测试:X-Real-IP 优先、XFF 回退取末段、空 X-Real-IP 回退与回环兜底
- ADR:身份与去重键节写明 IP 取 nginx 覆盖的 X-Real-IP、XFF 只取末段,并追加 2026-10-04 修订记录
- 玩法链路:去重与限流节标注 IP 来源
- decision-log:2026-10-04 条目补安全修正说明与标题
Merge remote-tracking branch 'origin/master' into feat/play-count-with-cache
Project CI / Backend tests (pull_request) Failing after 36s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m28s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m41s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
57e259278d
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
Author
Member
  • 1. 复用后台请求的鉴权降级策略(已修复,提交 815728ab0)

  • 2. 客户端 IP 只信 X-Real-IP(已修复,提交 95705cbb1 + 文档 08020e664)
    现状(修复前):client_ip_from_headers 优先取 X-Forwarded-For 第一段,而 nginx 用 $proxy_add_x_forwarded_for 会保留调用方自带值,第一段可伪造;X-Real-IP 才是 nginx 覆盖写入的真实 TCP 对端。
    确认:无 CDN/LB 前置,deploy 也没用 nginx real_ip 模块,所以 $remote_addr(= X-Real-IP)= 真实客户端。
    改动:优先 x-real-ip;x-forwarded-for 只作回退且取最后一段(nginx 追加的真实对端);都没有才回环兜底。同时修好游玩上报的匿名身份/限流键与微信支付下单的 payer_client_ip。
    测试:X-Real-IP 优先、XFF 回退取末段、空 X-Real-IP 回退与回环兜底。

  • 3. 关停不再强制 flush(已按你的决定处理,提交 75ad087e2 + 文档 c0148dda7)
    你的决定:连“关停时最后冲一次库”都不要。
    改动:main.rs 的 finalize_shutdown 删除游戏游玩计数强制 flush;worker 删除 flush_game_play_counter_for_shutdown,flush_deltas 去掉 requeue_on_build 参数;take_pending 改为仅测试使用。ADR 追加修订记录,玩法链路与 decision-log 同步。
    效果:崩溃、被杀、正常关停都允许丢最后一个未落库窗口,关停路径不再为计数等待网络;平时零性能开销。

  • 4. 玩家每次点“开始游戏”都先查一次数据库,才轮到内存限流(已修复,提交 3f0a77e65)
    改动(大白话):接口现在先做内存限流预检——超过 IP+game 每分钟 60 次的请求直接 429,不再去打 SpacetimeDB;通过的请求才去问“游戏是不是公开”。真正计数时 record 仍会重新判一次,所以预检只用来省一次远端查询、不承担正确性。
    新增只读 is_rate_limited(不消耗额度、不改状态)与对应测试。
    代价(你已接受):非公开游戏在超限时可能先收到 429 而不是 404。

  • 5. 排序移出互斥锁(已修复,提交 03f182856)

  • 6. 去重/限流键改用元组(已修复,提交 7c3398029,含分隔符碰撞回归测试)

  • 7. 锁中毒时留日志(已修复,提交 ea8734b70)

  • 8. delta 无上界 / saturating_add(不修)
    你的判断:u64 溢出在现实中不可能。reviewer 的 checked_add 改法还会让整批回滚、连累同批合法增量。已去掉。

  • 9. input.clone()(reviewer 建议有误,已核实否决)
    try_with_tx 要求闭包是 Fn,不能把捕获的 input move 出去;21 处 procedure 全部用 input.clone(),改成 move 会编译失败。不要改。

  • 10. 统一 recordGamePlay 返回类型(已修复,提交 9de9f4b41)

  • 11. flush 失败后直接丢弃剩余分片(已修复,提交 28ac8a103)
    你的决定:剩余分片直接丢。
    改动:flush_deltas 改成 while 循环;任一分片写失败即终止本次 flush,不再为后续分片逐个重建连接。Build(确定未发出)只把当前分片放回下一轮,剩余直接丢弃;其余错误连本批一起丢弃,并记录丢弃增量。效果:数据库不通时不再逐片各等一次 30s 超时把 worker 卡住几分钟。

  • 12. 关停 flush 不再假承诺重试(已被 3 取代,提交 2122dc946 → 75ad087e2 整体删除关停 flush)

- [x] 1. 复用后台请求的鉴权降级策略(已修复,提交 815728ab0) - [x] 2. 客户端 IP 只信 X-Real-IP(已修复,提交 95705cbb1 + 文档 08020e664) 现状(修复前):client_ip_from_headers 优先取 X-Forwarded-For 第一段,而 nginx 用 `$proxy_add_x_forwarded_for` 会保留调用方自带值,第一段可伪造;X-Real-IP 才是 nginx 覆盖写入的真实 TCP 对端。 确认:无 CDN/LB 前置,deploy 也没用 nginx real_ip 模块,所以 `$remote_addr`(= X-Real-IP)= 真实客户端。 改动:优先 x-real-ip;x-forwarded-for 只作回退且取最后一段(nginx 追加的真实对端);都没有才回环兜底。同时修好游玩上报的匿名身份/限流键与微信支付下单的 payer_client_ip。 测试:X-Real-IP 优先、XFF 回退取末段、空 X-Real-IP 回退与回环兜底。 - [x] 3. 关停不再强制 flush(已按你的决定处理,提交 75ad087e2 + 文档 c0148dda7) 你的决定:连“关停时最后冲一次库”都不要。 改动:main.rs 的 finalize_shutdown 删除游戏游玩计数强制 flush;worker 删除 flush_game_play_counter_for_shutdown,flush_deltas 去掉 requeue_on_build 参数;take_pending 改为仅测试使用。ADR 追加修订记录,玩法链路与 decision-log 同步。 效果:崩溃、被杀、正常关停都允许丢最后一个未落库窗口,关停路径不再为计数等待网络;平时零性能开销。 - [x] 4. 玩家每次点“开始游戏”都先查一次数据库,才轮到内存限流(已修复,提交 3f0a77e65) 改动(大白话):接口现在先做内存限流预检——超过 `IP+game` 每分钟 60 次的请求直接 429,不再去打 SpacetimeDB;通过的请求才去问“游戏是不是公开”。真正计数时 record 仍会重新判一次,所以预检只用来省一次远端查询、不承担正确性。 新增只读 is_rate_limited(不消耗额度、不改状态)与对应测试。 代价(你已接受):非公开游戏在超限时可能先收到 429 而不是 404。 - [x] 5. 排序移出互斥锁(已修复,提交 03f182856) - [x] 6. 去重/限流键改用元组(已修复,提交 7c3398029,含分隔符碰撞回归测试) - [x] 7. 锁中毒时留日志(已修复,提交 ea8734b70) - [x] 8. delta 无上界 / saturating_add(不修) 你的判断:u64 溢出在现实中不可能。reviewer 的 checked_add 改法还会让整批回滚、连累同批合法增量。已去掉。 - [x] 9. input.clone()(reviewer 建议有误,已核实否决) try_with_tx 要求闭包是 Fn,不能把捕获的 input move 出去;21 处 procedure 全部用 input.clone(),改成 move 会编译失败。不要改。 - [x] 10. 统一 recordGamePlay 返回类型(已修复,提交 9de9f4b41) - [x] 11. flush 失败后直接丢弃剩余分片(已修复,提交 28ac8a103) 你的决定:剩余分片直接丢。 改动:flush_deltas 改成 while 循环;任一分片写失败即终止本次 flush,不再为后续分片逐个重建连接。`Build`(确定未发出)只把当前分片放回下一轮,剩余直接丢弃;其余错误连本批一起丢弃,并记录丢弃增量。效果:数据库不通时不再逐片各等一次 30s 超时把 worker 卡住几分钟。 - [x] 12. 关停 flush 不再假承诺重试(已被 3 取代,提交 2122dc946 → 75ad087e2 整体删除关停 flush)
k88936 changed title from WIP: Feat/play count with cache to WIP: Feat/游玩计数 2026-10-04 12:08:24 +08:00
k88936 added 1 commit 2026-10-04 12:08:33 +08:00
Merge branch 'master' into feat/play-count-with-cache
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m20s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m28s
Project CI / Backend tests (pull_request) Successful in 7m46s
Project CI / Frontend tests (pull_request) Successful in 3m55s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m18s
Project CI / Native shell tests (pull_request) Successful in 8m37s
Project CI / Repository checks (pull_request) Failing after 6m40s
ff83b67196
k88936 added 1 commit 2026-10-04 12:58:00 +08:00
Add gamePlayClientId.ts to module and test exclusions
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m14s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m54s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m0s
Project CI / Backend tests (pull_request) Successful in 6m54s
Project CI / Frontend tests (pull_request) Successful in 2m54s
Project CI / Native shell tests (pull_request) Successful in 6m47s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m8s
Project CI / Repository checks (pull_request) Successful in 5m9s
9ce43071ad
k88936 marked the pull request as ready for review 2026-10-04 13:20:28 +08:00
k88936 merged commit a6fb2f01c1 into master 2026-10-04 13:21:04 +08:00
k88936 deleted branch feat/play-count-with-cache 2026-10-04 13:21:05 +08:00
Sign in to join this conversation.