ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

computer 仓库中的 capnweb RPC:stub 生命周期与释放契约实战指南

computer 仓库中的 capnweb RPC:stub 生命周期与释放契约实战指南 computer 仓库中的 capnweb RPCstub 生命周期与释放契约实战指南【免费下载链接】computerGive your agent a computer 项目地址: https://gitcode.com/GitHub_Trending/computer1/computer本指南以 .agents/skills/capnweb/SKILL.md 为骨架系统讲解本仓库中 Durable Object ↔computerd之间 capnweb RPC 的传输形态、对象能力object-capability模型以及最重要的stub 生命周期与释放disposal契约。读完本文你将掌握 caller-disposes 规则、using/try-finally/ 显式Symbol.dispose三种释放模式、.dup()与.map()的正确用法并能用enableStubTracking()stubSnapshot()与script/computerd-stub-soak.mjs对 RPC 边界做泄漏验证。capnweb 在本仓库中的定位capnweb 是 Durable Object 与computerd之间的 RPC 帧协议framing。它是一套对象能力 RPC 系统具备三个核心特征Promise 流水线promise pipelining可以在不等待前一个调用完成的前提下把结果直接作为参数继续调用减少往返次数structured-clone 风格的 stub 传输stub 可以作为参数或返回值跨连接传递双向调用连接两端都可以发起调用而非严格的 client/server 单向模型。本仓库的线格式wire format是长连接 WebSocket 上的文本 JSON。这是唯一被使用的 carriercapnweb 的 HTTP 批传输batch transport无法承载从调用中返回的流而本接口上每一次读取本质上都是一个流change 流、对象字节流、exec 事件流所以只能走 WebSocket 长连接。从代码结构看这正是 packages/rpc/src/client.ts 中createSyncClient/createWorkspaceClient通过newWebSocketRpcSession(ws)拨号、并由 packages/rpc/src/server.ts 的acceptWebSocketSession(ws, rpc)挂载服务端的根本原因——两端共享同一套 capnweb 会话。Where things live代码地图本仓库把 capnweb 相关的类型、服务端、客户端、驱动与调试面拆在packages/rpc包内各司其职文件职责packages/rpc/src/interface.ts类型化的线协议面。WorkspaceRPC是根 stub组合SyncRPC与ShellRPC。新增方法先改这里。packages/rpc/src/server.ts以Database为后端的具体实现。Durable Object 与容器内 computerd 共同导入它。packages/rpc/src/client.ts基于 WebSocket carrier 的类型化 stub。Durable Object 使用deferred transport使 stub 可以在 WebSocket upgrade 完成之前就创建。packages/rpc/src/sync-driver.tspullOnce、pushOnce、tick。包装流式方法并在内部处理释放。packages/rpc/src/debug.tsenableStubTracking、stubSnapshot用于泄漏排查。docs/08_capnweb_interface.md线协议的设计意图与完整接口说明。docs/11_lifecycle.md本仓库的 stub 释放契约与跨生命周期矩阵。其中 packages/rpc/src/interface.ts 定义了组合根接口export interface WorkspaceRPC { sync: SyncRPC; shell: ShellRPC; }线面上只暴露一个稳定根 stub两个半区同步面sync、进程监督面shell保持内部可分离、可独立测试。服务端实现 packages/rpc/src/server.ts 用getter暴露sync/shell这是因为 capnweb 的RpcTarget拒绝遍历普通实例属性会抛出 instance properties cannot be accessed over RPCgetter 在分发路径上看起来像方法可以被正常穿透。心智模型stub 是能力不是可回收的引用一个 stub 就是一份能力capability持有它就意味着拥有调用远端对象的权利。capnweb 的 stub不会跨网络被垃圾回收本地 GC 对远端的对象图没有任何可见性远端运行时也不知道你是否处于内存压力之下如果你不显式释放 stub就会在连接的另一端泄漏资源。这一点在本仓库比多数 capnweb 部署更关键因为连接是长连接。短命会话在结束时会把一切释放掉而 Durable Object 与computerd之间的这条 WebSocket在整个 Workspace 生命周期内保持开启所以每一个未释放的 stub 都会一直存活到这条连接断开为止。结合 docs/11_lifecycle.md 的描述capnweb 会话持有的导出表export table、应答表answer table、活动流与 socket 引用全部是纯内存的不跨 isolate 驱逐、OOM 或容器重启存活。泄漏的 stub 会以导出表条目的形式长期钉在长连接上直到会话死亡。caller-disposes 规则capnweb 的基本法则来自 capnweb 规范的根本原则是调用方负责释放所有 stub。具体展开为三条细则作为参数传入的 stub 仍归调用方所有。被调用方收到的是 RPC 系统在调用完成时自动释放的副本。你可以在调用后立即释放自己的原始 stub——它在发送时已经被复制了。结果中返回的 stub 将所有权转移给调用方。调用方必须释放它们。RPC 系统在确认不会再有流水线调用到达后会释放被调用方的副本。结果信封result envelope总是带 disposer即使你认为它不包含 stub。无论如何都释放它——未来某个 API 变更可能加入 stub而你的调用方会因此静默泄漏。为什么信封也要释放驱动层的实证packages/rpc/src/sync-driver.ts 中的maybeDispose是这条规则在驱动层的落地// Best-effort dispose of a capnweb result envelope. Real envelopes // expose [Symbol.dispose]; the test fakes return plain objects, so // the symbol may be absent. function maybeDispose(value: unknown): void { const d (value as { [Symbol.dispose]?: () void } | null | undefined)?.[Symbol.dispose]; if (typeof d function) d.call(value); }fetchChanges的返回信封里同时含有currentCursor、appliedPushCursor与stream三个字段其中stream本身是一个导出表条目中的流 stub。pullOnce在 packages/rpc/src/sync-driver.ts 用try/finally保证无论正常排空、提前返回、跨端不变量触发还是批内抛错信封都会被释放从而连带拆除内层流 stub、释放远端迭代器。How to dispose三种释放模式按偏好顺序排列// 1. using 声明——stub 是作用域局部时首选。 using result await client.sync.fetchChanges({ sinceRev }); for await (const entry of result.stream) { // ... } // 块退出时 result 自动释放。// 2. try/finally——当 using 不可用或作用域别扭时。 const result await client.sync.fetchChanges({ sinceRev }); try { for await (const entry of result.stream) { // ... } } finally { result[Symbol.dispose](); }// 3. 显式释放——当所有权跨边界转移时。 const stub api.getThing(); // ... 把 stub 传给别处 ... stub[Symbol.dispose]();using依赖 TypeScript 5.2 的显式资源管理Explicit Resource Management提案。它把作用域结束即释放内建进语言是驱动代码和直接流式调用方最不容易出错的选择。Repo-specific disposal contract本仓库的释放契约在通用规则之上本仓库还有四条明确约定见 packages/rpc/README.md 与 docs/11_lifecycle.md驱动driver拥有自己的 stub。pullOnce/pushOnce会释放它们接触到的每一个信封。走驱动的代码无需考虑释放问题。驱动不拥有定时器——调用方决定何时调用pullOnce()与pushOnce()生产环境是轮询循环测试里是手动tick()以保证收敛确定性。直接流式调用拥有自己的结果。如果你直接伸手到client.sync/client.shell调用fetchChanges、fetchObjects、shell.exec、shell.getExec你就拥有那个信封。用using绑定或在finally里释放。信封上的流是正文排空流并不会释放信封本身。关闭会释放根。createSyncClient/createWorkspaceClient在调用client.close()时释放根 stub。不要自己拆除底层 WebSocket让close()级联完成。在 packages/rpc/src/client.ts 中可以看到close()先对根 stub 调用Symbol.dispose让 RPC 层在传输死亡前发送干净的 abort 帧再关闭 socket最后用 200ms 超时兜底避免 await 永远挂起——整个过程幂等。不要为了看一眼而 await 一个 stub。await 一个RpcPromise会解析它如果它解析出 stub你从此就拥有那个 stub必须负责释放。跨端不变量释放之外的对称检查pullOnce还在驱动层实现了跨端水印不变量检查见 packages/rpc/src/sync-driver.ts当appliedPushCursor.rev localPushRev或currentCursor after远端日志比我们记忆的更短典型场景是 WebSocket 存活期间 computerd 进程重启时把分歧的游标重置为 0、取消在途流、重试一次第二次分歧则视为真正的协议破坏直接抛错而非无限循环。这与reconcileWatermarks连接时调用共同构成了协议为断流重试而设计的耐久性底座。Duplicating stubs with.dup()如果你需要把 stub 传给一个会释放它的地方同时又想在本端继续使用调用stub.dup()。底层目标会一直存活直到每一份副本都被释放。.dup()同样适用于 stub 或 promise 的属性。这是在不额外往返的前提下抓取一个 stub 形态属性的惯用法// 立即抓取 authedApi 作为 stub无需 await。 using authedApi api.authenticate(token).dup(); // 立刻用于流水线调用。 const userId await authedApi.getUserId();Listening for disposal on the server服务端监听释放一个RpcTarget可以声明Symbol.dispose方法。capnweb 会在每一个指向该 target 的 stub 都被释放后调用它一次class SessionTarget extends RpcTarget { // ... [Symbol.dispose]() { // Release any per-session resources. } }如果同一个 target 被多次传给 RPC你会得到每次 stub 对应一次dispose 调用。要把多次释放折叠成一次用new RpcStub(target)包装一次然后到处传递这个 stub 即可。本仓库的 packages/rpc/src/server.ts 正是这么做的SyncRPCServer、ShellRPCServer、WorkspaceRPCServer都在构造函数里调用trackStub(this)在[Symbol.dispose]()里调用untrackStub(this)与 packages/rpc/src/debug.ts 的泄漏计数器挂钩。Listening for disconnect监听断连stub.onRpcBroken(cb)在 stub 变得不可用时触发——典型场景是底层连接断开或对 promise 而言是 promise 被拒绝。回调执行后该 stub 上的每一个方法调用都会抛错。packages/computer已经把onRpcBroken折叠进了 Workspace 的 closed promise复用那套管道而不是另接一套并行监听器。结合 docs/11_lifecycle.md 的说明会话死亡在 Workspace 后端边界处理close 事件、capnwebonRpcBroken回调、容器退出或分类后的传输错误都会移除并关闭对应 handle安全可重放的操作会自动重连一次。Promise pipelining一个 RpcPromise 也是 stub一个RpcPromise同时也是其最终结果的 stub。除非你确实需要本地值否则不要 await 它// 三次调用一次往返。 using authed api.authenticate(token).dup(); const profile await api.getUserProfile(authed.getUserId());你可以把RpcPromise作为参数传给另一个 RPC。capnweb 会在接收方交付调用前用解析后的值替换它。需要注意对 stub 或 promise 做属性访问返回的RpcPromise没有自己的 disposer——你必须释放它来源的那个 stub 或 promise。属性可以作为参数或返回值传递但这永远不会导致任何东西被隐式释放。这正是 deferred transport 的价值所在见 packages/rpc/src/client.ts在 WebSocket 达到 OPEN 之前stub 上的首次调用会排队upgrade 一完成就冲刷出去——stub 可以在连接建立前就创建流水线因此从一开始就可用。The magic.map()一次往返内跑同步回调.map()在 promise 的远端解析值上运行一个同步回调且只需一次往返const idsPromise api.listUserIds(); const names await idsPromise.map(id [id, api.getUserName(id)]);限制条件回调必须是同步的不能await回调会先在本地以记录模式record mode运行一次因此除 RPC 调用外不得有任何副作用回调捕获的任何 stub 都会随记录一起发送给对端。把被捕获的 stub 视为已暴露给对端只使用源自同一对端的 stub。Streams are first-class流是一等公民ReadableStreamT是 capnweb 的常规值。线协议绝不内联 blob 字节——变更流携带内容寻址的(hash, size)记录接收方通过hasObjects/pushObjects回叫获取缺失子集。新增 RPC 时优先流式传输而非单个大载荷。从 packages/rpc/src/interface.ts 可以印证对象传输的方向性设计fetchObjects容器 → DO与pushObjectsDO → 容器都接受ReadableStream{ hash, bytes }而非数组这样发送方可以在 wire 发送过程中交错读取对象把峰值内存保持在有界范围内。ChangeEntry由 packages/dofs/src/sync/changes.ts 定义。当你收到一个流式结果时信封拥有这个流。排空流并不会释放信封释放信封才会释放其中包含的每一个 stub。pullOnce用PULL_BATCH_SIZE 256分批排空流packages/rpc/src/sync-driver.ts每批处理完立即释放保证pullOnce的峰值内存是O(PULL_BATCH_SIZE)而非O(stream)。反向背压exec 事件流shell.exec/shell.getExec返回的ReadableStreamExecEvent把消费端背压一直传播到被派生进程的内核管道见 docs/08_capnweb_interface.md消费者停止拉取runner 就停止read()子进程的 stdout/stderr内核管道填满后子进程阻塞在write上——话痨命令会像在普通 shell 里遇到慢速tee或less一样自我调节。ExecEvent的每个帧都携带每 id 单调递增的seqpackages/rpc/src/interface.ts断线后可用getExec({ id, after: seq })从已知点续播。Do / Dont本仓库的 RPC 守则Do在 packages/rpc/src/interface.ts 的WorkspaceRPC上先定义新方法再在任一端实现。把 stub 当作能力对待。除非你确实想授予访问权否则不要把 stub 传出会话边界。能用pullOnce/pushOnce驱动同步轮次就用它们。对直接流式调用的每个 await 结果信封声明using。即使你认为结果信封不包含 stub也释放它——为未来做预防是廉价的。需要一份能活过被调用方自动释放的 stub 副本时调用.dup()而不是 await 两次。改动任何 RPC 生命周期相关代码时运行泄漏 harnessscript/computerd-stub-soak.mjs并检查session.getStats()是否有漂移。Dont不要发送二进制 WebSocket 帧。线格式是文本 JSON。不要直接拆除底层 carrier。始终走client.close()让根 stub 先释放。不要只改一侧就改动WorkspaceRPC接口——线契约是共享的。不要声明 TypeScriptprivate方法并以为它不会暴露给 RPC。用#前缀名才能在运行时真正私有。不要指望垃圾回收清理 stub。它不会。共享线契约与错误码接口没有版本协商docs/08_capnweb_interface.md请求/响应形状变更属于硬性线破坏需要 Durable Object 与 computerd 锁步发布。线上的错误携带结构化代码packages/rpc/src/interface.ts调用方不必做字符串匹配CodeMeaningENOENT接收方路径不存在或getExec/disposeExec引用了未知 idEUNKNOWN_HASHfetchObjects引用了接收方无记录的 hashEEXEC_BUSYexec使用了正在运行的 idELOG_TRUNCATEDgetExec续播点早于保留日志ESHUTDOWN保留服务端正在关闭重启后重连EAUTH保留握手认证失败EPROTOCOL保留线帧或版本不匹配Testing for leaks如何验证没有泄漏三种手段层层递进单元级在测试中使用enableStubTracking()stubSnapshot()断言一次本该清理干净的往返后没有任何 stub 存活。浸泡级用script/computerd-stub-soak.mjs对释放敏感的改动做边界浸泡测试它读取session.getStats()来检测漂移drift。服务端级packages/rpc与packages/computer中的服务端 RPC 行为测试直接跑在真实Database与真实驱动 helper 之上。packages/rpc/src/debug.ts 展示了计数器的开关机制通过CAPNWEB_TRACK_STUBS1环境变量、globalThis.CAPNWEB_TRACK_STUBS覆盖或编程式enableStubTracking()用于 workerd 这类 env 变量不落到process.env的运行环境。计数按类维护构造时trackStub、dispose 时untrackStub并用WeakSet去重——capnweb 对共享 target 可能多次调用[Symbol.dispose]重复释放会被忽略。它是测量而非强制snapshot()返回每类存活数浸泡脚本断言静默期之后每个计数器都归零任何非零值都意味着泄漏。computerd在开启该标志时还会在GET /__computerd/stubs暴露快照见 packages/rpc/README.md配合 packages/computer/tests/stub-soak.test.ts 的 workerd 浸泡测试可以在持续负载下证明无无界增长。进一步阅读docs/08_capnweb_interface.md — 线协议设计意图SyncRPC/ShellRPC全接口、push/fetch 语义、往返次数表、exec 背压与流回放、错误模型与可观测性钩子。docs/11_lifecycle.md — 本仓库的 stub 释放契约以及 DO / 容器 / capnweb 会话三个生命周期层的完整交互矩阵与休眠hibernation展望。packages/rpc/README.md —cloudflare/computer-rpc包的四个入口线类型、server、client、driver、debug与释放、调试面的使用说明。packages/rpc/src/interface.ts — 线契约的单一事实来源。packages/rpc/src/sync-driver.ts —pullOnce/pushOnce/tick/reconcileWatermarks的完整实现是驱动拥有 stub与跨端水印不变量检查的权威参考。如需更完整的 wire 契约上下文请继续阅读 docs/02_sync_protocol.mdpush/fetch 轮次如何组合与 docs/07_injected_service.md承载这些 RPC 的 computerd 服务端与引导序列。【免费下载链接】computerGive your agent a computer 项目地址: https://gitcode.com/GitHub_Trending/computer1/computer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表