使用服务绑定通过 RPC 调用另一个 Worker 时,会占用被调用 Worker 的内存。考虑以下示例:
let user = await env.USER_SERVICE.findUser(id);假设服务端 findUser() 返回继承 RpcTarget 的对象,因此客户端的 user 最终是指向该远程对象的 stub。
只要客户端上仍存在 stub,服务端上的对应对象就无法被垃圾回收。但每个 isolate 都有自己的垃圾回收器,无法查看其他 isolate。因此,为了让服务端的 isolate 知道对象可以被回收,调用方 isolate 必须发送明确的信号,称为「释放(disposing)」stub。
在许多情况下(如下所述),系统会自动识别 stub 何时不再需要并自动释放。但为获得最佳性能,代码应在不再使用 stub 时显式释放。
为确保资源得到正确释放,应使用显式资源管理(Explicit Resource Management) ↗,这是一项新的 JavaScript 语言特性,允许你明确指示何时可以释放资源。显式资源管理是 TC39 第 3 阶段提案——即将进入 V8 ↗。
显式资源管理添加了以下语言特性:
如果变量使用 using 声明,当变量超出作用域时,将调用该变量的释放器。例如:
function sendEmail(id, message) {
using user = await env.USER_SERVICE.findUser(id);
await user.sendEmail(message);
// user[Symbol.dispose]() is implicitly called at the end of the scope.
}using 声明有助于确保不会忘记释放 stub——即使代码被异常中断。
Wrangler v4+ 原生支持 using 关键字。如果你使用的是较早版本的 Wrangler,则需要手动释放资源。
以下代码:
{
using counter = await env.COUNTER_SERVICE.newCounter();
await counter.increment(2);
await counter.increment(4);
}...等价于:
{
const counter = await env.COUNTER_SERVICE.newCounter();
try {
await counter.increment(2);
await counter.increment(4);
} finally {
counter[Symbol.dispose]();
}
}RPC 系统在以下情况下会自动释放 stub:
当事件处理程序「完成」时,作为该事件一部分创建的所有 stub 会自动释放。
例如,考虑处理传入 HTTP 事件的 fetch() 处理程序。处理程序可能在处理事件时发起出站 RPC,这些 RPC 可能返回 stub。当最终 HTTP 响应发送后,处理程序「完成」,所有 stub 会立即释放。
更精确地说,事件有一个「执行上下文(execution context)」,从处理程序首次调用时开始,到 HTTP 响应发送时结束。如果客户端在收到响应之前断开连接,执行上下文也可能提前结束;或者通过调用 ctx.waitUntil() 可以延长其正常结束点。
例如,下面的 Worker 未使用 using 声明,但一旦 fetch() 处理程序返回响应,stub 就会被释放:
export default {
async fetch(request, env, ctx) {
let authResult = await env.AUTH_SERVICE.checkCookie(
req.headers.get("Cookie"),
);
if (!authResult.authorized) {
return new Response("Not authorized", { status: 403 });
}
let profile = await authResult.user.getProfile();
return new Response(`Hello, ${profile.name}!`);
},
};通过 RPC 调用的 Worker 也有执行上下文。当调用 WorkerEntrypoint 上的 RPC 方法时,上下文开始。如果此 RPC 的参数或结果中未传递任何 stub,则 RPC 返回时上下文结束(事件「完成」)。但是,如果传递了任何 stub,则执行上下文会隐式延长,直到所有此类 stub 都被释放(以及通过它们发起的所有调用都已返回)。与 HTTP 一样,如果客户端断开连接,服务端的执行上下文会立即取消,无论 stub 是否仍然存在。作为另一个 Worker 的客户端,在其自身执行上下文结束时被视为已断开连接。同样,可以通过 ctx.waitUntil() 延长上下文。
当 stub 作为 RPC 参数接收时,调用返回时会自动释放。如果你希望保留 stub 更长时间,必须对其调用 dup() 方法。
当 RPC 返回任何类型的对象时,系统会为该对象添加释放器。释放它会释放调用返回的所有 stub。例如,如果 RPC 返回包含四个 stub 的数组,数组本身会有一个释放器来释放全部四个 stub。RPC 返回值没有释放器的唯一情况是原始值,如数字或字符串。这些类型无法添加释放器,但由于它们本身不能包含 stub,因此在此情况下不需要释放器。
这意味着你几乎应始终将 RPC 的结果存储到 using 声明中:
using result = stub.foo();这样,如果结果包含任何 stub,它们会被释放。即使你不期望 RPC 返回 stub,如果它返回任何类型的对象,将其存储到 using 声明中也是好主意。这样,如果将来 RPC 扩展为返回 stub,你的代码已做好准备。
如果你决定在 using 声明作用域之外保留返回的 stub,可以在作用域结束前对其调用 dup()。(请记住稍后显式释放副本。)
继承 RpcTarget 的类可以选择实现释放器:
class Foo extends RpcTarget {
[Symbol.dispose]() {
// ...
}
}RpcTarget 的释放器在最后一个 stub 被释放后运行。请注意,客户端对 stub 释放器的调用不会等待服务端释放器被调用;服务端释放器稍后才被调用。因此,释放器抛出的任何异常不会传播到客户端;而是作为未捕获异常报告。请注意,RpcTarget 的释放器必须声明为 Symbol.dispose。不支持 Symbol.asyncDispose。
有时,你需要将 stub 传递给会在使用完毕后释放 stub 的函数,但你也想保留 stub 以供后续使用。为解决此问题,可以「dup」stub:
let stub = await env.SOME_SERVICE.getThing();
// Create a duplicate.
let stub2 = stub.dup();
// Call some function that will dispose the stub.
await func(stub);
// stub2 is still valid你可以将 dup() 视为与同名 Unix 系统调用 ↗类似:它创建一个指向同一目标的新句柄,必须独立关闭(释放)。
如果 stub 指向的 RpcTarget 类实例具有释放器,则只有在所有副本都被释放后才会调用释放器。但这仅适用于源自同一 stub 的副本。如果同一 RpcTarget 实例多次通过 RPC 传递,每次都会创建新 stub,这些 stub 不被视为彼此的副本。因此,释放器会在 RpcTarget 每次被发送时调用一次。
为避免这种情况,你可以在本地手动创建 stub,然后多次通过 RPC 传递该 stub。通过 RPC 传递 stub 时,stub 的所有权会转移给接收方,因此每次发送时都必须 dup():
import { RpcTarget, RpcStub } from "cloudflare:workers";
class Foo extends RpcTarget {
// ...
}
let obj = new Foo();
let stub = new RpcStub(obj);
await rpc1(stub.dup()); // sends a dup of `stub`
await rpc2(stub.dup()); // sends another dup of `stub`
stub[Symbol.dispose](); // disposes the original stub
// obj's disposer will be called when the other two stubs
// are disposed remotely.