跳到主要内容

一次调用的一生

30 秒导读: 在 iii 里,"调用一个函数"不是一次普通的 Rust 函数调用——发起方和执行方常常是两台不同的进程(引擎 vs 外部 worker)。本章端到端追一次调用:从谁发起、引擎怎么记住它、结果怎么按 id 精确回投,到中途断线、入队改写、fire-and-forget 这几条支线,最后看 HTTP 外部函数这条"调用落到别人家 URL 上"的岔路。

本章属于 iii 系列的一章。上游背景见 引擎中枢:注册表与消息分发(消息怎么进到 router_msg)与 三原语与线上协议(Message 长什么样);触发器如何把一个事件变成一次调用见 触发器体系与内置 worker。全景与阅读地图见 index


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

一句话定义: "一次调用"就是"让某个已注册的函数 f 吃一份 JSON 输入、吐一份 JSON 输出(或一个错误)"这件事的完整生命周期——从发起,到执行,到结果回到发起方手里。

为什么它值得单独讲一章? 因为在 iii 里,发起方和执行方经常不在同一个进程:

  • 你的业务函数(order::create)跑在一个 Node/Python worker 里,通过 WebSocket 连到引擎。
  • 引擎自己不知道怎么执行 order::create——它只知道"这个函数登记在某个 worker 名下"。
  • 于是"调用"必须跨进程:引擎把请求过去,worker 执行完再把结果回来。

这就带来一个核心难题:引擎发出请求后,当前这段代码要不要原地等? 等的话,一个慢函数会卡住;不等的话,结果回来时怎么知道它对应哪一次调用?

iii 的答卷(一句话直觉): 把每一次调用想成寄一封带回执编号的快递

  • 发起时:撕一张回执单(oneshot channel),编号是 invocation_id,把回执存进一本登记簿(DashMap<Uuid, Invocation>)。
  • 能当场办完(内置函数)就当场把结果贴到回执上;办不完(要 worker 跑)就先把回执留在登记簿里,人先走。
  • 结果快递(InvocationResult)回来时,凭编号在登记簿里找到那张回执,把结果投进去,发起方的 .await 立刻醒来。

本节不出现代码。记住三个词就行:回执单(oneshot)、登记簿(DashMap)、回执编号(invocation_id)


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

2.1 参与者

参与者干什么在哪
发起方 worker想调用某个函数,发 InvokeFunction外部进程,WS 连入
router_msg引擎消息总机,把 InvokeFunction / InvocationResult 分流engine/mod.rs:589
InvocationHandler调用的"登记簿" + 生命周期管家invocation/mod.rs:46 InvocationHandler
Function.handler真正执行的闭包(内置逻辑 或 "转发给 worker")function.rs:29 Function
执行方 worker真正跑用户代码,跑完发 InvocationResult 回来外部进程

2.2 一次调用的两种命运

关键在于:引擎调用 Function.handler 后,拿到的 FunctionResult 有两种走向。

引擎收到 InvokeFunction

spawn_invoke_function 起一个后台任务

handle_invocation

撕回执单(oneshot)+ 建 Invocation

调用 Function.handler

┌────────────────────┴─────────────────────┐
①当场就有结果(内置函数) ②要 worker 跑(Deferred)
Success / NoResult / Failure │
│ 把 Invocation 存进登记簿 DashMap
直接 sender.send(结果) │
│ handler 先返回,后台任务在
│ receiver.await 上挂起
│ │
└──────────┐ (稍后)worker 发回 InvocationResult
│ │
│ router_msg 凭 invocation_id 从登记簿取回执
│ invocation.sender.send(结果)
│ │
└────────────► receiver.await 醒来,结果回到发起方

怎么读这张图: 左路(①)是"内置函数当场办完";右路(②)是"外部 worker 异步办"。两路最终都汇到同一个 receiver.await——发起方感知不到差异,它只是 await 一个结果。区别只在于:结果是同一个后台任务当场塞进去的,还是另一条消息回来时塞进去的。

2.3 主线走一遍(高层)

  1. 发起方 worker 发来 InvokeFunction { invocation_id, function_id, data, action, … }
  2. router_msg 命中 InvokeFunction 分支,先过 RBAC / middleware 闸门。
  3. action 分派:普通调用 → spawn_invoke_function;Enqueue → 丢进队列;Void → fire-and-forget。
  4. 普通路径进 handle_invocation:撕 oneshot 回执、建 Invocation、调 Function.handler
  5. 内置函数当场 send 结果;worker 函数返回 Deferred,Invocation 存入登记簿等待。
  6. worker 跑完发 InvocationResult 回来,router_msginvocation_id 取回执、投结果。
  7. 发起方 spawn_invoke_function 里的后台任务拿到结果,再打包成一条 InvocationResult 发回发起方 worker。

3. 核心机制(逐个看)

3.1 回执单 + 登记簿:InvocationInvocationHandler

它要解决的小问题: 结果可能很久以后才回来(worker 慢),回来时得知道它是哪一次调用的。

数据结构。 一次调用被建模成一个 Invocation:

// invocation/mod.rs:33 struct Invocation
pub struct Invocation {
pub id: Uuid,
pub function_id: String,
pub worker_id: Option<Uuid>,
pub sender: oneshot::Sender<Result<Option<Value>, ErrorBody>>, // ← 回执单的“投递口”
pub traceparent: Option<String>, // W3C 分布式追踪
pub baggage: Option<String>, // W3C 跨切面上下文
}

sender 是一个 tokiooneshot:一次性的、只能投递一个值的 channel。谁 await 对应的 receiver,谁就在等这次调用的结果。

登记簿。 InvocationHandler 里就一张并发哈希表,Uuid → Invocation:

// invocation/mod.rs:44 type Invocations
type Invocations = Arc<DashMap<Uuid, Invocation>>;

DashMap(分段锁并发 map)是因为:成千上万次调用可能同时在飞,登记/注销发生在不同 tokio 任务里,需要无全局锁的并发读写。

两个管家动作:

方法作用位置
remove从登记簿摘掉一次调用,交出它的 Invocation(含 sender)invocation/mod.rs:57 remove
halt_invocation摘掉并往回执里投一个 invocation_stopped 错误(强行叫醒等待方)invocation/mod.rs:63 halt_invocation

halt_invocation 是"取消"的实现——它不 kill 远端 worker,只是给本地等待方一个失败结果,让它别再傻等(见 §3.6)。

3.2 handle_invocation:同步完成 vs 异步 Deferred

它要解决的小问题: 同一套代码,既要能处理"内置函数当场返回",又要能处理"worker 稍后才返回",还不能让调用方感知差异。

InvocationHandler::handle_invocation(invocation/mod.rs:76)是整章的心脏。核心步骤:

// invocation/mod.rs:149 invocation_fut(简化摘录)
let (sender, receiver) = tokio::sync::oneshot::channel(); // 撕回执单
let invocation_id = invocation_id.unwrap_or(Uuid::new_v4());
let invocation = Invocation { id: invocation_id, sender, /* … */ };

let result = function_handler
.call_handler(Some(invocation_id), body, session) // 调真正的 handler
.await;

拿到 result: FunctionResult 后,按四种变体分流(invocation/mod.rs:172-317):

FunctionResult含义处理引用
Success(v)当场成功记指标,invocation.sender.send(Ok(v))mod.rs:173
NoResult当场成功、无返回值sender.send(Ok(None))mod.rs:260
Failure(e)当场失败把错误挂到 span,sender.send(Err(e))mod.rs:204
Deferred"我转给 worker 了,结果稍后到"不 send,把 invocation 存进登记簿mod.rs:288

Deferred 是精髓那一步:

// invocation/mod.rs:288 FunctionResult::Deferred 分支
FunctionResult::Deferred => {
// …记 deferred 指标…
// 结果稍后由 worker 的 InvocationResult 送达,先把回执登记起来
self.invocations.insert(invocation_id, invocation);
}

注意:Success / Failure / NoResult 三种没有 insert 进登记簿——它们当场就把结果 send 掉了,回执用完即弃。只有 Deferred 需要把回执寄存起来,等未来那条 InvocationResult 来认领。

统一的收尾。 四路之后都会执行同一个 receiver.await(invocation/mod.rs:319):

  • Success/Failure/NoResult:sender 刚 send 过,receiver.await 立即返回。
  • Deferred:sender 还留在登记簿里没 send,receiver.await 在此挂起,直到 §3.4 的 InvocationResult 分支把结果投进去。

这就是"两种命运、一个出口"的实现——调用方永远只是 await 一个 receiver,不关心结果是当场还是隔了三秒。

worker 函数为什么返回 Deferred? 因为它的 handler 其实只做一件事:把请求转发成一条 InvokeFunction 发给 worker,然后立即返回 Deferred:

// worker_connections/traits.rs:119 WorkerConnection::handle_function(摘录)
let send_result = self.channel
.send(Outbound::Protocol(Message::InvokeFunction { invocation_id, function_id, data: input, /* … */ }))
.await;
match send_result {
Ok(_) => {
self.invocations.write().await.insert(id); // worker 侧也记一笔
FunctionResult::Deferred // ← 关键:我没结果,稍后给
}
Err(err) => FunctionResult::Failure(/* channel_send_failed */),
}

3.3 内置 vs worker 路由,与 span 抑制决策

它要解决的小问题: 一次调用要不要让引擎自己发一个链路追踪(trace)的 span?发多了噪音大、还会和 worker 自己发的 span 重复。

handle_invocation 一开头先判定这个函数是不是内置(built-in):

// invocation/mod.rs:89
let is_builtin = crate::workers::telemetry::is_iii_builtin_function_id(&function_id);

is_iii_builtin_function_id(workers/telemetry/mod.rs:202)靠前缀认内置函数:engine::state::stream::configuration::iii::iii-http::motia:: 等,以及少数裸名(publish)。这类函数在引擎进程内原地执行,高频、低价值。

抑制规则(invocation/mod.rs:102):

let suppress_span = if is_builtin {
!crate::workers::telemetry::trace_builtins_enabled() // 内置:默认抑制
} else {
true // worker 路由:总是抑制
};

用表说清两类为什么都倾向抑制:

函数类型谁真正执行引擎为何抑制自己的 span开关
内置(state::* 等)引擎进程内高频、低追踪价值,量大噪音大III_OTEL_TRACE_BUILTINS=true 可开(telemetry/mod.rs:228 trace_builtins_enabled)
worker 路由(用户函数)外部 workerworker 会发自己那条 call <fn> span;引擎再发就是跨服务重复无——恒抑制

被抑制时 span 直接是 tracing::Span::none()(invocation/mod.rs:109);不抑制时才建一个带 FaaS 语义约定字段的 call <fn> span(invocation/mod.rs:111-123),并用 with_parent_headers 把 W3C 的 traceparent/baggage 接成父子关系。

上下文照样要传。 有个巧妙点:即使 span 被抑制,只要 caller 带了 traceparent/baggage,dispatch 仍会跑在 caller 的 OTel context 下(invocation/mod.rs:140 dispatch_cxmod.rs:332 with_context)。原因:

  • worker 路由:引擎 span 没了,得靠这个 ambient context 让 worker 自己的 span 挂到 caller 的 trace 上,而不是孤立成新 trace。
  • 内置:state/stream 写入会触发 trigger 的 fan-out,靠它让那些子调用嵌进写入者之下(如 approval::resolve → 状态写入 → turn::on_approval)。

链路追踪细节属于可观测性,更全的展开见 活的系统:发现、运行时扩展与可观测性

3.4 引擎中枢:InvokeFunction 找目标、InvocationResult 回投

这是"跨进程调用"真正落地的地方,都在 router_msg 里。

发起侧:InvokeFunction 分支(engine/mod.rs:883)。收到后依次:

  1. RBAC 闸门(mod.rs:902-938):若 session 存在且该函数不在允许集,直接回一条 FORBIDDENInvocationResult,并给出补救提示("加进 rbac.expose_functions" 之类)。
  2. middleware 拦截(mod.rs:940-980):若 session 配了 middleware_function_id(且非 engine:: 内部调用),把原始调用打包成输入交给中间件函数,由它的结果作为回复——原函数不被直接执行
  3. action 分派(mod.rs:983):见下表。
action语义处理引用
None普通调用,要结果spawn_invoke_function(…, *invocation_id)mod.rs:1099
Voidfire-and-forget,不回结果spawn_invoke_function(…, None) 强制 id 为 Nonemod.rs:1085
Enqueue { queue }改写成入队,不直接执行丢进 queue_module,回一个 messageReceiptIdmod.rs:984

普通路径进 spawn_invoke_function(engine/mod.rs:462)。它起一个 tokio 后台任务:

  • data 注入 _caller_worker_id 作为标准元数据(mod.rs:485-494)——这样执行方知道谁调的自己。
  • remember_invocation(mod.rs:405):按 function_id 从注册表 self.functions.get 找到目标 Function(找不到就回 function_not_found,mod.rs:449),再交给 handle_invocation
  • 拿到结果后,只有当 invocation_idSome 才把结果打包成一条 InvocationResult 发回发起方 worker(mod.rs:516-582)。这正是 fire-and-forget 的实现:invocation_idNone 时,这整段回投被跳过。

回投侧:InvocationResult 分支(engine/mod.rs:1113)。这是 §3.2 里那些挂起的 Deferred 被唤醒的地方:

// engine/mod.rs:1113 Message::InvocationResult 分支(摘录)
worker.remove_invocation(invocation_id).await;
if let Some(invocation) = self.invocations.remove(invocation_id) { // 凭 id 从登记簿取回执
if let Some(err) = error {
let _ = invocation.sender.send(Err(err.clone()));
} else {
let _ = invocation.sender.send(Ok(result.clone())); // ← 投进回执,叫醒 receiver.await
};
return Ok(());
} else {
// 登记簿里没有 → caller 早就断线了(见 §3.6),迟到的结果无处可投
tracing::debug!(invocation_id = %invocation_id, "Did not find caller for invocation (caller already disconnected)");
}

这就是"按 invocation_id 把结果 route 回 originator"的全部秘密: invocation_id 是回执编号;self.invocations.remove(invocation_id) 就是凭编号取回那张寄存的回执;invocation.sender.send 就是投递。发起方在 receiver.await 上的挂起当即结束。找不到回执不是错误——那是 caller 先走了的正常情况,压到 debug 级别避免刷屏。

对照 omit invocation_id 的 fire-and-forget: Void 动作把 invocation_id 强制成 None,于是 §3.2 里 handle_invocation 生成一个临时 uuid 执行,但 spawn_invoke_function 的回投段(mod.rs:516 if let Some(invocation_id))被跳过——结果算出来也不往回发。适合"发通知、记日志"这类不关心返回值的调用。

内置函数的直连入口 call 除了 WS 进来的 InvokeFunction,引擎内部(hooks、middleware、fire_triggers)还有一条同进程调用入口 Engine::call(engine/mod.rs:1794):它同样 self.functions.get 找函数、直接 handle_invocation,但 invocation_id/worker_id 都传 None——因为发起方就是引擎自己,不需要跨进程回投。它还刻意触发 notify_user_function_invoked(那只为真实用户调用而设,cron 之类的自发调用不该唤醒 boot 心跳)。

3.5 TriggerAction::Enqueue / Void:把一次触发改写

它要解决的小问题: 有时"触发一个函数"不该立刻同步执行——要么想削峰填谷排进队列,要么只想发出去不等回音。

TriggerAction(protocol.rs:35)只有两个变体,内联在 InvokeFunction 消息里,由 router_msgmatch action 落地。

Enqueue { queue }(engine/mod.rs:984):不执行函数,而是把 payload 入队:

// engine/mod.rs:1024(摘录)
qm.enqueue_to_function_queue(&queue, &function_id, data, message_receipt_id, traceparent, baggage).await
  • queue_module(运行时可选,没加载就回 QueueModule not loaded,mod.rs:1036)。
  • 成功时若 invocation_id 存在,回一个 { "messageReceiptId": … } 作为结果(mod.rs:1048)——回执编号证明"已入队",而非"已执行"。真正的执行由队列消费者稍后完成。
  • 它还顺手修正 baggage:把 iii.function.id 改写成被入队的目标函数而非入队者,让 span 归属正确(mod.rs:1003-1011)。

Void(engine/mod.rs:1085):就是 §3.4 说的 fire-and-forget,spawn_invoke_function(…, None)

一句话对照:Enqueue = "改道去队列,给你一张入队回执";Void = "照常执行,但别回话"。 两者都把"一次直接调用"改写成了别的语义。触发器如何产生带 action 的调用,见 触发器体系与内置 worker

3.6 中断:halt_invocation 与 caller 断线

它要解决的小问题: 发起方在结果回来前就断了(Ctrl-C、超时、崩溃),那些挂在 receiver.await 上的调用不能永远悬着。

当一个 worker 连接清理时(cleanup_worker),引擎遍历它名下所有在飞的调用逐个 halt:

// engine/mod.rs:1700(摘录)
let worker_invocations = worker.invocations.read().await;
for invocation_id in worker_invocations.iter() {
self.invocations.halt_invocation(invocation_id); // 给每个等待方投 invocation_stopped
}

halt_invocation(invocation/mod.rs:63)把回执从登记簿摘掉,并投进一个 invocation_stopped 错误——凡是在 receiver.await 上等它的地方立刻收到失败而醒来,不再泄漏。

这也解释了 §3.4 里那条 else 分支:caller 断线时它名下的 Invocation 已被 halt_invocation 摘走,稍后那条迟到的 InvocationResult 在登记簿里就找不到回执了——属于正常竞态,记 debug 即可。

worker.invocations(worker_connections/mod.rs:230)是每个 worker 各自维护的一份"我发起了哪些调用"的集合,add_invocation/remove_invocation(mod.rs:348/352)在调用登记与结果回投时同步增删,正是为了断线时能一网打尽。


4. 支线:HTTP 外部函数调用

前面讲的执行方都是 WS worker。iii 还支持第三种执行方:一个外部 HTTP 端点。你把一个函数登记成"其实是去 POST 某个 URL",引擎照样把它当普通函数对待。这条支线全在 invocation/ 下的几个文件里。

4.1 各文件职责

文件职责关键符号
http_function.rsHTTP 函数的配置模型:URL、method、超时、headers、auth 引用http_function.rs:15 HttpFunctionConfig
method.rsHTTP 方法枚举 + 解析后的 auth 凭据method.rs:11 HttpMethodmethod.rs:21 HttpAuth
auth.rsauth 配置(引用 env 变量名)与解析成凭据auth.rs:13 HttpAuthConfigauth.rs:70 resolve_auth_ref
signature.rs给请求体做 HMAC-SHA256 签名signature.rs:10 sign_request
url_validator.rs发请求前的 URL 安全校验(反 SSRF)url_validator.rs:59 UrlValidator
http_invoker.rs真正发 HTTP 请求、解析响应成 FunctionResulthttp_invoker.rs:251 invoke_http

4.2 它怎么接进"普通调用"

关键在于:HTTP 函数注册时,包了一个闭包 handler,内部调 invoke_http——所以对 handle_invocation 而言它就是一个当场返回 Success/Failure 的普通内置函数(不是 Deferred):

// workers/http_functions/mod.rs:42 create_handler_wrapper(摘录)
match invoker.invoke_http(&function_path, &endpoint, Uuid::new_v4(), input, None, None).await {
Ok(result) => FunctionResult::Success(result),
Err(e) => FunctionResult::Failure(e),
}

也就是说:HTTP 支线复用了 §3.2 的同步完成那一路,只是"执行"这一步从"跑本地逻辑"换成了"阻塞地发一次 HTTP 请求再等响应"。

4.3 发请求的三道关

HttpInvoker::invoke_http(http_invoker.rs:251)按顺序:

  1. URL 校验(反 SSRF)。validate_url(http_invoker.rs:191)过 UrlValidator。默认配置(url_validator.rs:19 UrlValidatorConfig::default)会:要求 HTTPS、拦截私网 IP、按 glob allowlist 过滤。失败抛 SecurityError(url_validator.rs:29,如 PrivateIpBlocked/HttpsRequired)。这是防止别人把函数指向 http://169.254.169.254 之类内网元数据地址的护栏。
  2. 组请求 + 注入 iii 头。 build_base_request(http_invoker.rs:98)带上 x-iii-Function-Pathx-iii-Invocation-IDx-iii-Timestamp 等头,body 是序列化后的 JSON。
  3. 加认证。 apply_auth(http_invoker.rs:124)按凭据类型给请求盖章。

4.4 认证:配置引用 env,而非明文存密钥

HttpAuthConfig(auth.rs:13)三种,注意它存的是环境变量名,不是密钥本身:

类型配置字段落到请求上引用
Bearertoken_key(env 名)Authorization: Bearer <token>method.rs:23http_invoker.rs:136
ApiKeyheader + value_key自定义头 <header>: <value>http_invoker.rs:137
Hmacsecret_key(env 名)x-iii-Signature: sha256=…signature.rs:10 sign_request
  • 两阶段解析。 注册时 validate(auth.rs:30)只校验 env 变量存在(不读值);真要发请求前 resolve_auth_ref(auth.rs:70)才把 env 值取出来变成 HttpAuth 凭据。这样密钥不长期驻留在配置对象里。
  • HMAC 签名(signature.rs:10):对 timestamp:base64(body)HMAC-SHA256 签,输出 sha256=<hex>。接收方用同样的 secret 复算即可验真——防篡改、防重放(timestamp 参与签名)。

4.5 响应怎么翻译成结果

invoke_http 尾部(http_invoker.rs:293-315)按状态码分档:

  • 2xx:期望是 JSON(或空)。空 body → Ok(None);能解析 → Ok(Some(json));解析失败 → invalid_response 错误(视作契约违反)。
  • 1xx/3xx(is_non_error_status,http_invoker.rs:29,< 400 即非错):当作"无结果的成功",body 能当 JSON 就带上,不能也不报错(比如 3xx 带 HTML body)。
  • ≥400:parse_error_response(http_invoker.rs:142)尝试从 { "error": { code, message } } 提取结构化错误,提不出就退化成 HTTP <status>

5. 巧妙之处(可借鉴)

  • oneshot + DashMap = 跨进程"同步调用"的最小骨架。 用一次性 channel 当"回执",用并发 map 当"编号→回执"的登记簿,就把"发起方 await / 结果异步回投"缝成了一个对调用方透明的 await 点。invocation/mod.rs:149-341
  • 四态 FunctionResult 里只有 Deferred 需要寄存。 同步三态当场 send 用完即弃,只有 Deferred insert 进登记簿——省掉了"每次调用都登记再注销"的开销。invocation/mod.rs:172-316
  • action 把"调用"变成一等的可改写语义。 同一条 InvokeFunction,靠一个可选 action 就能表达"同步要结果 / 入队 / 发了不管"三种意图,而不必设计三种消息。protocol.rs:35engine/mod.rs:983
  • span 抑制的双重理由讲得很清楚。 内置抑制是为降噪(可用 env 开),worker 抑制是为避免跨服务重复——但上下文照传,保证 worker 的 span 仍挂在 caller 的 trace 下。invocation/mod.rs:102-147
  • auth 配置存 env 变量名而非明文密钥,两阶段解析。 注册期只验存在、调用期才取值,缩小密钥驻留面。auth.rs:30/auth.rs:70
  • URL 校验默认拦私网 + 强制 HTTPS,是内建的反 SSRF 护栏,而不是留给用户自觉。url_validator.rs:19/url_validator.rs:73

6. 边界与局限

  • halt_invocation 不 kill 远端。 它只给本地等待方投 invocation_stopped,让其停止等待;真正在 worker 里跑的代码不会被强行终止。invocation/mod.rs:63
  • 迟到结果被静默丢弃。 caller 断线后回来的 InvocationResult 找不到回执,只记 debug 后丢掉,不会重试也不落盘。engine/mod.rs:1138-1150
  • Enqueue 依赖运行时装了 queue_module 没加载则直接 enqueue_error: QueueModule not loadedengine/mod.rs:1036
  • HTTP 支线走同步阻塞。 handler 里 await invoke_http 期间该后台任务被占住;它不是 Deferred 那种真正的异步交还。超时由 timeout_ms 或默认 30s 兜底(http_invoker.rs:63)。
  • HTTP 客户端禁用重定向。 redirect::Policy::none()(http_invoker.rs:83)——3xx 不自动跟随,被当作无结果成功返回,避免被重定向绕过 URL 校验。
  • 本章不覆盖: 触发器如何注册、内置 worker(state/stream/queue 等)如何实现 → 见 04;消息如何从 WS 解码进 router_msg → 见 02

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

主题文件符号
一次调用的数据模型(回执 + 追踪上下文)engine/src/invocation/mod.rsInvocation
登记簿 + 生命周期管家engine/src/invocation/mod.rsInvocationHandler
撕回执、调 handler、四态分流、Deferred 寄存engine/src/invocation/mod.rshandle_invocation
摘回执 / 强制失败叫醒engine/src/invocation/mod.rsremovehalt_invocation
内置识别 + span 抑制开关engine/src/workers/telemetry/mod.rsis_iii_builtin_function_idtrace_builtins_enabled
四态结果枚举 + handler 契约engine/src/function.rsFunctionResultFunction::call_handler
worker handler:转发成 InvokeFunction、返回 Deferredengine/src/worker_connections/traits.rshandle_function
发起侧分支:RBAC / middleware / action 分派engine/src/engine/mod.rsrouter_msg(InvokeFunction 臂)
普通调用后台任务 + 结果回投engine/src/engine/mod.rsspawn_invoke_functionremember_invocation
回投侧:凭 id 取回执并投递engine/src/engine/mod.rsrouter_msg(InvocationResult 臂)
引擎内直连调用入口engine/src/engine/mod.rsEngine::call
断线清理时批量 haltengine/src/engine/mod.rscleanup_worker
worker 各自的在飞调用集合engine/src/worker_connections/mod.rsadd_invocationremove_invocation
调用改写动作枚举engine/src/protocol.rsTriggerAction(Enqueue/Void)
HTTP 函数配置模型engine/src/invocation/http_function.rsHttpFunctionConfig
HTTP 方法 + 解析后凭据engine/src/invocation/method.rsHttpMethodHttpAuth
auth 配置(引用 env)与解析engine/src/invocation/auth.rsHttpAuthConfigresolve_auth_ref
HMAC 请求签名engine/src/invocation/signature.rssign_request
反 SSRF 的 URL 校验engine/src/invocation/url_validator.rsUrlValidatorUrlValidatorConfig
发 HTTP 请求 + 解析响应engine/src/invocation/http_invoker.rsHttpInvoker::invoke_http
把 invoke_http 包成普通 handlerengine/src/workers/http_functions/mod.rscreate_handler_wrapper