Prhub

#1952 Expire finished calls that were never retrieved

原始 PR 作者 fzyzcjy 合并时间 2026-09-04 13:31 文件变更 2 提交数 1 评论 1 代码增减 +226 / -11

执行摘要

为已完成但未检索的 RPC 调用记录添加 12 小时的自动过期,解决内存泄漏。

PR 提交信息明确指出动机:‘A record used to live until its result was collected, so a call whose result nobody ever polls for stayed in the store forever.’ 即,旧的存储机制会导致那些客户端从未轮询(检索)结果的已完成调用记录永远留在内存中,造成长期运行的 RPC 服务内存泄漏。

该 PR 是一个清晰、聚焦的 bugfix,修复了实际存在的内存泄漏问题。实现方案简洁,测试覆盖全面。值得相关模块(RPC 服务器、存储)的维护者精读,特别是其中关于 TTL 判断逻辑的设计(_is_live 方法)。

讨论亮点

PR 只有审核者 guapisolo 的一条批准评论,其内容指出了本次变更的核心价值:‘fixes the unbounded retention of finished, unpolled RPC calls from #1942 by adding a finished-record TTL’。这确认了本次修改是对之前引入问题的直接修复,且修复被正确地集成到了后续的主干版本中。

实现拆解

  1. 引入新常量和配置:在 miles/utils/workers/rpc/server/store.py 中新增 FINISHED_TTL_SECONDS 常量(12小时),并在 CallStore.__init__ 方法中增加 finished_ttl_seconds 参数,允许配置该超时时间。
  2. 修改记录生命周期判断逻辑:在 CallStore 中新增私有方法 _is_live(record, now),替代 _purge_expired 方法中原有的简单判断。该方法实现了更复杂的过期规则:未完成的记录永远保留;已检索的记录根据 retrieved_ttl 判断;未检索的已完成记录根据新的 finished_ttl 判断。
  3. 更新数据结构:在 _CallRecord 数据类中增加 finished_at 字段,用于在 finish() 方法中记录调用完成的时间戳,这是实现完成时间超时的基础。
  4. 完善测试覆盖:在 tests/fast/utils/workers/rpc/server/test_store.py 中新增了大量测试用例(如 test_never_retrieved_record_purged_after_finished_ttl, test_never_retrieved_records_do_not_accumulate 等),全面验证新功能,包括边界情况(如失败的结果、检索刚好在截止时间前)。测试辅助函数 _make_store 和 _retrieved_call 也进行了更新以支持新的 finished_ttl 参数。
文件 模块 状态 重要度
miles/utils/workers/rpc/server/store.py RPC 存储 modified 7.23
tests/fast/utils/workers/rpc/server/test_store.py 测试覆盖 modified 7.76

关键符号

CallStore.__init__ CallStore.finish CallStore._is_live CallStore._purge_expired

关键源码片段

miles/utils/workers/rpc/server/store.py core-logic

核心源码文件,包含了本次修复的主要逻辑变更:新增完成时间记录、修改过期判断逻辑。

# --- miles/utils/workers/rpc/server/store.py 中的核心逻辑变更 ---
​
# 新增常量,定义已完成记录的默认过期时间(12 小时)
FINISHED_TTL_SECONDS = 12 * 3600.0
​
class CallStore:
    def __init__(
        self,
        *,
        retrieved_ttl_seconds: float = RETRIEVED_TTL_SECONDS,
        finished_ttl_seconds: float = FINISHED_TTL_SECONDS,
    ) -> None:
        self._retrieved_ttl_seconds = retrieved_ttl_seconds
        self._finished_ttl_seconds = finished_ttl_seconds # 新增:存储完成过期时间
        self._records: dict[str, _CallRecord] = {}
​
    def finish(self, *, call_id: str, outcome: CallStatusResponse) -> None:
        record = self._records[call_id]
        if record.outcome is not None:
            raise RuntimeError(f"call {call_id} finished twice")
        record.outcome = outcome
        record.finished_at = time.monotonic() # 新增:记录完成时的时间戳
        record.finished_event.set()
        # ...
​
    def _purge_expired(self) -> None:
        now = time.monotonic()
        # 重构过期判断,委托给新的 _is_live 方法
        retained = {call_id: record for call_id, record in self._records.items() if self._is_live(record, now=now)}
        # ...
​
    def _is_live(self, record: _CallRecord, *, now: float) -> bool:
        """判断一条调用记录是否仍然应该保留在内存中。"""
        # 规则 1:未完成的调用永远保留(等待结果)
        if record.finished_at is None:
            return True
        # 规则 2:已检索过的调用,由检索过期时间(retrieved_ttl)决定生命周期
        if record.first_retrieved_at is not None:
            return now - record.first_retrieved_at <= self._retrieved_ttl_seconds
        # 规则 3:已完成但未检索的调用,由完成过期时间(finished_ttl)决定生命周期
        return now - record.finished_at <= self._finished_ttl_seconds
​
class _CallRecord:
    finished_event: asyncio.Event
    outcome: CallStatusResponse | None = None
    finished_at: float | None = None # 新增字段
    first_retrieved_at: float | None = None

评论区精华

修复已完成 RPC 调用记录的无界内存保留问题 正确性

审核者 guapisolo 在批准评论中明确指出:'#1952 fixes the unbounded retention of finished, unpolled RPC calls from #1942 by adding a finished-record TTL'。这确认了本次修正是针对前序 PR (#1942) 引入的一个正确性 / 内存管理问题的直接修复。

结论:修复被确认为解决了一个真实的内存泄漏问题,方案(增加 finished TTL)被审核接受。 · 已解决

风险与影响

技术风险:风险较低。变更逻辑集中,是增加性修改而非破坏性重构。核心风险点在于默认的 12 小时过期时间是否在所有生产场景下都足够合理,如果客户端有长时间未轮询的合法场景,可能需要调整该值。兼容性风险:CallStore 构造函数新增了带默认值的参数,向后兼容。回归风险:现有测试得到保留并扩展,新测试覆盖了核心逻辑,回归风险可控。

对系统影响:提升了 RPC 服务的内存稳定性,防止了因客户端行为(不轮询)导致的内存持续增长,对系统的长期可靠性有积极影响。对用户影响:无直接用户可见的功能变更,但系统将更稳定。对团队影响:为团队解决类似问题提供了模式:即通过多层 TTL 机制管理内存中的数据对象。

默认超时值需场景适配

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论