Skip to content

refactor(runtime): consolidate service event loop ownership - #4572

Open
qin-ctx wants to merge 7 commits into
mainfrom
refactor/single-event-loop-runtime
Open

refactor(runtime): consolidate service event loop ownership#4572
qin-ctx wants to merge 7 commits into
mainfrom
refactor/single-event-loop-runtime

Conversation

@qin-ctx

@qin-ctx qin-ctx commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Description

本 PR 通过重新划分执行职责,根治 Service Event Loop 被同步 CPU、日志格式化以及跨 Event Loop 调度阻塞的问题。它不为 A/B/C 分别增加补丁,而是收敛为三个明确的执行边界:

  1. Service Event Loop:拥有 QueueFS 生命周期、锁、取消、异步存储/模型 I/O、处理完成和 ACK。
  2. CPU Worker Thread:只执行无 I/O 的消息规范化、工具输出摘要/哈希/选择、Token 估算、JSONL 编解码和 Prompt 构造。
  3. Logging Listener:拥有 LogRecord 格式化和 stdout/stderr/file I/O;业务调用线程只入队原始 LogRecord。

Before

  • QueueManager 为每个 Queue 创建一个 daemon thread 和私有 Event Loop。Queue handler 先在 Queue Loop 中运行,再通过 run_coroutine_threadsafe 跳回 Service Loop;启动、停止、取消和 ACK 分散在两套生命周期中。
  • HTTP/MCP/内部资源链路虽然是 async 入口,消息构造、工具输出摘要/哈希、Token 估算和 JSONL 处理仍可能直接运行在 Service Loop;工具输出持久化还会通过同步 run_async 再跨一次 Loop。
  • Message.estimated_tokens 每次读取都会重新扫描全部 Parts。同一批消息在 pending-token、retention 和 context budget 阶段会重复计算。
  • VLM adapter 在发请求前同步脱敏并 json.dumps(..., indent=2) 整个请求体;QueueHandler.prepare() 也在调用线程完成日志格式化,因此“大日志”仍会阻塞主循环。

旧流程:

HTTP / MCP (Service Loop)
  -> 同步消息准备、Token/JSON 计算
  -> run_async (另一 Event Loop)

QueueFS daemon thread + private Event Loop
  -> dequeue
  -> run_coroutine_threadsafe(Service Loop)
  -> handler
  -> 返回 Queue Loop
  -> ACK

After

  • OpenVikingService.initialize() / close() 直接 await QueueManager.start() / stop();每个 durable queue 是 Service Loop 上的一个 asyncio.Task,按 dequeue -> await handler -> ACK 单向执行。
  • AddResource、SessionCommit、UserDeletion processor 直接 await 其 Service,不再保存 service_loop,也不再使用 run_coroutine_threadsafe / wrap_future
  • 新增 MessagePreparer 作为纯 CPU Owner:在 worker thread 中完成消息构造、Turn 级工具输出选择、摘要、哈希和 Token 定稿;Service Loop 只执行 ToolResultStore 异步 I/O 和权威消息追加。
  • Message 的 Token 估算改为内存懒缓存,消息变更后由变更 Owner 显式刷新;本 PR 不增加 Token 持久化、历史回填或 JSONL Schema。
  • Session 的 JSONL 编解码、retention/budget 计算、checkpoint 构造和 Prompt 格式化移出 Service Loop。HTTP、MCP 和资源记忆链路直接调用 async Session API,不再保留猜测式 sync fallback。
  • LiteLLM、OpenAI、VolcEngine adapter 不再记录完整请求体;调用线程只入队原始 LogRecord,listener 再执行格式化和输出 I/O。

新流程:

HTTP / MCP / QueueFS task (Service Loop)
  -> to_thread(纯 CPU 消息准备、Token/JSON/Prompt)
  -> await 异步存储或模型 I/O
  -> direct await handler
  -> 完成状态
  -> ACK

Log caller
  -> enqueue raw LogRecord
  -> listener format + I/O

同一场景的前后对比

输入保持一致:向 Session 写入一个包含约 100 KB tool_output 的消息,随后触发 commit。

关键字段/阶段 Before After
输入 rolepartstool_output 完全相同
消息结果 生成 message.idtool_output_ref 完全相同
Queue 状态 durable message 处理完成后 ACK 完全相同,仍为 at-least-once
消息准备 Service Loop 同步摘要、哈希、Token 计算 CPU worker thread 生成 PreparedMessageBatch
工具结果写入 同步入口跨 Loop 调用异步 Store Service Loop 直接 await Store I/O
SessionCommit Queue thread/private Loop 再跳回 Service Loop Service Loop 上的 Queue task 直接 await processor
VLM 日志 Service Loop 脱敏、格式化完整请求 JSON 不记录完整请求体

对客户端返回字段、Session 消息格式、工具输出引用、Queue ACK 时机和失败恢复语义均不变。并发配置现在在配置边界要求 embedding.max_concurrent > 0vlm.max_concurrent > 0,运行时不再静默猜测无效值。

Human Involvement

  • A human participated in the implementation or review loop
  • This PR was generated entirely by AI agents without human participation in the loop

Related Issue

无公开 GitHub Issue;问题来自内部对 Service Event Loop 阻塞问题 A/B/C 的复盘。

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement
  • Test update

Changes Made

  • 将 QueueFS 从“每 Queue 一个线程和私有 Event Loop”收敛为 Service Loop 上受并发上限控制的 async task,并保持 handler 完成后 ACK 的因果顺序。
  • 将 Session 消息准备、工具输出预处理、Token/retention、JSONL 和 Prompt CPU 工作移到 worker thread;删除 Session 内旧的同步外部化路径和 async/sync fallback。
  • 删除 VLM 完整请求体日志,重构标准日志队列,让 listener 负责格式化和 I/O;Tracer 统一使用 stdlib logging。

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows

验证结果:

  • 272 个聚焦测试通过,覆盖 Message/Token、retention、Session commit recovery、Working Memory、工具输出外部化、QueueManager、SessionCommit processor、配置加载、AddResource processor 和资源记忆链路。
  • ruff check 通过。
  • python -m compileall -q openviking openviking_cli 通过。
  • git diff --check origin/main...HEAD 通过。

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation(无公开 API/存储 Schema 文档变化)
  • My changes generate no new warnings
  • Any dependent changes have been merged and published(无依赖变更)

Screenshots (if applicable)

不适用。

Additional Notes

  • 相对最新 origin/main715 insertions / 955 deletions,净减少 240 行;Session 本身净减少 352 行。
  • 新的 MessagePreparer 替代 Session 内消息构造、Turn 级工具输出选择和同步外部化职责;新旧两套机制没有并存。
  • Queue 状态计数仅保留一个窄锁,因为同步 metrics collector 会从其他线程读取快照;Queue 消费、handler、取消和 ACK 均由 Service Loop 独占。
  • 本 PR 不包含 Token 持久化、历史回填、兼容别名或猜测式 fallback。

Keep async orchestration on the service loop while moving pure CPU preparation and log I/O to their dedicated execution boundaries.
Preserve request observability while removing full Base64 scans and temporary payload copies.
Keep the stateful local model serialized while moving its synchronous inference to a worker thread.
Replace accepted Base64 image URLs with a fixed placeholder before trace and Langfuse export.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant