Skip to content

Commit

Permalink
feat(gateway): Implement new supergraphSdl() config option for dyna…
Browse files Browse the repository at this point in the history
…mic gateway updates (#1246)

(copied from the changelog entry)

This change improves the `supergraphSdl` configuration option to provide a clean and flexible interface for updating gateway schema on load and at runtime. This PR brings a number of updates and deprecations to the gateway. Previous options for loading the gateway's supergraph (`serviceList`, `localServiceList`, `experimental_updateServiceDefinitions`, `experimental_supergraphSdl`) are all deprecated going forward. The migration paths all point to the updated `supergraphSdl` configuration option.

The most notable change here is the introduction of the concept of a `SupergraphManager` (one new possible type of `supergraphSdl`). This interface (when implemented) provides a means for userland code to update the gateway supergraph dynamically, perform subgraph healthchecks, and access subgraph datasources. All of the mentioned deprecated configurations now either use an implementation of a `SupergraphManager` internally or export one to be configured by the user (`IntrospectAndCompose` and `LocalCompose`).

For now: all of the mentioned deprecated configurations will still continue to work as expected. Their usage will come with deprecation warnings advising a switch to `supergraphSdl`.
* `serviceList` users should switch to the now-exported `IntrospectAndCompose` class.
* `localServiceList` users should switch to the similar `LocalCompose` class.
* `experimental_{updateServiceDefinitions|supergraphSdl}` users should migrate their implementation to a custom `SupergraphSdlHook` or `SupergraphManager`.

Since the gateway itself is no longer responsible for composition:
* `experimental_didUpdateComposition` has been renamed more appropriately to `experimental_didUpdateSupergraph` (no signature change)
* `experimental_compositionDidFail` hook is removed

`experimental_pollInterval` is deprecated and will issue a warning. Its renamed equivalent is `pollIntervalInMs`.

Some defensive code around gateway shutdown has been removed which was only relevant to users who are running the gateway within `ApolloServer` before v2.18. If you are still running one of these versions, server shutdown may not happen as smoothly.
  • Loading branch information
trevor-scheer committed Jan 11, 2022
1 parent b15459f commit 41dd9f3
Show file tree
Hide file tree
Showing 39 changed files with 6,694 additions and 5,173 deletions.
4 changes: 2 additions & 2 deletions codegen.yml
Expand Up @@ -4,8 +4,8 @@ schema: [
"https://outofbandreporter.api.apollographql.com/",
]
documents:
- gateway-js/src/loadSupergraphSdlFromStorage.ts
- gateway-js/src/outOfBandReporter.ts
- gateway-js/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts
- gateway-js/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts
generates:
gateway-js/src/__generated__/graphqlTypes.ts:
plugins:
Expand Down
178 changes: 173 additions & 5 deletions docs/source/api/apollo-gateway.mdx
Expand Up @@ -100,11 +100,20 @@ const gateway = new ApolloGateway({

###### `supergraphSdl`

`String`
`string | SupergraphSdlHook | SupergraphManager`
</td>
<td>

A [supergraph schema](../federated-types/overview/#supergraph-schema) ([generated with the Rover CLI](https://www.apollographql.com/docs/rover/supergraphs/#composing-a-supergraph-schema)) that's composed from your subgraph schemas. The supergraph schema includes directives that specify routing information for each subgraph.
You provide your supergraph schema to the gateway with this option. You can provide it as a `string`, via a `SupergraphSdlHook`, or via a `SupergraphManager`.

**When `supergraphSdl` is a `string`:** A [supergraph schema](../#federated-schemas) ([generated with the Rover CLI](https://www.apollographql.com/docs/rover/supergraphs/#composing-a-supergraph-schema)) that's composed from your subgraph schemas. The supergraph schema includes directives that specify routing information for each subgraph.

**When `supergraphSdl` is a `SupergraphSdlHook`:** This is an `async` function that returns an object containing a `supergraphSdl` string as well as a `cleanup` function. The hook accepts an object containing the following properties:
- `update`: A function that updates the supergraph schema
- `healthCheck`: A function that issues a health check against the subgraphs
- `getDataSource`: A function that gets a data source for a particular subgraph from the gateway

**When `supergraphSdl` is a `SupergraphManager`:** An object containing an `initialize` property. `initialize` is an `async` function of the `SupergraphSdlHook` type described directly above.

**If you are using managed federation,** do not provide this field.

Expand All @@ -124,7 +133,7 @@ A [supergraph schema](../federated-types/overview/#supergraph-schema) ([generate
</td>
<td>

**This option is discouraged in favor of [`supergraphSdl`](#supergraphsdl).**
**This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).**

An array of objects that each specify the `name` and `url` of one subgraph in your federated graph. On startup, the gateway uses this array to obtain your subgraph schemas via introspection and compose a supergraph schema.

Expand All @@ -143,11 +152,13 @@ You can specify any string value for the `name` field, which is used for identif

###### `introspectionHeaders`

`Object | (service: ServiceEndpointDefinition) => Promise<Object> | Object`
`Object | (service: ServiceEndpointDefinition) => (Promise<Object> | Object)`
</td>
<td>

An object, or an (optionally) async function returning an object, containing the names and values of HTTP headers that the gateway includes _only_ when making introspection requests to your subgraphs.
**This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).**

An object (or an optionally async function _returning_ an object) that contains the names and values of HTTP headers that the gateway includes _only_ when making introspection requests to your subgraphs.

**If you are using managed federation,** do not provide this field.

Expand Down Expand Up @@ -521,3 +532,160 @@ The details of the `fetch` response sent by the subgraph.
</tr>
</tbody>
</table>

## `class IntrospectAndCompose`

`IntrospectAndCompose` is a development tool for fetching and composing subgraph SDL into a supergraph for your gateway. Given a list of subgraphs and their URLs, `IntrospectAndCompose` will issue queries for their SDL, compose them into a supergraph, and provide that supergraph to the gateway. It can also be configured to update via polling and perform subgraph health checks to ensure that supergraphs are updated safely. `IntrospectAndCompose` implements the `SupergraphManager` interface and is passed in to `ApolloGateway`'s `supergraphSdl` constructor option.

> `IntrospectAndCompose` is the drop-in replacement for `serviceList`.
### Methods

#### `constructor`

Returns an initialized `IntrospectAndCompose` instance, which you can then pass to the `supergraphSdl` configuration option of the `ApolloGateway` constructor, like so:

```javascript{3-7}
const server = new ApolloServer({
gateway: new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
// ...
],
}),
}),
});
```

Takes an `options` object as a parameter. Supported properties of this object are described below.

##### Examples

###### Providing a `subgraphs` list and headers to authorize introspection

```js
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'products', url: 'https://products-service.dev/graphql',
{ name: 'reviews', url: 'https://reviews-service.dev/graphql' },
],
introspectionHeaders: {
Authorization: 'Bearer abc123'
},
}),
});
```
###### Configuring the subgraph fetcher
`IntrospectAndCompose` uses the data sources constructed by `ApolloGateway`. To customize the gateway's data sources, you can provide a [`buildService`](#buildservice) function to the `ApolloGateway` constructor. In the example below, `IntrospectAndCompose` makes authenticated requests to the subgraphs
via the `AuthenticatedDataSource`s that we construct in the gateway's `buildService` function.
```js
const gateway = new ApolloGateway({
buildService({ name, url }) {
return new AuthenticatedDataSource({ url });
},
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'products', url: 'https://products-service.dev/graphql',
{ name: 'reviews', url: 'https://reviews-service.dev/graphql' },
],
}),
});
```
##### Options
<table class="field-table">
<thead>
<tr>
<th>Name /<br/>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
###### `subgraphs`
`Array<ServiceEndpointDefinition>`
</td>
<td>
An array of objects that each specify the `name` and `url` of one subgraph in your federated graph. On startup, `IntrospectAndCompose` uses this array to obtain your subgraph schemas via introspection and compose a supergraph schema.
The `name` field is a string that should be treated as a subgraph's unique identifier. It is used for query planning, logging, and reporting metrics to Apollo Studio.
> For Studio users, subgraph names **must:**
- Begin with a letter (capital or lowercase)
- Include only letters, numbers, underscores (_), and hyphens (-)
- Have a maximum of 64 characters
</td>
</tr>
<tr>
<td>
###### `introspectionHeaders`
`Object | (service: ServiceEndpointDefinition) => (Promise<Object> | Object)`
</td>
<td>
An object (or an optionally async function _returning_ an object)that contains the names and values of HTTP headers that the gateway includes _only_ when making introspection requests to your subgraphs.
**If you define a [`buildService`](#buildservice) function in your `ApolloGateway` config, ** specify these headers in that function instead of providing this option. This ensures that your `buildService` function doesn't inadvertently overwrite the values of any headers you provide here.
</td>
</tr>
<tr>
<td>
###### `pollIntervalInMs`
`number`
</td>
<td>
Specify this option to enable supergraph updates via subgraph polling. `IntrospectAndCompose` polls each subgraph at the given interval.
</td>
</tr>
<tr>
<td>
###### `subgraphHealthCheck`
`boolean`
</td>
<td>
> This option applies only to subgraphs that are configured for polling via the `pollIntervalInMs` option.
If `true`, the gateway performs a health check on each subgraph before performing a supergraph update. Errors during health checks will result in skipping the supergraph update, but polling will continue. The health check is a simple GraphQL query (`query __ApolloServiceHealthCheck__ { __typename }`) to ensure that subgraphs are reachable and can successfully respond to GraphQL requests.
**This option is the `IntrospectAndCompose` equivalent of `ApolloGateway`'s `serviceHealthCheck` option. If you are using `IntrospectAndCompose`, enabling `serviceHealthCheck` on your `ApolloGateway` instance has no effect.**
</td>
</tr>
<tr>
<td>
###### `logger`
[`Logger`](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-types/src/index.ts#L166-L172)
</td>
<td>
An object to use for logging in place of `console`. If provided, this object must implement all methods of [the `Logger` interface](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-types/src/index.ts#L166-L172).
`IntrospectAndCompose` doesn't share the same logger as the `ApolloGateway` it's configured with. In most cases, you probably want to pass the same logger to both `ApolloGateway` and `IntrospectAndCompose`.
</td>
</tr>
</tbody>
</table>
2 changes: 1 addition & 1 deletion docs/source/entities.md
Expand Up @@ -225,7 +225,7 @@ We can perform this migration safely with the following steps:

* _If you **are** using managed federation, Apollo Studio does **not** publish an updated configuration, and the gateway continues to resolve the `Bill` entity in the `payments` subgraph._

* _If you are **not** using managed federation, your gateway starts resolving the `Bill` entity in whichever subgraph is listed **last** in your gateway's [`serviceList`](/api/apollo-gateway/#constructor)._
* _If you are **not** using managed federation, your gateway starts resolving the `Bill` entity in whichever subgraph is listed **last** in your gateway's [`IntrospectAndCompose`](/api/apollo-gateway/#class-introspectandcompose) `subgraphs` list._

4. In the `payments` subgraph's schema, remove the `Bill` entity. If you're using managed federation, register this schema change with Studio.

Expand Down
137 changes: 135 additions & 2 deletions docs/source/gateway.mdx
Expand Up @@ -34,12 +34,12 @@ const supergraphSdl = gql` ${schemaString} `;
// Initialize an ApolloGateway instance and pass it
// the supergraph schema
const gateway = new ApolloGateway({
supergraphSdl
supergraphSdl,
});

// Pass the ApolloGateway to the ApolloServer constructor
const server = new ApolloServer({
gateway
gateway,
});

server.listen().then(({ url }) => {
Expand All @@ -57,6 +57,139 @@ To learn how to compose your supergraph schema, see [Supported methods](./federa
On startup, the gateway processes your `supergraphSdl`, which includes routing information for your subgraphs. It then begins accepting incoming requests and creates query plans for them that execute across one or more subgraphs.

### Updating the supergraph schema

In the above example, we provide a _static_ supergraph schema to the gateway. This approach requires the gateway to restart in order to update the supergraph schema. This is undesirable for many applications, so we also provide the ability to update the supergraph schema dynamically.

```js:title=index.js
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');
const { readFile } = require('fs/promises');

let supergraphUpdate;
const gateway = new ApolloGateway({
async supergraphSdl({ update }) {
// `update` is a function which we'll save for later use
supergraphUpdate = update;
return {
supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'),
}
},
});

// Pass the ApolloGateway to the ApolloServer constructor
const server = new ApolloServer({
gateway,
});

server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```

There are a few things happening here. Let's take a look at each of them individually.

Note that `supergraphSdl` is now an `async` function. This function is called exactly once, when `ApolloServer` initializes the gateway. It has the following responsibilities:
- It receives the `update` function, which we use to update the supergraph schema.
- It returns the initial supergraph schema, which the gateway uses at startup.

With the `update` function, we can now programatically update the supergraph. Polling, webhooks, and file watchers are all good examples of ways we can go about updating the supergraph.

The code below demonstrates a more complete example using a file watcher. In this example, assume that we're updating the `supergraphSdl.graphql` file with the Rover CLI.

```js:title=index.js
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');
const { watch } = require('fs');
const { readFile } = require('fs/promises');

const server = new ApolloServer({
gateway: new ApolloGateway({
async supergraphSdl({ update, healthCheck }) {
// create a file watcher
const watcher = watch('./supergraph.graphql');
// subscribe to file changes
watcher.on('change', async () => {
// update the supergraph schema
try {
const updatedSupergraph = await readFile('./supergraph.graphql', 'utf-8');
// optional health check update to ensure our services are responsive
await healthCheck(updatedSupergraph);
// update the supergraph schema
update(updatedSupergraph);
} catch (e) {
// handle errors that occur during health check or while updating the supergraph schema
console.error(e);
}
});

return {
supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'),
// cleanup is called when the gateway is stopped
async cleanup() {
watcher.close();
}
}
},
}),
});

server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```

This example is a bit more complete. Let's take a look at what we've added.

In the `supergraphSdl` callback, we also receive a `healthCheck` function. This enables us to run a health check against each of the services in our future supergraph schema. This is useful for ensuring that our services are responsive and that we don't perform an update when it's unsafe.

We've also wrapped our call to `update` and `healthCheck` in a `try` block. If an error occurs during either of these, we want to handle this gracefully. In this example, we continue running the existing supergraph schema and log an error.

Finally, we return a `cleanup` function. This is a callback that's called when the gateway is stopped. This enables us to cleanly shut down any ongoing processes (such as file watching or polling) when the gateway is shut down via a call to `ApolloServer.stop`. The gateway expects `cleanup` to return a `Promise` and `await`s it before shutting down.

#### Advanced usage

In a more complex application, you might want to create a class that handles the `update` and `healthCheck` functions, along with any additional state. In this case, you can instead provide an object (or class) with an `initialize` function. This function is called just like the `supergraphSdl` function discussed above. For an example of this, see the [`IntrospectAndCompose` source code](https://github.com/apollographql/federation/blob/main/packages/apollo-gateway/src/supergraphManagers/IntrospectAndCompose/index.ts).

### Composing subgraphs with `IntrospectAndCompose`

> Looking for `serviceList`? In `@apollo/gateway` version 0.46.0 and later,`IntrospectAndCompose` is the new drop-in replacement for the `serviceList` option. The `serviceList` option will be removed in an upcoming release of `@apollo/gateway`, but `IntrospectAndCompose` will continue to be supported. We recommend using the Rover CLI to manage local composition, but `IntrospectAndCompose` is still useful for various development and testing workflows.
> We strongly recommend _against_ using `IntrospectAndCompose` in production. For details, [see below](#limitations-of-introspectandcompose).
Alternatively, you can provide a `subgraph` array to the `IntrospectAndCompose` constructor, like so:

```js:title=index.js
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');

const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'accounts', url: 'http://localhost:4001' },
{ name: 'products', url: 'http://localhost:4002' },
// ...additional subgraphs...
],
}),
});
```

Each item in the array is an object that specifies the `name` and `url` of one of your subgraphs. You can specify any string value for `name`, which is used primarily for query planner output, error messages, and logging.

On startup, the gateway fetches each subgraph's schema from its `url` and composes those schemas into a supergraph schema. It then begins accepting incoming requests and creates query plans for them that execute across one or more subgraphs.

Additional configuration options can be found in the [`IntrospectAndCompose` API documentation](./api/apollo-gateway#class-introspectandcompose).

However, `IntrospectAndCompose` has important [limitations](#limitations-of-introspectandcompose).

#### Limitations of `IntrospectAndCompose`

The `IntrospectAndCompose` option can sometimes be helpful for local development, but it is strongly discouraged for any other environment. Here are some reasons why:

* **Composition might fail.** With `IntrospectAndCompose`, your gateway performs composition dynamically on startup, which requires network communication with each subgraph. If composition fails, your gateway [throws errors](./errors/) and experiences unplanned downtime.
* With the static or dynamic `supergraphSdl` configuration, you instead provide a supergraph schema that has _already_ been composed successfully. This prevents composition errors and enables faster startup.
* **Gateway instances might differ.** If you deploy multiple instances of your gateway _while_ deploying updates to your subgraphs, your gateway instances might fetch different schemas from the _same_ subgraph. This can result in sporadic composition failures or inconsistent supergraph schemas between instances.
* When you deploy multiple instances with `supergraphSdl`, you provide the exact same static artifact to each instance, enabling more predictable behavior.

## Updating the gateway

> Before updating your gateway's version, check the [changelog](https://github.com/apollographql/federation/blob/main/gateway-js/CHANGELOG.md) for potential breaking changes.
Expand Down

0 comments on commit 41dd9f3

Please sign in to comment.