Skip to content

Commit

Permalink
[do not merge] Revamp Service Bindings docs for RPC
Browse files Browse the repository at this point in the history
- Consolidates Service binding documentation to be within the Runtime APIs section of the docs. Currently docs for configuring a Service binding, and docs for how to write code around Service bindings are in separate sections of the docs, which makes getting started hard, requires jumping back and forth between pages. Relevant content from [the configuration section](https://github.com/cloudflare/cloudflare-docs/blob/production/content/workers/configuration/bindings/about-service-bindings.md) has been moved here, and will be removed.
- Explains what Service bindings are and what to use them for.
- Provides separate code examples RPC and HTTP modes of working with Service bindings.

refs cloudflare/workerd#1658, cloudflare/workerd#1692, cloudflare/workerd#1729, cloudflare/workerd#1756, cloudflare/workerd#1757
  • Loading branch information
irvinebroque committed Mar 30, 2024
1 parent 2e55983 commit 0c0c356
Showing 1 changed file with 173 additions and 35 deletions.
208 changes: 173 additions & 35 deletions content/workers/runtime-apis/bindings/service-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,198 @@ meta:

## About Service bindings

[Service bindings](/workers/configuration/bindings/about-service-bindings/) are an API that facilitate Worker-to-Worker communication via explicit bindings defined in your configuration. A Service binding allows you to send HTTP requests to another Worker without those requests going over the Internet. The request immediately invokes the downstream Worker, reducing latency as compared to a request to a third-party service. You can invoke other Workers directly from your code.
[Service bindings](/workers/configuration/bindings/about-service-bindings/) facilitate Worker-to-Worker communication. A Service binding allows Worker A to call a method on Worker B, or to forward a request from Worker A to Worker B.

To use Service bindings in your code, you must first [create a Service binding from one Worker to another](/workers/configuration/bindings/about-service-bindings/).
Service bindings provide the separation of concerns that microservice or service-oriented architectures provide, without configuration pain, performance overhead or knowledge of RPC protocols.

- *Service bindings are fast.* When you use Service Bindings, communication between two Workers stays within Cloudflare. When one Worker invokes another, there is no network delay and the request is executed immediately.
- *Service bindings are not just HTTP.* Worker A can expose methods that can be directly called by Worker B. Communicating between services only requires writing JavaScript methods and classes.

Service bindings are commonly used to:

- *Provide a shared internal service to multiple Workers.* For example, you can deploy an authentication service as its own Worker, and then have any number of separate Workers communicate with it via Service bindings.
- *Isolate services from the public Internet.* You can deploy a Worker that is not reachable via the public Internet, and can only be reached via an explicit Service binding that another Worker declares.
- *Allow teams to deploy code independently.* Team A can deploy their Worker on their own release schedule, and Team B can deploy their Worker separately.

## Configuration

```toml
---
filename: wrangler.toml
---
services = [
{ binding = "<BINDING_NAME>", service = "<WORKER_NAME>" }
]
```

* `binding`: The name of the key you want to expose on the `env` object.
* `service`: The name of the target Worker you would like to communicate with. This Worker must be on your Cloudflare account.

## Interfaces

Worker A that declares a Service binding to Worker B can call Worker B in two different ways:

- *RPC* By calling any public method on the class that Worker B exports (`await env.BINDING_NAME.myMethod(arg1)`)
- *HTTP* By calling the `fetch()` method on the binding (`env.BINDING_NAME.fetch(request)`)

### RPC (`WorkerEntrypoint`)

### Interface
If you write your Worker as a JavaScript class that extends the built-in `WorkerEntryptoint` class, public methods on the class you export are exposed via Service bindings and can be called.

For example, the following Worker implements the public method `add(a, b)`:

```toml
---
filename: wrangler.toml
---
name = "worker_b"
main = "./src/workerB.js"
```

{{<tabs labels="js | ts">}}
{{<tab label="js" default="true">}}
```js
---
filename: workerB.js
---
import { WorkerEntrypoint } from "cloudflare:workers";

export class WorkerB extends WorkerEntrypoint {
async add(a, b) { return a + b; }
}
```

Which the following Worker declares a binding to:

```toml
---
filename: wrangler.toml
---
name = "worker_a"
main = "./src/workerA.js"
services = [
{ binding = "WORKER_B", service = "worker_b" }
]
```

And then calls as an async method:

```js
---
filename: workerA.js
---
export default {
async fetch(req, env) {
return await env.BINDING.fetch(req);
},
};
async fetch(request, env) {
const result = await env.WORKER_B.add(1, 2);
return new Response(result);
}
}
```
{{</tab>}}
{{<tab label="ts">}}
```ts
interface Environment {
BINDING: Fetcher;

This is a remote procedure call (RPC) protocol, without having to think about the protocol. The client, in this case Worker A, calls Worker B and tells it to execute a specific procedure using specific arguments that the client provides. But with Service bindings, there is no need to learn, implement or interact with a specific RPC protocol. It's just JavaScript.

#### Remote objects

Objects that Worker A receives back from Worker B can expose methods. These methods can be called from within Worker A. Consider the following example:

```js
---
filename: workerB.js
---
import { WorkerEntrypoint } from "cloudflare:workers";

export class WorkerB extends WorkerEntrypoint {
async foo() {
return {
bar: () => Math.random(),
}
}
```
export default <ExportedHandler<Environment>> {
async fetch(req, env) {
return await env.BINDING.fetch(req);
},
};
And then calls as an async method:
```js
---
filename: workerA.js
---
export default {
async fetch(request, env) {
const foo = await env.WORKER_B.foo();
const bar = await foo.bar();
return new Response(bar);
}
}
```
{{</tab>}}
{{</tabs>}}
Service bindings use the standard [Fetch](/workers/runtime-apis/fetch/) API. A Service binding will invoke the [`fetch()` handler](/workers/runtime-apis/handlers/fetch/) of a target Worker. To access a target Worker from a parent Worker, you must first configure the target Worker with a binding for that target Worker. The binding definition includes a variable name on which the `fetch()` method will be accessible. The `fetch()` method has the exact same signature as the [global `fetch`](/workers/runtime-apis/fetch/). However, instead of sending an HTTP request to the Internet, the request is always sent to the Worker to which the Service binding points.
#### Durable Objects
### Shared resources
You can also expose methods on [Durable Objects](/durable-objects/) that can be called remotely on any of your Workers that declare a binding to the Durable Object namespace. Refer to <TODO> docs for more.
Workers connected to one another via Service bindings share the CPU resources of the top-level request. A single thread is allocated and reused amongst these Workers. This means no idle resources are wasted while work is performed across various Workers.
#### Error handling & stacktraces
### Lifecycle
<TODO>
Lifecycle is tied to the top-level Worker. If a child Worker is still processing, and the parent Worker does not await the completion of a child Worker, the child Worker will be terminated and cleaned up. It is important to use `await` and `event.waitUntil` to manage the lifecycle of any child processes invoked via Service bindings.
### HTTP (`fetch()`)
### Context
Worker A that declares a Service binding to Worker B can forward a [`Request`](/workers/runtime-apis/request/) object to Worker B, by calling the `fetch()` method that is exposed on the binding object.
Service bindings live on the environment context. This means Service bindings can be used from within a [Durable Object](/durable-objects/), as long as the environment context remains intact.
For example, consider the following Worker that implements a [`fetch()` handler](/workers/runtime-apis/handlers/fetch/):
### Limits
```toml
---
filename: wrangler.toml
---
name = "worker_b"
main = "./src/workerB.js"
```
Service bindings have the following limits:
```js
---
filename: workerB.js
---
export default {
async fetch(request, env, ctx) {
return new Response("Hello World!");
}
}
```
* Each request to a Worker via Service bindings count toward your [subrequest limit](/workers/platform/limits/#subrequests).
* Nested calls to child Workers increase the depth of your Worker Pipeline. Maximum Pipeline depth is 32, including the first Worker. Subsequent calls will throw an exception.
* [Simultaneous open connection limits](/workers/platform/limits/#simultaneous-open-connections) are Pipeline-wide, meaning subrequests from multiple different Workers incur a global concurrent subrequest limit. However, a `fetch` call on a Service binding does not count as an open connection.
The following Worker declares a binding to the Worker above:
```toml
---
filename: wrangler.toml
---
name = "worker_a"
main = "./src/workerA.js"
services = [
{ binding = "WORKER_B", service = "worker_b" }
]
```
And then forwards a request to:
```js
---
filename: workerA.js
---
export default {
async fetch(request, env) {
return await env.BINDING_NAME.fetch(request);
},
};
```
## Lifecycle
The Service bindings API is asynchronous — you must `await` any method you call. If Worker A invokes Worker B via a Service binding, and Worker A does not await the completion of Worker B, Worker B will be terminated early.
## Local development
Local development is supported for Service bindings. For each Worker, open a new terminal and use [`wrangler dev`](/workers/wrangler/commands/#dev) in the relevant directory or use the `SCRIPT` option to specify the relevant Worker's entrypoint.
## Limits
Service bindings have the following limits:
## Related resources
- [About Service bindings](/workers/configuration/bindings/about-service-bindings/)
* Each request to a Worker via a Service binding counts toward your [subrequest limit](/workers/platform/limits/#subrequests).
* A single request has a maximum of 32 Worker invocations, and each call to a Service binding counts towards this limit. Subsequent calls will throw an exception.
* Calling a service binding does not count towards [simultaneous open connection limits](/workers/platform/limits/#simultaneous-open-connections)

0 comments on commit 0c0c356

Please sign in to comment.