iii 核心概念解析:Worker、Function、Trigger 与 Engine 四要素如何构成跨语言事件驱动系统
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
在 iii 中,所有软件能力都被归约为同一个接口:Worker 承载运行、Function 是具体的工作、Trigger 决定工作何时执行、Engine 负责在它们之间路由。读完本文,你将掌握这套心智模型的完整细节——包括service::name函数标识规范、Trigger 的三要素与条件函数、同步/fire-and-forget 调用模式、Worker 断连时的注册表清理语义,并能结合 Quickstart 教程 在仓库源码中逐一验证这些机制。
四要素总览
Unix 给进程提供了单一接口,React 给组件提供了单一接口。iii 给每一类软件(队列、调度器、Agent、前端、沙箱、业务逻辑等)提供了同一个接口。一旦建立这四个部件的心智模型,iii 中其余一切都只是主题的变化:
| 要素 | 职责 | 一句话定义 |
|---|---|---|
| Worker | 承载运行 | 任何连接到 Engine 并注册 Trigger 与 Function 的进程,可运行在笔记本、容器、浏览器标签页或 microVM 上,语言不限,前提是能向 Engine 打开 WebSocket |
| Trigger | 触发时机 | 使 Function 运行的东西。有类型(HTTP、cron、队列消息、状态变更、另一个 Function 调用trigger)、配置(哪个路径、哪个计划、哪个队列)以及被调用的 function ID |
| Function | 具体工作 | Worker 内部的命名 handler,接收 payload 并返回结果;标识符遵循service::name约定,在 Worker 重启与语言边界间保持稳定 |
| Engine | 路由中枢 | 协调者。接受 Worker 连接、维护 Function 与 Trigger 的实时注册表、将调用路由到当前提供目标 Function 的 Worker |
以 Quickstart 为例的运行拓扑
本文以 Quickstart 教程 为贯穿示例。该教程最终产出两个连接到同一 Engine 的 Worker:
math-worker:Python Worker,注册math::add;caller-worker:TypeScript Worker,注册math::add_two_numbers,它通过 Engine 调用math::add。
教程进行到最后,系统还包含state与httpWorker、一个把math::add_two_numbers暴露在POST /math/add-two-numbers上的 HTTP Trigger,以及一个持有running_total的名为math的键值作用域。运行时拓扑如下:
每一条箭头都是 Worker 与 Engine 之间的 WebSocket 连接,系统内不存在 worker 到 worker 的直接流量。当caller-worker调用math::add时,调用经过 Engine:Engine 在注册表中查找math::add的当前位置,把调用路由到math-worker。
Worker:独立进程与语言无关的参与方
Worker 是 iii 系统中真正执行工作的部分。每一种能力类别都构建为 Worker:队列、调度、沙箱、可观测性、Agent、业务逻辑、设备,甚至浏览器中执行的代码。
具体而言,Worker 是一个通过 WebSocket 连接 Engine 的进程,它声明自己能运行的一组 Function 和要注册的 Trigger。连接建立后,这些 Function 可以从系统任何位置调用,这些 Trigger 会响应各自的事件——调用方与 Worker 之间不需要任何成对集成代码。
Worker 概念被刻意保持狭窄:它不是微服务、任务运行器或 sidecar,而是 Engine 实时注册表中的一个参与方,贡献 Function 与 Trigger。无论该 Worker 是承载每秒数千次调用的长驻进程,还是连接、注册、执行一次就退出的短命进程,Engine 的对待方式完全一致。
Worker 隔离性
Worker 被设计为相互独立的进程。一个 Worker 崩溃不影响其他 Worker:Engine 通过独立 WebSocket 连接每个 Worker,只把调用路由到当前已连接的 Worker。某个 Worker 的崩溃、重启或网络分区不会传播到其他 Worker——崩溃 Worker 的 Function 和 Trigger 在断连时从路由表中移除,其余 Worker 继续服务。
语言无关在实际中意味着什么
Quickstart 中的 Python 与 TypeScript Worker 是不同语言、不同运行时、可能在不同机器上的独立进程,彼此不知道对方的执行上下文。它们只与 Engine 对话,其余由 Engine 处理。这就是"任意语言、任意运行时"的实际含义:Worker 契约小到任何能使用 WebSocket 和 JSON 的语言都可以实现,且 Engine 无论 Worker 如何构建、运行在哪里,都一视同仁。
从源码看,Worker 与 Engine 之间的通信契约定义在 协议定义 中,Engine 侧的 Worker 连接管理位于 worker_connections 模块 与 引擎主模块。
Trigger:决定 Function 何时运行的绑定
Trigger 是一个告诉 iii 何时调用 Function 的绑定,由三部分组成:
type:事件类型,如http或cron;config:该类型的细节,如 HTTP 路径或 cron 表达式;function_id:要调用的 Function。
三者合起来告诉 iii:监听什么事件、如何监听、事件发生时调用什么。HTTP 请求、cron 计划、队列消息、状态变更、日志事件、流事件,全部通过 Trigger 变成 Function 调用。
Trigger 类型来自已连接的 Worker
Trigger 类型由已连接的 Worker 提供。能产生事件的 Worker 会连同配置 schema 一起声明一个或多个 trigger 类型:http Worker 提供http类型,cron Worker 提供cron类型,state Worker 提供state类型。某类型的 Trigger 只能在该类型的提供者 Worker 处于连接状态时注册——因为正是那个 Worker 产生触发它的事件。
Engine 内置了一张已知 trigger 类型到提供者 Worker 包的映射表(trigger.rs),可确认仓库当前内置的类型及其提供者:
| Trigger 类型 | 提供者 Worker 包 |
|---|---|
http | http |
cron | cron |
subscribe | pubsub |
state | state |
durable:subscriber | queue |
stream/stream:join/stream:leave | iii-stream |
log/trace | iii-observability |
configuration | configuration |
值得注意的实现细节:从源码结构看,trigger 类型注册使用了(namespace, id)二元组作为键(trigger.rs 中的TypeKey)。注释说明了原因——早期仅用 id 作键时,两个项目提供相同 id 会互相覆盖,且败者的绑定会被静默带入胜者的提供者;引入 namespace 后该问题被消除。
条件函数:在 handler 之前的一道门槛
Trigger 还可以指定可选的condition_function_id,它在 handler 之前运行。Trigger 触发时,Engine 用 handler 将收到的同一份 payload 调用条件函数:返回真值则 handler 执行,否则调用被跳过。由于 Trigger 关心"何时做"、Function 关心"做什么",条件函数保持了这一分离——Function 专注于自身工作,不必累积逐 Trigger 的守卫逻辑。
Engine 侧的条件求值逻辑实现在 condition.rs 的check_condition中,比文档表述更精确:
- 条件函数返回结果且不是显式
false→ 放行(result.as_bool() != Some(false)); - 条件函数没有返回任何结果→ 记录警告后仍放行;
- 条件函数显式返回
false→ 跳过 handler; - 调用本身失败 → 以错误返回,不静默吞掉。
同时该函数在触发器目标 Function 所在的namespace内解析条件函数,确保命名空间 Worker 的条件检查命中自己的条件函数,而不是default中同名的函数。
Trigger 流水线与动作
Trigger 触发时,Engine 在实时注册表中查找其function_id,找到当前提供该 Function 的 Worker 并派发调用。函数 handler 只看到 payload 本身——它永远不知道触发来源或触发事件类型。
Function 调用可通过Trigger Action控制:
- 默认同步模式:阻塞直到 Function 返回结果或配置的超时触发。调用方需要 Function 返回值时使用。
TriggerAction.Void(fire-and-forget):立即返回,调度 Function 运行而不等待结果。适用于调用方无需等待的副作用工作。
这一点可以在协议层得到确认:protocol.rs 中TriggerAction枚举定义了Enqueue { queue: String }与Void两个变体;Void与Enqueue的具体处理路径分别位于 engine/mod.rs 与 engine/mod.rs。
此外,Worker 也可以定义自己的TriggerAction。queue Worker 就提供TriggerAction.Enqueue({queue}),把调用路由到具名队列并带重试,这是把触发器与重试/持久化语义组合起来的扩展点。
Trigger 生命周期
Trigger 经历四个状态:
registered:Trigger 已向 Engine 声明;active:Trigger 正在监听其事件;invoked:事件已触发该 Trigger;unregistered:Trigger 已被移除。
当拥有 Trigger 的 Worker 断连时,它的所有 Trigger 会连同其 Function 一起被自动注销。
Quickstart 中的三种触发方式
Quickstart 演示了以三种不同方式触发 Function:
- CLI:
iii trigger math::add a=2 b=3是一个由 CLI 自身触发的 Trigger,Engine 把调用路由到提供math::add的 Worker; - SDK 调用:
worker.trigger({ function_id: 'math::add', ... })是同一思路的另一个版本——一个 Worker 内的 Function 触发一个调用另一个 Function 的 Trigger,经由 Engine 路由,与 CLI 版本一致; - 显式 HTTP Trigger:由
httpWorker 通过worker.registerTrigger()添加,是响应式实现 Trigger 的常见方式。
前两条路径针对任何已注册的 Function 都有效,无需注册显式 Trigger:每次registerFunction()都天然获得一个可通过这两种方式调用的 Trigger。
以 Quickstart 第 7 步的 HTTP Trigger 为例(quickstart.mdx):
worker.registerFunction( "http::add_two_numbers", async (payload: { body: { a: number; b: number } }) => { const result = await worker.trigger<{ a: number; b: number }, { c: number; running_total: number }>({ function_id: "math::add_two_numbers", payload: payload.body, }); return { status_code: 200, body: { c: result.c, running_total: result.running_total }, headers: { "Content-Type": "application/json" }, }; }, ); worker.registerTrigger({ type: "http", function_id: "http::add_two_numbers", config: { api_path: "/math/add-two-numbers", http_method: "POST" }, });httpWorker 拥有 HTTP 套接字,当请求到达POST /math/add-two-numbers时:
http查找匹配的 Trigger,发出一个指向math::addFunction 的请求;- Engine 收到请求,把调用路由到
caller-worker; - 响应沿同一条路径流回。
math::addFunction 全程看不到任何 HTTP 请求——它看到的只是 payload,与任何其他调用无异。
Function:service::name标识与直接调用
从 iii 系统视角看,Function 由其名称标识,可跨越语言与位置边界寻址。调用方不知道提供该 Function 的是哪个 Worker、handler 用什么语言编写、Worker 运行在哪里。Engine 把每次调用路由到当前提供目标 Function 的 Worker。
Function 除了"payload 进 / result 出"之外没有固定形状:有的是纯计算,有的执行副作用(写状态、HTTP 调用、入队),有的是 Agent 式的、会连锁调用其他 Function。Engine 不加区分——它们的路由方式相同。
函数标识符规范
函数标识符使用service::name约定:service段把相关 Function 归组为命名空间、作用域或 Worker 名,name段是具体的 handler。math::add、state::get、http::serve都遵循该约定。
需要明确:该约定是建议而非硬性规则。Engine 层面任何字符串都是合法的 function ID,但service::name形式让 Function 的意图对读者一目了然,并避免不同 Worker 注册的无关 Function 之间发生碰撞。Quickstart 中的math::add与math::add_two_numbers即为例:math命名空间归组了相关 Function,名称标识具体 handler。这种分组完全是任意的——iii 内部不强制path::to::functions结构。
直接调用与多 Trigger 复用
用registerFunction()注册 Function 后,它即刻可以通过两种方式直接调用:任意已连接 Worker 的worker.trigger(),以及iii triggerCLI 命令。这两条路径不需要显式 Trigger 注册,是每个已注册 Function 的基线调用面;其他触发源(HTTP、cron、queue、state、stream)则是把显式 Trigger 绑定到同一个function_id上。
单个 Function 可以是任意数量 Trigger 的目标:同一个 Function 可以同时被 HTTP 请求、cron 计划、队列消息调用——注册三个共享同一function_id的独立 Trigger 即可。函数代码不变,变的只是 Trigger 注册。这正是让单个业务逻辑 Function 能响应众多事件源、无需逐源变体的关键。
Quickstart 中直接调用的真实输出:
iii trigger math::add_two_numbers a=10 b=20 # 输出 { "c": 30 }另外,Function ID 在 Worker 重启间保持稳定:math-worker停止再启动后,调用方无需感知——它们继续调用math::add,Engine 把调用路由到当前提供该 Function 的实例。Function 以同步方式定义,但由于 Trigger 与 Function 之间的解耦,可以被异步调用。
Engine:注册表、路由与断连清理
Engine 是持有所有已连接 Worker、所有已注册 Function 与 Trigger 的注册表的单一进程:
- Worker 连接时,Engine 记录它提供哪些 Function;
- Worker 断连时,Engine 移除其 Function、取消这些 Function 的所有在途调用,并通知系统其余部分拓扑已变更——其余 Worker 继续服务。
路由与语言、运行时、位置无关。Engine 不需要知道math::add运行在 Docker 里、Raspberry Pi 上还是浏览器标签页中——它只知道某个Worker 提供了它。同一份教程可以跨不同运行时重新部署,而不必触碰任何函数代码。
更完整的 Engine 行为——启动流程(解析命令行参数 → 加载config.yaml→ 应用 config 中的 Worker 声明 → 开始接受连接)、config.yaml的运行时热重载(解析、diff、校验、提交;未变更的 Worker 保持运行,无效配置导致 Engine 退出而非进入不确定状态)、以及engine::*::list快照查询与engine::workers-available/engine::functions-available订阅事件,详见 Engine 文档。Engine 的默认配置可参考仓库中的 config.yaml。
小结:一个接口,四个角色
回到开头的心智模型:
- Worker是注册表参与方——独立进程,可长驻可短命,任意语言任意位置,断连即隔离;
- Function是稳定的寻址单元——
service::name约定、payload 进 result 出、重启后 ID 不变、天然具备 CLI 与 SDK 两条直接调用面; - Trigger是时机绑定——
type+config+function_id三要素,外加可选条件函数与Void/Enqueue动作,四状态生命周期随 Worker 断连自动注销; - Engine是唯一的路由事实来源——实时注册表 + 架构无关路由 + 断连清理。
四者关系即 Quickstart 拓扑中那条"所有箭头都指向 Engine"的不变式:无 worker 到 worker 直连,一切跨语言、跨位置的协作都经由注册表完成。掌握这四块积木后,iii 文档中的队列、流、可观测性、沙箱等能力,都只是同一主题的变体。
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考