Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

ライフサイクル

最終更新 Markdown で表示Agent セットアップ

寿命、メモリ、リソース管理

Service binding 経由で RPC によって別の Worker を呼ぶと、呼び出し先の Worker のメモリを使います。次の例を考えてください。

let user = await env.USER_SERVICE.findUser(id);

サーバー側の findUser()RpcTarget を拡張したオブジェクトを返すとすると、クライアント側の user は、そのリモートオブジェクトを指す stub になります。

クライアント側に stub がある限り、サーバー側の対応するオブジェクトはガベージコレクションできません。ただし、各 isolate には独自のガベージコレクターがあり、他の isolate の中は見えません。そのためサーバー側 isolate が「このオブジェクトは回収してよい」と知るには、呼び出し元 isolate が明示的な合図を送る必要があります。これを stub の dispose と呼びます。

多くの場合(後述)、システムは stub が不要になったことを自動で検知し、自動で dispose します。ただし性能のため、使い終わった stub はコード側で明示的に dispose してください。

明示的なリソース管理

リソースを確実に解放するには、Explicit Resource Management を使います。これは、リソースを解放できるタイミングを明示するための新しい JavaScript 言語機能です。Explicit Resource Management は Stage 3 の TC39 提案で、まもなく V8 に入ります

Explicit Resource Management は、次の言語機能を追加します。

変数を using で宣言すると、その変数がスコープを外れたときに disposer が呼ばれます。例:

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 の dispose 忘れを防げます。

Worker で using 宣言を使う

Wrangler v4 以降は、using キーワードをネイティブにサポートします。それより前の Wrangler を使っている場合は、リソースを手動で dispose する必要があります。

次のコードは、

{
	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]();
	}
}

自動 dispose と実行コンテキスト

RPC システムは、次の場合に stub を自動で dispose します。

イベントハンドラー / 実行コンテキストの終了

イベントハンドラーが「完了」すると、そのイベントの一部として作られた stub は自動で dispose されます。

たとえば、受信 HTTP イベントを扱う fetch() ハンドラー を考えます。ハンドラーはイベント処理の一環で外部 RPC を呼び、それらが stub を返すことがあります。最終的な HTTP レスポンスを送るとハンドラーは「完了」し、すべての stub はすぐに dispose されます。

より正確には、イベントには「実行コンテキスト」があります。ハンドラーが最初に呼ばれたときに始まり、HTTP レスポンスを送ったときに終わります。クライアントがレスポンスを受け取る前に切断した場合、実行コンテキストは早く終わることもあります。ctx.waitUntil() を呼ぶと、通常の終了時点を超えて延長できます。

たとえば、次の Worker は using 宣言を使いませんが、fetch() ハンドラーがレスポンスを返すと stub は dispose されます。

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 がすべて dispose されるまで(およびそれら経由の呼び出しがすべて返るまで)暗黙に延長されます。HTTP と同様、クライアントが切断すると、stub が残っていてもサーバーの実行コンテキストはすぐにキャンセルされます。クライアント自身が別の Worker である場合、その Worker の実行コンテキストが終わった時点で切断とみなされます。こちらも ctx.waitUntil() でコンテキストを延長できます。

RPC 呼び出しの引数として受け取った stub

RPC の引数として受け取った stub は、その呼び出しが返ると自動で dispose されます。それより長く残したい場合は、dup() メソッドを呼ぶ必要があります。

RPC オブジェクトを dispose すると、その一部である stub も dispose される

RPC が何らかのオブジェクトを返すと、システムがそのオブジェクトに disposer を付けます。それを dispose すると、その呼び出しが返したすべての stub が dispose されます。たとえば RPC が 4 つの stub の配列を返すと、配列自体に 4 つすべてを dispose する disposer が付きます。RPC の戻り値に disposer が付かないのは、数値や文字列などのプリミティブのときだけです。これらの型には disposer を付けられませんが、stub を含められないため、この場合は disposer は不要です。

つまり、RPC の結果はほぼつねに using 宣言に入れるべきです。

using result = stub.foo();

こうすると、結果に stub が含まれていても dispose されます。RPC が stub を返すと思わなくても、何らかのオブジェクトを返すなら using 宣言に入れるのがよいです。将来 RPC が stub を返すように拡張されても、コード側の準備ができています。

using 宣言のスコープを超えて返ってきた stub を残したい場合は、スコープが終わる前に stub に対して dup() を呼びます。(あとで複製を明示的に dispose してください。)

disposer と RpcTarget クラス

RpcTarget を拡張するクラスは、任意で disposer を実装できます。

class Foo extends RpcTarget {
	[Symbol.dispose]() {
		// ...
	}
}

RpcTarget の disposer は、最後の stub が dispose されたあとに走ります。クライアント側の stub disposer 呼び出しは、サーバー側 disposer の完了を待ちません。サーバーの disposer はあとから呼ばれます。そのため、disposer が投げた例外はクライアントに伝播しません。未捕捉例外として報告されます。RpcTarget の disposer は Symbol.dispose として宣言する必要があります。Symbol.asyncDispose は未対応です。

dup() メソッド

関数に stub を渡し、関数側が使い終わったら dispose するが、自分でもあとからその stub を使いたい場合があります。このときは stub を dup します。

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 システムコール と同様に考えられます。同じ対象を指す新しいハンドルを作り、それぞれ独立して close(dispose)する必要があります。

stub が指す RpcTarget クラス のインスタンスに disposer がある場合、disposer はすべての複製が dispose されたときにだけ呼ばれます。ただし、これは同じ stub から作った複製に限ります。同じ RpcTarget インスタンスを RPC で複数回渡すと、そのたびに新しい stub が作られ、それらは互いの複製とはみなされません。そのため disposer は、その RpcTarget を送った回数だけ呼ばれます。

この状況を避けるには、手元で stub を手動作成し、その stub を RPC で複数回渡します。stub を 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()); // 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.

役に立ちましたか?