修复 SpacetimeDB 连接池请求取消后槽位泄漏导致池耗尽
- spacetime-client:PooledConnectionLease 增加 Drop 兜底,请求 future 被取消时也复位槽位并归还连接与 permit - spacetime-client:槽位改为 AtomicBool 占用标记 + Mutex 连接存放,acquire 改为 CAS 抢占,删除可能永久空转的扫描循环 - spacetime-client:release_connection 与取消路径统一走租约 Drop 归还逻辑 - spacetime-client:新增 dropped_lease_releases_slot_and_permit 等单元测试复现并锁定该故障机制 - docs:新增 SpacetimeDB 连接池租约 Drop 兜底与取消安全文档记录根因、复现与验收
This commit is contained in:
40
docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md
Normal file
40
docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# SpacetimeDB 连接池租约 Drop 兜底与取消安全
|
||||
|
||||
- 日期:2026-06-11
|
||||
- 关联故障:release 环境 api-server 周期性全量 `spacetime_stage="pool_acquire" elapsed_ms=45000` 超时,`/readyz` 503(`reason=spacetime_unhealthy, stage=pool_acquire`),重启后临时恢复。
|
||||
- 涉及代码:`server-rs/crates/spacetime-client/src/lib.rs`
|
||||
|
||||
## 故障根因
|
||||
|
||||
修复前的连接池存在两个叠加缺陷:
|
||||
|
||||
1. **租约没有 Drop 兜底**。`PooledConnectionLease` 只能通过显式 `release_connection` 归还。当 HTTP 请求方在等待 StDB 回包期间断开(前端超时、用户刷新、Nginx 截断),axum/hyper 会直接丢弃 handler future,租约被 Drop:permit 因 `OwnedSemaphorePermit` 自动归还,但槽位的 `in_use` 标记永远不会复位。
|
||||
2. **acquire 在槽位泄漏后永久空转**。后续请求拿到 permit 后进入 `loop { 扫描槽位; yield_now }`,找不到空闲槽位就无限自旋,且这段自旋不受 `procedure_timeout` 约束,自旋期间 permit 不归还。
|
||||
|
||||
叠加效果:StDB 一旦变慢(请求占用连接接近 45 秒),客户端取消请求的概率大增,每次取消泄漏一个槽位并连带吞掉一个 permit;泄漏数量达到 `pool_size`(release 为 8)后,所有业务请求与健康检查全部在 `pool_acquire` 阶段 45 秒超时,服务表现为"连不上 StDB",只有重启能恢复。
|
||||
|
||||
## 本地复现
|
||||
|
||||
不需要真实 SpacetimeDB,单元测试即可复现机制(位于 `spacetime-client` tests 模块):
|
||||
|
||||
- 修复前:将一个槽位置为 `in_use=true` 后调用 `acquire_connection_with_timeout(200ms)`,acquire 在 5 秒守护窗口内不返回(永久自旋),测试红。
|
||||
- `dropped_lease_releases_slot_and_permit`:模拟"请求被取消、租约未经 release 直接 Drop",断言槽位与 permit 都被复位归还。
|
||||
- `acquire_times_out_at_pool_acquire_when_pool_is_busy`:池内 permit 全部被占用时,acquire 必须在超时窗口内返回 `PoolAcquire + Timeout`,不允许无限等待。
|
||||
|
||||
## 修复方案
|
||||
|
||||
1. `PooledConnectionSlot` 改为 `in_use: AtomicBool + connection: Mutex<Option<PooledConnection>>`,槽位占用标记不再依赖异步锁。
|
||||
2. `PooledConnectionLease` 持有 `Arc<SpacetimeConnectionPool>` 并实现 `Drop`:无论显式归还还是 future 被取消,统一在 Drop 中复位槽位、按 broken 状态决定连接是否回池,permit 随后自动归还。Drop 体先复位 `in_use` 再释放 permit(字段在 Drop 体之后析构),保证新请求拿到 permit 时必有空闲槽位。
|
||||
3. acquire 改为 CAS 抢占槽位:持有 permit 即保证并发持有者不超过 `pool_size`,扫描一轮必然命中空闲槽位,彻底删除自旋循环;建连失败直接返回错误,槽位由租约 Drop 复位。
|
||||
4. `release_connection` 退化为 `drop(lease)`,显式与隐式归还共用同一条兜底路径。
|
||||
|
||||
## 验收
|
||||
|
||||
- `cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`(35 通过,含上述新测试)
|
||||
- `cargo test -p api-server --manifest-path server-rs/Cargo.toml readyz`(2 通过)
|
||||
- `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
|
||||
|
||||
## 运维提示
|
||||
|
||||
- 此修复解决的是"取消导致的永久泄漏"。StDB 真慢时仍会出现成批 45 秒超时(连接被在途请求合法占用),那是容量/上游问题,应结合 `GENARRATIVE_SPACETIME_POOL_SIZE` 与 StDB 负载排查,不要再怀疑池泄漏。
|
||||
- 健康检查 `/readyz` 在池被在途请求占满时仍可能短暂 503(stage=pool_acquire),恢复后自动转好,无需重启。
|
||||
Reference in New Issue
Block a user