コンテンツにスキップ

リモートプロシージャコール (RPC)

Workers は、組み込みの JavaScript ネイティブの RPC (リモートプロシージャコール) システムを提供しており、これにより次のことが可能になります:

  • 同じ Cloudflare アカウント内の他の Workers から呼び出すことができる、Worker 上の公開メソッドを定義することができます。Service Bindings を介して。
  • 同じ Cloudflare アカウント内の他の Workers から呼び出すことができる Durable Objects 上の公開メソッドを定義することができます。

RPC システムは、同じ Worker 内の JavaScript 関数を呼び出すのとできるだけ似た感覚で設計されています。ほとんどの場合、すべてが単一の Worker 内にあるかのようにコードを書くことができます。

For example, if Worker B implements the public method add(a, b):

name = "worker_b"
main = "./src/workerB.js"
import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint {
async fetch() { return new Response("Hello from Worker B"); }
add(a, b) { return a + b; }
}

Worker A can declare a binding to Worker B:

name = "worker_a"
main = "./src/workerA.js"
services = [
{ binding = "WORKER_B", service = "worker_b" }
]

Making it possible for Worker A to call the add() method from Worker B:

export default {
async fetch(request, env) {
const result = await env.WORKER_B.add(1, 2);
return new Response(result);
}
}

この場合、クライアントである Worker A は Worker B を呼び出し、クライアントが提供する特定の引数を使用して特定の手続きを実行するように指示します。これは標準の JavaScript クラスを使用して実現されます。

すべての呼び出しは非同期です

呼び出しているメソッドがサーバー側で非同期として宣言されているかどうかにかかわらず、クライアント側ではそのように動作します。結果を await する必要があります。

RPC 呼び出しは実際には Promise を返さないことに注意してくださいが、Promise のように振る舞う型を返します。この型は「カスタム thenable」であり、then() メソッドを実装しています。JavaScript は任意の「thenable」型を待機することをサポートしているため、ほとんどの場合、戻り値を Promise のように扱うことができます。

(この型が実際には Promise ではない理由は後で見ていきます。)

構造化クローン可能な型とその他

ほぼすべての型が 構造化クローン可能 であり、RPC メソッドのパラメータまたは戻り値として使用できます。これには、オブジェクト、配列、文字列、数値など、JavaScript の基本的な「値」型が含まれます。

構造化クローンの例外として、アプリケーション定義のクラス(またはカスタムプロトタイプを持つオブジェクト)は、以下に説明する場合を除いて、RPC を介して渡すことはできません。

RPC システムは、構造化クローン可能ではないいくつかの型もサポートしています。これには以下が含まれます:

  • 送信者にコールバックするスタブに置き換えられる関数。
  • RpcTarget を拡張するアプリケーション定義のクラスも同様にスタブに置き換えられます。
  • 自動ストリーミングフロー制御を持つ ReadableStreamWriteableStream
  • HTTP メッセージを便利に表現するための RequestResponse
  • 他の Worker から受信したスタブであっても、RPC スタブ自体。

関数

RPC を介して関数を送信することができます。そうすると、関数は「スタブ」に置き換えられます。受信者はスタブを関数のように呼び出すことができますが、そうすると関数が発生した場所に新しい RPC が戻ります。

RPC メソッドからの戻り関数

Consider the following two Workers, connected via a Service Binding. The counter service provides the RPC method newCounter(), which returns a function:

name = "counter-service"
main = "./src/counterService.js"
import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint {
async fetch() { return new Response("Hello from counter-service"); }
async newCounter() {
let value = 0;
return (increment = 0) => {
value += increment;
return value;
}
}
}

This function can then be called by the client Worker:

name = "client_worker"
main = "./src/clientWorker.js"
services = [
{ binding = "COUNTER_SERVICE", service = "counter-service" }
]
export default {
async fetch(request, env) {
using f = await env.COUNTER_SERVICE.newCounter();
await f(2); // returns 2
await f(1); // returns 3
const count = await f(-5); // returns -2
return new Response(count);
}
}

How is this possible? The system is not serializing the function itself. When the function returned by CounterService is called, it runs within CounterService — even if it is called by another Worker.

Under the hood, the caller is not really calling the function itself directly, but calling what is called a “stub”. A “stub” is a Proxy object that allows the client to call the remote service as if it were local, running in the same Worker. Behind the scenes, it calls back to the Worker that implements CounterService and asks it to execute the function closure that had been returned earlier.

RPC メソッドのパラメータとして関数を送信する

RPC のパラメータとして関数を送信することもできます。これにより、「サーバー」が「クライアント」にコールバックでき、関係の方向が逆転します。

このため、RPC について話すときに「クライアント」と「サーバー」という言葉はあいまいになることがあります。「サーバー」は Durable Object または WorkerEntrypoint であり、「クライアント」はバインディングを介してサーバーを呼び出した Worker です。しかし、RPC は両者の間で双方向に流れることができます。個々の RPC について話すときは、「呼び出し元」と「呼び出し先」という言葉を使用することをお勧めします。

クラスインスタンス

RPC メソッドのパラメータまたは戻り値として定義したクラスのインスタンスを使用するには、組み込みの RpcTarget クラスを拡張する必要があります。

次の例を考えてみましょう:

name = "counter"
main = "./src/counter.js"
import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers";
class Counter extends RpcTarget {
#value = 0;
increment(amount) {
this.#value += amount;
return this.#value;
}
get value() {
return this.#value;
}
}
export class CounterService extends WorkerEntrypoint {
async newCounter() {
return new Counter();
}
}
export default {
fetch() {
return new Response("ok")
}
}

メソッド increment はクライアントによって直接呼び出すことができ、公開プロパティ value も同様です:

name = "client-worker"
main = "./src/clientWorker.js"
services = [
{ binding = "COUNTER_SERVICE", service = "counter", entrypoint = "CounterService" }
]
export default {
async fetch(request, env) {
using counter = await env.COUNTER_SERVICE.newCounter();
await counter.increment(2); // returns 2
await counter.increment(1); // returns 3
await counter.increment(-5); // returns -2
const count = await counter.value; // returns -2
return new Response(count);
}
}

RpcTarget を拡張するクラスは、関数のように動作します:オブジェクト自体はシリアライズされず、代わりにスタブに置き換えられます。この場合、スタブ自体は呼び出し可能ではありませんが、そのメソッドは呼び出し可能です。スタブ上の任意のメソッドを呼び出すことは、実際には元のオブジェクトが作成された場所に RPC を戻します。

上記のように、クラスのプロパティにもアクセスできます。プロパティは引数を取らない RPC メソッドのように振る舞います — プロパティを待機して、その現在の値を非同期に取得します。プロパティにアクセスする際に await を使用しないと、プロパティは取得されません。

Promise パイプライン

RPC メソッドを呼び出してオブジェクトを取得したとき、通常はすぐにそのオブジェクトのメソッドを呼び出すことが一般的です:

// 2回の往復。
using counter = await env.COUNTER_SERVICE.getCounter();
await counter.increment();

しかし、呼び出している Worker サービスがネットワークを越えて遠くにある場合、スマートプレースメントDurable Objects の場合を考えてみてください。上記のコードは、getCounter() を呼び出す際に1回、.increment() を呼び出す際にもう1回、合計2回の往復を行います。これを避けたいと思います。

ほとんどの RPC システムでは、この問題を回避する唯一の方法は、2つの呼び出しを単一の「バッチ」呼び出しに統合することです。たとえば、getCounterAndIncrement() と呼ばれるかもしれません。しかし、これではインターフェースが悪化します。ローカルインターフェースをこのように設計することはありません。

Workers RPC では、異なるアプローチを許可します:最初の await を単に省略できます:

// 1回の往復だけ! `await` が欠落していることに注意。
using promiseForCounter = env.COUNTER_SERVICE.getCounter();
await promiseForCounter.increment();

このコードでは、getCounter() がカウンターの Promise を返します。通常、Promise で行うことは await することだけです。しかし、Workers RPC の Promise は特別です:それらは、Promise の将来の結果に対して推測的な呼び出しを開始することも許可します。これらの呼び出しは、初期呼び出しが完了するのを待たずにサーバーに即座に送信されます。したがって、複数の連鎖した呼び出しを単一の往復で完了させることができます。

これは、RPC メソッドによって返されたオブジェクトのプロパティを呼び出すときにも機能します。たとえば:

import { WorkerEntrypoint } from "cloudflare:workers";
export class MyService extends WorkerEntrypoint {
async foo() {
return {
bar: {
baz: () => "qux"
}
}
}
}
export default {
async fetch(request, env) {
using foo = env.MY_SERVICE.foo();
let baz = await foo.bar.baz();
return new Response(baz);
}
}

最初の RPC が例外をスローした場合、パイプラインされた呼び出しも同じ例外で失敗します。

ReadableStream、WriteableStream、Request および Response

RPC メソッドを使用して ReadableStreamWriteableStreamRequest、および Response を送受信できます。この際、ボディ内のバイトは適切なフロー制御で自動的にストリーミングされます。

サポートされているのは、バイト指向ストリームtype: "bytes" の基盤バイトソースを持つストリーム)のみです。

すべての場合において、ストリームの所有権は受信者に移転されます。送信者は送信後にストリームを読み書きできなくなります。送信者が自分のコピーを保持したい場合は、ReadableStreamtee() メソッド または Request または Response の [clone() メソッド) を使用できます。これを行うと、システムがバイトをバッファリングし、フロー制御の利点を失う可能性があることに注意してください。

RPC スタブの転送

1 つの Worker から RPC を介して受信したスタブは、別の Worker に RPC を介して転送できます。

using counter = env.COUNTER_SERVICE.getCounter();
await env.ANOTHER_SERVICE.useCounter(counter);

ここでは、3 つの異なる Worker が関与しています:

  1. 呼び出し Worker(これを「紹介者」と呼びます)
  2. COUNTER_SERVICE
  3. ANOTHER_SERVICE

ANOTHER_SERVICE が渡された counter のメソッドを呼び出すと、この呼び出しは自動的に紹介者を介してプロキシされ、COUNTER_SERVICE によって実装された RpcTarget クラスに送信されます。

このようにして、紹介者 Worker は、直接接続する能力がない 2 つの Worker を接続できます。

現在、このプロキシは Worker の実行コンテキストの終了までしか持続しません。プロキシ接続は後で使用するために持続できません。

詳細

制限事項

  • スマートプレースメント は、RPC 呼び出しを行う際に現在無視されています。Worker A にスマートプレースメントが有効になっている場合、Worker B がそれに Service Binding を宣言すると、Worker B が RPC を介して Worker A を呼び出すと、Worker A はローカルで、同じマシン上で実行されます。