iii Adapter Pattern 实战:用薄 Worker 将存量服务接入 iii 函数网格
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
Adapter Pattern(适配器模式)是 iii 中一种将存量系统(第三方 API、内部微服务、遗留库)接入 iii 平台而无需重写的标准做法:用一个薄 Worker 包装既有服务,将其能力以service::name形式的 iii 函数暴露给整个网格。读完本文你将掌握适配器 Worker 的结构、注册与调用链路、错误映射与密钥处理的实现要点,并能够参考仓库源码写出可运行的适配器。
模式是什么:包装,而非重写
iii 的架构是一个 WebSocket 路由的 Worker 网格:单个引擎进程(默认端口49134)维护所有已连接 Worker、每个 Worker 暴露的函数以及绑定到函数的触发器的实时注册表。Worker 是独立的进程,通过 WebSocket 连接引擎并注册Functions(service::name处理器)和Triggers(触发这些函数的事件);Worker 之间没有直连流量,所有调用都经由引擎路由——即caller → engine → handler,函数 ID 是任意两个 Worker 之间唯一的契约(见 engine 内建iii-engine-functionsWorker 的架构说明)。
Adapter Pattern 正是建立在这个模型之上:当你希望把一个既有服务引入 iii 系统而不重写它时,就把它包在一个薄 Worker 里,让该 Worker 把服务的接口翻译成 iii 的函数调用形态(function-call shape)。调用方因此可以像对待网格里任何其他 Worker 一样对待它——按函数 ID 寻址,而不是按 HTTP 端点或库特有的调用形态。
何时使用该模式
当你有一个希望从 iii 使用、但不想重写的既有服务,并且希望调用方以与其他 Worker 相同的方式(通过函数 ID,而不是 HTTP 或库特有的调用形态)来寻址它时,就使用此模式。
适用场景包括:
- 第三方 API(如支付、AI、消息推送服务);
- 内部自研微服务,已有 REST/gRPC/私有协议接口;
- 仓库内已有的库或工具,需要暴露为网格可调用能力;
- 需要在不改动调用方的前提下,为某个服务增加可观测性、触发绑定、队列化等 iii 能力。
结构:三步薄 Worker
一个 Adapter 就是一个薄 iii Worker,它:
- 连接引擎——通过 SDK 建立与引擎的 WebSocket 连接;
- 为每个要暴露的操作注册一个函数——每个函数对应存量服务的一个操作;
- 在每个处理器内部调用存量服务,并把结果作为函数的响应返回——翻译层就在这一步。
这与仓库中 functions.mdx 文档 描述的"编写函数"模型完全一致:Worker 通过注册函数向系统贡献能力,每个函数有一个service::name形式的 ID、一个接收 payload 并返回结果的 handler,以及可选的描述请求/响应形态的 JSON Schema。
连接引擎:registerWorker 与地址解析
适配器 Worker 的第一步是与引擎建立连接。以 Node/TypeScript SDK 为例:
import { registerWorker } from "iii-sdk"; const url = process.env.III_URL; if (!url) throw new Error("III_URL must be set"); const worker = registerWorker(url);从 SDK 源码看,registerWorker(address?, options?)返回一个已连接的 Worker 客户端,WebSocket 连接会自动建立。地址解析遵循明确的优先级(resolveAddress):
- 显式传入的
address参数(优先级最高); - 环境变量
III_URL——通常由孵化该进程的监督者(iii compose、容器运行时、systemd)设置,与III_NAMESPACE、III_WORKER_NAME一并注入; - 兜底默认值
DEFAULT_ENGINE_URL = 'ws://127.0.0.1:49134'(定义于 iii.ts)。
注意:默认地址特意写成 IPv4 回环地址,因为
localhost在主机仅监听 IPv4 时可能解析到::1。
注册函数:service::name 命名与 JSON Schema
连接建立后,为存量服务的每个操作注册一个函数。函数 ID 采用service::name形式——这既是调用方的寻址契约,也是触发器绑定时的function_id。在 Node SDK 的registerFunction中,函数 ID 不能为空且不能重复注册(重复会抛出function id already registered错误)。
建议的命名约定:service段使用被包装服务的域名化缩写(例如stripe、openai、orders),name段使用该服务的具体操作(例如create-payment、completion、get-by-id)。这样整个网格的函数清单(可通过engine::functions::list查看)一目了然,也便于用prefix过滤出某个适配器的全部函数。
附带请求/响应 Schema
可以在注册时为请求和响应附带 JSON Schema,让契约随函数一起文档化——这些 Schema 会与函数一同存储,并在 iii console、iii trigger --help等界面呈现(见 functions.mdx 的 Schema 章节)。需要明确的是,当前引擎不做运行时校验:Schema 仅作为函数调用、Agent 与 console 的契约文档,引擎不会拒绝与 Schema 不匹配的 payload 或返回值。
worker.registerFunction( "orders::get", async (payload: { orderId: string }) => { // 调用存量订单服务…… return { orderId: payload.orderId, status: "shipped" }; }, { request_format: { type: "object", properties: { orderId: { type: "string" } }, required: ["orderId"], }, response_format: { type: "object", properties: { orderId: { type: "string" }, status: { type: "string" }, }, required: ["orderId", "status"], }, }, );一个完整的适配器示例
以下是一个包装内部订单 REST 微服务的示意适配器(API 签名均取自仓库 SDK 的真实实现)。它演示了三步结构的完整落地:连接引擎、按操作注册函数、在 handler 内调用存量服务并映射结果。
import { registerWorker, InvocationError } from "iii-sdk"; const url = process.env.III_URL; if (!url) throw new Error("III_URL must be set"); const worker = registerWorker(url, { workerName: "orders-adapter" }); // 存量服务客户端:可以是内部 REST 客户端、SDK 或数据库连接 const ordersService = createOrdersClient({ baseUrl: process.env.ORDERS_SERVICE_URL, // 由部署环境注入 apiKey: process.env.ORDERS_SERVICE_API_KEY, // 密钥走环境变量,不进代码 }); worker.registerFunction( "orders::get", async (payload: { orderId: string }) => { const res = await ordersService.get(payload.orderId); // 调用存量服务 if (!res.ok) { throw new InvocationError({ code: "UPSTREAM_ERROR", message: `orders service returned ${res.status}`, function_id: "orders::get", }); } return res.json(); // 返回标准 iii 函数响应 }, { description: "Fetch an order from the legacy orders service", request_format: { type: "object", properties: { orderId: { type: "string" } }, required: ["orderId"] }, response_format: { type: "object" }, }, );Python 与 Rust 的写法可参考 functions.mdx 中的多语言示例:
import os from iii import register_worker, InitOptions worker = register_worker( os.environ.get("III_URL"), InitOptions(worker_name="orders-adapter"), ) def get_order_handler(payload: dict) -> dict: return orders_service.get(payload["orderId"]) # 调用存量服务 worker.register_function("orders::get", get_order_handler)错误映射:从上游错误到 iii 函数错误
Adapter 的翻译层不仅要翻译成功响应,还要翻译失败。Node SDK 提供了统一的类型化错误InvocationError:它包装线上的ErrorBody形态({ code, message, stacktrace? })以及被调用的function_id,使调用方在所有失败模式(RBAC 拒绝 FORBIDDEN、handler 级失败、等待引擎响应的超时)下都能通过同一个错误类型区分问题,且能通过err.code快速识别失败类别。其 message 以code: message形式呈现,错误对象自带code、function_id、stacktrace字段,自我描述性强。
实践建议:
- 上游服务返回业务错误时,映射为带语义化
code的InvocationError(如UPSTREAM_ERROR、NOT_FOUND、RATE_LIMITED),让调用方可以按 code 编程式处理; - 不要把上游的原始堆栈直接透传,而是保留 message 并适当记录日志;
- 若适配器同时暴露多个操作,为每个 handler 的
InvocationError带上对应的function_id,便于追踪是哪个适配操作失败。
另外,SDK 中还有RegistrationRejectedError:当引擎拒绝 Worker 的身份注册(例如同一(namespace, worker_name)已被另一个存活 Worker 占用)时抛出,SDK 会停止且不重连。命名空间冲突(FUNCTION_NAMESPACE_CONFLICT)则是非致命的,仅记录日志。这提醒我们:部署适配器时要注意 Worker 名称与命名空间的唯一性。
认证与密钥处理
原文档的 TODO 特别点名了如何在外包服务时处理认证/密钥。结合仓库约定,推荐做法是:
- 密钥一律走环境变量或部署平台提供的注入机制,绝不硬编码进源码。SDK 本身依赖
III_URL、III_NAMESPACE、III_WORKER_NAME等环境变量完成引导(见resolveAddress),适配器可沿用同样的约定,读取ORDERS_SERVICE_API_KEY之类的变量来构造存量服务客户端; - 上游认证失败应映射为明确的错误 code(如
AUTH_FAILED、UNAUTHORIZED),而非让调用方看到裸的 HTTP 401; - 不要在接受外部 payload 的 handler 中拼接敏感凭据到响应里;如需审计,记录到日志/可观测性系统而不是返回给调用方。
关于可观测性:SDK 的 handler 包装层会在调用前后记录iii.invocation.input/iii.invocation.output追踪事件(受III_DISABLE_TRACE_PAYLOADS环境变量控制,见 iii.ts 中的 handler 包装)。也就是说,适配器天然获得请求/响应 payload 的追踪能力,便于观察"调用方 → 引擎 → 适配器 → 存量服务"整条链路。
暴露适配器:用 Trigger 把函数接到事件上
适配器注册的函数默认只能被显式调用(worker.trigger/iii trigger/ SDK 调用)。若要让它响应事件,就绑定 Trigger。SDK 的registerTrigger将一个 trigger 配置绑定到已注册函数上,返回带unregister()的句柄:
// 暴露为 HTTP 端点 worker.registerTrigger({ type: "http", function_id: "orders::get", config: { api_path: "/orders/get", http_method: "GET" }, }); // 定时轮询存量服务并同步状态 worker.registerTrigger({ type: "cron", function_id: "orders::sync", config: { expression: "0 * * * * *" }, // 7 字段:sec min hr dom mon dow year });仓库中的引擎内建 Worker README 归纳了常用 trigger 类型及其配置键(见 engine_fn README 的 Canonical trigger types 表):
type | Provider | 配置键 |
|---|---|---|
http | http | { http_method, api_path } |
cron | cron | { expression }— 7 字段:sec min hr dom mon dow year |
queue | queue | { queue, retries } |
state | state | { scope, condition_function_id } |
stream | iii-stream | { stream_name } |
一个值得注意的命名空间语义:registerTrigger若未显式指定命名空间,则落在该 Worker 自己的命名空间,因为函数注册在 Worker 的命名空间下——命名空间不一致会导致 trigger 触发后解析不到函数(见 registerTrigger 的源码注释)。适配器与调用方在同一命名空间时通常无需显式指定。
通过引擎内建函数观察适配器
部署适配器后,可以用引擎自带的engine::*内省函数验证它是否正确入网(无需额外安装,内建于引擎,见 engine_fn README):
engine::functions::list { prefix: "orders::" }— 确认适配器注册的函数已出现在注册表;engine::functions::info { function_id: "orders::get" }— 查看该函数的 Schema、属主 Worker 与已绑定的触发器;engine::workers::list/engine::workers::info { name: "orders-adapter" }— 查看连接中的 Worker 及其完整暴露面。
小结与工程建议
Adapter Pattern 在 iii 中是一个"薄壳"模式:翻译层只做三件事——连接引擎、按操作注册函数、在 handler 里调用存量服务并映射结果。它把 HTTP 或库特有的调用形态统一收敛为service::name函数契约,让调用方与存量服务的耦合降到最低。工程落地时重点把握四条原则:
- 每个存量操作 = 一个函数,命名遵循
service::name,并附带请求/响应 Schema 作为契约文档; - 错误统一走类型化错误(如
InvocationError),用语义化code区分失败类别; - 密钥走环境变量,由部署环境注入,不在源码与响应中暴露;
- 用 Trigger 决定暴露方式(HTTP、定时、队列等),命名空间要与函数注册保持一致。
参考实现与文档入口:适配器骨架与多语言写法见 functions.mdx,引擎路由模型与内省函数见 engine_fn README,SDK 连接与注册 API 见 iii.ts、errors.ts。
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考