Skip to content

Commit

Permalink
feat: implement support for service bindings
Browse files Browse the repository at this point in the history
This adds experimental support for service bindings, aka worker-to-worker bindings. It's lets you "call" a worker from another worker, without incurring any network cost, and (ideally) with much less latency. To use it, define a `[services]` field in `wrangler.toml`, which is a map of bindings to worker names (and environment). Let's say you already have a worker named "my-worker" deployed. In another worker's configuration, you can create a service binding to it like so:

```toml
[[services]]
binding = "MYWORKER"
service = "my-worker"
environment = "production" # optional, defaults to the worker's `default_environment` for now
```

And in your worker, you can call it like so:

```js
export default {
  fetch(req, env, ctx) {
    return env.MYWORKER.fetch(new Request("./some-path"));
  },
};
```

Fixes #1026
  • Loading branch information
Daniel Rivas authored and threepointone committed May 17, 2022
1 parent ffce3e3 commit 66f07c5
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 49 deletions.
26 changes: 26 additions & 0 deletions .changeset/hot-insects-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"wrangler": patch
---

feat: implement support for service bindings

This adds experimental support for service bindings, aka worker-to-worker bindings. It's lets you "call" a worker from another worker, without incurring any network cost, and (ideally) with much less latency. To use it, define a `[services]` field in `wrangler.toml`, which is a map of bindings to worker names (and environment). Let's say you already have a worker named "my-worker" deployed. In another worker's configuration, you can create a service binding to it like so:

```toml
[[services]]
binding = "MYWORKER"
service = "my-worker"
environment = "production" # optional, defaults to the worker's `default_environment` for now
```

And in your worker, you can call it like so:

```js
export default {
fetch(req, env, ctx) {
return env.MYWORKER.fetch(new Request("./some-path"));
},
};
```

Fixes https://github.com/cloudflare/wrangler2/issues/1026
178 changes: 159 additions & 19 deletions packages/wrangler/src/__tests__/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe("normalizeAndValidateConfig()", () => {
migrations: [],
name: undefined,
r2_buckets: [],
services: [],
route: undefined,
routes: undefined,
rules: [],
Expand Down Expand Up @@ -644,6 +645,13 @@ describe("normalizeAndValidateConfig()", () => {
preview_bucket_name: "R2_PREVIEW_2",
},
],
services: [
{
binding: "SERVICE_BINDING_1",
service: "SERVICE_BINDING_1",
environment: "SERVICE_BINDING_ENVIRONMENT_1",
},
],
unsafe: {
bindings: [
{ name: "UNSAFE_BINDING_1", type: "UNSAFE_TYPE_1" },
Expand All @@ -667,9 +675,10 @@ describe("normalizeAndValidateConfig()", () => {
);
expect(diagnostics.hasErrors()).toBe(false);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"unsafe\\" fields are experimental and may change or break at any time."
`);
"Processing wrangler configuration:
- \\"unsafe\\" fields are experimental and may change or break at any time.
- \\"services\\" fields are experimental and may change or break at any time."
`);
});

it("should error on invalid environment values", () => {
Expand Down Expand Up @@ -1395,6 +1404,151 @@ describe("normalizeAndValidateConfig()", () => {
});
});

describe("services field", () => {
it("should error if services is an object", () => {
const { config, diagnostics } = normalizeAndValidateConfig(
{ services: {} } as unknown as RawConfig,
undefined,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({ services: expect.anything })
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field \\"services\\" should be an array but got {}."
`);
});

it("should error if services is a string", () => {
const { config, diagnostics } = normalizeAndValidateConfig(
{ services: "BAD" } as unknown as RawConfig,
undefined,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({ services: expect.anything })
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field \\"services\\" should be an array but got \\"BAD\\"."
`);
});

it("should error if services is a number", () => {
const { config, diagnostics } = normalizeAndValidateConfig(
{ services: 999 } as unknown as RawConfig,
undefined,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({ services: expect.anything })
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field \\"services\\" should be an array but got 999."
`);
});

it("should error if services is null", () => {
const { config, diagnostics } = normalizeAndValidateConfig(
{ services: null } as unknown as RawConfig,
undefined,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({ services: expect.anything })
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field \\"services\\" should be an array but got null."
`);
});

it("should error if services bindings are not valid", () => {
const { config, diagnostics } = normalizeAndValidateConfig(
{
services: [
{},
{ binding: "SERVICE_BINDING_1" },
{ binding: 123, service: 456 },
{ binding: 123, service: 456, environment: 789 },
{ binding: "SERVICE_BINDING_1", service: 456, environment: 789 },
{
binding: 123,
service: "SERVICE_BINDING_SERVICE_1",
environment: 789,
},
{
binding: 123,
service: 456,
environment: "SERVICE_BINDING_ENVIRONMENT_1",
},
],
} as unknown as RawConfig,
undefined,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({
services: { bindings: expect.anything },
})
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services[0]\\" bindings should have a string \\"binding\\" field but got {}.
- \\"services[0]\\" bindings should have a string \\"service\\" field but got {}.
- \\"services[1]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\"}.
- \\"services[2]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456}.
- \\"services[2]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456}.
- \\"services[3]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}.
- \\"services[3]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}.
- \\"services[3]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}.
- \\"services[4]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":456,\\"environment\\":789}.
- \\"services[4]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":456,\\"environment\\":789}.
- \\"services[5]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":789}.
- \\"services[5]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":123,\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":789}.
- \\"services[6]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\"}.
- \\"services[6]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\"}."
`);
});
});

describe("unsafe field", () => {
it("should error if unsafe is an array", () => {
const { config, diagnostics } = normalizeAndValidateConfig(
Expand Down Expand Up @@ -1659,14 +1813,7 @@ describe("normalizeAndValidateConfig()", () => {
- Deprecation: \\"zone_id\\":
This is unnecessary since we can deduce this from routes directly.
- Deprecation: \\"experimental_services\\":
The \\"experimental_services\\" field is no longer supported. Instead, use [[unsafe.bindings]] to enable experimental features. Add this to your wrangler.toml:
\`\`\`
[[unsafe.bindings]]
name = \\"mock-name\\"
type = \\"service\\"
service = \\"SERVICE\\"
environment = \\"ENV\\"
\`\`\`"
The \\"experimental_services\\" field is no longer supported. Simply rename the [experimental_services] field to [services]."
`);
});
});
Expand Down Expand Up @@ -2906,14 +3053,7 @@ describe("normalizeAndValidateConfig()", () => {
- Deprecation: \\"zone_id\\":
This is unnecessary since we can deduce this from routes directly.
- Deprecation: \\"experimental_services\\":
The \\"experimental_services\\" field is no longer supported. Instead, use [[unsafe.bindings]] to enable experimental features. Add this to your wrangler.toml:
\`\`\`
[[unsafe.bindings]]
name = \\"mock-name\\"
type = \\"service\\"
service = \\"SERVICE\\"
environment = \\"ENV\\"
\`\`\`"
The \\"experimental_services\\" field is no longer supported. Simply rename the [experimental_services] field to [services]."
`);
});
});
Expand Down
28 changes: 28 additions & 0 deletions packages/wrangler/src/__tests__/dev.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,34 @@ describe("wrangler dev", () => {
`);
});
});

describe("service bindings", () => {
it("should warn when using service bindings", async () => {
writeWranglerToml({
services: [
{ binding: "WorkerA", service: "A" },
{ binding: "WorkerB", service: "B" },
],
});
fs.writeFileSync("index.js", `export default {};`);
await runWrangler("dev index.js");
expect(std).toMatchInlineSnapshot(`
Object {
"debug": "",
"err": "",
"out": "",
"warn": "▲ [WARNING] Processing wrangler.toml configuration:
- \\"services\\" fields are experimental and may change or break at any time.
▲ [WARNING] This worker is bound to live services: WorkerA (A), WorkerB (B)
",
}
`);
});
});
});

function mockGetZones(domain: string, zones: { id: string }[] = []) {
Expand Down
53 changes: 47 additions & 6 deletions packages/wrangler/src/__tests__/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3165,6 +3165,12 @@ addEventListener('fetch', event => {});`
mockUploadWorkerRequest({
expectedType: "sw",
expectedBindings: [
{ json: 123, name: "ENV_VAR_ONE", type: "json" },
{
name: "ENV_VAR_TWO",
text: "Hello, I'm an environment variable",
type: "plain_text",
},
{
name: "KV_NAMESPACE_ONE",
namespace_id: "kv-ns-one-id",
Expand Down Expand Up @@ -3198,12 +3204,6 @@ addEventListener('fetch', event => {});`
name: "R2_BUCKET_TWO",
type: "r2_bucket",
},
{ json: 123, name: "ENV_VAR_ONE", type: "json" },
{
name: "ENV_VAR_TWO",
text: "Hello, I'm an environment variable",
type: "plain_text",
},
{
name: "WASM_MODULE_ONE",
part: "WASM_MODULE_ONE",
Expand Down Expand Up @@ -4125,6 +4125,47 @@ addEventListener('fetch', event => {});`
});
});

describe("[services]", () => {
it("should support service bindings", async () => {
writeWranglerToml({
services: [
{
binding: "FOO",
service: "foo-service",
environment: "production",
},
],
});
writeWorkerSource();
mockSubDomainRequest();
mockUploadWorkerRequest({
expectedBindings: [
{
type: "service",
name: "FOO",
service: "foo-service",
environment: "production",
},
],
});

await runWrangler("publish index.js");
expect(std.out).toMatchInlineSnapshot(`
"Uploaded test-name (TIMINGS)
Published test-name (TIMINGS)
test-name.test-sub-domain.workers.dev"
`);
expect(std.err).toMatchInlineSnapshot(`""`);
expect(std.warn).toMatchInlineSnapshot(`
"▲ [WARNING] Processing wrangler.toml configuration:
- \\"services\\" fields are experimental and may change or break at any time.
"
`);
});
});

describe("[unsafe]", () => {
it("should warn if using unsafe bindings", async () => {
writeWranglerToml({
Expand Down
12 changes: 12 additions & 0 deletions packages/wrangler/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,18 @@ interface EnvironmentNonInheritable {
preview_bucket_name?: string;
}[];

/**
* Specifies service bindings (worker-to-worker) that are bound to this Worker environment.
*/
services: {
/** The binding name used to refer to the bound service. */
binding: string;
/** The name of the service. */
service: string;
/** The environment of the service (e.g. production, staging, etc). */
environment?: string;
}[];

/**
* "Unsafe" tables for features that aren't directly supported by wrangler.
*
Expand Down
Loading

0 comments on commit 66f07c5

Please sign in to comment.