跳到主要内容

引擎中枢:注册表与消息分发

30 秒导读: iii 的引擎是一个 Rust 进程,它自己不跑任何业务逻辑——它只做两件事:①在内存里维护一组"谁提供了什么"的注册表(函数、触发器、服务、连着的 worker);②当某个 worker 通过 WebSocket 发来一条消息,用一张叫 router_msg 的大分发表,把这条消息落到对应的注册表。本章讲这个进程的骨架结构和这张分发表;至于"一次调用怎么等回结果"看 03,"每种触发器怎么实现"看 04

本章上游是 01-primitives-and-protocol.md(三原语:函数 / 触发器 / 服务,以及线上 Message 协议)。读本章前你只需知道:worker 用 WebSocket 连到引擎,双方互发 JSON 编码的 Message


1. 这是什么(零基础也能懂)

一句话定义: 引擎中枢是 iii 引擎进程里那个"接线员 + 电话簿"——Engine 结构体持有全部注册表(电话簿),router_msg 是接线员,负责把每条进来的消息接到正确的簿子上。

它解决什么问题: iii 是一个"多进程 / 多语言的函数网络"。你的 Python worker、Node worker、内置 Rust worker 各自启动后,要告诉引擎"我提供了 email::send 这个函数""我要监听 cron 这种触发器"。引擎必须:

  • 记住谁提供了什么(这样别人调 email::send 时能找到它);
  • 在 worker 断线 / 重启时及时清理过期的记录(否则会调到一个已经死掉的 worker);
  • 在多个 worker 并发注册 / 断线时不把彼此的记录搞乱

一句话直觉:Engine 想成一个公司总机。每个员工(worker)入职时打电话来登记自己的分机和职责(RegisterFunction);总机小姐(router_msg)把登记信息记到对应的册子(注册表)里;员工离职(断线)时总机把他的条目划掉(cleanup_worker)。本章的精华在最后一个环节:如果一个员工"闪电离职又用同名分机重新入职",怎么保证旧登记的清理不会误删掉新登记? —— 这就是 function_owners 那套所有权(ownership)机制要解决的事。


2. 顶层全景(引擎大概怎么转)

2.1 一条消息的旅程

worker 的每一帧 WebSocket 文本,最终都汇入 router_msg 这个单一入口,再扇出到各注册表:

worker (Python/Node/Rust)
│ WebSocket 帧 (JSON 编码的 Message)

handle_worker ── 每连接一个循环,收帧、解码 ──┐
│ │ (二进制帧若带 OTLP/MTRC/LOGS 前缀
│ serde 反序列化成 Message │ 走 handle_telemetry_frame,不进 router)
▼ │
┌──────────────── router_msg(worker, &msg) ───┴────────────────┐
│ 一个大 match,按 Message 变体分发: │
│ │
│ RegisterFunction ─▶ FunctionsRegistry + function_owners │
│ RegisterTrigger ─▶ TriggerRegistry │
│ RegisterTriggerType─▶ TriggerRegistry.trigger_types │
│ RegisterService ─▶ ServicesRegistry │
│ InvokeFunction ─▶ (spawn 后台任务, 见 03) │
│ InvocationResult ─▶ 回填等待的调用者 (见 03) │
│ Ping/Pong ─▶ 就地应答 │
└──────────────────────────────────────────────────────────────┘

▼ 连接结束时
cleanup_worker ── 按所有权逐条撤销该 worker 的注册

怎么读这张图:上到下是一帧消息进来后的处理顺序;router_msg 那个框里每一行是"哪种消息 → 落到哪个注册表"。本章重点是这个框和它下面的 cleanup_worker

handle_worker 是每个 WebSocket 连接的主循环,它收帧、解码成 Message、调 router_msg,直到连接关闭再调 cleanup_worker(engine/src/engine/mod.rs:1429 handle_worker,分发调用在 :1510,清理在 :1547)。

2.2 引擎持有的部件

Engine 结构体的每个字段就是一本"册子"或一个协作模块(engine/src/engine/mod.rs:229 pub struct Engine):

字段白话职责类型要点在哪
worker_registry谁连着我(WebSocket worker 名册)Arc<WorkerConnectionRegistry>mod.rs:230
runtime_workers内置 worker 的清单(给 engine::workers::list 看)Arc<DashMap<String, RuntimeWorkerInfo>>mod.rs:231
functions函数注册表(id → 可调用 handler)Arc<FunctionsRegistry>mod.rs:232
trigger_registry触发器 + 触发器类型注册表Arc<TriggerRegistry>mod.rs:233
service_registry服务分组(按函数 id 前缀归组)+ 模块服务Arc<ServicesRegistry>mod.rs:234
invocations在途调用的"待回填"表(见 03)Arc<InvocationHandler>mod.rs:235
channel_managerworker 间的流式 channel(见 05)Arc<ChannelManager>mod.rs:236
queue_module可选的队列后端(Enqueue 动作用)Arc<RwLock<Option<Arc<dyn QueueEnqueuer>>>>mod.rs:237
function_owners常规函数 id 的当前 WS 属主(防误删)Arc<DashMap<String, Uuid>>mod.rs:244
external_function_owners外部/HTTP函数 id 的当前 WS 属主Arc<DashMap<String, Uuid>>mod.rs:247
active_scope进程级"作用域",给内置 worker 归拢注册用Arc<Mutex<Option<ScopeBuilder>>>mod.rs:248
worker_manager_port生效的 worker-manager 端口(一次写死)Arc<OnceLock<u16>>mod.rs:254

三个要点先记住:

  1. 每个字段都是 Arc<...> Engine 自己 #[derive(Clone)](mod.rs:228),克隆的是一堆 Arc 指针——所以引擎可以被廉价地 clone()movetokio::spawn 的后台任务,而所有克隆共享同一批注册表。这是全章后台任务里到处 let engine = self.clone(); 的原因。
  2. 注册表大多是 DashMap(分片并发哈希表),天然支持多任务并发读写,不用外层锁。
  3. function_owners / external_function_owners 是本章的精华,单独在 §5 讲。

Engine::new()(mod.rs:276)就是把上表每个字段初始化成空注册表;注意 functions 是用 FunctionsRegistry::with_scope(active_scope.clone()) 建的——函数注册表和 active_scope 共享同一把作用域句柄(§4 会用到)。


3. 分发表:router_msg 如何把每种消息落到注册表

router_msg(engine/src/engine/mod.rs:589)是一个 async fn,核心是 match msg { ... } 的一张大表。它对每一类 Message 变体做一件确定的事。下面按"注册类"消息展开(调用类 InvokeFunction / InvocationResult 属于调用生命周期,留给 03;触发器类型的具体落地逻辑属于 04)。

3.1 分发全表

Message 变体router_msg 做什么落到哪arm 行号
RegisterFunction认领所有权 → 注册服务分组 → 落函数(或 HTTP 外部函数)functions + function_owners(或 external_function_owners)mod.rs:1216
UnregisterFunction按属主校验后撤销同上,反向mod.rs:1153
RegisterTriggerType(可选 RBAC 钩子改写)→ 注册触发器类型trigger_registry.trigger_typesmod.rs:663
RegisterTrigger(可选 RBAC 钩子)→ 加会话前缀 → 注册触发器trigger_registry.triggersmod.rs:743
UnregisterTrigger撤销触发器trigger_registrymod.rs:868
TriggerRegistrationResult校验发送方是注册者后,把失败结果转发回原发起 workertrigger_registry(仅在 error 时删除)mod.rs:591
RegisterService插入一个显式服务节点(可带父服务)service_registry.servicesmod.rs:1357
InvokeFunctionRBAC/中间件闸门后 spawn 后台调用(见 03)invocationsmod.rs:883
InvocationResult回填等待的调用者(见 03)invocationsmod.rs:1113
Ping / Pong就地回 Pong / 忽略mod.rs:1387
WorkerRegistered这是引擎→worker 方向的消息,收到就忽略mod.rs:1392

一个反复出现的模式值得先点破:注册类消息如果配了会话(session),先过 RBAC 钩子。以 RegisterTriggerType 为例,它先看 session.allow_trigger_type_registration 是否放行,再看有没有 on_trigger_type_registration_function_id 钩子函数——若有,就 self.call(hook_fn_id, ...) 让那个函数改写或否决这次注册(mod.rs:679-720)。RegisterTrigger(mod.rs:763-815)和 RegisterFunction(mod.rs:1235-1280)是同一套结构。这三处的钩子回填 reg_id / reg_description 等"以钩子返回值为准"。RBAC 的细节不是本章重点,记住"注册前有一道可编程闸门"即可。

3.2 RegisterFunction:注册一个函数的完整落地

这是最能代表"router_msg 怎么落表"的一条。忽略 RBAC 钩子后,它的骨架是(mod.rs:1216-1356):

// 摘自 mod.rs:1282-1354,已省略 RBAC 钩子与错误分支
reg_id = resolve_registration_id(worker, &reg_id); // ① 加会话前缀

if invocation.is_some() { // ② 先认领所有权
self.claim_external_function(worker.id, &reg_id);
} else {
self.claim_function(worker.id, &reg_id);
}

self.service_registry
.register_service_from_function_id(&reg_id); // ③ 归入服务分组

if let Some(invocation) = invocation { // ④a 外部(HTTP)函数
// ... 注册进 http_functions 模块,记入 worker.external_function_ids
worker.include_external_function_id(&reg_id).await;
return Ok(());
}

self.register_function(/* RegisterFunctionRequest */, Box::new(worker.clone())); // ④b 常规函数
worker.include_function_id(&reg_id).await; // 记入 worker.function_ids

四步,对应四张"表"被触及:

  1. resolve_registration_id(§3.3)——把 id 加上会话前缀。
  2. claim_function / claim_external_function——在动任何全局状态之前先在 function_owners 里记下"这个 id 现在归我(worker.id)"。注释写得很明确:必须先认领,否则旧 worker 的 cleanup_worker 在另一个任务上跑时,会看到认领前的旧属主条目、匹配上自己的 id,把我们正要写的注册撕掉(mod.rs:1284-1294)。这是 §5 快速重启竞态的关键一环。
  3. register_service_from_function_id——把 svc::fn 形式的 id 归入名为 svc 的服务(§6)。
  4. 按有没有 invocation 字段分两路: = 这是个"外部函数"(引擎不亲自跑,而是转发到一个 HTTP 端点),注册进 http_functions 服务模块并记入 worker 的 external_function_ids;没有 = 常规函数,register_function 把一个包着 worker.clone() 的 handler 塞进 functions 注册表,并记入 worker 的 function_ids

register_function(mod.rs:1861)把 handler 包成一个 Function 存进 FunctionsRegistry——注意这里存的 handler 内部持有 worker.clone(),也就是说"调用这个函数"最终会通过那个 worker 的 channel 把请求发回去。这条"怎么真的把请求发给 worker 并等回结果"的线,是 03 的主题。

3.3 resolve_registration_id:会话前缀

多租户 / 多项目场景下,不同 worker 可能都想注册叫 db::query 的函数。为避免撞名,会话可以带一个 function_registration_prefix,注册时自动加在 id 前面(engine/src/engine/mod.rs:257 resolve_registration_id):

fn resolve_registration_id(worker: &WorkerConnection, id: &str) -> String {
if let Some(prefix) = worker.session.as_ref()
.and_then(|s| s.function_registration_prefix.as_ref())
{
format!("{prefix}::{id}") // 例: "tenant-a::db::query"
} else {
id.to_string() // 无会话/无前缀:原样
}
}

关键坑(有回归测试守着):注册和反注册必须用同一个解析后的 id。历史 bug iii-hq/iii#1508 就是注册时加了前缀、反注册时查了裸 id,导致 functions 里的条目永远删不掉。所以 UnregisterFunction(mod.rs:1159)开头也调 resolve_registration_id,并有专门的回归测试 test_router_msg_unregister_function_with_prefix(mod.rs:2198)和外部函数版本(mod.rs:2257)。RegisterTrigger 里也对 function_id 手动加了同样的前缀(mod.rs:817-823)。


4. 内置 worker 的作用域:begin/end_worker_scope

上面讲的是"WebSocket worker 主动发消息来注册"。但引擎里还有一类 worker 是进程内(in-process)的内置 worker(如 iii-statehttp_functions),它们不走 WebSocket,而是直接调 Engine 的注册方法。问题来了:这些内置 worker 支持"重载(reload)"和"销毁(destroy)",引擎需要知道"这次重载期间这个 worker 注册了哪些函数",才能在下次重载时把旧的撤掉。

active_scope 就是干这个的(engine/src/engine/mod.rs:248)。它是一把进程级的锁,包着一个可选的 ScopeBuilder:

  • begin_worker_scope(worker_name)(mod.rs:345):打开作用域,断言此刻没有别的作用域开着(作用域不嵌套)。开启后,FunctionsRegistry 里发生的注册会被这个 ScopeBuilder 记账——因为函数注册表和 active_scope 共享同一句柄(回忆 §2.2 的 with_scope)。
  • end_worker_scope()(mod.rs:357):关闭作用域,取出期间捕获的所有注册,返回一个 WorkerRegistrations
  • remove_worker_registrations(regs)(mod.rs:376):销毁 / 重载时,把 regs 里的每个函数 id 从全局注册表里删掉——但会跳过当前被某个 WS worker 持有(在 function_ownersexternal_function_owners 里)的 id

那道"跳过"检查(mod.rs:377-388)正是所有权机制在内置 worker 侧的应用:

for id in &regs.function_ids {
if self.function_owners.contains_key(id)
|| self.external_function_owners.contains_key(id)
{
// 一个 WS worker 现在拥有这个 id,别删——否则销毁一个恰好
// 和某 WS worker 撞了函数 id 的内置 worker,会连带撕掉那个
// 活着的 WS worker 的注册。in-process worker 自己不写这两张
// 属主表,所以"两张表都没有" = "没有 WS 属主" = 可以安全删。
continue;
}
self.remove_function_from_engine(id);
}

4.1 已知竞态(代码里的 FIXME)

begin_worker_scope 头上挂着一条诚实的 FIXME(mod.rs:336-344),值得原样转述其风险:

active_scope进程级的。在 begin_worker_scopeend_worker_scope 之间的窗口里,一个不相关的、WebSocket 连着的 worker 发来的 RegisterFunction 可能被误捕进这个作用域,导致后续 remove_worker_registrations 删掉一个不属于该作用域 worker 的函数。

代码作者对风险的评估是:实际风险低,因为 register_functions 是同步的、窗口极短;但为了正确性,应该用"每个 worker 一个注册令牌(per-worker registrar token)"来取代这把全局 Arc<Mutex<Option<ScopeBuilder>>>。这是一处上游自己标注的、尚未修的设计债——文档如实记录,不粉饰。


5. 精华:function_owners 与快速重启竞态

这是全章最不显然、也最值得带走的设计。

5.1 问题:闪断重连(fast-restart)

设想一个 worker 提供函数 email::send,然后它崩了又立刻用同一份代码重启。时间线上会发生两件几乎同时的事:

  • 旧连接handle_worker 循环发现断线,开始跑 cleanup_worker,准备把 email::sendfunctions 里删掉;
  • 新连接已经建立,发来 RegisterFunction { email::send },正把它写进 functions

如果清理是"无脑删除"——cleanup_worker 就会把 worker 刚写好的、活着的 email::send 注册删掉。之后谁调 email::send 都找不到人。这就是快速重启竞态。

5.2 解法:所有权 + 比较并交换(CAS)

function_owners: DashMap<String, Uuid> 记录"每个函数 id 当前归哪个 worker(UUID)所有"(mod.rs:244)。规则只有两条:

  • 注册时先认领:claim_function(worker_id, id)(mod.rs:1726)把 id -> worker_id 插入属主表。因为是"先认领,再动全局状态",新 worker 的认领会覆盖旧 worker 的属主记录。
  • 清理时只删自己仍拥有的:release_function_if_owner(worker_id, id)(mod.rs:1765)用 DashMap::remove_if(id, |_, owner| owner == worker_id)——只有当属主仍是我时才删

remove_if 提供的是比较并交换(CAS,compare-and-swap)语义:判断"属主是不是我"和"删除"这两步是原子的,中间没有别的线程能插进来。所以一旦新 worker 认领覆盖了属主,旧 worker 的 release_function_if_owner 谓词就为假,什么都不删——新 worker 的注册安然无恙(mod.rs:1759-1775 的注释明确说这关掉了 check-then-remove 会留下的 TOCTOU 窗口)。

时间轴 (email::send):

旧 worker 断线 ──▶ cleanup_worker 开始

新 worker 连上 ──▶ claim_function(new_id, "email::send") ← 属主变成 new_id
│ │
│ ▼ (新 worker 继续写 functions,注册完成)

release_function_if_owner(old_id, "email::send")
remove_if( owner == old_id ) ── 属主已是 new_id ── 谓词假 ── 不删 ✔

claim_function 还带一个观测点:若插入时发现旧属主是另一个仍在 worker_registry 里的 live worker,就打一条 WARN(mod.rs:1733-1738),让运维能 grep 到"函数 id 疑似被两个活 worker 抢注(squatting)"。

5.3 三处一致地使用这套机制

同一套所有权检查在三个撤销路径上出现,理解一处即懂全部:

路径位置怎么用所有权
正常断线清理cleanup_worker mod.rs:1626常规函数走 release_function_if_owner;外部函数"先快照属主、teardown 后再 CAS 释放",防 teardown 中途被 racing claim 抢走后误删新状态(mod.rs:1641-1697)
主动反注册UnregisterFunction arm mod.rs:1153撤销前 release_*_if_owner;若属主已变则跳过(mod.rs:1168:1205)
内置 worker 销毁remove_worker_registrations mod.rs:376只要 id 在两张属主表任一之内(= 有 WS 属主)就跳过

外部函数版本(HTTP 调用)的清理尤其小心:它不是"先释放再 teardown",而是"保持所有权贯穿 teardown,最后才 CAS 释放"(mod.rs:1646-1696)。原因写在注释里:unregister_http_function 里的 .await 是个让点(yield point),一个 racing 的 claim 可能在这个 .await 期间抢到所有权;若提前释放,后续对共享的 service_registry 的清理就会擦掉新属主刚建立的状态。所以每步 teardown 后都重新核对一次属主是不是自己(mod.rs:1681-1688)。

为什么分两张属主表?因为常规函数活在 functions 注册表里,而外部函数活在每个 WorkerConnection 自己的 external_function_ids 集合 + http_functions 模块里——它们的存储位置和撤销步骤不同,所以属主索引也分开(mod.rs:244-247 的字段注释)。


6. WorkerConnection 与 worker 名册

前面反复出现的 worker,类型是 WorkerConnection。它代表一条活着的 WebSocket 连接,由 worker_registry 这本名册统一管理。

6.1 WorkerConnection:一条连接的全部状态

engine/src/worker_connections/mod.rs:225 pub struct WorkerConnection,关键字段:

字段作用行号
id: Uuid连接的唯一标识——就是 §5 所有权表里的那个 UUID:226
channel: mpsc::Sender<Outbound>往这个 worker 发消息的出口(引擎→worker 只经此):227
function_ids: Arc<RwLock<HashSet<String>>>这个 worker 注册的常规函数 id:228
external_function_ids: Arc<RwLock<HashSet<String>>>这个 worker 注册的外部/HTTP函数 id:229
invocations: Arc<RwLock<HashSet<Uuid>>>这个 worker 身上在途的调用(断线时要 halt,见 03):230
session: Option<Arc<Session>>RBAC 会话(前缀、放行/禁止函数、钩子):242
runtime / version / name / os / pid / isolation / telemetry由后续 update_worker_metadata 回填的元数据:231-241

外部 vs 常规函数 id 分两个集合,和 §5 的两张属主表一一对应。相关方法都是成对的:include_function_id / remove_function_id(:310:317)对常规,include_external_function_id / remove_external_function_id / has_external_function_id(:321:328:332)对外部。get_function_ids(:300)返回两者的并集,而 get_regular_function_ids(:306)/get_external_function_ids(:339)分别只返回一类——cleanup_worker 正是靠这两个"分类取"来分别走两条撤销路径的(mod.rs:1627-1628)。

两种构造:new(:246,无会话,测试常用)和 with_session(:268,握手后带 RBAC 会话)。两者都随机生成 id

6.2 WorkerConnectionRegistry:按 id 查连接

worker_connections/mod.rs:55 WorkerConnectionRegistry 就是 workers: Arc<DashMap<Uuid, WorkerConnection>> 加一组方法:

方法做什么行号
register_worker插入连接;更新 workers_active 等指标:78
unregister_worker移除连接;更新 death 指标(找不到 id 也不 panic):101
get_worker(id)按 UUID 取(克隆整个 WorkerConnection):67
get_worker_name(id)只取名字,避免克隆整个连接:74
list_workers列出全部:129
update_worker_metadata握手后回填 runtime/pid/name/...:137
update_worker_status改状态并重算每状态的 worker 计数指标:171

register_workerhandle_worker 在握手成功后调用(mod.rs:1483),unregister_workercleanup_worker 在收尾时调用(mod.rs:1709)。注意 get_worker_name 是个刻意的性能优化——当调用方只想"按名字归属所有权"时,不必克隆整条连接(含函数 id 集合、telemetry、session)。

6.3 连接状态机

WorkerConnectionStatus(worker_connections/mod.rs:191)是个四态枚举,默认 Connected:

Connected ──▶ Available ──▶ Busy
(默认) │ │
└──────┬─────┘

Disconnected

as_str(:201)给指标打标签用;FromStr(:211)做宽容解析——任何不认识的字符串都落回 Connected,所以协议里传来奇怪的状态字符串不会崩,只会当"已连接"处理。


7. ServicesRegistry:按函数 id 前缀归组

services.rs 里的 ServicesRegistry(engine/src/services.rs:11)干两件相对独立的事,靠两张表分开:

  • services: DashMap<String, Service>(:12)——"服务分组"。iii 的函数 id 约定是 service::function(如 email::send),这里把同一前缀的函数归成一个 Service
  • module_services: DashMap<String, Arc<dyn Any + ...>>(:13)——"模块服务",存的是任意类型的 Rust 对象(如 http_functions 模块本身),靠 Any + downcast 做类型擦除的注册/取回(register_service :23 / get_service :28)。§3.2 里 get_service::<HttpFunctionsWorker>("http_functions") 用的就是后者。

7.1 从函数 id 自动建服务

register_service_from_function_id(function_id)(services.rs:90)是 RegisterFunction 每次都会调的那一步(回忆 §3.2 第③步)。逻辑:

"email::send"
│ split("::")

service_name = "email" (第 0 段)
function_name = "send" (第 1 段起 join 回去)


services 里没有 "email"? ── 建一个空 Service("email")


把 "send" 塞进 Service("email").functions

拆分靠两个辅助函数:get_service_name_from_function_id(:74,取 :: 前第一段)和 get_function_name_from_function_id(:82,取第一段之后 join("::") 回来——所以 a::b::c 的函数名是 b::c)。少于两段(没有 ::)的 id 直接被忽略,不建服务(:76:84parts.len() < 2 判据)。

7.2 撤销:空了就连服务一起删

remove_function_from_services(function_id)(services.rs:37)是反向操作:把函数从对应 Service 里删掉;若该 Service 删完后不再有任何函数,就把整个服务也删掉(:62-71should_remove_service)。这保证了服务分组不会留下空壳。Service 本身(:120)只是 name + parent_service_id(可选父服务)+ 一个 functions: HashSet<String>

RegisterService 消息(mod.rs:1357)走的是另一条:它用 insert_service(Service::with_parent(...)) 显式插入一个带父服务的服务节点(services.rs:106:138),用于表达服务层级,而不是从函数 id 自动推导。名字为空时回退用 id 当名字(mod.rs:1363)。


8. 边界与局限(本章范围内诚实说明)

  • active_scope 的进程级竞态尚未修:见 §4.1 的 FIXME。作者判断实际风险低但正确性上应改为 per-worker token。文档如实记录。
  • 所有权机制只覆盖"函数注册":function_owners / external_function_owners 保护的是函数 id 的注册/撤销。触发器的清理走的是 trigger_registry.unregister_worker(mod.rs:1707),channel 走 channel_manager.remove_channels_by_worker(mod.rs:1708),它们各有各的按-worker 清理,不共用这两张属主表。
  • 服务分组假设 service::function 命名:不含 :: 的函数 id 不会进 services 分组(services.rs:76),但仍是合法的可调用函数——分组只是一层组织视图。
  • 本章不覆盖:一次调用如何 spawn、等待、超时、回填结果(InvokeFunction / InvocationResult 的后台任务与 invocations 表)→ 03;各触发器类型(cron / http / workers-available 等)的具体实现与内置 worker → 04;运行时发现与可观测性 → 05;SDK 侧握手 → 06

9. 代码地图(导航索引)

主题文件路径符号名
引擎结构体(全部注册表字段)engine/src/engine/mod.rsEngine(struct)
引擎初始化engine/src/engine/mod.rsEngine::new
消息大分发表engine/src/engine/mod.rsEngine::router_msg
每连接主循环 + 收帧解码engine/src/engine/mod.rsEngine::handle_worker
会话前缀解析engine/src/engine/mod.rsresolve_registration_id
认领所有权(常规 / 外部)engine/src/engine/mod.rsclaim_function / claim_external_function
CAS 释放所有权engine/src/engine/mod.rsrelease_function_if_owner / release_external_function_if_owner
断线清理engine/src/engine/mod.rsEngine::cleanup_worker
内置 worker 作用域 + FIXMEengine/src/engine/mod.rsbegin_worker_scope / end_worker_scope / remove_worker_registrations
函数落地(handler 包 worker)engine/src/engine/mod.rsEngine::register_function
一条 WS 连接的状态engine/src/worker_connections/mod.rsWorkerConnection
外部 vs 常规函数 id 跟踪engine/src/worker_connections/mod.rsinclude_function_id / include_external_function_id / get_regular_function_ids
worker 名册engine/src/worker_connections/mod.rsWorkerConnectionRegistry(register_worker / get_worker / list_workers)
连接状态枚举engine/src/worker_connections/mod.rsWorkerConnectionStatus
服务分组注册表engine/src/services.rsServicesRegistry
从函数 id 建服务engine/src/services.rsregister_service_from_function_id
撤销 + 空服务回收engine/src/services.rsremove_function_from_services
模块服务(类型擦除存取)engine/src/services.rsregister_service / get_service