ライフサイクル
サービスバインディングを使用してRPC経由で別のWorkerを呼び出すと、呼び出しているWorkerのメモリを使用しています。以下の例を考えてみましょう:
let user = await env.USER_SERVICE.findUser(id);サーバー側のfindUser()がRpcTargetを拡張したオブジェクトを返すと仮定すると、クライアント側のuserはそのリモートオブジェクトを指すスタブになります。
スタブがクライアント上に存在する限り、サーバー上の対応するオブジェクトはガベージコレクションされることはありません。しかし、各アイソレートには他のアイソレートを見えない独自のガベージコレクタがあります。したがって、サーバーのアイソレートがオブジェクトを収集できることを知るためには、呼び出し元のアイソレートが「スタブを破棄する」という明示的な信号を送信する必要があります。
多くの場合(以下で説明)、システムはスタブがもはや必要ないことを自動的に認識し、自動的に破棄します。しかし、最良のパフォーマンスを得るためには、コードがスタブを使用し終わったときに明示的に破棄するべきです。
リソースが適切に破棄されることを保証するために、明示的リソース管理 ↗を使用するべきです。これは、リソースを破棄できるときに明示的に信号を送ることを可能にする新しいJavaScriptの言語機能です。明示的リソース管理は、Stage 3 TC39の提案であり、V8に近日登場予定です ↗。
明示的リソース管理は、以下の言語機能を追加します:
変数がusingで宣言されている場合、その変数がスコープ内に存在しなくなると、変数の破棄者が呼び出されます。例えば:
function sendEmail(id, message) { using user = await env.USER_SERVICE.findUser(id); await user.sendEmail(message);
// user[Symbol.dispose]()はスコープの終わりに暗黙的に呼び出されます。}using宣言は、スタブを破棄するのを忘れないようにするために便利です — たとえコードが例外によって中断されても。
まだV8に実装されていないため、Workersランタイムではusingキーワードは直接利用できません。コードで使用するには、Wrangler CLIのプレリリース版を使用してWorkerを実行およびデプロイする必要があります:
npx wrangler@using-keyword-experimental devこのWranglerのバージョンは、usingをSymbol.dispose()への直接呼び出しにトランスパイルし、コードを実行またはCloudflareにデプロイします。
以下のコード:
{ 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システムは、以下のケースでスタブを自動的に破棄します:
イベントハンドラーが「完了」すると、イベントの一部として作成されたスタブは自動的に破棄されます。
例えば、受信HTTPイベントを処理するfetch()ハンドラーを考えてみましょう。ハンドラーは、イベントを処理する一環として外部RPCを行い、それらはスタブを返すことがあります。最終的なHTTPレスポンスが送信されると、ハンドラーは「完了」し、すべてのスタブは即座に破棄されます。
より正確には、イベントには「実行コンテキスト」があり、これはハンドラーが最初に呼び出されたときに始まり、HTTPレスポンスが送信されると終了します。クライアントがレスポンスを受け取る前に切断した場合、実行コンテキストは早期に終了することもあります。また、ctx.waitUntil()を呼び出すことで、通常の終了ポイントを超えて延長することもできます。
例えば、以下のWorkerはusing宣言を使用していませんが、fetch()ハンドラーがレスポンスを返すとスタブは破棄されます:
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(`こんにちは、${profile.name}さん!`); },};RPCを介して呼び出されたWorkerにも実行コンテキストがあります。コンテキストは、WorkerEntrypointのRPCメソッドが呼び出されたときに始まります。RPCのパラメータや結果にスタブが渡されない場合、コンテキストはRPCが返されると終了します(イベントは「完了」)。しかし、スタブが渡された場合、実行コンテキストはすべてのスタブが破棄されるまで暗黙的に延長されます(およびそれらを介して行われたすべての呼び出しが返されるまで)。HTTPと同様に、クライアントが切断された場合、サーバーの実行コンテキストは即座にキャンセルされます。別のWorkerであるクライアントは、自身の実行コンテキストが終了したときに切断されたと見なされます。再度、コンテキストはctx.waitUntil()で延長できます。
RPCのパラメータとしてスタブが受け取られた場合、それらのスタブは呼び出しが返されると自動的に破棄されます。それらのスタブをそれ以上保持したい場合は、dup()メソッドを呼び出す必要があります。
RPCが任意の種類のオブジェクトを返すと、そのオブジェクトにはシステムによって破棄者が追加されます。それを破棄すると、呼び出しによって返されたすべてのスタブが破棄されます。たとえば、RPCが4つのスタブの配列を返す場合、配列自体にはすべての4つのスタブを破棄する破棄者が追加されます。RPCによって返される値が破棄者を持たないのは、数値や文字列などのプリミティブ値である場合のみです。これらのタイプには破棄者を追加できませんが、これらのタイプはスタブを含むことができないため、この場合に破棄者が必要ないのです。
これは、RPCの結果をほぼ常にusing宣言に格納するべきであることを意味します:
using result = stub.foo();このようにすると、結果にスタブが含まれている場合、それらは破棄されます。RPCがスタブを返すことを期待しない場合でも、何らかのオブジェクトを返す場合は、それをusing宣言に格納するのが良いアイデアです。このようにすると、将来的にRPCがスタブを返すように拡張された場合でも、コードは準備が整っています。
using宣言のスコープを超えて返されたスタブを保持したい場合は、スコープの終わりの前にスタブでdup()を呼び出すことができます。(後で重複を明示的に破棄することを忘れないでください。)
RpcTargetを拡張するクラスは、オプションで破棄者を実装できます:
class Foo extends RpcTarget { [Symbol.dispose]() { // ... }}RpcTargetの破棄者は、最後のスタブが破棄された後に実行されます。スタブの破棄者へのクライアント側の呼び出しは、サーバー側の破棄者が呼び出されるのを待ちません。サーバーの破棄者は後で呼び出されます。このため、破棄者によってスローされた例外はクライアントに伝播しません。代わりに、それらは未捕捉の例外として報告されます。RpcTargetの破棄者はSymbol.disposeとして宣言する必要があります。Symbol.asyncDisposeはサポートされていません。
時には、スタブを破棄する関数にスタブを渡す必要がありますが、後で使用するためにスタブを保持したい場合があります。この問題を解決するために、スタブを「複製」することができます:
let stub = await env.SOME_SERVICE.getThing();
// 複製を作成します。let stub2 = stub.dup();
// スタブを破棄する関数を呼び出します。await func(stub);
// stub2はまだ有効ですdup()は、Unixの同名のシステムコール ↗のように考えることができます:それは同じターゲットを指す新しいハンドルを作成し、独立して閉じる(破棄する)必要があります。
スタブが指すRpcTargetクラスのインスタンスに破棄者がある場合、破棄者はすべての複製が破棄されるまで呼び出されません。ただし、これは同じスタブから派生した複製にのみ適用されます。同じRpcTargetのインスタンスがRPCを介して複数回渡されると、毎回新しいスタブが作成され、これらは互いに複製とは見なされません。したがって、破棄者はRpcTargetが送信された回数だけ呼び出されます。
この状況を避けるために、ローカルでスタブを手動で作成し、そのスタブをRPCを介して複数回渡すことができます。スタブをRPCを介して渡すと、スタブの所有権は受信者に移転するため、送信するたびにdup()を作成する必要があります:
import { RpcTarget, RpcStub } from "cloudflare:workers";
class Foo extends RpcTarget { // ...}
let obj = new Foo();let stub = new RpcStub(obj);await rpc1(stub.dup()); // `stub`の複製を送信await rpc2(stub.dup()); // `stub`の別の複製を送信stub[Symbol.dispose](); // 元のスタブを破棄
// objの破棄者は、他の2つのスタブがリモートで破棄されるときに呼び出されます。