From c2f2e68367101d63fad549473ecdf56649d6716b Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 24 Nov 2021 11:00:01 -0800 Subject: [PATCH 01/82] Initial update of types, introduce deprecation warnings and prelim TODOs --- .../integration/configuration.test.ts | 56 ++++++++++++++ gateway-js/src/config.ts | 75 ++++++++++++++----- gateway-js/src/index.ts | 40 +++++++++- 3 files changed, 151 insertions(+), 20 deletions(-) diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 3122b16bdd..e02af0dd68 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -13,6 +13,7 @@ import { } from './nockMocks'; import { getTestingSupergraphSdl } from '../execution-utils'; import { MockService } from './networkRequests.test'; +import { fixtures } from 'apollo-federation-integration-testsuite'; let logger: Logger; @@ -358,3 +359,58 @@ describe('gateway config / env behavior', () => { }); }); }); + +describe('deprecation warnings', () => { + it('warns with `experimental_updateSupergraphSdl` option set', async () => { + new ApolloGateway({ + async experimental_updateSupergraphSdl() { + return { + id: 'supergraph', + supergraphSdl: getTestingSupergraphSdl(), + }; + }, + logger, + }); + + expect(logger.warn).toHaveBeenCalledWith( + 'The `experimental_updateSupergraphSdl` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + }); + + it('warns with `experimental_updateServiceDefinitions` option set', async () => { + new ApolloGateway({ + async experimental_updateServiceDefinitions() { + return { + isNewSchema: false, + }; + }, + logger, + }); + + expect(logger.warn).toHaveBeenCalledWith( + 'The `experimental_updateServiceDefinitions` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + }); + + it('warns with `serviceList` option set', async () => { + new ApolloGateway({ + serviceList: [{ name: 'accounts', url: 'http://localhost:4001' }], + logger, + }); + + expect(logger.warn).toHaveBeenCalledWith( + 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + }); + + it('warns with `localServiceList` option set', async () => { + new ApolloGateway({ + localServiceList: fixtures, + logger, + }); + + expect(logger.warn).toHaveBeenCalledWith( + 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + }); +}); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 4fc229cc19..429c2f41ec 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -1,8 +1,11 @@ -import { GraphQLError, GraphQLSchema } from "graphql"; -import { HeadersInit } from "node-fetch"; +import { GraphQLError, GraphQLSchema } from 'graphql'; +import { HeadersInit } from 'node-fetch'; import { fetch } from 'apollo-server-env'; -import { GraphQLRequestContextExecutionDidStart, Logger } from "apollo-server-types"; -import { ServiceDefinition } from "@apollo/federation"; +import { + GraphQLRequestContextExecutionDidStart, + Logger, +} from 'apollo-server-types'; +import { ServiceDefinition } from '@apollo/federation'; import { GraphQLDataSource } from './datasources/types'; import { QueryPlan } from '@apollo/query-planner'; import { OperationContext } from './operationContext'; @@ -80,7 +83,9 @@ export interface SupergraphSdlUpdate { supergraphSdl: string; } -export function isSupergraphSdlUpdate(update: CompositionUpdate): update is SupergraphSdlUpdate { +export function isSupergraphSdlUpdate( + update: CompositionUpdate, +): update is SupergraphSdlUpdate { return 'supergraphSdl' in update; } @@ -128,11 +133,16 @@ interface GatewayConfigBase { serviceHealthCheck?: boolean; } +// TODO(trevor:removeServiceList) export interface RemoteGatewayConfig extends GatewayConfigBase { + // @deprecated: use `supergraphSdl` in its function form instead serviceList: ServiceEndpointDefinition[]; + // @deprecated: use `supergraphSdl` in its function form instead introspectionHeaders?: | HeadersInit - | ((service: ServiceEndpointDefinition) => Promise | HeadersInit); + | (( + service: ServiceEndpointDefinition, + ) => Promise | HeadersInit); } export interface ManagedGatewayConfig extends GatewayConfigBase { @@ -145,45 +155,69 @@ export interface ManagedGatewayConfig extends GatewayConfigBase { uplinkMaxRetries?: number; } +// TODO(trevor:removeServiceList): migrate users to `supergraphSdl` function option interface ManuallyManagedServiceDefsGatewayConfig extends GatewayConfigBase { + // @deprecated: use `supergraphSdl` in its function form instead experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions; } +// TODO(trevor:removeServiceList): migrate users to `supergraphSdl` function option +interface ExperimentalManuallyManagedSupergraphSdlGatewayConfig + extends GatewayConfigBase { + // @deprecated: use `supergraphSdl` in its function form instead + experimental_updateSupergraphSdl: Experimental_UpdateSupergraphSdl; +} + interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { - experimental_updateSupergraphSdl: Experimental_UpdateSupergraphSdl + supergraphSdl: ( + update: (updatedSupergraphSdl: string) => void, + ) => Promise void }>; } type ManuallyManagedGatewayConfig = | ManuallyManagedServiceDefsGatewayConfig + | ExperimentalManuallyManagedSupergraphSdlGatewayConfig | ManuallyManagedSupergraphSdlGatewayConfig; +// TODO(trevor:removeServiceList) interface LocalGatewayConfig extends GatewayConfigBase { + // @deprecated: use `supergraphSdl` in its function form instead localServiceList: ServiceDefinition[]; } -interface SupergraphSdlGatewayConfig extends GatewayConfigBase { +interface StaticSupergraphSdlGatewayConfig extends GatewayConfigBase { supergraphSdl: string; } -export type StaticGatewayConfig = LocalGatewayConfig | SupergraphSdlGatewayConfig; +export type StaticGatewayConfig = + | LocalGatewayConfig + | StaticSupergraphSdlGatewayConfig; type DynamicGatewayConfig = -| ManagedGatewayConfig -| RemoteGatewayConfig -| ManuallyManagedGatewayConfig; + | ManagedGatewayConfig + | RemoteGatewayConfig + | ManuallyManagedGatewayConfig; export type GatewayConfig = StaticGatewayConfig | DynamicGatewayConfig; -export function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { +// TODO(trevor:removeServiceList) +export function isLocalConfig( + config: GatewayConfig, +): config is LocalGatewayConfig { return 'localServiceList' in config; } -export function isRemoteConfig(config: GatewayConfig): config is RemoteGatewayConfig { +// TODO(trevor:removeServiceList) +export function isRemoteConfig( + config: GatewayConfig, +): config is RemoteGatewayConfig { return 'serviceList' in config; } -export function isSupergraphSdlConfig(config: GatewayConfig): config is SupergraphSdlGatewayConfig { - return 'supergraphSdl' in config; +export function isStaticSupergraphSdlConfig( + config: GatewayConfig, +): config is StaticSupergraphSdlGatewayConfig { + return 'supergraphSdl' in config && typeof config.supergraphSdl === 'string'; } // A manually managed config means the user has provided a function which @@ -192,6 +226,7 @@ export function isManuallyManagedConfig( config: GatewayConfig, ): config is ManuallyManagedGatewayConfig { return ( + ('supergraphSdl' in config && typeof config.supergraphSdl === 'function') || 'experimental_updateServiceDefinitions' in config || 'experimental_updateSupergraphSdl' in config ); @@ -205,14 +240,16 @@ export function isManagedConfig( 'schemaConfigDeliveryEndpoint' in config || (!isRemoteConfig(config) && !isLocalConfig(config) && - !isSupergraphSdlConfig(config) && + !isStaticSupergraphSdlConfig(config) && !isManuallyManagedConfig(config)) ); } // A static config is one which loads synchronously on start and never updates -export function isStaticConfig(config: GatewayConfig): config is StaticGatewayConfig { - return isLocalConfig(config) || isSupergraphSdlConfig(config); +export function isStaticConfig( + config: GatewayConfig, +): config is StaticGatewayConfig { + return isLocalConfig(config) || isStaticSupergraphSdlConfig(config); } // A dynamic config is one which loads asynchronously and (can) update via polling diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index fa7c6b191d..8cd521a307 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -31,7 +31,10 @@ import { } from './executeQueryPlan'; import { getServiceDefinitionsFromRemoteEndpoint } from './loadServicesFromRemoteEndpoint'; -import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types'; +import { + GraphQLDataSource, + GraphQLDataSourceRequestKind, +} from './datasources/types'; import { RemoteGraphQLDataSource } from './datasources/RemoteGraphQLDataSource'; import { getVariableValues } from 'graphql/execution/values'; import fetcher from 'make-fetch-happen'; @@ -268,6 +271,7 @@ export class ApolloGateway implements GraphQLService { this.updateServiceDefinitions = this.loadServiceDefinitions; } + this.issueDeprecationWarningsIfApplicable(); if (isDynamicConfig(this.config)) { this.issueDynamicWarningsIfApplicable(); } @@ -324,6 +328,7 @@ export class ApolloGateway implements GraphQLService { } // Warn against using the pollInterval and a serviceList simultaneously + // TODO(trevor:removeServiceList) if (this.config.experimental_pollInterval && isRemoteConfig(this.config)) { this.logger.warn( 'Polling running services is dangerous and not recommended in production. ' + @@ -346,6 +351,36 @@ export class ApolloGateway implements GraphQLService { } } + private issueDeprecationWarningsIfApplicable() { + // TODO(trevor:removeServiceList) + if ('experimental_updateSupergraphSdl' in this.config) { + this.logger.warn( + 'The `experimental_updateSupergraphSdl` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + } + + // TODO(trevor:removeServiceList) + if ('experimental_updateServiceDefinitions' in this.config) { + this.logger.warn( + 'The `experimental_updateServiceDefinitions` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + } + + // TODO(trevor:removeServiceList) + if ('serviceList' in this.config) { + this.logger.warn( + 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + } + + // TODO(trevor:removeServiceList) + if ('localServiceList' in this.config) { + this.logger.warn( + 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + } + } + public async load(options?: { apollo?: ApolloConfigFromAS2Or3; engine?: GraphQLServiceEngineConfig; @@ -819,6 +854,8 @@ export class ApolloGateway implements GraphQLService { }; } + // TODO(trevor:removeServiceList): gateway shouldn't be responsible for polling + // in the future. // This function waits an appropriate amount, updates composition, and calls itself // again. Note that it is an async function whose Promise is not actually awaited; // it should never throw itself other than due to a bug in its state machine. @@ -940,6 +977,7 @@ export class ApolloGateway implements GraphQLService { protected async loadServiceDefinitions( config: RemoteGatewayConfig | ManagedGatewayConfig, ): Promise { + // TODO(trevor:removeServiceList) if (isRemoteConfig(config)) { const serviceList = config.serviceList.map((serviceDefinition) => ({ ...serviceDefinition, From 78a6e514c3d0d7e918dcd6fe850a388b495f095b Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 25 Nov 2021 08:48:38 -0800 Subject: [PATCH 02/82] Implementation and tests --- gateway-js/src/__tests__/execution-utils.ts | 23 +- .../src/__tests__/gateway/composedSdl.test.ts | 44 ---- .../__tests__/gateway/lifecycle-hooks.test.ts | 1 - .../__tests__/gateway/supergraphSdl.test.ts | 204 ++++++++++++++++++ gateway-js/src/config.ts | 11 +- gateway-js/src/index.ts | 108 ++++++++-- 6 files changed, 325 insertions(+), 66 deletions(-) delete mode 100644 gateway-js/src/__tests__/gateway/composedSdl.test.ts create mode 100644 gateway-js/src/__tests__/gateway/supergraphSdl.test.ts diff --git a/gateway-js/src/__tests__/execution-utils.ts b/gateway-js/src/__tests__/execution-utils.ts index d922f887b9..95a4688677 100644 --- a/gateway-js/src/__tests__/execution-utils.ts +++ b/gateway-js/src/__tests__/execution-utils.ts @@ -117,8 +117,27 @@ export function getTestingSupergraphSdl(services: typeof fixtures = fixtures) { ); } -export function wait(ms: number) { - return new Promise(r => setTimeout(r, ms)); +export function wait(ms: number, toResolveTo?: any) { + return new Promise((r) => setTimeout(() => r(toResolveTo), ms)); +} + +export function waitUntil() { + let userResolved: (value: T | PromiseLike) => void; + let userRejected: (reason?: any) => void; + const promise = new Promise( + (r) => ((userResolved = r), (userRejected = r)), + ); + return [ + promise, + // @ts-ignore + userResolved, + // @ts-ignore + userRejected, + ] as [ + Promise, + (value: T | PromiseLike) => void, + (reason?: any) => void, + ]; } export function printPlan(queryPlan: QueryPlan): string { diff --git a/gateway-js/src/__tests__/gateway/composedSdl.test.ts b/gateway-js/src/__tests__/gateway/composedSdl.test.ts deleted file mode 100644 index 1f8ac19569..0000000000 --- a/gateway-js/src/__tests__/gateway/composedSdl.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ApolloGateway } from '@apollo/gateway'; -import { ApolloServer } from 'apollo-server'; -import { fetch } from '../../__mocks__/apollo-server-env'; -import { getTestingSupergraphSdl } from '../execution-utils'; - -async function getSupergraphSdlGatewayServer() { - const server = new ApolloServer({ - gateway: new ApolloGateway({ - supergraphSdl: getTestingSupergraphSdl(), - }), - }); - - await server.listen({ port: 0 }); - return server; -} - -describe('Using supergraphSdl configuration', () => { - it('successfully starts and serves requests to the proper services', async () => { - const server = await getSupergraphSdlGatewayServer(); - - fetch.mockJSONResponseOnce({ - data: { me: { username: '@jbaxleyiii' } }, - }); - - const result = await server.executeOperation({ - query: '{ me { username } }', - }); - - expect(result.data).toMatchInlineSnapshot(` - Object { - "me": Object { - "username": "@jbaxleyiii", - }, - } - `); - - const [url, request] = fetch.mock.calls[0]; - expect(url).toEqual('https://accounts.api.com'); - expect(request?.body).toEqual( - JSON.stringify({ query: '{me{username}}', variables: {} }), - ); - await server.stop(); - }); -}); diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index c1e9f0badc..de5dd2c2ae 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -209,7 +209,6 @@ describe('lifecycle hooks', () => { experimental_pollInterval: 10, logger, }); - expect(logger.warn).toHaveBeenCalledTimes(1); expect(logger.warn).toHaveBeenCalledWith( 'Polling running services is dangerous and not recommended in production. Polling should only be used against a registry. If you are polling running services, use with caution.', ); diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts new file mode 100644 index 0000000000..7c4b853c6f --- /dev/null +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -0,0 +1,204 @@ +import { ApolloGateway } from '@apollo/gateway'; +import { fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; +import { createHash } from 'crypto'; +import { ApolloServer } from 'apollo-server'; +import { Logger } from 'apollo-server-types'; +import { fetch } from '../../__mocks__/apollo-server-env'; +import { getTestingSupergraphSdl, waitUntil } from '../execution-utils'; + +async function getSupergraphSdlGatewayServer() { + const server = new ApolloServer({ + gateway: new ApolloGateway({ + supergraphSdl: getTestingSupergraphSdl(), + }), + }); + + await server.listen({ port: 0 }); + return server; +} + +let logger: Logger; +beforeEach(() => { + logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; +}); + +describe('Using supergraphSdl static configuration', () => { + it('successfully starts and serves requests to the proper services', async () => { + const server = await getSupergraphSdlGatewayServer(); + + fetch.mockJSONResponseOnce({ + data: { me: { username: '@jbaxleyiii' } }, + }); + + const result = await server.executeOperation({ + query: '{ me { username } }', + }); + + expect(result.data).toMatchInlineSnapshot(` + Object { + "me": Object { + "username": "@jbaxleyiii", + }, + } + `); + + const [url, request] = fetch.mock.calls[0]; + expect(url).toEqual('https://accounts.api.com'); + expect(request?.body).toEqual( + JSON.stringify({ query: '{me{username}}', variables: {} }), + ); + await server.stop(); + }); +}); + +describe('Using supergraphSdl dynamic configuration', () => { + it('starts and remains in `initialized` state until user Promise resolves', async () => { + const [forGatewayToHaveCalledSupergraphSdl, resolve] = waitUntil(); + + const gateway = new ApolloGateway({ + async supergraphSdl() { + resolve(); + return new Promise(() => {}); + }, + }); + + await forGatewayToHaveCalledSupergraphSdl; + expect(gateway.__testing().state.phase).toEqual('initialized'); + }); + + it('starts and waits in `initialized` state after calling load but before user Promise resolves', async () => { + const gateway = new ApolloGateway({ + async supergraphSdl() { + return new Promise(() => {}); + }, + }); + + gateway.load(); + + expect(gateway.__testing().state.phase).toEqual('initialized'); + }); + + it('moves from `initialized` to `loaded` state after calling `load()` and after user Promise resolves', async () => { + const [userPromise, resolveSupergraph] = + waitUntil<{ supergraphSdl: string }>(); + + const gateway = new ApolloGateway({ + async supergraphSdl() { + return userPromise; + }, + }); + + const loadPromise = gateway.load(); + expect(gateway.__testing().state.phase).toEqual('initialized'); + + const supergraphSdl = getTestingSupergraphSdl(); + const expectedCompositionId = createHash('sha256') + .update(supergraphSdl) + .digest('hex'); + resolveSupergraph({ supergraphSdl }); + + await loadPromise; + const { state, compositionId } = gateway.__testing(); + expect(state.phase).toEqual('loaded'); + expect(compositionId).toEqual(expectedCompositionId); + }); + + it('updates its supergraph after user calls update function', async () => { + const [userPromise, resolveSupergraph] = + waitUntil<{ supergraphSdl: string }>(); + + let userUpdateFn: (updatedSupergraphSdl: string) => Promise; + const gateway = new ApolloGateway({ + async supergraphSdl(update) { + userUpdateFn = update; + return userPromise; + }, + }); + + const supergraphSdl = getTestingSupergraphSdl(); + const expectedId = createHash('sha256').update(supergraphSdl).digest('hex'); + resolveSupergraph({ supergraphSdl: getTestingSupergraphSdl() }); + await gateway.load(); + expect(gateway.__testing().compositionId).toEqual(expectedId); + + const updatedSupergraphSdl = getTestingSupergraphSdl(fixturesWithUpdate); + const expectedUpdatedId = createHash('sha256') + .update(updatedSupergraphSdl) + .digest('hex'); + await userUpdateFn!(updatedSupergraphSdl); + expect(gateway.__testing().compositionId).toEqual(expectedUpdatedId); + }); + + it('calls user-provided `cleanup` function when stopped', async () => { + const cleanup = jest.fn(() => Promise.resolve()); + const gateway = new ApolloGateway({ + async supergraphSdl() { + return { + supergraphSdl: getTestingSupergraphSdl(), + cleanup, + }; + }, + }); + + await gateway.load(); + const { state, compositionId } = gateway.__testing(); + expect(state.phase).toEqual('loaded'); + expect(compositionId).toEqual( + '562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b', + ); + + await gateway.stop(); + expect(cleanup).toHaveBeenCalledTimes(1); + }); + + describe('errors', () => { + it('fails to load if user-provided `supergraphSdl` function throws', async () => { + const gateway = new ApolloGateway({ + async supergraphSdl() { + throw new Error('supergraphSdl failed'); + }, + logger, + }); + + await expect(() => + gateway.load(), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"User provided \`supergraphSdl\` function did not return an object containing a \`supergraphSdl\` property"`, + ); + + expect(gateway.__testing().state.phase).toEqual('failed to load'); + expect(logger.error).toHaveBeenCalledWith( + 'User-defined `supergraphSdl` function threw error: supergraphSdl failed', + ); + }); + + it('gracefully handles Promise rejections from user `cleanup` function', async () => { + const rejectionMessage = 'thrown from cleanup function'; + const cleanup = jest.fn(() => + Promise.reject(rejectionMessage), + ); + const gateway = new ApolloGateway({ + async supergraphSdl() { + return { + supergraphSdl: getTestingSupergraphSdl(), + cleanup, + }; + }, + logger, + }); + + await gateway.load(); + await expect(gateway.stop()).resolves.toBeUndefined(); + expect(cleanup).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + 'Error occured while calling user provided `cleanup` function: ' + + rejectionMessage, + ); + }); + }); +}); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 429c2f41ec..a1ce77216d 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -168,10 +168,17 @@ interface ExperimentalManuallyManagedSupergraphSdlGatewayConfig experimental_updateSupergraphSdl: Experimental_UpdateSupergraphSdl; } +export function isManuallyManagedSupergraphSdlGatewayConfig( + config: GatewayConfig, +): config is ManuallyManagedSupergraphSdlGatewayConfig { + return ( + 'supergraphSdl' in config && typeof config.supergraphSdl === 'function' + ); +} interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { supergraphSdl: ( - update: (updatedSupergraphSdl: string) => void, - ) => Promise void }>; + update: (updatedSupergraphSdl: string) => Promise, + ) => Promise<{ supergraphSdl: string; cleanup?: () => Promise }>; } type ManuallyManagedGatewayConfig = diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 8cd521a307..fd39d22ba6 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -69,6 +69,7 @@ import { ServiceDefinitionUpdate, SupergraphSdlUpdate, CompositionUpdate, + isManuallyManagedSupergraphSdlGatewayConfig, } from './config'; import { buildComposedSchema } from '@apollo/query-planner'; import { loadSupergraphSdlFromUplinks } from './loadSupergraphSdlFromStorage'; @@ -76,6 +77,7 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { OpenTelemetrySpanNames, tracer } from './utilities/opentelemetry'; import { CoreSchema } from '@apollo/core-schema'; import { featureSupport } from './core'; +import { createHash } from 'crypto'; type DataSourceMap = { [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; @@ -200,7 +202,7 @@ export class ApolloGateway implements GraphQLService { private experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; // Used for overriding the default service list fetcher. This should return // an array of ServiceDefinition. *This function must be awaited.* - private updateServiceDefinitions: Experimental_UpdateComposition; + private updateServiceDefinitions?: Experimental_UpdateComposition; // how often service defs should be loaded/updated (in ms) private experimental_pollInterval?: number; // Configure the endpoints by which gateway will access its precomposed schema. @@ -208,6 +210,13 @@ export class ApolloGateway implements GraphQLService { // * `undefined` means the gateway is not using managed federation private uplinkEndpoints?: string[]; private uplinkMaxRetries?: number; + // The Promise case is strictly for handling the case where the user-provided + // function throws an error. + private manualConfigPromise?: Promise<{ + supergraphSdl: string; + cleanup?: () => Promise; + } | void>; + private toDispose: (() => Promise)[] = []; constructor(config?: GatewayConfig) { this.config = { @@ -262,6 +271,16 @@ export class ApolloGateway implements GraphQLService { } else if ('experimental_updateServiceDefinitions' in this.config) { this.updateServiceDefinitions = this.config.experimental_updateServiceDefinitions; + } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { + this.manualConfigPromise = this.config + .supergraphSdl(this.updateWithSupergraphSdl.bind(this)) + .catch((e) => { + // Not swallowing the error here results in an uncaught rejection. + // An error will be thrown when this promise resolves to nothing. + this.logger.error( + 'User-defined `supergraphSdl` function threw error: ' + e.message, + ); + }); } else { throw Error( 'Programming error: unexpected manual configuration provided', @@ -441,9 +460,13 @@ export class ApolloGateway implements GraphQLService { this.maybeWarnOnConflictingConfig(); // Handles initial assignment of `this.schema`, `this.queryPlanner` - isStaticConfig(this.config) - ? this.loadStatic(this.config) - : await this.loadDynamic(unrefTimer); + if (isStaticConfig(this.config)) { + this.loadStatic(this.config); + } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { + await this.loadManuallyManaged(); + } else { + await this.loadDynamic(unrefTimer); + } const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; this.logger.info( @@ -496,6 +519,25 @@ export class ApolloGateway implements GraphQLService { } } + private async loadManuallyManaged() { + try { + const result = await this.manualConfigPromise; + if (!result?.supergraphSdl) + throw new Error( + 'User provided `supergraphSdl` function did not return an object containing a `supergraphSdl` property', + ); + if (result?.cleanup) { + this.toDispose.push(result.cleanup); + } + await this.updateWithSupergraphSdl(result.supergraphSdl); + } catch (e) { + this.state = { phase: 'failed to load' }; + throw e; + } + + this.state = { phase: 'loaded' }; + } + private shouldBeginPolling() { return isManagedConfig(this.config) || this.experimental_pollInterval; } @@ -503,6 +545,11 @@ export class ApolloGateway implements GraphQLService { private async updateSchema(): Promise { this.logger.debug('Checking for composition updates...'); + if (!this.updateServiceDefinitions) { + throw new Error( + 'Programming error: `updateSchema` was called unexpectedly.', + ); + } // This may throw, but an error here is caught and logged upstream const result = await this.updateServiceDefinitions(this.config); @@ -576,19 +623,28 @@ export class ApolloGateway implements GraphQLService { } private async updateWithSupergraphSdl( - result: SupergraphSdlUpdate, + result: SupergraphSdlUpdate | string, ): Promise { - if (result.id === this.compositionId) { + if (typeof result === 'string') { + } else if (result.id === this.compositionId) { this.logger.debug('No change in composition since last check.'); return; } + const supergraphSdl = + typeof result === 'string' ? result : result.supergraphSdl; + + const id = + typeof result === 'string' + ? createHash('sha256').update(result).digest('hex') + : result.id; + // TODO(trevor): #580 redundant parse // This may throw, so we'll calculate early (specifically before making any updates) // In the case that it throws, the gateway will: // * on initial load, throw the error // * on update, log the error and don't update - const parsedSupergraphSdl = parse(result.supergraphSdl); + const parsedSupergraphSdl = parse(supergraphSdl); const previousSchema = this.schema; const previousSupergraphSdl = this.parsedSupergraphSdl; @@ -598,28 +654,27 @@ export class ApolloGateway implements GraphQLService { this.logger.info('Updated Supergraph SDL was found.'); } - await this.maybePerformServiceHealthCheck(result); + await this.maybePerformServiceHealthCheck({ supergraphSdl, id }); - this.compositionId = result.id; - this.supergraphSdl = result.supergraphSdl; + this.compositionId = id; + this.supergraphSdl = supergraphSdl; this.parsedSupergraphSdl = parsedSupergraphSdl; - const { schema, supergraphSdl } = this.createSchemaFromSupergraphSdl( - result.supergraphSdl, - ); + const { schema, supergraphSdl: generatedSupergraphSdl } = + this.createSchemaFromSupergraphSdl(supergraphSdl); - if (!supergraphSdl) { + if (!generatedSupergraphSdl) { this.logger.error( "A valid schema couldn't be composed. Falling back to previous schema.", ); } else { - this.updateWithSchemaAndNotify(schema, supergraphSdl); + this.updateWithSchemaAndNotify(schema, generatedSupergraphSdl); if (this.experimental_didUpdateComposition) { this.experimental_didUpdateComposition( { - compositionId: result.id, - supergraphSdl: result.supergraphSdl, + compositionId: id, + supergraphSdl: supergraphSdl, schema, }, previousCompositionId && previousSupergraphSdl && previousSchema @@ -1244,6 +1299,17 @@ export class ApolloGateway implements GraphQLService { // schema polling). Can be called multiple times safely. Once it (async) // returns, all gateway background activity will be finished. public async stop() { + Promise.all( + this.toDispose.map((p) => + p().catch((e) => { + this.logger.error( + 'Error occured while calling user provided `cleanup` function: ' + + (e.message ?? e), + ); + }), + ), + ); + this.toDispose = []; switch (this.state.phase) { case 'initialized': case 'failed to load': @@ -1299,6 +1365,14 @@ export class ApolloGateway implements GraphQLService { throw new UnreachableCaseError(this.state); } } + + public __testing() { + return { + state: this.state, + compositionId: this.compositionId, + supergraphSdl: this.supergraphSdl, + } + } } ApolloGateway.prototype.onSchemaChange = deprecate( From b7dfbacf9c701d4af5b8b5ebe769a9d9e55204d7 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 29 Nov 2021 09:47:30 -0800 Subject: [PATCH 03/82] Types and signature cleanup --- .../__tests__/gateway/supergraphSdl.test.ts | 10 ++++------ gateway-js/src/config.ts | 11 +++++++++-- gateway-js/src/index.ts | 18 +++++++++++------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 7c4b853c6f..26417968a1 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -1,4 +1,4 @@ -import { ApolloGateway } from '@apollo/gateway'; +import { ApolloGateway, SupergraphSdlUpdateFunction } from '@apollo/gateway'; import { fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; import { createHash } from 'crypto'; import { ApolloServer } from 'apollo-server'; @@ -112,9 +112,9 @@ describe('Using supergraphSdl dynamic configuration', () => { const [userPromise, resolveSupergraph] = waitUntil<{ supergraphSdl: string }>(); - let userUpdateFn: (updatedSupergraphSdl: string) => Promise; + let userUpdateFn: SupergraphSdlUpdateFunction; const gateway = new ApolloGateway({ - async supergraphSdl(update) { + async supergraphSdl({ update }) { userUpdateFn = update; return userPromise; }, @@ -179,9 +179,7 @@ describe('Using supergraphSdl dynamic configuration', () => { it('gracefully handles Promise rejections from user `cleanup` function', async () => { const rejectionMessage = 'thrown from cleanup function'; - const cleanup = jest.fn(() => - Promise.reject(rejectionMessage), - ); + const cleanup = jest.fn(() => Promise.reject(rejectionMessage)); const gateway = new ApolloGateway({ async supergraphSdl() { return { diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index a1ce77216d..470bb3e66d 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -175,9 +175,16 @@ export function isManuallyManagedSupergraphSdlGatewayConfig( 'supergraphSdl' in config && typeof config.supergraphSdl === 'function' ); } + +export interface SupergraphSdlUpdateFunction { + (updatedSupergraphSdl: string): Promise +} +export interface SupergraphSdlUpdateOptions { + update: SupergraphSdlUpdateFunction; +} interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { supergraphSdl: ( - update: (updatedSupergraphSdl: string) => Promise, + opts: SupergraphSdlUpdateOptions, ) => Promise<{ supergraphSdl: string; cleanup?: () => Promise }>; } @@ -233,7 +240,7 @@ export function isManuallyManagedConfig( config: GatewayConfig, ): config is ManuallyManagedGatewayConfig { return ( - ('supergraphSdl' in config && typeof config.supergraphSdl === 'function') || + isManuallyManagedSupergraphSdlGatewayConfig(config) || 'experimental_updateServiceDefinitions' in config || 'experimental_updateSupergraphSdl' in config ); diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index fd39d22ba6..497b34b707 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -273,7 +273,7 @@ export class ApolloGateway implements GraphQLService { this.config.experimental_updateServiceDefinitions; } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { this.manualConfigPromise = this.config - .supergraphSdl(this.updateWithSupergraphSdl.bind(this)) + .supergraphSdl({ update: this.updateWithSupergraphSdl.bind(this) }) .catch((e) => { // Not swallowing the error here results in an uncaught rejection. // An error will be thrown when this promise resolves to nothing. @@ -625,12 +625,6 @@ export class ApolloGateway implements GraphQLService { private async updateWithSupergraphSdl( result: SupergraphSdlUpdate | string, ): Promise { - if (typeof result === 'string') { - } else if (result.id === this.compositionId) { - this.logger.debug('No change in composition since last check.'); - return; - } - const supergraphSdl = typeof result === 'string' ? result : result.supergraphSdl; @@ -639,6 +633,11 @@ export class ApolloGateway implements GraphQLService { ? createHash('sha256').update(result).digest('hex') : result.id; + if (id === this.compositionId) { + this.logger.debug('No change in composition since last check.'); + return; + } + // TODO(trevor): #580 redundant parse // This may throw, so we'll calculate early (specifically before making any updates) // In the case that it throws, the gateway will: @@ -1427,3 +1426,8 @@ export { }; export * from './datasources'; + +export { + SupergraphSdlUpdateOptions, + SupergraphSdlUpdateFunction, +} from './config'; From e7786a644c0932b3e906db985bed71058d0f1f00 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 1 Dec 2021 19:01:19 -0800 Subject: [PATCH 04/82] Implement serviceList shim --- .../src/fixtures/index.ts | 18 ++- federation-js/src/__tests__/joinSpec.test.ts | 2 +- gateway-js/package.json | 1 + .../__tests__/gateway/supergraphSdl.test.ts | 45 ++++-- .../integration/configuration.test.ts | 20 ++- .../integration/networkRequests.test.ts | 4 +- .../src/__tests__/integration/nockMocks.ts | 25 ++- gateway-js/src/config.ts | 17 ++- gateway-js/src/index.ts | 36 ++--- .../legacy/__tests__/serviceListShim.test.ts | 138 +++++++++++++++++ gateway-js/src/legacy/__tests__/tsconfig.json | 8 + gateway-js/src/legacy/serviceListShim.ts | 142 ++++++++++++++++++ .../src/loadServicesFromRemoteEndpoint.ts | 6 +- package-lock.json | 12 ++ 14 files changed, 421 insertions(+), 53 deletions(-) create mode 100644 gateway-js/src/legacy/__tests__/serviceListShim.test.ts create mode 100644 gateway-js/src/legacy/__tests__/tsconfig.json create mode 100644 gateway-js/src/legacy/serviceListShim.ts diff --git a/federation-integration-testsuite-js/src/fixtures/index.ts b/federation-integration-testsuite-js/src/fixtures/index.ts index f2bf9f40d4..bb089d8f19 100644 --- a/federation-integration-testsuite-js/src/fixtures/index.ts +++ b/federation-integration-testsuite-js/src/fixtures/index.ts @@ -7,8 +7,24 @@ import * as reviews from './reviews'; import * as reviewsWithUpdate from './special-cases/reviewsWithUpdate'; import * as accountsWithoutTag from './special-cases/accountsWithoutTag'; import * as reviewsWithoutTag from './special-cases/reviewsWithoutTag'; +import { DocumentNode } from 'graphql'; +import { GraphQLResolverMap } from 'apollo-graphql'; -const fixtures = [accounts, books, documents, inventory, product, reviews]; +export interface Fixture { + name: string; + url: string; + typeDefs: DocumentNode; + resolvers?: GraphQLResolverMap +} + +const fixtures: Fixture[] = [ + accounts, + books, + documents, + inventory, + product, + reviews, +]; const fixturesWithUpdate = [ accounts, diff --git a/federation-js/src/__tests__/joinSpec.test.ts b/federation-js/src/__tests__/joinSpec.test.ts index 1605e755d3..a7d23a5da1 100644 --- a/federation-js/src/__tests__/joinSpec.test.ts +++ b/federation-js/src/__tests__/joinSpec.test.ts @@ -1,7 +1,7 @@ import { fixtures } from 'apollo-federation-integration-testsuite'; import { getJoinDefinitions } from "../joinSpec"; -const questionableNamesRemap = { +const questionableNamesRemap: Record = { accounts: 'ServiceA', books: 'serviceA', documents: 'servicea_2', diff --git a/gateway-js/package.json b/gateway-js/package.json index 421b5f1870..88141fd3aa 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -37,6 +37,7 @@ "apollo-server-env": "^3.0.0 || ^4.0.0", "apollo-server-errors": "^2.5.0 || ^3.0.0", "apollo-server-types": "^0.9.0 || ^3.0.0", + "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", "pretty-format": "^27.3.1" diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 26417968a1..1bf8c2dbbd 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -57,18 +57,40 @@ describe('Using supergraphSdl static configuration', () => { }); describe('Using supergraphSdl dynamic configuration', () => { - it('starts and remains in `initialized` state until user Promise resolves', async () => { - const [forGatewayToHaveCalledSupergraphSdl, resolve] = waitUntil(); + it(`calls the user provided function after gateway.load() is called`, async () => { + const spy = jest.fn(async () => ({ + supergraphSdl: getTestingSupergraphSdl(), + })); + + const gateway = new ApolloGateway({ + supergraphSdl: spy, + }); + expect(spy).not.toHaveBeenCalled(); + await gateway.load(); + expect(spy).toHaveBeenCalled(); + }); + + it('starts and remains in `initialized` state until user Promise resolves', async () => { + const [promise, resolve] = waitUntil(); const gateway = new ApolloGateway({ async supergraphSdl() { - resolve(); - return new Promise(() => {}); + await promise; + return { + supergraphSdl: getTestingSupergraphSdl(), + }; }, }); - await forGatewayToHaveCalledSupergraphSdl; expect(gateway.__testing().state.phase).toEqual('initialized'); + + // If we await here, we'll get stuck. + const gatewayLoaded = gateway.load(); + expect(gateway.__testing().state.phase).toEqual('initialized'); + + resolve(); + await gatewayLoaded; + expect(gateway.__testing().state.phase).toEqual('loaded'); }); it('starts and waits in `initialized` state after calling load but before user Promise resolves', async () => { @@ -158,23 +180,18 @@ describe('Using supergraphSdl dynamic configuration', () => { describe('errors', () => { it('fails to load if user-provided `supergraphSdl` function throws', async () => { + const failureMessage = 'Error from supergraphSdl function'; const gateway = new ApolloGateway({ async supergraphSdl() { - throw new Error('supergraphSdl failed'); + throw new Error(failureMessage); }, logger, }); - await expect(() => - gateway.load(), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"User provided \`supergraphSdl\` function did not return an object containing a \`supergraphSdl\` property"`, - ); + await expect(() => gateway.load()).rejects.toThrowError(failureMessage); expect(gateway.__testing().state.phase).toEqual('failed to load'); - expect(logger.error).toHaveBeenCalledWith( - 'User-defined `supergraphSdl` function threw error: supergraphSdl failed', - ); + expect(logger.error).toHaveBeenCalledWith(failureMessage); }); it('gracefully handles Promise rejections from user `cleanup` function', async () => { diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index e02af0dd68..aa4bf75fac 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -2,7 +2,7 @@ import gql from 'graphql-tag'; import http from 'http'; import mockedEnv from 'mocked-env'; import { Logger } from 'apollo-server-types'; -import { ApolloGateway } from '../..'; +import { ApolloGateway, RemoteGraphQLDataSource } from '../..'; import { mockSdlQuerySuccess, mockSupergraphSdlRequestSuccess, @@ -12,12 +12,12 @@ import { mockCloudConfigUrl3, } from './nockMocks'; import { getTestingSupergraphSdl } from '../execution-utils'; -import { MockService } from './networkRequests.test'; +import { Fixture } from './networkRequests.test'; import { fixtures } from 'apollo-federation-integration-testsuite'; let logger: Logger; -const service: MockService = { +const service: Fixture = { name: 'accounts', url: 'http://localhost:4001', typeDefs: gql` @@ -413,4 +413,18 @@ describe('deprecation warnings', () => { 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); }); + + it('warns with `buildService` option set', async () => { + new ApolloGateway({ + serviceList: [{ name: 'accounts', url: 'http://localhost:4001' }], + buildService(definition) { + return new RemoteGraphQLDataSource({url: definition.url}); + }, + logger, + }); + + expect(logger.warn).toHaveBeenCalledWith( + 'The `buildService` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + }); }); diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index a86437f56a..2edce096fb 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -27,13 +27,13 @@ import { import { getTestingSupergraphSdl } from '../execution-utils'; import { nockAfterEach, nockBeforeEach } from '../nockAssertions'; -export interface MockService { +export interface Fixture { name: string; url: string; typeDefs: DocumentNode; } -const simpleService: MockService = { +const simpleService: Fixture = { name: 'accounts', url: 'http://localhost:4001', typeDefs: gql` diff --git a/gateway-js/src/__tests__/integration/nockMocks.ts b/gateway-js/src/__tests__/integration/nockMocks.ts index 2789825742..4938795382 100644 --- a/gateway-js/src/__tests__/integration/nockMocks.ts +++ b/gateway-js/src/__tests__/integration/nockMocks.ts @@ -1,10 +1,9 @@ import nock from 'nock'; -import { MockService } from './networkRequests.test'; import { HEALTH_CHECK_QUERY, SERVICE_DEFINITION_QUERY } from '../..'; import { SUPERGRAPH_SDL_QUERY } from '../../loadSupergraphSdlFromStorage'; import { getTestingSupergraphSdl } from '../../__tests__/execution-utils'; import { print } from 'graphql'; -import { fixtures } from 'apollo-federation-integration-testsuite'; +import { Fixture, fixtures as testingFixtures } from 'apollo-federation-integration-testsuite'; export const graphRef = 'federated-service@current'; export const apiKey = 'service:federated-service:DD71EBbGmsuh-6suUVDwnA'; @@ -19,31 +18,43 @@ export const mockApolloConfig = { }; // Service mocks -function mockSdlQuery({ url }: MockService) { +function mockSdlQuery({ url }: Fixture) { return nock(url).post('/', { query: SERVICE_DEFINITION_QUERY, }); } -export function mockSdlQuerySuccess(service: MockService) { +export function mockSdlQuerySuccess(service: Fixture) { return mockSdlQuery(service).reply(200, { data: { _service: { sdl: print(service.typeDefs) } }, }); } -export function mockServiceHealthCheck({ url }: MockService) { +export function mockAllServicesSdlQuerySuccess( + fixtures: Fixture[] = testingFixtures, +) { + return fixtures.map((fixture) => + mockSdlQuery(fixture).reply(200, { + data: { _service: { sdl: print(fixture.typeDefs) } }, + }), + ); +} + +export function mockServiceHealthCheck({ url }: Fixture) { return nock(url).post('/', { query: HEALTH_CHECK_QUERY, }); } -export function mockServiceHealthCheckSuccess(service: MockService) { +export function mockServiceHealthCheckSuccess(service: Fixture) { return mockServiceHealthCheck(service).reply(200, { data: { __typename: 'Query' }, }); } -export function mockAllServicesHealthCheckSuccess() { +export function mockAllServicesHealthCheckSuccess( + fixtures: Fixture[] = testingFixtures, +) { return fixtures.map((fixture) => mockServiceHealthCheck(fixture).reply(200, { data: { __typename: 'Query' }, diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 470bb3e66d..12ee3c437b 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -179,13 +179,20 @@ export function isManuallyManagedSupergraphSdlGatewayConfig( export interface SupergraphSdlUpdateFunction { (updatedSupergraphSdl: string): Promise } -export interface SupergraphSdlUpdateOptions { +export interface SupergraphSdlHookOptions { update: SupergraphSdlUpdateFunction; } -interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { - supergraphSdl: ( - opts: SupergraphSdlUpdateOptions, - ) => Promise<{ supergraphSdl: string; cleanup?: () => Promise }>; + +export type SupergraphSdlHookReturn = Promise<{ + supergraphSdl: string; + cleanup?: () => Promise; +}>; + +export interface SupergraphSdlHook { + (options: SupergraphSdlHookOptions): SupergraphSdlHookReturn; +} +export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { + supergraphSdl: SupergraphSdlHook; } type ManuallyManagedGatewayConfig = diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 497b34b707..e18d21c973 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -70,6 +70,7 @@ import { SupergraphSdlUpdate, CompositionUpdate, isManuallyManagedSupergraphSdlGatewayConfig, + ManuallyManagedSupergraphSdlGatewayConfig, } from './config'; import { buildComposedSchema } from '@apollo/query-planner'; import { loadSupergraphSdlFromUplinks } from './loadSupergraphSdlFromStorage'; @@ -216,6 +217,7 @@ export class ApolloGateway implements GraphQLService { supergraphSdl: string; cleanup?: () => Promise; } | void>; + // Functions to call during gateway cleanup (when stop() is called) private toDispose: (() => Promise)[] = []; constructor(config?: GatewayConfig) { @@ -272,15 +274,7 @@ export class ApolloGateway implements GraphQLService { this.updateServiceDefinitions = this.config.experimental_updateServiceDefinitions; } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { - this.manualConfigPromise = this.config - .supergraphSdl({ update: this.updateWithSupergraphSdl.bind(this) }) - .catch((e) => { - // Not swallowing the error here results in an uncaught rejection. - // An error will be thrown when this promise resolves to nothing. - this.logger.error( - 'User-defined `supergraphSdl` function threw error: ' + e.message, - ); - }); + // TODO: do nothing maybe? } else { throw Error( 'Programming error: unexpected manual configuration provided', @@ -398,6 +392,13 @@ export class ApolloGateway implements GraphQLService { 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); } + + // TODO(trevor:removeServiceList) + if ('buildService' in this.config) { + this.logger.warn( + 'The `buildService` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + } } public async load(options?: { @@ -463,7 +464,7 @@ export class ApolloGateway implements GraphQLService { if (isStaticConfig(this.config)) { this.loadStatic(this.config); } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { - await this.loadManuallyManaged(); + await this.loadManuallyManaged(this.config); } else { await this.loadDynamic(unrefTimer); } @@ -519,18 +520,22 @@ export class ApolloGateway implements GraphQLService { } } - private async loadManuallyManaged() { + private async loadManuallyManaged(config: ManuallyManagedSupergraphSdlGatewayConfig) { try { - const result = await this.manualConfigPromise; - if (!result?.supergraphSdl) + const result = await config.supergraphSdl({ + update: this.updateWithSupergraphSdl.bind(this), + }); + if (!result?.supergraphSdl) { throw new Error( 'User provided `supergraphSdl` function did not return an object containing a `supergraphSdl` property', ); + } if (result?.cleanup) { this.toDispose.push(result.cleanup); } await this.updateWithSupergraphSdl(result.supergraphSdl); } catch (e) { + this.logger.error(e.message ?? e); this.state = { phase: 'failed to load' }; throw e; } @@ -1427,7 +1432,4 @@ export { export * from './datasources'; -export { - SupergraphSdlUpdateOptions, - SupergraphSdlUpdateFunction, -} from './config'; +export { SupergraphSdlUpdateFunction } from './config'; diff --git a/gateway-js/src/legacy/__tests__/serviceListShim.test.ts b/gateway-js/src/legacy/__tests__/serviceListShim.test.ts new file mode 100644 index 0000000000..1cd5d085df --- /dev/null +++ b/gateway-js/src/legacy/__tests__/serviceListShim.test.ts @@ -0,0 +1,138 @@ +import nock from 'nock'; +import { fixtures, fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; +import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; +import { ServiceListShim } from '../serviceListShim'; +import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; +import { wait, waitUntil } from '../../__tests__/execution-utils'; + +describe('ServiceListShim', () => { + beforeEach(async () => { + if (!nock.isActive()) nock.activate(); + }); + afterEach(async () => { + expect(nock.isDone()).toBeTruthy(); + nock.cleanAll(); + nock.restore(); + }); + + it('constructs', () => { + expect( + () => + new ServiceListShim({ + serviceList: fixtures, + }), + ).not.toThrow(); + }); + + it('is instance callable (simulating the gateway calling it)', async () => { + mockAllServicesSdlQuerySuccess(); + const shim = new ServiceListShim({ serviceList: fixtures }); + await expect(shim({ async update() {} })).resolves.toBeTruthy(); + }); + + function getDataSourceSpy(definition: ServiceEndpointDefinition) { + const datasource = new RemoteGraphQLDataSource({ + url: definition.url, + }); + const processSpy = jest.fn(datasource.process); + datasource.process = processSpy; + return { datasource, processSpy }; + } + + it('uses `GraphQLDataSource`s provided by the `buildService` function', async () => { + mockAllServicesSdlQuerySuccess(); + + const processSpies: jest.Mock[] = []; + + const shim = new ServiceListShim({ + serviceList: fixtures, + buildService(def) { + const { datasource, processSpy } = getDataSourceSpy(def); + processSpies.push(processSpy); + return datasource; + }, + }); + + await shim({ async update() {} }); + + expect(processSpies.length).toBe(fixtures.length); + for (const processSpy of processSpies) { + expect(processSpy).toHaveBeenCalledTimes(1); + } + }); + + it('polls services when a `pollInterval` is set and stops when `cleanup` is called', async () => { + // This is mocked 4 times to include the initial load (followed by 3 polls) + // We need to alternate schemas, else the update will be ignored + mockAllServicesSdlQuerySuccess(); + mockAllServicesSdlQuerySuccess(fixturesWithUpdate); + mockAllServicesSdlQuerySuccess(); + mockAllServicesSdlQuerySuccess(fixturesWithUpdate); + + const [p1, r1] = waitUntil(); + const [p2, r2] = waitUntil(); + const [p3, r3] = waitUntil(); + + // `update` (below) is called each time we poll (and there's an update to + // the supergraph), so this is a reasonable hook into "when" the poll + // happens and drives this test cleanly with `Promise`s. + const updateSpy = jest + .fn() + .mockImplementationOnce(() => r1()) + .mockImplementationOnce(() => r2()) + .mockImplementationOnce(() => r3()); + + const shim = new ServiceListShim({ + serviceList: fixtures, + pollIntervalInMs: 10, + }); + + const { cleanup } = await shim({ + async update(supergraphSdl) { + updateSpy(supergraphSdl); + }, + }); + + await Promise.all([p1, p2, p3]); + + expect(updateSpy).toHaveBeenCalledTimes(3); + + // stop polling + await cleanup!(); + + // ensure we cancelled the timer + // @ts-ignore + expect(shim.timerRef).toBe(null); + }); + + // TODO: useFakeTimers (though I'm struggling to get this to work as expected) + it("doesn't call `update` when there's no change to the supergraph", async () => { + // mock for initial load and a few polls against an unchanging schema + mockAllServicesSdlQuerySuccess(); + mockAllServicesSdlQuerySuccess(); + mockAllServicesSdlQuerySuccess(); + mockAllServicesSdlQuerySuccess(); + + const shim = new ServiceListShim({ + serviceList: fixtures, + pollIntervalInMs: 100, + }); + + const updateSpy = jest.fn(); + const { cleanup } = await shim({ + async update(supergraphSdl) { + updateSpy(supergraphSdl); + }, + }); + + // let the shim poll through all the active mocks + while (nock.activeMocks().length > 0) { + await wait(10); + } + + // stop polling + await cleanup!(); + + expect(updateSpy).toHaveBeenCalledTimes(0); + }); +}); diff --git a/gateway-js/src/legacy/__tests__/tsconfig.json b/gateway-js/src/legacy/__tests__/tsconfig.json new file mode 100644 index 0000000000..0a2bbf99d9 --- /dev/null +++ b/gateway-js/src/legacy/__tests__/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.test", + "include": ["**/*"], + "references": [ + { "path": "../../../" }, + { "path": "../../../../federation-integration-testsuite-js" }, + ] +} diff --git a/gateway-js/src/legacy/serviceListShim.ts b/gateway-js/src/legacy/serviceListShim.ts new file mode 100644 index 0000000000..a17f7c9bbd --- /dev/null +++ b/gateway-js/src/legacy/serviceListShim.ts @@ -0,0 +1,142 @@ +import { + composeAndValidate, + compositionHasErrors, + ServiceDefinition, +} from '@apollo/federation'; +import CallableInstance from 'callable-instance'; +import { HeadersInit } from 'node-fetch'; +import { + GraphQLDataSource, + RemoteGraphQLDataSource, + ServiceEndpointDefinition, + SupergraphSdlUpdateFunction, +} from '..'; +import { SupergraphSdlHookOptions, SupergraphSdlHookReturn } from '../config'; +import { + getServiceDefinitionsFromRemoteEndpoint, + Service, +} from '../loadServicesFromRemoteEndpoint'; + +export interface ServiceListShimOptions { + serviceList: ServiceEndpointDefinition[]; + introspectionHeaders?: + | HeadersInit + | (( + service: ServiceEndpointDefinition, + ) => Promise | HeadersInit); + buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; + pollIntervalInMs?: number; +} + +export class ServiceListShim extends CallableInstance< + [SupergraphSdlHookOptions], + SupergraphSdlHookReturn +> { + private update?: SupergraphSdlUpdateFunction; + private serviceList: Service[]; + private introspectionHeaders?: + | HeadersInit + | (( + service: ServiceEndpointDefinition, + ) => Promise | HeadersInit); + private buildService?: ( + definition: ServiceEndpointDefinition, + ) => GraphQLDataSource; + private serviceSdlCache: Map = new Map(); + private pollIntervalInMs?: number; + private timerRef: NodeJS.Timeout | null = null; + + constructor(options: ServiceListShimOptions) { + super('instanceCallableMethod'); + // this.buildService needs to be assigned before this.serviceList is built + this.buildService = options.buildService; + this.pollIntervalInMs = options.pollIntervalInMs; + this.serviceList = options.serviceList.map((serviceDefinition) => ({ + ...serviceDefinition, + dataSource: this.createDataSource(serviceDefinition), + })); + this.introspectionHeaders = options.introspectionHeaders; + } + + // @ts-ignore noUsedLocals + private async instanceCallableMethod( + ...[{ update }]: [SupergraphSdlHookOptions] + ) { + this.update = update; + + const initialSupergraphSdl = await this.updateSupergraphSdl(); + // Start polling after we resolve the first supergraph + if (this.pollIntervalInMs) { + this.beginPolling(); + } + + return { + supergraphSdl: initialSupergraphSdl, + cleanup: async () => { + if (this.timerRef) { + this.timerRef.unref(); + clearInterval(this.timerRef); + this.timerRef = null; + } + }, + }; + } + + private async updateSupergraphSdl() { + const result = await getServiceDefinitionsFromRemoteEndpoint({ + serviceList: this.serviceList, + getServiceIntrospectionHeaders: async (service) => { + return typeof this.introspectionHeaders === 'function' + ? await this.introspectionHeaders(service) + : this.introspectionHeaders; + }, + serviceSdlCache: this.serviceSdlCache, + }); + + if (!result.isNewSchema) { + return null; + } + + return this.createSupergraphFromServiceList(result.serviceDefinitions!); + } + + private createDataSource( + serviceDef: ServiceEndpointDefinition, + ): GraphQLDataSource { + return ( + this.buildService?.(serviceDef) ?? + new RemoteGraphQLDataSource({ + url: serviceDef.url, + }) + ); + } + + private createSupergraphFromServiceList(serviceList: ServiceDefinition[]) { + const compositionResult = composeAndValidate(serviceList); + + if (compositionHasErrors(compositionResult)) { + const { errors } = compositionResult; + throw Error( + "A valid schema couldn't be composed. The following composition errors were found:\n" + + errors.map((e) => '\t' + e.message).join('\n'), + ); + } else { + const { supergraphSdl } = compositionResult; + return supergraphSdl; + } + } + + private beginPolling() { + this.poll(); + } + + private poll() { + this.timerRef = global.setTimeout(async () => { + const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); + if (maybeNewSupergraphSdl) { + this.update?.(maybeNewSupergraphSdl); + } + this.poll(); + }, this.pollIntervalInMs!); + } +} diff --git a/gateway-js/src/loadServicesFromRemoteEndpoint.ts b/gateway-js/src/loadServicesFromRemoteEndpoint.ts index 0936e75128..171951089b 100644 --- a/gateway-js/src/loadServicesFromRemoteEndpoint.ts +++ b/gateway-js/src/loadServicesFromRemoteEndpoint.ts @@ -3,10 +3,10 @@ import { parse } from 'graphql'; import { Headers, HeadersInit } from 'node-fetch'; import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types'; import { SERVICE_DEFINITION_QUERY } from './'; -import { CompositionUpdate, ServiceEndpointDefinition } from './config'; +import { ServiceDefinitionUpdate, ServiceEndpointDefinition } from './config'; import { ServiceDefinition } from '@apollo/federation'; -type Service = ServiceEndpointDefinition & { +export type Service = ServiceEndpointDefinition & { dataSource: GraphQLDataSource; }; @@ -20,7 +20,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({ service: ServiceEndpointDefinition, ) => Promise; serviceSdlCache: Map; -}): Promise { +}): Promise { if (!serviceList || !serviceList.length) { throw new Error( 'Tried to load services from remote endpoints but none provided', diff --git a/package-lock.json b/package-lock.json index 28ebb37554..94ea1f3a8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,6 +162,7 @@ "apollo-server-env": "^3.0.0 || ^4.0.0", "apollo-server-errors": "^2.5.0 || ^3.0.0", "apollo-server-types": "^0.9.0 || ^3.0.0", + "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", "pretty-format": "^27.3.1" @@ -8149,6 +8150,11 @@ "get-intrinsic": "^1.0.2" } }, + "node_modules/callable-instance": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-2.0.0.tgz", + "integrity": "sha512-wOBp/J1CRZLsbFxG1alxefJjoG1BW/nocXkUanAe2+leiD/+cVr00j8twSZoDiRy03o5vibq9pbrZc+EDjjUTw==" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -23657,6 +23663,7 @@ "apollo-server-env": "^3.0.0 || ^4.0.0", "apollo-server-errors": "^2.5.0 || ^3.0.0", "apollo-server-types": "^0.9.0 || ^3.0.0", + "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", "pretty-format": "^27.3.1" @@ -30214,6 +30221,11 @@ "get-intrinsic": "^1.0.2" } }, + "callable-instance": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-2.0.0.tgz", + "integrity": "sha512-wOBp/J1CRZLsbFxG1alxefJjoG1BW/nocXkUanAe2+leiD/+cVr00j8twSZoDiRy03o5vibq9pbrZc+EDjjUTw==" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", From 748d4334464b20abbc779092443747509dd35b1a Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 1 Dec 2021 20:01:45 -0800 Subject: [PATCH 05/82] Use createHash from apollo-graphql --- gateway-js/src/__tests__/gateway/supergraphSdl.test.ts | 2 +- gateway-js/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 1bf8c2dbbd..b798f0f652 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -1,6 +1,6 @@ import { ApolloGateway, SupergraphSdlUpdateFunction } from '@apollo/gateway'; import { fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; -import { createHash } from 'crypto'; +import { createHash } from 'apollo-graphql/lib/utilities/createHash'; import { ApolloServer } from 'apollo-server'; import { Logger } from 'apollo-server-types'; import { fetch } from '../../__mocks__/apollo-server-env'; diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index e18d21c973..9293905190 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -78,7 +78,7 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { OpenTelemetrySpanNames, tracer } from './utilities/opentelemetry'; import { CoreSchema } from '@apollo/core-schema'; import { featureSupport } from './core'; -import { createHash } from 'crypto'; +import { createHash } from 'apollo-graphql/lib/utilities/createHash'; type DataSourceMap = { [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; From f7006419aed3f4dcde067aed919868dde4783ccc Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 3 Dec 2021 15:44:07 -0800 Subject: [PATCH 06/82] Refactor health checks and provide external health check hook to new update function --- .../__tests__/gateway/supergraphSdl.test.ts | 112 +++++++++++--- gateway-js/src/config.ts | 21 ++- gateway-js/src/index.ts | 137 +++++++++++------- .../legacy/__tests__/serviceListShim.test.ts | 8 +- gateway-js/src/legacy/serviceListShim.ts | 8 +- 5 files changed, 189 insertions(+), 97 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index b798f0f652..9a66e8ac71 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -1,10 +1,18 @@ -import { ApolloGateway, SupergraphSdlUpdateFunction } from '@apollo/gateway'; +import { + ApolloGateway, + SubgraphHealthCheckFunction, + SupergraphSdlUpdateFunction, +} from '@apollo/gateway'; import { fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; import { createHash } from 'apollo-graphql/lib/utilities/createHash'; import { ApolloServer } from 'apollo-server'; import { Logger } from 'apollo-server-types'; import { fetch } from '../../__mocks__/apollo-server-env'; import { getTestingSupergraphSdl, waitUntil } from '../execution-utils'; +import { + mockAllServicesHealthCheckSuccess, + mockAllServicesSdlQuerySuccess, +} from '../integration/nockMocks'; async function getSupergraphSdlGatewayServer() { const server = new ApolloServer({ @@ -18,6 +26,7 @@ async function getSupergraphSdlGatewayServer() { } let logger: Logger; +let gateway: ApolloGateway | null; beforeEach(() => { logger = { debug: jest.fn(), @@ -27,6 +36,13 @@ beforeEach(() => { }; }); +afterEach(async () => { + if (gateway) { + await gateway.stop(); + gateway = null; + } +}); + describe('Using supergraphSdl static configuration', () => { it('successfully starts and serves requests to the proper services', async () => { const server = await getSupergraphSdlGatewayServer(); @@ -57,25 +73,33 @@ describe('Using supergraphSdl static configuration', () => { }); describe('Using supergraphSdl dynamic configuration', () => { - it(`calls the user provided function after gateway.load() is called`, async () => { - const spy = jest.fn(async () => ({ + it('calls the user provided function after `gateway.load()` is called', async () => { + const callbackSpy = jest.fn(async () => ({ supergraphSdl: getTestingSupergraphSdl(), })); const gateway = new ApolloGateway({ - supergraphSdl: spy, + supergraphSdl: callbackSpy, }); - expect(spy).not.toHaveBeenCalled(); + expect(callbackSpy).not.toHaveBeenCalled(); await gateway.load(); - expect(spy).toHaveBeenCalled(); + expect(callbackSpy).toHaveBeenCalled(); }); - it('starts and remains in `initialized` state until user Promise resolves', async () => { - const [promise, resolve] = waitUntil(); + it('starts and remains in `initialized` state until `supergraphSdl` Promise resolves', async () => { + const [ + promiseGuaranteeingWeAreInTheCallback, + resolvePromiseGuaranteeingWeAreInTheCallback, + ] = waitUntil(); + const [ + promiseGuaranteeingWeStayInTheCallback, + resolvePromiseGuaranteeingWeStayInTheCallback, + ] = waitUntil(); const gateway = new ApolloGateway({ async supergraphSdl() { - await promise; + resolvePromiseGuaranteeingWeAreInTheCallback(); + await promiseGuaranteeingWeStayInTheCallback; return { supergraphSdl: getTestingSupergraphSdl(), }; @@ -84,27 +108,15 @@ describe('Using supergraphSdl dynamic configuration', () => { expect(gateway.__testing().state.phase).toEqual('initialized'); - // If we await here, we'll get stuck. const gatewayLoaded = gateway.load(); + await promiseGuaranteeingWeAreInTheCallback; expect(gateway.__testing().state.phase).toEqual('initialized'); - resolve(); + resolvePromiseGuaranteeingWeStayInTheCallback(); await gatewayLoaded; expect(gateway.__testing().state.phase).toEqual('loaded'); }); - it('starts and waits in `initialized` state after calling load but before user Promise resolves', async () => { - const gateway = new ApolloGateway({ - async supergraphSdl() { - return new Promise(() => {}); - }, - }); - - gateway.load(); - - expect(gateway.__testing().state.phase).toEqual('initialized'); - }); - it('moves from `initialized` to `loaded` state after calling `load()` and after user Promise resolves', async () => { const [userPromise, resolveSupergraph] = waitUntil<{ supergraphSdl: string }>(); @@ -152,7 +164,8 @@ describe('Using supergraphSdl dynamic configuration', () => { const expectedUpdatedId = createHash('sha256') .update(updatedSupergraphSdl) .digest('hex'); - await userUpdateFn!(updatedSupergraphSdl); + + userUpdateFn!(updatedSupergraphSdl); expect(gateway.__testing().compositionId).toEqual(expectedUpdatedId); }); @@ -178,6 +191,31 @@ describe('Using supergraphSdl dynamic configuration', () => { expect(cleanup).toHaveBeenCalledTimes(1); }); + it('performs a successful health check on subgraphs', async () => { + mockAllServicesSdlQuerySuccess(); + mockAllServicesHealthCheckSuccess(); + + let healthCheckCallback: SubgraphHealthCheckFunction; + const supergraphSdl = getTestingSupergraphSdl(); + const gateway = new ApolloGateway({ + async supergraphSdl({ healthCheck }) { + healthCheckCallback = healthCheck; + return { + supergraphSdl, + }; + }, + }); + + await gateway.load(); + const { state, compositionId } = gateway.__testing(); + expect(state.phase).toEqual('loaded'); + expect(compositionId).toEqual( + '562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b', + ); + + await expect(healthCheckCallback!(supergraphSdl)).resolves.toBeUndefined(); + }); + describe('errors', () => { it('fails to load if user-provided `supergraphSdl` function throws', async () => { const failureMessage = 'Error from supergraphSdl function'; @@ -215,5 +253,31 @@ describe('Using supergraphSdl dynamic configuration', () => { rejectionMessage, ); }); + + it('throws an error when `healthCheck` rejects', async () => { + mockAllServicesSdlQuerySuccess(); + + let healthCheckCallback: SubgraphHealthCheckFunction; + const supergraphSdl = getTestingSupergraphSdl(); + const gateway = new ApolloGateway({ + async supergraphSdl({ healthCheck }) { + healthCheckCallback = healthCheck; + return { + supergraphSdl, + }; + }, + }); + + await gateway.load(); + const { state, compositionId } = gateway.__testing(); + expect(state.phase).toEqual('loaded'); + expect(compositionId).toEqual( + '562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b', + ); + + await expect(healthCheckCallback!(supergraphSdl)).rejects.toThrowError( + /The gateway subgraphs health check failed. Updating to the provided `supergraphSdl` will likely result in future request failures to subgraphs. The following error occurred during the health check/, + ); + }); }); }); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 12ee3c437b..b744c6f580 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -176,20 +176,17 @@ export function isManuallyManagedSupergraphSdlGatewayConfig( ); } -export interface SupergraphSdlUpdateFunction { - (updatedSupergraphSdl: string): Promise -} -export interface SupergraphSdlHookOptions { - update: SupergraphSdlUpdateFunction; -} - -export type SupergraphSdlHookReturn = Promise<{ - supergraphSdl: string; - cleanup?: () => Promise; -}>; +export type SupergraphSdlUpdateFunction = (updatedSupergraphSdl: string) => void; +export type SubgraphHealthCheckFunction = (supergraphSdl: string) => Promise; export interface SupergraphSdlHook { - (options: SupergraphSdlHookOptions): SupergraphSdlHookReturn; + (options: { + update: SupergraphSdlUpdateFunction; + healthCheck: SubgraphHealthCheckFunction; + }): Promise<{ + supergraphSdl: string; + cleanup?: () => Promise; + }>; } export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { supergraphSdl: SupergraphSdlHook; diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 9293905190..7a8eb278bc 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -520,10 +520,17 @@ export class ApolloGateway implements GraphQLService { } } - private async loadManuallyManaged(config: ManuallyManagedSupergraphSdlGatewayConfig) { + private getIdForSupergraphSdl(supergraphSdl: string) { + return createHash('sha256').update(supergraphSdl).digest('hex'); + } + + private async loadManuallyManaged( + config: ManuallyManagedSupergraphSdlGatewayConfig, + ) { try { const result = await config.supergraphSdl({ - update: this.updateWithSupergraphSdl.bind(this), + update: this.externalSupergraphUpdateCallback.bind(this), + healthCheck: this.externalSubgraphHealthCheckCallback.bind(this), }); if (!result?.supergraphSdl) { throw new Error( @@ -533,7 +540,8 @@ export class ApolloGateway implements GraphQLService { if (result?.cleanup) { this.toDispose.push(result.cleanup); } - await this.updateWithSupergraphSdl(result.supergraphSdl); + + this.externalSupergraphUpdateCallback(result.supergraphSdl); } catch (e) { this.logger.error(e.message ?? e); this.state = { phase: 'failed to load' }; @@ -558,10 +566,11 @@ export class ApolloGateway implements GraphQLService { // This may throw, but an error here is caught and logged upstream const result = await this.updateServiceDefinitions(this.config); + await this.maybePerformServiceHealthCheck(result); if (isSupergraphSdlUpdate(result)) { - await this.updateWithSupergraphSdl(result); + this.updateWithSupergraphSdl(result); } else if (isServiceDefinitionUpdate(result)) { - await this.updateByComposition(result); + this.updateByComposition(result); } else { throw new Error( 'Programming error: unexpected result type from `updateServiceDefinitions`', @@ -569,9 +578,7 @@ export class ApolloGateway implements GraphQLService { } } - private async updateByComposition( - result: ServiceDefinitionUpdate, - ): Promise { + private updateByComposition(result: ServiceDefinitionUpdate) { if ( !result.serviceDefinitions || JSON.stringify(this.serviceDefinitions) === @@ -588,9 +595,6 @@ export class ApolloGateway implements GraphQLService { if (previousSchema) { this.logger.info('New service definitions were found.'); } - - await this.maybePerformServiceHealthCheck(result); - this.compositionMetadata = result.compositionMetadata; this.serviceDefinitions = result.serviceDefinitions; @@ -627,22 +631,43 @@ export class ApolloGateway implements GraphQLService { } } - private async updateWithSupergraphSdl( - result: SupergraphSdlUpdate | string, - ): Promise { - const supergraphSdl = - typeof result === 'string' ? result : result.supergraphSdl; + private externalSupergraphUpdateCallback(supergraphSdl: string) { + this.updateWithSupergraphSdl({ + supergraphSdl, + id: this.getIdForSupergraphSdl(supergraphSdl), + }); + } - const id = - typeof result === 'string' - ? createHash('sha256').update(result).digest('hex') - : result.id; + private async externalSubgraphHealthCheckCallback(supergraphSdl: string) { + const parsedSupergraphSdl = + supergraphSdl === this.supergraphSdl + ? this.parsedSupergraphSdl + : parse(supergraphSdl); + + const serviceList = this.serviceListFromSupergraphSdl(parsedSupergraphSdl!); + // Here we need to construct new datasources based on the new schema info + // so we can check the health of the services we're _updating to_. + const serviceMap = serviceList.reduce((serviceMap, serviceDef) => { + serviceMap[serviceDef.name] = { + url: serviceDef.url, + dataSource: this.createDataSource(serviceDef), + }; + return serviceMap; + }, Object.create(null) as DataSourceMap); - if (id === this.compositionId) { - this.logger.debug('No change in composition since last check.'); - return; + try { + await this.serviceHealthCheck(serviceMap); + } catch (e) { + throw new Error( + 'The gateway subgraphs health check failed. Updating to the provided ' + + '`supergraphSdl` will likely result in future request failures to ' + + 'subgraphs. The following error occurred during the health check:\n' + + e.message, + ); } + } + private updateWithSupergraphSdl({ supergraphSdl, id }: SupergraphSdlUpdate) { // TODO(trevor): #580 redundant parse // This may throw, so we'll calculate early (specifically before making any updates) // In the case that it throws, the gateway will: @@ -658,8 +683,6 @@ export class ApolloGateway implements GraphQLService { this.logger.info('Updated Supergraph SDL was found.'); } - await this.maybePerformServiceHealthCheck({ supergraphSdl, id }); - this.compositionId = id; this.supergraphSdl = supergraphSdl; this.parsedSupergraphSdl = parsedSupergraphSdl; @@ -678,7 +701,7 @@ export class ApolloGateway implements GraphQLService { this.experimental_didUpdateComposition( { compositionId: id, - supergraphSdl: supergraphSdl, + supergraphSdl, schema, }, previousCompositionId && previousSupergraphSdl && previousSchema @@ -741,34 +764,34 @@ export class ApolloGateway implements GraphQLService { private async maybePerformServiceHealthCheck(update: CompositionUpdate) { // Run service health checks before we commit and update the new schema. // This is the last chance to bail out of a schema update. - if (this.config.serviceHealthCheck) { - const serviceList = isSupergraphSdlUpdate(update) - ? // TODO(trevor): #580 redundant parse - // Parsing could technically fail and throw here, but parseability has - // already been confirmed slightly earlier in the code path - this.serviceListFromSupergraphSdl(parse(update.supergraphSdl)) - : // Existence of this is determined in advance with an early return otherwise - update.serviceDefinitions!; - // Here we need to construct new datasources based on the new schema info - // so we can check the health of the services we're _updating to_. - const serviceMap = serviceList.reduce((serviceMap, serviceDef) => { - serviceMap[serviceDef.name] = { - url: serviceDef.url, - dataSource: this.createDataSource(serviceDef), - }; - return serviceMap; - }, Object.create(null) as DataSourceMap); + if (!this.config.serviceHealthCheck) return; + + const serviceList = isSupergraphSdlUpdate(update) + ? // TODO(trevor): #580 redundant parse + // Parsing could technically fail and throw here, but parseability has + // already been confirmed slightly earlier in the code path + this.serviceListFromSupergraphSdl(parse(update.supergraphSdl)) + : // Existence of this is determined in advance with an early return otherwise + update.serviceDefinitions!; + // Here we need to construct new datasources based on the new schema info + // so we can check the health of the services we're _updating to_. + const serviceMap = serviceList.reduce((serviceMap, serviceDef) => { + serviceMap[serviceDef.name] = { + url: serviceDef.url, + dataSource: this.createDataSource(serviceDef), + }; + return serviceMap; + }, Object.create(null) as DataSourceMap); - try { - await this.serviceHealthCheck(serviceMap); - } catch (e) { - throw new Error( - 'The gateway did not update its schema due to failed service health checks. ' + - 'The gateway will continue to operate with the previous schema and reattempt updates. ' + - 'The following error occurred during the health check:\n' + - e.message, - ); - } + try { + await this.serviceHealthCheck(serviceMap); + } catch (e) { + throw new Error( + 'The gateway did not update its schema due to failed service health checks. ' + + 'The gateway will continue to operate with the previous schema and reattempt updates. ' + + 'The following error occurred during the health check:\n' + + e.message, + ); } } @@ -1375,7 +1398,7 @@ export class ApolloGateway implements GraphQLService { state: this.state, compositionId: this.compositionId, supergraphSdl: this.supergraphSdl, - } + }; } } @@ -1432,4 +1455,8 @@ export { export * from './datasources'; -export { SupergraphSdlUpdateFunction } from './config'; +export { + SupergraphSdlUpdateFunction, + SubgraphHealthCheckFunction, + SupergraphSdlHook, +} from './config'; diff --git a/gateway-js/src/legacy/__tests__/serviceListShim.test.ts b/gateway-js/src/legacy/__tests__/serviceListShim.test.ts index 1cd5d085df..1436c8b532 100644 --- a/gateway-js/src/legacy/__tests__/serviceListShim.test.ts +++ b/gateway-js/src/legacy/__tests__/serviceListShim.test.ts @@ -27,7 +27,9 @@ describe('ServiceListShim', () => { it('is instance callable (simulating the gateway calling it)', async () => { mockAllServicesSdlQuerySuccess(); const shim = new ServiceListShim({ serviceList: fixtures }); - await expect(shim({ async update() {} })).resolves.toBeTruthy(); + await expect( + shim({ async update() {}, async healthCheck() {} }), + ).resolves.toBeTruthy(); }); function getDataSourceSpy(definition: ServiceEndpointDefinition) { @@ -53,7 +55,7 @@ describe('ServiceListShim', () => { }, }); - await shim({ async update() {} }); + await shim({ async update() {}, async healthCheck() {} }); expect(processSpies.length).toBe(fixtures.length); for (const processSpy of processSpies) { @@ -91,6 +93,7 @@ describe('ServiceListShim', () => { async update(supergraphSdl) { updateSpy(supergraphSdl); }, + async healthCheck() {}, }); await Promise.all([p1, p2, p3]); @@ -123,6 +126,7 @@ describe('ServiceListShim', () => { async update(supergraphSdl) { updateSpy(supergraphSdl); }, + async healthCheck() {}, }); // let the shim poll through all the active mocks diff --git a/gateway-js/src/legacy/serviceListShim.ts b/gateway-js/src/legacy/serviceListShim.ts index a17f7c9bbd..4a6dbfb409 100644 --- a/gateway-js/src/legacy/serviceListShim.ts +++ b/gateway-js/src/legacy/serviceListShim.ts @@ -9,9 +9,9 @@ import { GraphQLDataSource, RemoteGraphQLDataSource, ServiceEndpointDefinition, + SupergraphSdlHook, SupergraphSdlUpdateFunction, } from '..'; -import { SupergraphSdlHookOptions, SupergraphSdlHookReturn } from '../config'; import { getServiceDefinitionsFromRemoteEndpoint, Service, @@ -29,8 +29,8 @@ export interface ServiceListShimOptions { } export class ServiceListShim extends CallableInstance< - [SupergraphSdlHookOptions], - SupergraphSdlHookReturn + Parameters, + ReturnType > { private update?: SupergraphSdlUpdateFunction; private serviceList: Service[]; @@ -60,7 +60,7 @@ export class ServiceListShim extends CallableInstance< // @ts-ignore noUsedLocals private async instanceCallableMethod( - ...[{ update }]: [SupergraphSdlHookOptions] + ...[{ update }]: Parameters ) { this.update = update; From db77d2fa3cafcb622f6f8ba6a574c2228021bc0f Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 3 Dec 2021 15:47:06 -0800 Subject: [PATCH 07/82] Fix regex --- gateway-js/src/__tests__/gateway/supergraphSdl.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 9a66e8ac71..8e383b2841 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -276,7 +276,7 @@ describe('Using supergraphSdl dynamic configuration', () => { ); await expect(healthCheckCallback!(supergraphSdl)).rejects.toThrowError( - /The gateway subgraphs health check failed. Updating to the provided `supergraphSdl` will likely result in future request failures to subgraphs. The following error occurred during the health check/, + /The gateway subgraphs health check failed\. Updating to the provided `supergraphSdl` will likely result in future request failures to subgraphs\. The following error occurred during the health check/, ); }); }); From 3e3aa08e110da4c60bd3bff6574a22fc7a527a10 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 3 Dec 2021 19:51:09 -0800 Subject: [PATCH 08/82] Fix tests --- .../__tests__/gateway/supergraphSdl.test.ts | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 8e383b2841..023812a216 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -7,12 +7,10 @@ import { fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; import { createHash } from 'apollo-graphql/lib/utilities/createHash'; import { ApolloServer } from 'apollo-server'; import { Logger } from 'apollo-server-types'; +import nock from 'nock'; import { fetch } from '../../__mocks__/apollo-server-env'; import { getTestingSupergraphSdl, waitUntil } from '../execution-utils'; -import { - mockAllServicesHealthCheckSuccess, - mockAllServicesSdlQuerySuccess, -} from '../integration/nockMocks'; +import { mockAllServicesHealthCheckSuccess } from '../integration/nockMocks'; async function getSupergraphSdlGatewayServer() { const server = new ApolloServer({ @@ -28,6 +26,8 @@ async function getSupergraphSdlGatewayServer() { let logger: Logger; let gateway: ApolloGateway | null; beforeEach(() => { + if (!nock.isActive()) nock.activate(); + logger = { debug: jest.fn(), info: jest.fn(), @@ -37,6 +37,9 @@ beforeEach(() => { }); afterEach(async () => { + expect(nock.isDone()).toBeTruthy(); + nock.cleanAll(); + nock.restore(); if (gateway) { await gateway.stop(); gateway = null; @@ -78,7 +81,7 @@ describe('Using supergraphSdl dynamic configuration', () => { supergraphSdl: getTestingSupergraphSdl(), })); - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ supergraphSdl: callbackSpy, }); @@ -96,7 +99,7 @@ describe('Using supergraphSdl dynamic configuration', () => { promiseGuaranteeingWeStayInTheCallback, resolvePromiseGuaranteeingWeStayInTheCallback, ] = waitUntil(); - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ async supergraphSdl() { resolvePromiseGuaranteeingWeAreInTheCallback(); await promiseGuaranteeingWeStayInTheCallback; @@ -121,7 +124,7 @@ describe('Using supergraphSdl dynamic configuration', () => { const [userPromise, resolveSupergraph] = waitUntil<{ supergraphSdl: string }>(); - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ async supergraphSdl() { return userPromise; }, @@ -147,7 +150,7 @@ describe('Using supergraphSdl dynamic configuration', () => { waitUntil<{ supergraphSdl: string }>(); let userUpdateFn: SupergraphSdlUpdateFunction; - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ async supergraphSdl({ update }) { userUpdateFn = update; return userPromise; @@ -171,7 +174,7 @@ describe('Using supergraphSdl dynamic configuration', () => { it('calls user-provided `cleanup` function when stopped', async () => { const cleanup = jest.fn(() => Promise.resolve()); - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ async supergraphSdl() { return { supergraphSdl: getTestingSupergraphSdl(), @@ -192,12 +195,11 @@ describe('Using supergraphSdl dynamic configuration', () => { }); it('performs a successful health check on subgraphs', async () => { - mockAllServicesSdlQuerySuccess(); mockAllServicesHealthCheckSuccess(); let healthCheckCallback: SubgraphHealthCheckFunction; const supergraphSdl = getTestingSupergraphSdl(); - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ async supergraphSdl({ healthCheck }) { healthCheckCallback = healthCheck; return { @@ -219,23 +221,27 @@ describe('Using supergraphSdl dynamic configuration', () => { describe('errors', () => { it('fails to load if user-provided `supergraphSdl` function throws', async () => { const failureMessage = 'Error from supergraphSdl function'; - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ async supergraphSdl() { throw new Error(failureMessage); }, logger, }); - await expect(() => gateway.load()).rejects.toThrowError(failureMessage); + await expect(gateway.load()).rejects.toThrowError(failureMessage); expect(gateway.__testing().state.phase).toEqual('failed to load'); expect(logger.error).toHaveBeenCalledWith(failureMessage); + + // we don't want the `afterEach` to call `gateway.stop()` in this case + // since it would throw an error due to the gateway's failed to load state + gateway = null; }); it('gracefully handles Promise rejections from user `cleanup` function', async () => { const rejectionMessage = 'thrown from cleanup function'; const cleanup = jest.fn(() => Promise.reject(rejectionMessage)); - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ async supergraphSdl() { return { supergraphSdl: getTestingSupergraphSdl(), @@ -255,11 +261,10 @@ describe('Using supergraphSdl dynamic configuration', () => { }); it('throws an error when `healthCheck` rejects', async () => { - mockAllServicesSdlQuerySuccess(); - + // no mocks, so nock will reject let healthCheckCallback: SubgraphHealthCheckFunction; const supergraphSdl = getTestingSupergraphSdl(); - const gateway = new ApolloGateway({ + gateway = new ApolloGateway({ async supergraphSdl({ healthCheck }) { healthCheckCallback = healthCheck; return { From 8723c5f491007f4c057f33d046730348dab0356e Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Tue, 7 Dec 2021 10:52:40 -0800 Subject: [PATCH 09/82] add state to prevent extra polling after cleanup --- .../src/__tests__/integration/nockMocks.ts | 6 +---- .../legacy/__tests__/serviceListShim.test.ts | 26 +++++++++++++++---- gateway-js/src/legacy/serviceListShim.ts | 20 +++++++++++--- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/gateway-js/src/__tests__/integration/nockMocks.ts b/gateway-js/src/__tests__/integration/nockMocks.ts index 4938795382..5d93dd31f2 100644 --- a/gateway-js/src/__tests__/integration/nockMocks.ts +++ b/gateway-js/src/__tests__/integration/nockMocks.ts @@ -33,11 +33,7 @@ export function mockSdlQuerySuccess(service: Fixture) { export function mockAllServicesSdlQuerySuccess( fixtures: Fixture[] = testingFixtures, ) { - return fixtures.map((fixture) => - mockSdlQuery(fixture).reply(200, { - data: { _service: { sdl: print(fixture.typeDefs) } }, - }), - ); + return fixtures.map((fixture) => mockSdlQuerySuccess(fixture)); } export function mockServiceHealthCheck({ url }: Fixture) { diff --git a/gateway-js/src/legacy/__tests__/serviceListShim.test.ts b/gateway-js/src/legacy/__tests__/serviceListShim.test.ts index 1436c8b532..4b907fb063 100644 --- a/gateway-js/src/legacy/__tests__/serviceListShim.test.ts +++ b/gateway-js/src/legacy/__tests__/serviceListShim.test.ts @@ -1,14 +1,18 @@ import nock from 'nock'; -import { fixtures, fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; +import { + fixtures, + fixturesWithUpdate, +} from 'apollo-federation-integration-testsuite'; import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; import { ServiceListShim } from '../serviceListShim'; import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; import { wait, waitUntil } from '../../__tests__/execution-utils'; describe('ServiceListShim', () => { - beforeEach(async () => { + beforeEach(() => { if (!nock.isActive()) nock.activate(); }); + afterEach(async () => { expect(nock.isDone()).toBeTruthy(); nock.cleanAll(); @@ -103,6 +107,8 @@ describe('ServiceListShim', () => { // stop polling await cleanup!(); + expect(updateSpy).toHaveBeenCalledTimes(3); + // ensure we cancelled the timer // @ts-ignore expect(shim.timerRef).toBe(null); @@ -110,6 +116,11 @@ describe('ServiceListShim', () => { // TODO: useFakeTimers (though I'm struggling to get this to work as expected) it("doesn't call `update` when there's no change to the supergraph", async () => { + const fetcher = + jest.requireActual( + 'apollo-server-env', + ).fetch; + // mock for initial load and a few polls against an unchanging schema mockAllServicesSdlQuerySuccess(); mockAllServicesSdlQuerySuccess(); @@ -119,6 +130,12 @@ describe('ServiceListShim', () => { const shim = new ServiceListShim({ serviceList: fixtures, pollIntervalInMs: 100, + buildService(service) { + return new RemoteGraphQLDataSource({ + url: service.url, + fetcher, + }); + }, }); const updateSpy = jest.fn(); @@ -131,12 +148,11 @@ describe('ServiceListShim', () => { // let the shim poll through all the active mocks while (nock.activeMocks().length > 0) { - await wait(10); + await wait(0); } - // stop polling await cleanup!(); - expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).not.toHaveBeenCalled(); }); }); diff --git a/gateway-js/src/legacy/serviceListShim.ts b/gateway-js/src/legacy/serviceListShim.ts index 4a6dbfb409..6d61d2be9c 100644 --- a/gateway-js/src/legacy/serviceListShim.ts +++ b/gateway-js/src/legacy/serviceListShim.ts @@ -28,6 +28,11 @@ export interface ServiceListShimOptions { pollIntervalInMs?: number; } +type ShimState = + | { phase: 'initialized' } + | { phase: 'polling' } + | { phase: 'stopped' }; + export class ServiceListShim extends CallableInstance< Parameters, ReturnType @@ -45,6 +50,7 @@ export class ServiceListShim extends CallableInstance< private serviceSdlCache: Map = new Map(); private pollIntervalInMs?: number; private timerRef: NodeJS.Timeout | null = null; + private state: ShimState; constructor(options: ServiceListShimOptions) { super('instanceCallableMethod'); @@ -56,6 +62,7 @@ export class ServiceListShim extends CallableInstance< dataSource: this.createDataSource(serviceDefinition), })); this.introspectionHeaders = options.introspectionHeaders; + this.state = { phase: 'initialized' }; } // @ts-ignore noUsedLocals @@ -73,6 +80,7 @@ export class ServiceListShim extends CallableInstance< return { supergraphSdl: initialSupergraphSdl, cleanup: async () => { + this.state = { phase: 'stopped' }; if (this.timerRef) { this.timerRef.unref(); clearInterval(this.timerRef); @@ -127,16 +135,20 @@ export class ServiceListShim extends CallableInstance< } private beginPolling() { + this.state = { phase: 'polling' }; this.poll(); } private poll() { this.timerRef = global.setTimeout(async () => { - const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); - if (maybeNewSupergraphSdl) { - this.update?.(maybeNewSupergraphSdl); + if (this.state.phase === 'polling') { + const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); + if (maybeNewSupergraphSdl) { + this.update?.(maybeNewSupergraphSdl); + } + + this.poll(); } - this.poll(); }, this.pollIntervalInMs!); } } From 8bce79f1db067b7bcc24d72e5e77343dddb84055 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 8 Dec 2021 17:42:58 -0800 Subject: [PATCH 10/82] Prevent re-entrant calls to update callback --- .../__tests__/gateway/supergraphSdl.test.ts | 71 +++++++++++++++++++ gateway-js/src/index.ts | 23 +++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 023812a216..d1ae66fd8e 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -284,5 +284,76 @@ describe('Using supergraphSdl dynamic configuration', () => { /The gateway subgraphs health check failed\. Updating to the provided `supergraphSdl` will likely result in future request failures to subgraphs\. The following error occurred during the health check/, ); }); + + it('throws an error when `update` is called after gateway fails to load', async () => { + let updateCallback: SupergraphSdlUpdateFunction; + const supergraphSdl = getTestingSupergraphSdl(); + gateway = new ApolloGateway({ + async supergraphSdl({ update }) { + updateCallback = update; + return { + supergraphSdl: 'invalid SDL', + }; + }, + }); + + try { + await gateway.load(); + } catch {} + + expect(() => + updateCallback!(supergraphSdl), + ).toThrowErrorMatchingInlineSnapshot( + `"Can't call \`update\` callback after gateway failed to load."`, + ); + + // gateway failed to load, so we don't want the `afterEach` to call `gateway.stop()` + gateway = null; + }); + + it('throws an error when `update` is called while an update is in progress', async () => { + let updateCallback: SupergraphSdlUpdateFunction; + const supergraphSdl = getTestingSupergraphSdl(); + gateway = new ApolloGateway({ + async supergraphSdl({ update }) { + updateCallback = update; + return { + supergraphSdl, + }; + }, + experimental_didUpdateComposition() { + updateCallback(getTestingSupergraphSdl(fixturesWithUpdate)); + }, + }); + + await expect(gateway.load()).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't call \`update\` callback while supergraph update is in progress."`, + ); + + // gateway failed to load, so we don't want the `afterEach` to call `gateway.stop()` + gateway = null; + }); + + it('throws an error when `update` is called after gateway is stopped', async () => { + let updateCallback: SupergraphSdlUpdateFunction; + const supergraphSdl = getTestingSupergraphSdl(); + gateway = new ApolloGateway({ + async supergraphSdl({ update }) { + updateCallback = update; + return { + supergraphSdl, + }; + }, + }); + + await gateway.load(); + await gateway.stop(); + + expect(() => + updateCallback!(getTestingSupergraphSdl(fixturesWithUpdate)), + ).toThrowErrorMatchingInlineSnapshot( + `"Can't call \`update\` callback after gateway has been stopped."`, + ); + }); }); }); diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 7a8eb278bc..20e93e904d 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -133,7 +133,8 @@ type GatewayState = pollWaitTimer: NodeJS.Timer; doneWaiting: () => void; } - | { phase: 'polling'; pollingDonePromise: Promise }; + | { phase: 'polling'; pollingDonePromise: Promise } + | { phase: 'updating schema' }; // We want to be compatible with `load()` as called by both AS2 and AS3, so we // define its argument types ourselves instead of relying on imports. @@ -632,10 +633,19 @@ export class ApolloGateway implements GraphQLService { } private externalSupergraphUpdateCallback(supergraphSdl: string) { + if (this.state.phase === "failed to load") { + throw new Error("Can't call `update` callback after gateway failed to load."); + } else if (this.state.phase === "updating schema") { + throw new Error("Can't call `update` callback while supergraph update is in progress."); + } else if (this.state.phase === "stopped") { + throw new Error("Can't call `update` callback after gateway has been stopped."); + } + this.state = { phase: "updating schema" }; this.updateWithSupergraphSdl({ supergraphSdl, id: this.getIdForSupergraphSdl(supergraphSdl), }); + this.state = { phase: "loaded" }; } private async externalSubgraphHealthCheckCallback(supergraphSdl: string) { @@ -960,6 +970,11 @@ export class ApolloGateway implements GraphQLService { case 'loaded': // This is the normal case. break; + case 'updating schema': + // This should never happen + throw Error( + "ApolloGateway.pollServices called from an unexpected state 'updating schema'", + ); default: throw new UnreachableCaseError(this.state); } @@ -1326,7 +1341,7 @@ export class ApolloGateway implements GraphQLService { // schema polling). Can be called multiple times safely. Once it (async) // returns, all gateway background activity will be finished. public async stop() { - Promise.all( + await Promise.all( this.toDispose.map((p) => p().catch((e) => { this.logger.error( @@ -1388,6 +1403,10 @@ export class ApolloGateway implements GraphQLService { stoppingDone!(); return; } + case "updating schema": { + // This should never happen + throw Error("`ApolloGateway.stop` called from an unexpected state `updating schema`"); + } default: throw new UnreachableCaseError(this.state); } From 2f67f2d35373bf1bc74d0c4498ea3570eee4b1e5 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 9 Dec 2021 11:14:50 -0800 Subject: [PATCH 11/82] Catch unexpected state case and throw --- gateway-js/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 20e93e904d..5f88ee23c2 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -632,6 +632,9 @@ export class ApolloGateway implements GraphQLService { } } + /** + * @throws Error when called from a state other than `loaded` + */ private externalSupergraphUpdateCallback(supergraphSdl: string) { if (this.state.phase === "failed to load") { throw new Error("Can't call `update` callback after gateway failed to load."); @@ -639,7 +642,10 @@ export class ApolloGateway implements GraphQLService { throw new Error("Can't call `update` callback while supergraph update is in progress."); } else if (this.state.phase === "stopped") { throw new Error("Can't call `update` callback after gateway has been stopped."); + } else if (this.state.phase !== "loaded") { + throw new Error(`Called \`update\` callback from unexpected state: "${this.state.phase}". This is a bug.`); } + this.state = { phase: "updating schema" }; this.updateWithSupergraphSdl({ supergraphSdl, From 07ac06504d0565006514f052bfc6e086071880ce Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 9 Dec 2021 12:15:47 -0800 Subject: [PATCH 12/82] Add test for invalid update, fix uncovered bug --- .../__tests__/gateway/supergraphSdl.test.ts | 21 ++++++++ gateway-js/src/index.ts | 52 +++++++++++++------ 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index d1ae66fd8e..08952431cd 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -355,5 +355,26 @@ describe('Using supergraphSdl dynamic configuration', () => { `"Can't call \`update\` callback after gateway has been stopped."`, ); }); + + it('throws an error when `update` is called with an invalid supergraph', async () => { + let updateCallback: SupergraphSdlUpdateFunction; + const supergraphSdl = getTestingSupergraphSdl(); + gateway = new ApolloGateway({ + async supergraphSdl({ update }) { + updateCallback = update; + return { + supergraphSdl, + }; + }, + }); + + await gateway.load(); + + expect(() => + updateCallback!('invalid SDL'), + ).toThrowErrorMatchingInlineSnapshot( + `"Syntax Error: Unexpected Name \\"invalid\\"."`, + ); + }); }); }); diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 5f88ee23c2..07cdc21e78 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -633,25 +633,47 @@ export class ApolloGateway implements GraphQLService { } /** - * @throws Error when called from a state other than `loaded` + * @throws Error + * when called from a state other than `loaded` + * + * @throws Error + * when the provided supergraphSdl is invalid */ private externalSupergraphUpdateCallback(supergraphSdl: string) { - if (this.state.phase === "failed to load") { - throw new Error("Can't call `update` callback after gateway failed to load."); - } else if (this.state.phase === "updating schema") { - throw new Error("Can't call `update` callback while supergraph update is in progress."); - } else if (this.state.phase === "stopped") { - throw new Error("Can't call `update` callback after gateway has been stopped."); - } else if (this.state.phase !== "loaded") { - throw new Error(`Called \`update\` callback from unexpected state: "${this.state.phase}". This is a bug.`); + switch (this.state.phase) { + case 'failed to load': + throw new Error( + "Can't call `update` callback after gateway failed to load.", + ); + case 'updating schema': + throw new Error( + "Can't call `update` callback while supergraph update is in progress.", + ); + case 'stopped': + throw new Error( + "Can't call `update` callback after gateway has been stopped.", + ); + case 'loaded': + case 'initialized': + // typical case + break; + default: + // this should never happen + throw new Error( + `Called \`update\` callback from unexpected state: "${this.state.phase}". This is a bug.`, + ); } - this.state = { phase: "updating schema" }; - this.updateWithSupergraphSdl({ - supergraphSdl, - id: this.getIdForSupergraphSdl(supergraphSdl), - }); - this.state = { phase: "loaded" }; + this.state = { phase: 'updating schema' }; + try { + this.updateWithSupergraphSdl({ + supergraphSdl, + id: this.getIdForSupergraphSdl(supergraphSdl), + }); + } finally { + // if update fails, we still want to go back to `loaded` state + this.state = { phase: 'loaded' }; + } } private async externalSubgraphHealthCheckCallback(supergraphSdl: string) { From 559ed4703080ff7bb185a9650c21810843621fb6 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 9 Dec 2021 14:14:23 -0800 Subject: [PATCH 13/82] Handle update failures gracefully, address polling not finished bug w/ pollingPromise --- gateway-js/src/__tests__/execution-utils.ts | 19 ------ .../__tests__/gateway/supergraphSdl.test.ts | 3 +- .../legacy/__tests__/serviceListShim.test.ts | 62 +++++++++++++++++-- gateway-js/src/legacy/serviceListShim.ts | 33 +++++++--- 4 files changed, 85 insertions(+), 32 deletions(-) diff --git a/gateway-js/src/__tests__/execution-utils.ts b/gateway-js/src/__tests__/execution-utils.ts index 95a4688677..8a177fe4da 100644 --- a/gateway-js/src/__tests__/execution-utils.ts +++ b/gateway-js/src/__tests__/execution-utils.ts @@ -121,25 +121,6 @@ export function wait(ms: number, toResolveTo?: any) { return new Promise((r) => setTimeout(() => r(toResolveTo), ms)); } -export function waitUntil() { - let userResolved: (value: T | PromiseLike) => void; - let userRejected: (reason?: any) => void; - const promise = new Promise( - (r) => ((userResolved = r), (userRejected = r)), - ); - return [ - promise, - // @ts-ignore - userResolved, - // @ts-ignore - userRejected, - ] as [ - Promise, - (value: T | PromiseLike) => void, - (reason?: any) => void, - ]; -} - export function printPlan(queryPlan: QueryPlan): string { return prettyFormat(queryPlan, { plugins: [queryPlanSerializer, astSerializer], diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 08952431cd..6c8af6eb06 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -9,8 +9,9 @@ import { ApolloServer } from 'apollo-server'; import { Logger } from 'apollo-server-types'; import nock from 'nock'; import { fetch } from '../../__mocks__/apollo-server-env'; -import { getTestingSupergraphSdl, waitUntil } from '../execution-utils'; +import { getTestingSupergraphSdl } from '../execution-utils'; import { mockAllServicesHealthCheckSuccess } from '../integration/nockMocks'; +import { waitUntil } from '../../utilities/waitUntil'; async function getSupergraphSdlGatewayServer() { const server = new ApolloServer({ diff --git a/gateway-js/src/legacy/__tests__/serviceListShim.test.ts b/gateway-js/src/legacy/__tests__/serviceListShim.test.ts index 4b907fb063..ae755319bc 100644 --- a/gateway-js/src/legacy/__tests__/serviceListShim.test.ts +++ b/gateway-js/src/legacy/__tests__/serviceListShim.test.ts @@ -6,7 +6,9 @@ import { import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; import { ServiceListShim } from '../serviceListShim'; import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; -import { wait, waitUntil } from '../../__tests__/execution-utils'; +import { wait } from '../../__tests__/execution-utils'; +import { waitUntil } from '../../utilities/waitUntil'; +import { Logger } from 'apollo-server-types'; describe('ServiceListShim', () => { beforeEach(() => { @@ -32,7 +34,7 @@ describe('ServiceListShim', () => { mockAllServicesSdlQuerySuccess(); const shim = new ServiceListShim({ serviceList: fixtures }); await expect( - shim({ async update() {}, async healthCheck() {} }), + shim({ update() {}, async healthCheck() {} }), ).resolves.toBeTruthy(); }); @@ -59,7 +61,7 @@ describe('ServiceListShim', () => { }, }); - await shim({ async update() {}, async healthCheck() {} }); + await shim({ update() {}, async healthCheck() {} }); expect(processSpies.length).toBe(fixtures.length); for (const processSpy of processSpies) { @@ -94,7 +96,7 @@ describe('ServiceListShim', () => { }); const { cleanup } = await shim({ - async update(supergraphSdl) { + update(supergraphSdl) { updateSpy(supergraphSdl); }, async healthCheck() {}, @@ -140,13 +142,14 @@ describe('ServiceListShim', () => { const updateSpy = jest.fn(); const { cleanup } = await shim({ - async update(supergraphSdl) { + update(supergraphSdl) { updateSpy(supergraphSdl); }, async healthCheck() {}, }); // let the shim poll through all the active mocks + // wouldn't need to do this if I could get fakeTimers working as expected while (nock.activeMocks().length > 0) { await wait(0); } @@ -155,4 +158,53 @@ describe('ServiceListShim', () => { expect(updateSpy).not.toHaveBeenCalled(); }); + + describe('errors', () => { + it('logs an error when `update` function throws', async () => { + const [errorLoggedPromise, resolveErrorLoggedPromise] = waitUntil(); + + const errorSpy = jest.fn(() => { + resolveErrorLoggedPromise(); + }); + const logger: Logger = { + error: errorSpy, + debug() {}, + info() {}, + warn() {}, + }; + + // mock successful initial load + mockAllServicesSdlQuerySuccess(); + + // mock first update + mockAllServicesSdlQuerySuccess(fixturesWithUpdate); + + const shim = new ServiceListShim({ + serviceList: fixtures, + pollIntervalInMs: 1000, + logger, + }); + + const thrownErrorMessage = 'invalid supergraph'; + // simulate gateway throwing an error when `update` is called + const updateSpy = jest.fn().mockImplementationOnce(() => { + throw new Error(thrownErrorMessage); + }); + + const { cleanup } = await shim({ + update: updateSpy, + async healthCheck() {}, + }); + + await errorLoggedPromise; + // stop polling + await cleanup!(); + + expect(updateSpy).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + `IntrospectAndCompose failed to update supergraph with the following error: ${thrownErrorMessage}`, + ); + }); + }); }); diff --git a/gateway-js/src/legacy/serviceListShim.ts b/gateway-js/src/legacy/serviceListShim.ts index 6d61d2be9c..cd2252e610 100644 --- a/gateway-js/src/legacy/serviceListShim.ts +++ b/gateway-js/src/legacy/serviceListShim.ts @@ -3,6 +3,7 @@ import { compositionHasErrors, ServiceDefinition, } from '@apollo/federation'; +import { Logger } from 'apollo-server-types'; import CallableInstance from 'callable-instance'; import { HeadersInit } from 'node-fetch'; import { @@ -16,6 +17,7 @@ import { getServiceDefinitionsFromRemoteEndpoint, Service, } from '../loadServicesFromRemoteEndpoint'; +import { waitUntil } from '../utilities/waitUntil'; export interface ServiceListShimOptions { serviceList: ServiceEndpointDefinition[]; @@ -26,11 +28,12 @@ export interface ServiceListShimOptions { ) => Promise | HeadersInit); buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; pollIntervalInMs?: number; + logger?: Logger; } type ShimState = | { phase: 'initialized' } - | { phase: 'polling' } + | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; export class ServiceListShim extends CallableInstance< @@ -51,6 +54,7 @@ export class ServiceListShim extends CallableInstance< private pollIntervalInMs?: number; private timerRef: NodeJS.Timeout | null = null; private state: ShimState; + private logger?: Logger; constructor(options: ServiceListShimOptions) { super('instanceCallableMethod'); @@ -62,6 +66,7 @@ export class ServiceListShim extends CallableInstance< dataSource: this.createDataSource(serviceDefinition), })); this.introspectionHeaders = options.introspectionHeaders; + this.logger = options.logger; this.state = { phase: 'initialized' }; } @@ -69,6 +74,7 @@ export class ServiceListShim extends CallableInstance< private async instanceCallableMethod( ...[{ update }]: Parameters ) { + debugger; this.update = update; const initialSupergraphSdl = await this.updateSupergraphSdl(); @@ -80,6 +86,9 @@ export class ServiceListShim extends CallableInstance< return { supergraphSdl: initialSupergraphSdl, cleanup: async () => { + if (this.state.phase === 'polling') { + await this.state.pollingPromise; + } this.state = { phase: 'stopped' }; if (this.timerRef) { this.timerRef.unref(); @@ -140,15 +149,25 @@ export class ServiceListShim extends CallableInstance< } private poll() { - this.timerRef = global.setTimeout(async () => { + this.timerRef = setTimeout(async () => { if (this.state.phase === 'polling') { - const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); - if (maybeNewSupergraphSdl) { - this.update?.(maybeNewSupergraphSdl); + const [pollingPromise, donePolling] = waitUntil(); + this.state.pollingPromise = pollingPromise; + try { + const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); + if (maybeNewSupergraphSdl) { + this.update?.(maybeNewSupergraphSdl); + } + } catch (e) { + this.logger?.error( + 'IntrospectAndCompose failed to update supergraph with the following error: ' + + (e.message ?? e), + ); } - - this.poll(); + donePolling!(); } + + this.poll(); }, this.pollIntervalInMs!); } } From eeee2e699e545819008470b3f41472c8a29543fd Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 9 Dec 2021 14:18:52 -0800 Subject: [PATCH 14/82] Add missing file --- ...viceListShim.ts => IntrospectAndCompose.ts} | 0 ...im.test.ts => IntrospectAndCompose.test.ts} | 0 gateway-js/src/utilities/waitUntil.ts | 18 ++++++++++++++++++ 3 files changed, 18 insertions(+) rename gateway-js/src/legacy/{serviceListShim.ts => IntrospectAndCompose.ts} (100%) rename gateway-js/src/legacy/__tests__/{serviceListShim.test.ts => IntrospectAndCompose.test.ts} (100%) create mode 100644 gateway-js/src/utilities/waitUntil.ts diff --git a/gateway-js/src/legacy/serviceListShim.ts b/gateway-js/src/legacy/IntrospectAndCompose.ts similarity index 100% rename from gateway-js/src/legacy/serviceListShim.ts rename to gateway-js/src/legacy/IntrospectAndCompose.ts diff --git a/gateway-js/src/legacy/__tests__/serviceListShim.test.ts b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts similarity index 100% rename from gateway-js/src/legacy/__tests__/serviceListShim.test.ts rename to gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts diff --git a/gateway-js/src/utilities/waitUntil.ts b/gateway-js/src/utilities/waitUntil.ts new file mode 100644 index 0000000000..8a011efe6d --- /dev/null +++ b/gateway-js/src/utilities/waitUntil.ts @@ -0,0 +1,18 @@ +export function waitUntil() { + let userResolved: (value: T | PromiseLike) => void; + let userRejected: (reason?: any) => void; + const promise = new Promise( + (r) => ((userResolved = r), (userRejected = r)), + ); + return [ + promise, + // @ts-ignore + userResolved, + // @ts-ignore + userRejected, + ] as [ + Promise, + (value: T | PromiseLike) => void, + (reason?: any) => void, + ]; +} From cde70c2879cfc5cd22928cb288b681981278ce5f Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 9 Dec 2021 14:19:04 -0800 Subject: [PATCH 15/82] Rename to IntrospectAndCompose --- gateway-js/src/legacy/IntrospectAndCompose.ts | 11 ++++--- .../__tests__/IntrospectAndCompose.test.ts | 30 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/gateway-js/src/legacy/IntrospectAndCompose.ts b/gateway-js/src/legacy/IntrospectAndCompose.ts index cd2252e610..77401e8c6b 100644 --- a/gateway-js/src/legacy/IntrospectAndCompose.ts +++ b/gateway-js/src/legacy/IntrospectAndCompose.ts @@ -19,7 +19,7 @@ import { } from '../loadServicesFromRemoteEndpoint'; import { waitUntil } from '../utilities/waitUntil'; -export interface ServiceListShimOptions { +export interface IntrospectAndComposeOptions { serviceList: ServiceEndpointDefinition[]; introspectionHeaders?: | HeadersInit @@ -31,12 +31,12 @@ export interface ServiceListShimOptions { logger?: Logger; } -type ShimState = +type State = | { phase: 'initialized' } | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; -export class ServiceListShim extends CallableInstance< +export class IntrospectAndCompose extends CallableInstance< Parameters, ReturnType > { @@ -53,10 +53,10 @@ export class ServiceListShim extends CallableInstance< private serviceSdlCache: Map = new Map(); private pollIntervalInMs?: number; private timerRef: NodeJS.Timeout | null = null; - private state: ShimState; + private state: State; private logger?: Logger; - constructor(options: ServiceListShimOptions) { + constructor(options: IntrospectAndComposeOptions) { super('instanceCallableMethod'); // this.buildService needs to be assigned before this.serviceList is built this.buildService = options.buildService; @@ -74,7 +74,6 @@ export class ServiceListShim extends CallableInstance< private async instanceCallableMethod( ...[{ update }]: Parameters ) { - debugger; this.update = update; const initialSupergraphSdl = await this.updateSupergraphSdl(); diff --git a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts index ae755319bc..7c70807479 100644 --- a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts @@ -4,13 +4,13 @@ import { fixturesWithUpdate, } from 'apollo-federation-integration-testsuite'; import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; -import { ServiceListShim } from '../serviceListShim'; +import { IntrospectAndCompose } from '../IntrospectAndCompose'; import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; import { wait } from '../../__tests__/execution-utils'; import { waitUntil } from '../../utilities/waitUntil'; import { Logger } from 'apollo-server-types'; -describe('ServiceListShim', () => { +describe('IntrospectAndCompose', () => { beforeEach(() => { if (!nock.isActive()) nock.activate(); }); @@ -24,7 +24,7 @@ describe('ServiceListShim', () => { it('constructs', () => { expect( () => - new ServiceListShim({ + new IntrospectAndCompose({ serviceList: fixtures, }), ).not.toThrow(); @@ -32,9 +32,9 @@ describe('ServiceListShim', () => { it('is instance callable (simulating the gateway calling it)', async () => { mockAllServicesSdlQuerySuccess(); - const shim = new ServiceListShim({ serviceList: fixtures }); + const instance = new IntrospectAndCompose({ serviceList: fixtures }); await expect( - shim({ update() {}, async healthCheck() {} }), + instance({ update() {}, async healthCheck() {} }), ).resolves.toBeTruthy(); }); @@ -52,7 +52,7 @@ describe('ServiceListShim', () => { const processSpies: jest.Mock[] = []; - const shim = new ServiceListShim({ + const instance = new IntrospectAndCompose({ serviceList: fixtures, buildService(def) { const { datasource, processSpy } = getDataSourceSpy(def); @@ -61,7 +61,7 @@ describe('ServiceListShim', () => { }, }); - await shim({ update() {}, async healthCheck() {} }); + await instance({ update() {}, async healthCheck() {} }); expect(processSpies.length).toBe(fixtures.length); for (const processSpy of processSpies) { @@ -90,12 +90,12 @@ describe('ServiceListShim', () => { .mockImplementationOnce(() => r2()) .mockImplementationOnce(() => r3()); - const shim = new ServiceListShim({ + const instance = new IntrospectAndCompose({ serviceList: fixtures, pollIntervalInMs: 10, }); - const { cleanup } = await shim({ + const { cleanup } = await instance({ update(supergraphSdl) { updateSpy(supergraphSdl); }, @@ -113,7 +113,7 @@ describe('ServiceListShim', () => { // ensure we cancelled the timer // @ts-ignore - expect(shim.timerRef).toBe(null); + expect(instance.timerRef).toBe(null); }); // TODO: useFakeTimers (though I'm struggling to get this to work as expected) @@ -129,7 +129,7 @@ describe('ServiceListShim', () => { mockAllServicesSdlQuerySuccess(); mockAllServicesSdlQuerySuccess(); - const shim = new ServiceListShim({ + const instance = new IntrospectAndCompose({ serviceList: fixtures, pollIntervalInMs: 100, buildService(service) { @@ -141,14 +141,14 @@ describe('ServiceListShim', () => { }); const updateSpy = jest.fn(); - const { cleanup } = await shim({ + const { cleanup } = await instance({ update(supergraphSdl) { updateSpy(supergraphSdl); }, async healthCheck() {}, }); - // let the shim poll through all the active mocks + // let the instance poll through all the active mocks // wouldn't need to do this if I could get fakeTimers working as expected while (nock.activeMocks().length > 0) { await wait(0); @@ -179,7 +179,7 @@ describe('ServiceListShim', () => { // mock first update mockAllServicesSdlQuerySuccess(fixturesWithUpdate); - const shim = new ServiceListShim({ + const instance = new IntrospectAndCompose({ serviceList: fixtures, pollIntervalInMs: 1000, logger, @@ -191,7 +191,7 @@ describe('ServiceListShim', () => { throw new Error(thrownErrorMessage); }); - const { cleanup } = await shim({ + const { cleanup } = await instance({ update: updateSpy, async healthCheck() {}, }); From 212b67dc56a7d4262bd72faaa4af40dc9c10e685 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 9 Dec 2021 14:29:57 -0800 Subject: [PATCH 16/82] Migrate related file into legacy folder --- gateway-js/src/index.ts | 10 +++++++--- gateway-js/src/legacy/IntrospectAndCompose.ts | 2 +- .../__tests__/loadServicesFromRemoteEndpoint.test.ts | 2 +- .../src/{ => legacy}/loadServicesFromRemoteEndpoint.ts | 6 +++--- 4 files changed, 12 insertions(+), 8 deletions(-) rename gateway-js/src/{ => legacy}/__tests__/loadServicesFromRemoteEndpoint.test.ts (96%) rename gateway-js/src/{ => legacy}/loadServicesFromRemoteEndpoint.ts (96%) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 07cdc21e78..c8f0df6fb3 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -30,7 +30,7 @@ import { defaultFieldResolverWithAliasSupport, } from './executeQueryPlan'; -import { getServiceDefinitionsFromRemoteEndpoint } from './loadServicesFromRemoteEndpoint'; +import { getServiceDefinitionsFromRemoteEndpoint } from './legacy/loadServicesFromRemoteEndpoint'; import { GraphQLDataSource, GraphQLDataSourceRequestKind, @@ -1431,9 +1431,11 @@ export class ApolloGateway implements GraphQLService { stoppingDone!(); return; } - case "updating schema": { + case 'updating schema': { // This should never happen - throw Error("`ApolloGateway.stop` called from an unexpected state `updating schema`"); + throw Error( + '`ApolloGateway.stop` called from an unexpected state `updating schema`', + ); } default: throw new UnreachableCaseError(this.state); @@ -1507,3 +1509,5 @@ export { SubgraphHealthCheckFunction, SupergraphSdlHook, } from './config'; + +export { IntrospectAndCompose } from './legacy/IntrospectAndCompose'; diff --git a/gateway-js/src/legacy/IntrospectAndCompose.ts b/gateway-js/src/legacy/IntrospectAndCompose.ts index 77401e8c6b..ff4672bf29 100644 --- a/gateway-js/src/legacy/IntrospectAndCompose.ts +++ b/gateway-js/src/legacy/IntrospectAndCompose.ts @@ -16,7 +16,7 @@ import { import { getServiceDefinitionsFromRemoteEndpoint, Service, -} from '../loadServicesFromRemoteEndpoint'; +} from './loadServicesFromRemoteEndpoint'; import { waitUntil } from '../utilities/waitUntil'; export interface IntrospectAndComposeOptions { diff --git a/gateway-js/src/__tests__/loadServicesFromRemoteEndpoint.test.ts b/gateway-js/src/legacy/__tests__/loadServicesFromRemoteEndpoint.test.ts similarity index 96% rename from gateway-js/src/__tests__/loadServicesFromRemoteEndpoint.test.ts rename to gateway-js/src/legacy/__tests__/loadServicesFromRemoteEndpoint.test.ts index c5d189ebd9..be5b3c80c3 100644 --- a/gateway-js/src/__tests__/loadServicesFromRemoteEndpoint.test.ts +++ b/gateway-js/src/legacy/__tests__/loadServicesFromRemoteEndpoint.test.ts @@ -1,5 +1,5 @@ import { getServiceDefinitionsFromRemoteEndpoint } from '../loadServicesFromRemoteEndpoint'; -import { RemoteGraphQLDataSource } from '../datasources'; +import { RemoteGraphQLDataSource } from '../../datasources'; describe('getServiceDefinitionsFromRemoteEndpoint', () => { it('errors when no URL was specified', async () => { diff --git a/gateway-js/src/loadServicesFromRemoteEndpoint.ts b/gateway-js/src/legacy/loadServicesFromRemoteEndpoint.ts similarity index 96% rename from gateway-js/src/loadServicesFromRemoteEndpoint.ts rename to gateway-js/src/legacy/loadServicesFromRemoteEndpoint.ts index 171951089b..97a338cfbf 100644 --- a/gateway-js/src/loadServicesFromRemoteEndpoint.ts +++ b/gateway-js/src/legacy/loadServicesFromRemoteEndpoint.ts @@ -1,9 +1,9 @@ import { GraphQLRequest } from 'apollo-server-types'; import { parse } from 'graphql'; import { Headers, HeadersInit } from 'node-fetch'; -import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types'; -import { SERVICE_DEFINITION_QUERY } from './'; -import { ServiceDefinitionUpdate, ServiceEndpointDefinition } from './config'; +import { GraphQLDataSource, GraphQLDataSourceRequestKind } from '../datasources/types'; +import { SERVICE_DEFINITION_QUERY } from '../'; +import { ServiceDefinitionUpdate, ServiceEndpointDefinition } from '../config'; import { ServiceDefinition } from '@apollo/federation'; export type Service = ServiceEndpointDefinition & { From 35a9189d6e1848bbf885e64600583528f5351216 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 9 Dec 2021 22:08:09 -0800 Subject: [PATCH 17/82] Docs WIP --- docs/source/api/apollo-gateway.mdx | 130 ++++++++++++++++- docs/source/gateway.mdx | 132 +++++++++++++++--- gateway-js/src/legacy/IntrospectAndCompose.ts | 20 +-- .../__tests__/IntrospectAndCompose.test.ts | 12 +- 4 files changed, 257 insertions(+), 37 deletions(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index d4d62c58ef..e7c5368cd7 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -124,7 +124,7 @@ A [supergraph schema](../#federated-schemas) ([generated with the Rover CLI](htt -**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. @@ -143,6 +143,8 @@ You can specify any string value for the `name` field, which is used for identif ###### `introspectionHeaders` +**This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).** + `Object | (service: ServiceEndpointDefinition) => Promise | Object` @@ -231,7 +233,9 @@ The default value is `false`. -###### `buildService` +###### `buildService` (deprecated) + +**This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).** `Function` @@ -245,7 +249,9 @@ Define this function to customize your gateway's data transport to some or all o -##### The `buildService` function +##### The `buildService` function (deprecated) + +**This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).** If you provide this function, the gateway calls it once for each subgraph. The function must return an object that implements the [`GraphQLDataSource` interface](https://github.com/apollographql/apollo-server/blob/570f548b88/packages/apollo-gateway/src/datasources/types.ts), such as an instance of [RemoteGraphQLDataSource](#class-remotegraphqldatasource) or a subclass of it. @@ -521,3 +527,121 @@ The details of the `fetch` response sent by the subgraph. + +## `class IntrospectAndCompose` + +The drop-in replacement for `serviceList`. This class is meant to be passed directly to `ApolloGateway`'s `supergraphSdl` constructor option. + +### Methods + +#### `constructor` + +Returns an initialized `IntrospectAndCompose` instance, which you can then pass to the `supergraphSdl` configuration option to 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 + +If you provide a [`buildService` function](#the-buildservice-function) to the constructor of `IntrospectAndCompose`, the function is called once for each of your graph's subgraphs. This function can return a `RemoteGraphQLDataSource` with a custom `fetcher`, which is then used for all communication with subgraphs: + +```js +const gateway = new ApolloGateway({ + supergraphSdl: new IntrospectAndCompose({ + // ..., + buildService({ url, name }) { + return new (class extends RemoteGraphQLDataSource { + fetcher = require('make-fetch-happen').defaults({ + onRetry() { + console.log('We will retry!') + }, + }); + })({ + url, + name, + }); + } + }), +}); +``` + +###### `buildService` + +`Function` + + + +Define this function to customize your gateway's data transport to some or all of your subgraphs. This customization can include using a protocol besides HTTP. For details, see [The `buildService` function](#the-buildservice-function). + + + + + + + +##### The `buildService` function + +If you provide this function, the gateway calls it once for each subgraph. The function must return an object that implements the [`GraphQLDataSource` interface](https://github.com/apollographql/apollo-server/blob/570f548b88/packages/apollo-gateway/src/datasources/types.ts), such as an instance of [RemoteGraphQLDataSource](#class-remotegraphqldatasource) or a subclass of it. + +The example below demonstrates adding an `x-user-id` HTTP header to every request the gateway sends to a subgraph: + +```js{9-11} +class AuthenticatedDataSource extends RemoteGraphQLDataSource { + willSendRequest({ request, context }) { + request.http.headers.set('x-user-id', context.userId); + } +} + +const gateway = new ApolloGateway({ + // ...other options... + supergraphSdl: new IntrospectAndCompose({ + // ..., + buildService({ name, url }) { + return new AuthenticatedDataSource({ url }); + }, + }), +}); +``` + +###### `pollIntervalInMs` + +`number` + + + +Define this function to customize your gateway's data transport to some or all of your subgraphs. This customization can include using a protocol besides HTTP. For details, see [The `buildService` function](#the-buildservice-function). + + + + + + \ No newline at end of file diff --git a/docs/source/gateway.mdx b/docs/source/gateway.mdx index b0cc3ca63c..960d2e6442 100644 --- a/docs/source/gateway.mdx +++ b/docs/source/gateway.mdx @@ -34,12 +34,12 @@ const supergraphSdl = readFileSync('./supergraph.graphql').toString(); // 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 }) => { @@ -57,19 +57,117 @@ To learn how to compose your supergraph schema with the Rover CLI, see the [Fede 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. -### Composing with `serviceList` +### Updating the supergraph schema + +In the above example, we provide a _static_ supergraph schema to the gateway. This approach requires the gateway to be restarted in order to update the supergraph schema. This is less than ideal 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 { readFileSync } = require('fs'); + +const supergraphSdl = readFileSync('./supergraph.graphql').toString(); + +let supergraphUpdate; +const gateway = new ApolloGateway({ + async supergraphSdl({ update }) { + // `update` is a function which we'll save for later use + supergraphUpdate = update; + return { + supergraphSdl, + } + }, +}); + +// 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 only called once when `ApolloServer` initializes the gateway. This function has two primary responsibilities: +1. Provide us access to the `update` function, which we'll use to update the supergraph schema. +2. Return the initial supergraph schema which the gateway will use at startup. -> We strongly recommend _against_ using `serviceList`. For details, [see below](#limitations-of-servicelist). +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, we'll assume `rover` is updating the `supergraphSdl.graphql` file. + +```js:title=index.js +const { ApolloServer } = require('apollo-server'); +const { ApolloGateway } = require('@apollo/gateway'); +const { readFileSync, watch } = require('fs'); + +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 = readFileSync('./supergraph.graphql').toString(); + // 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: readFileSync('./supergraph.graphql').toString(), + // 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 fleshed out. Let's take a look at what we've added. + +In the `supergraphSdl` callback, we also receive a `healthCheck` function. This allows 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. + +You might've also noticed we're returning a `cleanup` function. This is a callback that is called when the gateway is stopped. This allows 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 will `await` it before shutting down. + +### Composing subgraphs with `IntrospectAndCompose` + +> Looking for `serviceList`? `IntrospectAndCompose` is the new drop-in replacement for the `serviceList` option. In the near future, the `serviceList` option will be removed, however `IntrospectAndCompose` will continue to be supported. Note that this is still considered an outdated workflow. Apollo recommends approaches utilize `rover` for managing local composition. +// TODO: do we have docs that demonstrate a rover-based workflow? + +> We strongly recommend _against_ using `IntrospectAndCompose` in production. For details, [see below](#limitations-of-introspectandcompose). Alternatively, you can provide a `serviceList` array to the `ApolloGateway` constructor, like so: ```js:title=index.js +const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway'); + const gateway = new ApolloGateway({ - serviceList: [ - { name: 'accounts', url: 'http://localhost:4001' }, - { name: 'products', url: 'http://localhost:4002' }, - // ...additional subgraphs... - ] + supergraphSdl: new IntrospectAndCompose({ + subgraphs: [ + { name: 'accounts', url: 'http://localhost:4001' }, + { name: 'products', url: 'http://localhost:4002' }, + // ...additional subgraphs... + ], + }), }); ``` @@ -77,21 +175,19 @@ Each item in the array is an object that specifies the `name` and `url` of one o 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. -However, the `serviceList` option has important [limitations](#limitations-of-servicelist). +Additional configuration options can be found in the [`IntrospectAndCompose` API documentation](./api/apollo-gateway#class-introspectandcompose). -#### Limitations of `serviceList` +However, `IntrospectAndCompose` has important [limitations](#limitations-of-introspectandcompose). -The `serviceList` option can sometimes be helpful for local development, but it is strongly discouraged for any other environment. Here are some reasons why: +#### Limitations of `IntrospectAndCompose` -* **Composition might fail.** With `serviceList`, 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 `supergraphSdl`, you instead provide a supergraph schema that has _already_ been composed successfully. This prevents composition errors and enables faster startup. +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. -> **We hope to deprecate the `serviceList` option in the coming months**, but we'd love to hear from you if it enables an important use case that either `supergraphSdl` or [managed federation](./managed-federation/overview/) currently doesn't. -> -> Please let us know by [creating an issue](https://github.com/apollographql/federation/issues/new/choose) or [replying to this forum topic](https://community.apollographql.com/t/1053). - ## 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. diff --git a/gateway-js/src/legacy/IntrospectAndCompose.ts b/gateway-js/src/legacy/IntrospectAndCompose.ts index ff4672bf29..bc5947e3ba 100644 --- a/gateway-js/src/legacy/IntrospectAndCompose.ts +++ b/gateway-js/src/legacy/IntrospectAndCompose.ts @@ -20,7 +20,7 @@ import { import { waitUntil } from '../utilities/waitUntil'; export interface IntrospectAndComposeOptions { - serviceList: ServiceEndpointDefinition[]; + subgraphs: ServiceEndpointDefinition[]; introspectionHeaders?: | HeadersInit | (( @@ -41,7 +41,7 @@ export class IntrospectAndCompose extends CallableInstance< ReturnType > { private update?: SupergraphSdlUpdateFunction; - private serviceList: Service[]; + private subgraphs: Service[]; private introspectionHeaders?: | HeadersInit | (( @@ -58,12 +58,12 @@ export class IntrospectAndCompose extends CallableInstance< constructor(options: IntrospectAndComposeOptions) { super('instanceCallableMethod'); - // this.buildService needs to be assigned before this.serviceList is built + // this.buildService needs to be assigned before this.subgraphs is built this.buildService = options.buildService; this.pollIntervalInMs = options.pollIntervalInMs; - this.serviceList = options.serviceList.map((serviceDefinition) => ({ - ...serviceDefinition, - dataSource: this.createDataSource(serviceDefinition), + this.subgraphs = options.subgraphs.map((subgraph) => ({ + ...subgraph, + dataSource: this.createDataSource(subgraph), })); this.introspectionHeaders = options.introspectionHeaders; this.logger = options.logger; @@ -100,7 +100,7 @@ export class IntrospectAndCompose extends CallableInstance< private async updateSupergraphSdl() { const result = await getServiceDefinitionsFromRemoteEndpoint({ - serviceList: this.serviceList, + serviceList: this.subgraphs, getServiceIntrospectionHeaders: async (service) => { return typeof this.introspectionHeaders === 'function' ? await this.introspectionHeaders(service) @@ -113,7 +113,7 @@ export class IntrospectAndCompose extends CallableInstance< return null; } - return this.createSupergraphFromServiceList(result.serviceDefinitions!); + return this.createSupergraphFromSubgraphList(result.serviceDefinitions!); } private createDataSource( @@ -127,8 +127,8 @@ export class IntrospectAndCompose extends CallableInstance< ); } - private createSupergraphFromServiceList(serviceList: ServiceDefinition[]) { - const compositionResult = composeAndValidate(serviceList); + private createSupergraphFromSubgraphList(subgraphs: ServiceDefinition[]) { + const compositionResult = composeAndValidate(subgraphs); if (compositionHasErrors(compositionResult)) { const { errors } = compositionResult; diff --git a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts index 7c70807479..3bf54a7f41 100644 --- a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts @@ -25,14 +25,14 @@ describe('IntrospectAndCompose', () => { expect( () => new IntrospectAndCompose({ - serviceList: fixtures, + subgraphs: fixtures, }), ).not.toThrow(); }); it('is instance callable (simulating the gateway calling it)', async () => { mockAllServicesSdlQuerySuccess(); - const instance = new IntrospectAndCompose({ serviceList: fixtures }); + const instance = new IntrospectAndCompose({ subgraphs: fixtures }); await expect( instance({ update() {}, async healthCheck() {} }), ).resolves.toBeTruthy(); @@ -53,7 +53,7 @@ describe('IntrospectAndCompose', () => { const processSpies: jest.Mock[] = []; const instance = new IntrospectAndCompose({ - serviceList: fixtures, + subgraphs: fixtures, buildService(def) { const { datasource, processSpy } = getDataSourceSpy(def); processSpies.push(processSpy); @@ -91,7 +91,7 @@ describe('IntrospectAndCompose', () => { .mockImplementationOnce(() => r3()); const instance = new IntrospectAndCompose({ - serviceList: fixtures, + subgraphs: fixtures, pollIntervalInMs: 10, }); @@ -130,7 +130,7 @@ describe('IntrospectAndCompose', () => { mockAllServicesSdlQuerySuccess(); const instance = new IntrospectAndCompose({ - serviceList: fixtures, + subgraphs: fixtures, pollIntervalInMs: 100, buildService(service) { return new RemoteGraphQLDataSource({ @@ -180,7 +180,7 @@ describe('IntrospectAndCompose', () => { mockAllServicesSdlQuerySuccess(fixturesWithUpdate); const instance = new IntrospectAndCompose({ - serviceList: fixtures, + subgraphs: fixtures, pollIntervalInMs: 1000, logger, }); From 97f6ecaff2162227ed7eec1cfe73d721529d2b8f Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 17 Dec 2021 14:15:30 -0800 Subject: [PATCH 18/82] Brief intermission to adjust buildService implementation... --- gateway-js/src/config.ts | 7 ++ gateway-js/src/index.ts | 17 +++-- gateway-js/src/legacy/IntrospectAndCompose.ts | 52 ++++---------- .../__tests__/IntrospectAndCompose.test.ts | 68 ++++++++++--------- 4 files changed, 67 insertions(+), 77 deletions(-) diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index b744c6f580..7c9eebaf54 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -179,10 +179,17 @@ export function isManuallyManagedSupergraphSdlGatewayConfig( export type SupergraphSdlUpdateFunction = (updatedSupergraphSdl: string) => void; export type SubgraphHealthCheckFunction = (supergraphSdl: string) => Promise; + +export type GetDataSourceFunction = ({ + name, + url, +}: ServiceEndpointDefinition) => GraphQLDataSource; + export interface SupergraphSdlHook { (options: { update: SupergraphSdlUpdateFunction; healthCheck: SubgraphHealthCheckFunction; + getDataSource: GetDataSourceFunction; }): Promise<{ supergraphSdl: string; cleanup?: () => Promise; diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index c8f0df6fb3..dd570534af 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -212,12 +212,6 @@ export class ApolloGateway implements GraphQLService { // * `undefined` means the gateway is not using managed federation private uplinkEndpoints?: string[]; private uplinkMaxRetries?: number; - // The Promise case is strictly for handling the case where the user-provided - // function throws an error. - private manualConfigPromise?: Promise<{ - supergraphSdl: string; - cleanup?: () => Promise; - } | void>; // Functions to call during gateway cleanup (when stop() is called) private toDispose: (() => Promise)[] = []; @@ -532,6 +526,7 @@ export class ApolloGateway implements GraphQLService { const result = await config.supergraphSdl({ update: this.externalSupergraphUpdateCallback.bind(this), healthCheck: this.externalSubgraphHealthCheckCallback.bind(this), + getDataSource: this.externalGetDataSourceCallback.bind(this), }); if (!result?.supergraphSdl) { throw new Error( @@ -705,6 +700,13 @@ export class ApolloGateway implements GraphQLService { } } + private externalGetDataSourceCallback({ + name, + url, + }: ServiceEndpointDefinition) { + return this.createAndCacheDataSource({ name, url }); + } + private updateWithSupergraphSdl({ supergraphSdl, id }: SupergraphSdlUpdate) { // TODO(trevor): #580 redundant parse // This may throw, so we'll calculate early (specifically before making any updates) @@ -1066,8 +1068,9 @@ export class ApolloGateway implements GraphQLService { if ( this.serviceMap[serviceDef.name] && serviceDef.url === this.serviceMap[serviceDef.name].url - ) + ) { return this.serviceMap[serviceDef.name].dataSource; + } const dataSource = this.createDataSource(serviceDef); diff --git a/gateway-js/src/legacy/IntrospectAndCompose.ts b/gateway-js/src/legacy/IntrospectAndCompose.ts index bc5947e3ba..c7f7b3edd5 100644 --- a/gateway-js/src/legacy/IntrospectAndCompose.ts +++ b/gateway-js/src/legacy/IntrospectAndCompose.ts @@ -7,8 +7,6 @@ import { Logger } from 'apollo-server-types'; import CallableInstance from 'callable-instance'; import { HeadersInit } from 'node-fetch'; import { - GraphQLDataSource, - RemoteGraphQLDataSource, ServiceEndpointDefinition, SupergraphSdlHook, SupergraphSdlUpdateFunction, @@ -26,7 +24,6 @@ export interface IntrospectAndComposeOptions { | (( service: ServiceEndpointDefinition, ) => Promise | HeadersInit); - buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; pollIntervalInMs?: number; logger?: Logger; } @@ -40,41 +37,31 @@ export class IntrospectAndCompose extends CallableInstance< Parameters, ReturnType > { + private config: IntrospectAndComposeOptions; private update?: SupergraphSdlUpdateFunction; - private subgraphs: Service[]; - private introspectionHeaders?: - | HeadersInit - | (( - service: ServiceEndpointDefinition, - ) => Promise | HeadersInit); - private buildService?: ( - definition: ServiceEndpointDefinition, - ) => GraphQLDataSource; + private subgraphs?: Service[]; private serviceSdlCache: Map = new Map(); private pollIntervalInMs?: number; private timerRef: NodeJS.Timeout | null = null; private state: State; - private logger?: Logger; constructor(options: IntrospectAndComposeOptions) { super('instanceCallableMethod'); - // this.buildService needs to be assigned before this.subgraphs is built - this.buildService = options.buildService; + + this.config = options; this.pollIntervalInMs = options.pollIntervalInMs; - this.subgraphs = options.subgraphs.map((subgraph) => ({ - ...subgraph, - dataSource: this.createDataSource(subgraph), - })); - this.introspectionHeaders = options.introspectionHeaders; - this.logger = options.logger; this.state = { phase: 'initialized' }; } // @ts-ignore noUsedLocals private async instanceCallableMethod( - ...[{ update }]: Parameters + ...[{ update, getDataSource }]: Parameters ) { this.update = update; + this.subgraphs = this.config.subgraphs.map((subgraph) => ({ + ...subgraph, + dataSource: getDataSource(subgraph), + })); const initialSupergraphSdl = await this.updateSupergraphSdl(); // Start polling after we resolve the first supergraph @@ -100,11 +87,11 @@ export class IntrospectAndCompose extends CallableInstance< private async updateSupergraphSdl() { const result = await getServiceDefinitionsFromRemoteEndpoint({ - serviceList: this.subgraphs, + serviceList: this.subgraphs!, getServiceIntrospectionHeaders: async (service) => { - return typeof this.introspectionHeaders === 'function' - ? await this.introspectionHeaders(service) - : this.introspectionHeaders; + return typeof this.config.introspectionHeaders === 'function' + ? await this.config.introspectionHeaders(service) + : this.config.introspectionHeaders; }, serviceSdlCache: this.serviceSdlCache, }); @@ -116,17 +103,6 @@ export class IntrospectAndCompose extends CallableInstance< return this.createSupergraphFromSubgraphList(result.serviceDefinitions!); } - private createDataSource( - serviceDef: ServiceEndpointDefinition, - ): GraphQLDataSource { - return ( - this.buildService?.(serviceDef) ?? - new RemoteGraphQLDataSource({ - url: serviceDef.url, - }) - ); - } - private createSupergraphFromSubgraphList(subgraphs: ServiceDefinition[]) { const compositionResult = composeAndValidate(subgraphs); @@ -158,7 +134,7 @@ export class IntrospectAndCompose extends CallableInstance< this.update?.(maybeNewSupergraphSdl); } } catch (e) { - this.logger?.error( + this.config.logger?.error( 'IntrospectAndCompose failed to update supergraph with the following error: ' + (e.message ?? e), ); diff --git a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts index 3bf54a7f41..01e24bdb8a 100644 --- a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts @@ -3,6 +3,7 @@ import { fixtures, fixturesWithUpdate, } from 'apollo-federation-integration-testsuite'; +import { nockBeforeEach, nockAfterEach } from '../../__tests__/nockAssertions'; import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; import { IntrospectAndCompose } from '../IntrospectAndCompose'; import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; @@ -11,15 +12,8 @@ import { waitUntil } from '../../utilities/waitUntil'; import { Logger } from 'apollo-server-types'; describe('IntrospectAndCompose', () => { - beforeEach(() => { - if (!nock.isActive()) nock.activate(); - }); - - afterEach(async () => { - expect(nock.isDone()).toBeTruthy(); - nock.cleanAll(); - nock.restore(); - }); + beforeEach(nockBeforeEach); + afterEach(nockAfterEach); it('constructs', () => { expect( @@ -34,34 +28,38 @@ describe('IntrospectAndCompose', () => { mockAllServicesSdlQuerySuccess(); const instance = new IntrospectAndCompose({ subgraphs: fixtures }); await expect( - instance({ update() {}, async healthCheck() {} }), + instance({ + update() {}, + async healthCheck() {}, + getDataSource({ url }) { + return new RemoteGraphQLDataSource({ url }); + }, + }), ).resolves.toBeTruthy(); }); - function getDataSourceSpy(definition: ServiceEndpointDefinition) { - const datasource = new RemoteGraphQLDataSource({ - url: definition.url, - }); - const processSpy = jest.fn(datasource.process); - datasource.process = processSpy; - return { datasource, processSpy }; - } - it('uses `GraphQLDataSource`s provided by the `buildService` function', async () => { mockAllServicesSdlQuerySuccess(); - const processSpies: jest.Mock[] = []; + const processSpies: jest.SpyInstance[] = []; + function getDataSourceSpy(definition: ServiceEndpointDefinition) { + const datasource = new RemoteGraphQLDataSource({ + url: definition.url, + }); + const processSpy = jest.spyOn(datasource, 'process'); + processSpies.push(processSpy); + return datasource; + } const instance = new IntrospectAndCompose({ subgraphs: fixtures, - buildService(def) { - const { datasource, processSpy } = getDataSourceSpy(def); - processSpies.push(processSpy); - return datasource; - }, }); - await instance({ update() {}, async healthCheck() {} }); + await instance({ + update() {}, + async healthCheck() {}, + getDataSource: getDataSourceSpy, + }); expect(processSpies.length).toBe(fixtures.length); for (const processSpy of processSpies) { @@ -100,6 +98,9 @@ describe('IntrospectAndCompose', () => { updateSpy(supergraphSdl); }, async healthCheck() {}, + getDataSource({ url }) { + return new RemoteGraphQLDataSource({ url }); + }, }); await Promise.all([p1, p2, p3]); @@ -132,12 +133,6 @@ describe('IntrospectAndCompose', () => { const instance = new IntrospectAndCompose({ subgraphs: fixtures, pollIntervalInMs: 100, - buildService(service) { - return new RemoteGraphQLDataSource({ - url: service.url, - fetcher, - }); - }, }); const updateSpy = jest.fn(); @@ -146,6 +141,12 @@ describe('IntrospectAndCompose', () => { updateSpy(supergraphSdl); }, async healthCheck() {}, + getDataSource({ url }) { + return new RemoteGraphQLDataSource({ + url, + fetcher, + }); + }, }); // let the instance poll through all the active mocks @@ -194,6 +195,9 @@ describe('IntrospectAndCompose', () => { const { cleanup } = await instance({ update: updateSpy, async healthCheck() {}, + getDataSource({ url }) { + return new RemoteGraphQLDataSource({ url }); + }, }); await errorLoggedPromise; From 8731e5a67f8e999333d0acd999b7119ca7d6875c Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 17 Dec 2021 15:06:52 -0800 Subject: [PATCH 19/82] More docs --- docs/source/api/apollo-gateway.mdx | 99 +++++++++++++++--------------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index e7c5368cd7..02d45fefb9 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -233,9 +233,7 @@ The default value is `false`. -###### `buildService` (deprecated) - -**This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).** +###### `buildService` `Function` @@ -249,9 +247,7 @@ Define this function to customize your gateway's data transport to some or all o -##### The `buildService` function (deprecated) - -**This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).** +##### The `buildService` function If you provide this function, the gateway calls it once for each subgraph. The function must return an object that implements the [`GraphQLDataSource` interface](https://github.com/apollographql/apollo-server/blob/570f548b88/packages/apollo-gateway/src/datasources/types.ts), such as an instance of [RemoteGraphQLDataSource](#class-remotegraphqldatasource) or a subclass of it. @@ -572,65 +568,55 @@ const gateway = new ApolloGateway({ ###### Configuring the subgraph fetcher -If you provide a [`buildService` function](#the-buildservice-function) to the constructor of `IntrospectAndCompose`, the function is called once for each of your graph's subgraphs. This function can return a `RemoteGraphQLDataSource` with a custom `fetcher`, which is then used for all communication with subgraphs: +`IntrospectAndCompose` will use the data sources constructed by `ApolloGateway`. In order to customize the gateway's data sources, you can specify a [`buildService`](#buildservice) function. -```js -const gateway = new ApolloGateway({ - supergraphSdl: new IntrospectAndCompose({ - // ..., - buildService({ url, name }) { - return new (class extends RemoteGraphQLDataSource { - fetcher = require('make-fetch-happen').defaults({ - onRetry() { - console.log('We will retry!') - }, - }); - })({ - url, - name, - }); - } - }), -}); -``` +##### Options -###### `buildService` + + + + + + + -`Function` + + + + + + -
Name /
Type
Description
+ +###### `subgraphs` + +`Array` -Define this function to customize your gateway's data transport to some or all of your subgraphs. This customization can include using a protocol besides HTTP. For details, see [The `buildService` function](#the-buildservice-function). +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. + +You can specify any string value for the `name` field, which is used for identifying the subgraph in log output and error messages, and for reporting metrics to Apollo Studio. +
-
+###### `introspectionHeaders` -##### The `buildService` function +`Object | (service: ServiceEndpointDefinition) => Promise | Object` + + -If you provide this function, the gateway calls it once for each subgraph. The function must return an object that implements the [`GraphQLDataSource` interface](https://github.com/apollographql/apollo-server/blob/570f548b88/packages/apollo-gateway/src/datasources/types.ts), such as an instance of [RemoteGraphQLDataSource](#class-remotegraphqldatasource) or a subclass of it. +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. -The example below demonstrates adding an `x-user-id` HTTP header to every request the gateway sends to a subgraph: +**If you are using managed federation,** do not provide this field. -```js{9-11} -class AuthenticatedDataSource extends RemoteGraphQLDataSource { - willSendRequest({ request, context }) { - request.http.headers.set('x-user-id', context.userId); - } -} +**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. + + -const gateway = new ApolloGateway({ - // ...other options... - supergraphSdl: new IntrospectAndCompose({ - // ..., - buildService({ name, url }) { - return new AuthenticatedDataSource({ url }); - }, - }), -}); -``` + + ###### `pollIntervalInMs` @@ -638,10 +624,21 @@ const gateway = new ApolloGateway({ -Define this function to customize your gateway's data transport to some or all of your subgraphs. This customization can include using a protocol besides HTTP. For details, see [The `buildService` function](#the-buildservice-function). +Specify this option to enable supergraph updates via subgraph polling. `IntrospectAndCompose` will poll each subgraph +###### `logger` + +[`Logger`](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-types/src/index.ts#L166-L172) + + + +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`. + + \ No newline at end of file From e1d081c30d17308b8bb77b5909e3769c56d6339e Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 17 Dec 2021 15:18:56 -0800 Subject: [PATCH 20/82] Fix docs? --- docs/source/api/apollo-gateway.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 02d45fefb9..f4f618d477 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -628,6 +628,9 @@ Specify this option to enable supergraph updates via subgraph polling. `Introspe + + + ###### `logger` [`Logger`](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-types/src/index.ts#L166-L172) From 1c4905104347a09f9c2f53493be560a95498c42f Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 20 Dec 2021 09:40:47 -0800 Subject: [PATCH 21/82] Use createHash implementation safe for non-node envs --- gateway-js/package.json | 3 ++- gateway-js/src/index.ts | 2 +- gateway-js/src/utilities/createHash.ts | 10 ++++++++++ gateway-js/src/utilities/isNodeLike.ts | 11 +++++++++++ package-lock.json | 8 +++++--- 5 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 gateway-js/src/utilities/createHash.ts create mode 100644 gateway-js/src/utilities/isNodeLike.ts diff --git a/gateway-js/package.json b/gateway-js/package.json index e254fc7594..8b3bac80e9 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -40,7 +40,8 @@ "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", - "pretty-format": "^27.3.1" + "pretty-format": "^27.3.1", + "sha.js": "^2.4.11" }, "peerDependencies": { "graphql": "^15.7.2" diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index dd570534af..4245ddce53 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -78,7 +78,7 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { OpenTelemetrySpanNames, tracer } from './utilities/opentelemetry'; import { CoreSchema } from '@apollo/core-schema'; import { featureSupport } from './core'; -import { createHash } from 'apollo-graphql/lib/utilities/createHash'; +import { createHash } from './utilities/createHash'; type DataSourceMap = { [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; diff --git a/gateway-js/src/utilities/createHash.ts b/gateway-js/src/utilities/createHash.ts new file mode 100644 index 0000000000..a7cf0ab828 --- /dev/null +++ b/gateway-js/src/utilities/createHash.ts @@ -0,0 +1,10 @@ +import isNodeLike from './isNodeLike'; + +export function createHash (kind: string): import('crypto').Hash { + if (isNodeLike) { + // Use module.require instead of just require to avoid bundling whatever + // crypto polyfills a non-Node bundler might fall back to. + return module.require('crypto').createHash(kind); + } + return require('sha.js')(kind); +} diff --git a/gateway-js/src/utilities/isNodeLike.ts b/gateway-js/src/utilities/isNodeLike.ts new file mode 100644 index 0000000000..e5fa3a2221 --- /dev/null +++ b/gateway-js/src/utilities/isNodeLike.ts @@ -0,0 +1,11 @@ +export default typeof process === 'object' && + process && + // We used to check `process.release.name === "node"`, however that doesn't + // account for certain forks of Node.js which are otherwise identical to + // Node.js. For example, NodeSource's N|Solid reports itself as "nsolid", + // though it's mostly the same build of Node.js with an extra addon. + process.release && + process.versions && + // The one thing which is present on both Node.js and N|Solid (a fork of + // Node.js), is `process.versions.node` being defined. + typeof process.versions.node === 'string'; diff --git a/package-lock.json b/package-lock.json index c04f10af21..1b04212a0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -147,7 +147,7 @@ }, "gateway-js": { "name": "@apollo/gateway", - "version": "0.45.0-alpha.0", + "version": "0.45.0-alpha.1", "license": "MIT", "dependencies": { "@apollo/core-schema": "^0.2.0", @@ -165,7 +165,8 @@ "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", - "pretty-format": "^27.3.1" + "pretty-format": "^27.3.1", + "sha.js": "^2.4.11" }, "engines": { "node": ">=12.13.0 <17.0" @@ -23666,7 +23667,8 @@ "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", - "pretty-format": "^27.3.1" + "pretty-format": "^27.3.1", + "sha.js": "^2.4.11" }, "dependencies": { "@jest/types": { From 7f8377e6752de0b5d707156c36103d0fade6a167 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 20 Dec 2021 09:47:38 -0800 Subject: [PATCH 22/82] Fix broken links --- docs/source/entities.mdx | 6 +++--- docs/source/subgraphs.mdx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 4c2f7a4e4b..56f63b1b3d 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -391,11 +391,11 @@ We're done! `Bill` now originates in a new subgraph, and it was resolvable durin - + -> ⚠️ We strongly recommend _against_ using `serviceList`. For details, see [Limitations of `serviceList`](./gateway/#limitations-of-servicelist). +> ⚠️ We strongly recommend _against_ using `IntrospectAndCompose`. For details, see [Limitations of `IntrospectAndCompose`](./gateway/#limitations-of-introspectandcompose). -When you provide a `serviceList` to `ApolloGateway`, it performs composition _itself_ on startup after fetching all of your subgraph schemas. If this runtime composition fails, the gateway fails to start up, resulting in downtime. +When you provide `IntrospectAndCompose` to `ApolloGateway`, it performs composition _itself_ on startup after fetching all of your subgraph schemas. If this runtime composition fails, the gateway fails to start up, resulting in downtime. To minimize downtime for your graph, you need to make sure all of your subgraph schemas successfully compose whenever your gateway starts up. When migrating an entity, this requires a **coordinated deployment** of your modified subgraphs and a restart of the gateway itself. diff --git a/docs/source/subgraphs.mdx b/docs/source/subgraphs.mdx index 8fac6a1ea5..846d90554c 100644 --- a/docs/source/subgraphs.mdx +++ b/docs/source/subgraphs.mdx @@ -197,7 +197,7 @@ The `sdl` field returns your subgraph's schema as an SDL string. This field has * Unlike introspection, the `sdl` field is _not_ disabled by default in production environments (this is safe if you properly [secure your subgraph](#securing-your-subgraphs)). * Unlike introspection, the `sdl` field's returned string includes federation-specific directives like `@key`. -Whenever your gateway needs to fetch a subgraph's schema (this occurs only if your gateway uses [the `serviceList` option](./gateway/#composing-with-servicelist)), it uses this field _instead of_ an introspection query so it can obtain federation-specific details. +Whenever your gateway needs to fetch a subgraph's schema (this occurs only if your gateway uses [`IntrospectAndCompose`](./gateway/#composing-subgraphs-with-introspectandcompose)), it uses this field _instead of_ an introspection query so it can obtain federation-specific details. ### `Query._entities` From 144e0cd85464916abf97c45bf7e29ce5dc7679db Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 12:11:59 -0800 Subject: [PATCH 23/82] Add example using buildService --- docs/source/api/apollo-gateway.mdx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 26c19eb922..96687f5581 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -604,7 +604,22 @@ const gateway = new ApolloGateway({ ###### Configuring the subgraph fetcher -`IntrospectAndCompose` will use the data sources constructed by `ApolloGateway`. In order to customize the gateway's data sources, you can specify a [`buildService`](#buildservice) function. +`IntrospectAndCompose` will use the data sources constructed by `ApolloGateway`. In order to customize the gateway's data sources, you can proved a [`buildService`](#buildservice) function to the `ApolloGateway` constructor. In the example below, `IntrospectAndCompose` will make 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 @@ -660,7 +675,7 @@ An object, or an (optionally) async function returning an object, containing the -Specify this option to enable supergraph updates via subgraph polling. `IntrospectAndCompose` will poll each subgraph +Specify this option to enable supergraph updates via subgraph polling. `IntrospectAndCompose` will poll each subgraph at the given interval. From 5221b9987bd29e191bc99f1d0cea5d517dbbe801 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 12:20:53 -0800 Subject: [PATCH 24/82] Add parens for clarity --- docs/source/api/apollo-gateway.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 96687f5581..094a65a6f5 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -145,7 +145,7 @@ You can specify any string value for the `name` field, which is used for identif **This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).** -`Object | (service: ServiceEndpointDefinition) => Promise | Object` +`Object | (service: ServiceEndpointDefinition) => (Promise | Object)` @@ -654,7 +654,7 @@ You can specify any string value for the `name` field, which is used for identif ###### `introspectionHeaders` -`Object | (service: ServiceEndpointDefinition) => Promise | Object` +`Object | (service: ServiceEndpointDefinition) => (Promise | Object)` From e9e191eff8ff4f3857a9cd4f2a5a828fe5f1f791 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 12:24:54 -0800 Subject: [PATCH 25/82] docs tweak --- docs/source/gateway.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/gateway.mdx b/docs/source/gateway.mdx index 960d2e6442..3411f9ac76 100644 --- a/docs/source/gateway.mdx +++ b/docs/source/gateway.mdx @@ -89,10 +89,10 @@ server.listen().then(({ url }) => { }); ``` -There are a few things happening here, let's take a look at each of them individually. +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 only called once when `ApolloServer` initializes the gateway. This function has two primary responsibilities: -1. Provide us access to the `update` function, which we'll use to update the supergraph schema. +Note that `supergraphSdl` is now an `async` function. This function is only called once when `ApolloServer` initializes the gateway, and has two primary responsibilities: +1. Receive the `update` function, which we'll use to update the supergraph schema. 2. Return the initial supergraph schema which the gateway will use 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. From 773578d2a8e67d6a7ad4b44b539b21d6fc8ef1e2 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 12:28:41 -0800 Subject: [PATCH 26/82] Use fs/promises for file read example --- docs/source/gateway.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/gateway.mdx b/docs/source/gateway.mdx index 3411f9ac76..8e8083dee2 100644 --- a/docs/source/gateway.mdx +++ b/docs/source/gateway.mdx @@ -102,7 +102,8 @@ The code below demonstrates a more complete example using a file watcher. In thi ```js:title=index.js const { ApolloServer } = require('apollo-server'); const { ApolloGateway } = require('@apollo/gateway'); -const { readFileSync, watch } = require('fs'); +const { watch } = require('fs'); +const { readFile } = require('fs/promises'); const server = new ApolloServer({ gateway: new ApolloGateway({ @@ -113,7 +114,7 @@ const server = new ApolloServer({ watcher.on('change', async () => { // update the supergraph schema try { - const updatedSupergraph = readFileSync('./supergraph.graphql').toString(); + 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 From 9a284d573afd189177a17e80ce55dbb00c16a034 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 13:28:25 -0800 Subject: [PATCH 27/82] Add clarity around subgraph names --- docs/source/api/apollo-gateway.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 094a65a6f5..3978910e0c 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -644,7 +644,8 @@ const gateway = new ApolloGateway({ 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. -You can specify any string value for the `name` field, which is used for identifying the subgraph in log output and error messages, and for reporting metrics to Apollo Studio. +The `name` field is a string which 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 also satisfy the following regex when publishing: `^[a-zA-Z][a-zA-Z0-9_-]{0,63}$`. From 399a7d1297e8a19185fbd9fbe71a5c48f0468965 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 13:29:56 -0800 Subject: [PATCH 28/82] remove extraneous note --- docs/source/api/apollo-gateway.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 3978910e0c..07fb801029 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -661,8 +661,6 @@ The `name` field is a string which should be treated as a subgraph's unique iden 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. -**If you are using managed federation,** do not provide this field. - **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. From c0b3c40d509273e87da1ba8f374f3eb0ec1f47ec Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 14:36:29 -0800 Subject: [PATCH 29/82] Implement object.initialize behavior for supergraphSdl property --- docs/source/api/apollo-gateway.mdx | 11 ++++++-- docs/source/gateway.mdx | 12 ++++---- gateway-js/package.json | 1 - .../__tests__/gateway/supergraphSdl.test.ts | 28 +++++++++++++++---- gateway-js/src/config.ts | 23 ++++++++++----- gateway-js/src/index.ts | 7 ++++- gateway-js/src/legacy/IntrospectAndCompose.ts | 25 ++++++----------- .../__tests__/IntrospectAndCompose.test.ts | 10 +++---- package-lock.json | 12 -------- 9 files changed, 74 insertions(+), 55 deletions(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 07fb801029..8de620e882 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -100,11 +100,18 @@ const gateway = new ApolloGateway({ ###### `supergraphSdl` -`String` +`string | SupergraphSdlHook | SupergraphSdlObject` -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 `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`: An `async` function that returns an object containing a `supergraphSdl` string as well as a `cleanup` function. The hook accepts an object containing 3 properties: +1. `update`: A function which updates the supergraph schema +2. `healthCheck`: A function which issues a health check against the subgraphs +3. `getDataSource`: A function which gets a data source for a particular subgraph from the gateway + +When `supergraphSdl` is a `SupergraphSdlObject`: An object containing an `initialize` property. `initialize` is an `async` function of the `SupergraphSdlHook` type mentioned directly above. **If you are using managed federation,** do not provide this field. diff --git a/docs/source/gateway.mdx b/docs/source/gateway.mdx index 8e8083dee2..77d6032778 100644 --- a/docs/source/gateway.mdx +++ b/docs/source/gateway.mdx @@ -64,9 +64,7 @@ In the above example, we provide a _static_ supergraph schema to the gateway. Th ```js:title=index.js const { ApolloServer } = require('apollo-server'); const { ApolloGateway } = require('@apollo/gateway'); -const { readFileSync } = require('fs'); - -const supergraphSdl = readFileSync('./supergraph.graphql').toString(); +const { readFile } = require('fs/promises'); let supergraphUpdate; const gateway = new ApolloGateway({ @@ -74,7 +72,7 @@ const gateway = new ApolloGateway({ // `update` is a function which we'll save for later use supergraphUpdate = update; return { - supergraphSdl, + supergraphSdl: await readFile('./supergraph.graphql', 'utf8'), } }, }); @@ -126,7 +124,7 @@ const server = new ApolloServer({ }); return { - supergraphSdl: readFileSync('./supergraph.graphql').toString(), + supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'), // cleanup is called when the gateway is stopped async cleanup() { watcher.close(); @@ -149,6 +147,10 @@ We've also wrapped our call to `update` and `healthCheck` in a `try` block. If a You might've also noticed we're returning a `cleanup` function. This is a callback that is called when the gateway is stopped. This allows 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 will `await` it before shutting down. +#### Advanced usage + +Your use case may grow to have some complexity which might be better managed within a class that handles the `update` and `healthCheck` functions as well as any additional state. In this case, you may instead provide an object (or class) with an `initialize` function. This function is called just the same as the `supergraphSdl` function discussed above. For a good example of this, check out the [`IntrospectAndCompose` source code](https://github.com/apollographql/federation/blob/main/packages/apollo-gateway/src/IntrospectAndCompose/index.ts). + ### Composing subgraphs with `IntrospectAndCompose` > Looking for `serviceList`? `IntrospectAndCompose` is the new drop-in replacement for the `serviceList` option. In the near future, the `serviceList` option will be removed, however `IntrospectAndCompose` will continue to be supported. Note that this is still considered an outdated workflow. Apollo recommends approaches utilize `rover` for managing local composition. diff --git a/gateway-js/package.json b/gateway-js/package.json index 9de7dff185..c67f8f7344 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -37,7 +37,6 @@ "apollo-server-env": "^3.0.0 || ^4.0.0", "apollo-server-errors": "^2.5.0 || ^3.0.0", "apollo-server-types": "^0.9.0 || ^3.0.0 || ^3.5.0-alpha.0", - "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", "pretty-format": "^27.3.1", diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 6c8af6eb06..760a00a841 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -7,11 +7,11 @@ import { fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; import { createHash } from 'apollo-graphql/lib/utilities/createHash'; import { ApolloServer } from 'apollo-server'; import { Logger } from 'apollo-server-types'; -import nock from 'nock'; import { fetch } from '../../__mocks__/apollo-server-env'; import { getTestingSupergraphSdl } from '../execution-utils'; import { mockAllServicesHealthCheckSuccess } from '../integration/nockMocks'; import { waitUntil } from '../../utilities/waitUntil'; +import { nockAfterEach, nockBeforeEach } from '../nockAssertions'; async function getSupergraphSdlGatewayServer() { const server = new ApolloServer({ @@ -27,7 +27,7 @@ async function getSupergraphSdlGatewayServer() { let logger: Logger; let gateway: ApolloGateway | null; beforeEach(() => { - if (!nock.isActive()) nock.activate(); + nockBeforeEach(); logger = { debug: jest.fn(), @@ -38,9 +38,8 @@ beforeEach(() => { }); afterEach(async () => { - expect(nock.isDone()).toBeTruthy(); - nock.cleanAll(); - nock.restore(); + nockAfterEach(); + if (gateway) { await gateway.stop(); gateway = null; @@ -219,6 +218,25 @@ describe('Using supergraphSdl dynamic configuration', () => { await expect(healthCheckCallback!(supergraphSdl)).resolves.toBeUndefined(); }); + it('calls `initialize` on an object provided to `supergraphSdl`', async () => { + const MockSdlUpdatingClass = { + initialize() { + return Promise.resolve({ + supergraphSdl: getTestingSupergraphSdl(), + }); + }, + }; + const initializeSpy = jest.spyOn(MockSdlUpdatingClass, 'initialize'); + + gateway = new ApolloGateway({ + supergraphSdl: MockSdlUpdatingClass, + }); + + expect(initializeSpy).not.toHaveBeenCalled(); + await gateway.load(); + expect(initializeSpy).toHaveBeenCalled(); + }); + describe('errors', () => { it('fails to load if user-provided `supergraphSdl` function throws', async () => { const failureMessage = 'Error from supergraphSdl function'; diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 7c9eebaf54..153d1b4ab3 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -172,7 +172,10 @@ export function isManuallyManagedSupergraphSdlGatewayConfig( config: GatewayConfig, ): config is ManuallyManagedSupergraphSdlGatewayConfig { return ( - 'supergraphSdl' in config && typeof config.supergraphSdl === 'function' + 'supergraphSdl' in config && + (typeof config.supergraphSdl === 'function' || + (typeof config.supergraphSdl === 'object' && + 'initialize' in config.supergraphSdl)) ); } @@ -185,18 +188,24 @@ export type GetDataSourceFunction = ({ url, }: ServiceEndpointDefinition) => GraphQLDataSource; +export interface SupergraphSdlHookOptions { + update: SupergraphSdlUpdateFunction; + healthCheck: SubgraphHealthCheckFunction; + getDataSource: GetDataSourceFunction; +} export interface SupergraphSdlHook { - (options: { - update: SupergraphSdlUpdateFunction; - healthCheck: SubgraphHealthCheckFunction; - getDataSource: GetDataSourceFunction; - }): Promise<{ + (options: SupergraphSdlHookOptions): Promise<{ supergraphSdl: string; cleanup?: () => Promise; }>; } + +export interface SupergraphSdlObject { + initialize: SupergraphSdlHook +} + export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { - supergraphSdl: SupergraphSdlHook; + supergraphSdl: SupergraphSdlHook | SupergraphSdlObject; } type ManuallyManagedGatewayConfig = diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 4245ddce53..abe7384a25 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -523,7 +523,12 @@ export class ApolloGateway implements GraphQLService { config: ManuallyManagedSupergraphSdlGatewayConfig, ) { try { - const result = await config.supergraphSdl({ + const initFunction = + typeof config.supergraphSdl === 'function' + ? config.supergraphSdl + : config.supergraphSdl.initialize; + + const result = await initFunction({ update: this.externalSupergraphUpdateCallback.bind(this), healthCheck: this.externalSubgraphHealthCheckCallback.bind(this), getDataSource: this.externalGetDataSourceCallback.bind(this), diff --git a/gateway-js/src/legacy/IntrospectAndCompose.ts b/gateway-js/src/legacy/IntrospectAndCompose.ts index c7f7b3edd5..d952f16f09 100644 --- a/gateway-js/src/legacy/IntrospectAndCompose.ts +++ b/gateway-js/src/legacy/IntrospectAndCompose.ts @@ -4,18 +4,14 @@ import { ServiceDefinition, } from '@apollo/federation'; import { Logger } from 'apollo-server-types'; -import CallableInstance from 'callable-instance'; import { HeadersInit } from 'node-fetch'; -import { - ServiceEndpointDefinition, - SupergraphSdlHook, - SupergraphSdlUpdateFunction, -} from '..'; +import { ServiceEndpointDefinition, SupergraphSdlUpdateFunction } from '..'; import { getServiceDefinitionsFromRemoteEndpoint, Service, } from './loadServicesFromRemoteEndpoint'; import { waitUntil } from '../utilities/waitUntil'; +import { SupergraphSdlObject, SupergraphSdlHookOptions } from '../config'; export interface IntrospectAndComposeOptions { subgraphs: ServiceEndpointDefinition[]; @@ -33,10 +29,7 @@ type State = | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; -export class IntrospectAndCompose extends CallableInstance< - Parameters, - ReturnType -> { +export class IntrospectAndCompose implements SupergraphSdlObject { private config: IntrospectAndComposeOptions; private update?: SupergraphSdlUpdateFunction; private subgraphs?: Service[]; @@ -46,17 +39,12 @@ export class IntrospectAndCompose extends CallableInstance< private state: State; constructor(options: IntrospectAndComposeOptions) { - super('instanceCallableMethod'); - this.config = options; this.pollIntervalInMs = options.pollIntervalInMs; this.state = { phase: 'initialized' }; } - // @ts-ignore noUsedLocals - private async instanceCallableMethod( - ...[{ update, getDataSource }]: Parameters - ) { + public async initialize({ update, getDataSource }: SupergraphSdlHookOptions) { this.update = update; this.subgraphs = this.config.subgraphs.map((subgraph) => ({ ...subgraph, @@ -70,7 +58,10 @@ export class IntrospectAndCompose extends CallableInstance< } return { - supergraphSdl: initialSupergraphSdl, + // on init, this supergraphSdl should never actually be `null`. + // `this.updateSupergraphSdl()` will only return null if the schema hasn't + // changed over the course of an _update_. + supergraphSdl: initialSupergraphSdl!, cleanup: async () => { if (this.state.phase === 'polling') { await this.state.pollingPromise; diff --git a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts index 01e24bdb8a..dbcde5100a 100644 --- a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts @@ -28,7 +28,7 @@ describe('IntrospectAndCompose', () => { mockAllServicesSdlQuerySuccess(); const instance = new IntrospectAndCompose({ subgraphs: fixtures }); await expect( - instance({ + instance.initialize({ update() {}, async healthCheck() {}, getDataSource({ url }) { @@ -55,7 +55,7 @@ describe('IntrospectAndCompose', () => { subgraphs: fixtures, }); - await instance({ + await instance.initialize({ update() {}, async healthCheck() {}, getDataSource: getDataSourceSpy, @@ -93,7 +93,7 @@ describe('IntrospectAndCompose', () => { pollIntervalInMs: 10, }); - const { cleanup } = await instance({ + const { cleanup } = await instance.initialize({ update(supergraphSdl) { updateSpy(supergraphSdl); }, @@ -136,7 +136,7 @@ describe('IntrospectAndCompose', () => { }); const updateSpy = jest.fn(); - const { cleanup } = await instance({ + const { cleanup } = await instance.initialize({ update(supergraphSdl) { updateSpy(supergraphSdl); }, @@ -192,7 +192,7 @@ describe('IntrospectAndCompose', () => { throw new Error(thrownErrorMessage); }); - const { cleanup } = await instance({ + const { cleanup } = await instance.initialize({ update: updateSpy, async healthCheck() {}, getDataSource({ url }) { diff --git a/package-lock.json b/package-lock.json index 43da149233..066bdbfd94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,7 +162,6 @@ "apollo-server-env": "^3.0.0 || ^4.0.0", "apollo-server-errors": "^2.5.0 || ^3.0.0", "apollo-server-types": "^0.9.0 || ^3.0.0 || ^3.5.0-alpha.0", - "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", "pretty-format": "^27.3.1", @@ -8250,11 +8249,6 @@ "get-intrinsic": "^1.0.2" } }, - "node_modules/callable-instance": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-2.0.0.tgz", - "integrity": "sha512-wOBp/J1CRZLsbFxG1alxefJjoG1BW/nocXkUanAe2+leiD/+cVr00j8twSZoDiRy03o5vibq9pbrZc+EDjjUTw==" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -23835,7 +23829,6 @@ "apollo-server-env": "^3.0.0 || ^4.0.0", "apollo-server-errors": "^2.5.0 || ^3.0.0", "apollo-server-types": "^0.9.0 || ^3.0.0 || ^3.5.0-alpha.0", - "callable-instance": "^2.0.0", "loglevel": "^1.6.1", "make-fetch-happen": "^8.0.0", "pretty-format": "^27.3.1", @@ -30477,11 +30470,6 @@ "get-intrinsic": "^1.0.2" } }, - "callable-instance": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-2.0.0.tgz", - "integrity": "sha512-wOBp/J1CRZLsbFxG1alxefJjoG1BW/nocXkUanAe2+leiD/+cVr00j8twSZoDiRy03o5vibq9pbrZc+EDjjUTw==" - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", From 961c36e988abb4f44b7e18bc0e1e9a671dac38db Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 14:41:24 -0800 Subject: [PATCH 30/82] move out of legacy directory --- .../__tests__/IntrospectAndCompose.test.ts | 2 +- .../__tests__/loadServicesFromRemoteEndpoint.test.ts | 0 .../{legacy => IntrospectAndCompose}/__tests__/tsconfig.json | 0 .../IntrospectAndCompose.ts => IntrospectAndCompose/index.ts} | 0 .../loadServicesFromRemoteEndpoint.ts | 0 gateway-js/src/index.ts | 4 ++-- 6 files changed, 3 insertions(+), 3 deletions(-) rename gateway-js/src/{legacy => IntrospectAndCompose}/__tests__/IntrospectAndCompose.test.ts (99%) rename gateway-js/src/{legacy => IntrospectAndCompose}/__tests__/loadServicesFromRemoteEndpoint.test.ts (100%) rename gateway-js/src/{legacy => IntrospectAndCompose}/__tests__/tsconfig.json (100%) rename gateway-js/src/{legacy/IntrospectAndCompose.ts => IntrospectAndCompose/index.ts} (100%) rename gateway-js/src/{legacy => IntrospectAndCompose}/loadServicesFromRemoteEndpoint.ts (100%) diff --git a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts similarity index 99% rename from gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts rename to gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts index dbcde5100a..139756ed7a 100644 --- a/gateway-js/src/legacy/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts @@ -5,7 +5,7 @@ import { } from 'apollo-federation-integration-testsuite'; import { nockBeforeEach, nockAfterEach } from '../../__tests__/nockAssertions'; import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; -import { IntrospectAndCompose } from '../IntrospectAndCompose'; +import { IntrospectAndCompose } from '..'; import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; import { wait } from '../../__tests__/execution-utils'; import { waitUntil } from '../../utilities/waitUntil'; diff --git a/gateway-js/src/legacy/__tests__/loadServicesFromRemoteEndpoint.test.ts b/gateway-js/src/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts similarity index 100% rename from gateway-js/src/legacy/__tests__/loadServicesFromRemoteEndpoint.test.ts rename to gateway-js/src/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts diff --git a/gateway-js/src/legacy/__tests__/tsconfig.json b/gateway-js/src/IntrospectAndCompose/__tests__/tsconfig.json similarity index 100% rename from gateway-js/src/legacy/__tests__/tsconfig.json rename to gateway-js/src/IntrospectAndCompose/__tests__/tsconfig.json diff --git a/gateway-js/src/legacy/IntrospectAndCompose.ts b/gateway-js/src/IntrospectAndCompose/index.ts similarity index 100% rename from gateway-js/src/legacy/IntrospectAndCompose.ts rename to gateway-js/src/IntrospectAndCompose/index.ts diff --git a/gateway-js/src/legacy/loadServicesFromRemoteEndpoint.ts b/gateway-js/src/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts similarity index 100% rename from gateway-js/src/legacy/loadServicesFromRemoteEndpoint.ts rename to gateway-js/src/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index abe7384a25..e1cd3b3fc5 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -30,7 +30,7 @@ import { defaultFieldResolverWithAliasSupport, } from './executeQueryPlan'; -import { getServiceDefinitionsFromRemoteEndpoint } from './legacy/loadServicesFromRemoteEndpoint'; +import { getServiceDefinitionsFromRemoteEndpoint } from './IntrospectAndCompose/loadServicesFromRemoteEndpoint'; import { GraphQLDataSource, GraphQLDataSourceRequestKind, @@ -1518,4 +1518,4 @@ export { SupergraphSdlHook, } from './config'; -export { IntrospectAndCompose } from './legacy/IntrospectAndCompose'; +export { IntrospectAndCompose } from './IntrospectAndCompose'; From 060540702380dd2be5a6307b8a2def91e9080b60 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 15:20:00 -0800 Subject: [PATCH 31/82] remove waitUntil, use resolvable --- .../__tests__/IntrospectAndCompose.test.ts | 18 +++---- gateway-js/src/IntrospectAndCompose/index.ts | 9 ++-- .../__tests__/gateway/lifecycle-hooks.test.ts | 16 +++--- .../__tests__/gateway/supergraphSdl.test.ts | 27 ++++------ .../integration/networkRequests.test.ts | 51 ++++++++----------- gateway-js/src/utilities/waitUntil.ts | 18 ------- package-lock.json | 1 + package.json | 1 + 8 files changed, 55 insertions(+), 86 deletions(-) delete mode 100644 gateway-js/src/utilities/waitUntil.ts diff --git a/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts index 139756ed7a..380e593b4c 100644 --- a/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts @@ -8,7 +8,7 @@ import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; import { IntrospectAndCompose } from '..'; import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; import { wait } from '../../__tests__/execution-utils'; -import { waitUntil } from '../../utilities/waitUntil'; +import resolvable from '@josephg/resolvable'; import { Logger } from 'apollo-server-types'; describe('IntrospectAndCompose', () => { @@ -75,18 +75,18 @@ describe('IntrospectAndCompose', () => { mockAllServicesSdlQuerySuccess(); mockAllServicesSdlQuerySuccess(fixturesWithUpdate); - const [p1, r1] = waitUntil(); - const [p2, r2] = waitUntil(); - const [p3, r3] = waitUntil(); + const p1 = resolvable(); + const p2 = resolvable(); + const p3 = resolvable(); // `update` (below) is called each time we poll (and there's an update to // the supergraph), so this is a reasonable hook into "when" the poll // happens and drives this test cleanly with `Promise`s. const updateSpy = jest .fn() - .mockImplementationOnce(() => r1()) - .mockImplementationOnce(() => r2()) - .mockImplementationOnce(() => r3()); + .mockImplementationOnce(() => p1.resolve()) + .mockImplementationOnce(() => p2.resolve()) + .mockImplementationOnce(() => p3.resolve()); const instance = new IntrospectAndCompose({ subgraphs: fixtures, @@ -162,10 +162,10 @@ describe('IntrospectAndCompose', () => { describe('errors', () => { it('logs an error when `update` function throws', async () => { - const [errorLoggedPromise, resolveErrorLoggedPromise] = waitUntil(); + const errorLoggedPromise = resolvable(); const errorSpy = jest.fn(() => { - resolveErrorLoggedPromise(); + errorLoggedPromise.resolve(); }); const logger: Logger = { error: errorSpy, diff --git a/gateway-js/src/IntrospectAndCompose/index.ts b/gateway-js/src/IntrospectAndCompose/index.ts index d952f16f09..e51a20b23c 100644 --- a/gateway-js/src/IntrospectAndCompose/index.ts +++ b/gateway-js/src/IntrospectAndCompose/index.ts @@ -10,7 +10,6 @@ import { getServiceDefinitionsFromRemoteEndpoint, Service, } from './loadServicesFromRemoteEndpoint'; -import { waitUntil } from '../utilities/waitUntil'; import { SupergraphSdlObject, SupergraphSdlHookOptions } from '../config'; export interface IntrospectAndComposeOptions { @@ -117,7 +116,11 @@ export class IntrospectAndCompose implements SupergraphSdlObject { private poll() { this.timerRef = setTimeout(async () => { if (this.state.phase === 'polling') { - const [pollingPromise, donePolling] = waitUntil(); + let pollingDone: () => void; + const pollingPromise = new Promise((resolve) => { + pollingDone = resolve; + }); + this.state.pollingPromise = pollingPromise; try { const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); @@ -130,7 +133,7 @@ export class IntrospectAndCompose implements SupergraphSdlObject { (e.message ?? e), ); } - donePolling!(); + pollingDone!(); } this.poll(); diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index de5dd2c2ae..7749a89296 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -13,6 +13,7 @@ import { documents, } from 'apollo-federation-integration-testsuite'; import { Logger } from 'apollo-server-types'; +import resolvable from '@josephg/resolvable'; // The order of this was specified to preserve existing test coverage. Typically // we would just import and use the `fixtures` array. @@ -148,16 +149,14 @@ describe('lifecycle hooks', () => { // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here gateway.experimental_pollInterval = 100; - let resolve1: Function; - let resolve2: Function; - const schemaChangeBlocker1 = new Promise(res => (resolve1 = res)); - const schemaChangeBlocker2 = new Promise(res => (resolve2 = res)); + const schemaChangeBlocker1 = resolvable(); + const schemaChangeBlocker2 = resolvable(); gateway.onSchemaChange( jest .fn() - .mockImplementationOnce(() => resolve1()) - .mockImplementationOnce(() => resolve2()), + .mockImplementationOnce(() => schemaChangeBlocker1.resolve()) + .mockImplementationOnce(() => schemaChangeBlocker2.resolve()), ); await gateway.load(); @@ -228,9 +227,8 @@ describe('lifecycle hooks', () => { logger, }); - let resolve: Function; - const schemaChangeBlocker = new Promise(res => (resolve = res)); - const schemaChangeCallback = jest.fn(() => resolve()); + const schemaChangeBlocker = resolvable(); + const schemaChangeCallback = jest.fn(() => schemaChangeBlocker.resolve()); gateway.onSchemaChange(schemaChangeCallback); await gateway.load(); diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 760a00a841..94cb9efd15 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -10,7 +10,7 @@ import { Logger } from 'apollo-server-types'; import { fetch } from '../../__mocks__/apollo-server-env'; import { getTestingSupergraphSdl } from '../execution-utils'; import { mockAllServicesHealthCheckSuccess } from '../integration/nockMocks'; -import { waitUntil } from '../../utilities/waitUntil'; +import resolvable from '@josephg/resolvable'; import { nockAfterEach, nockBeforeEach } from '../nockAssertions'; async function getSupergraphSdlGatewayServer() { @@ -91,17 +91,12 @@ describe('Using supergraphSdl dynamic configuration', () => { }); it('starts and remains in `initialized` state until `supergraphSdl` Promise resolves', async () => { - const [ - promiseGuaranteeingWeAreInTheCallback, - resolvePromiseGuaranteeingWeAreInTheCallback, - ] = waitUntil(); - const [ - promiseGuaranteeingWeStayInTheCallback, - resolvePromiseGuaranteeingWeStayInTheCallback, - ] = waitUntil(); + const promiseGuaranteeingWeAreInTheCallback = resolvable(); + const promiseGuaranteeingWeStayInTheCallback = resolvable(); + gateway = new ApolloGateway({ async supergraphSdl() { - resolvePromiseGuaranteeingWeAreInTheCallback(); + promiseGuaranteeingWeAreInTheCallback.resolve(); await promiseGuaranteeingWeStayInTheCallback; return { supergraphSdl: getTestingSupergraphSdl(), @@ -115,14 +110,13 @@ describe('Using supergraphSdl dynamic configuration', () => { await promiseGuaranteeingWeAreInTheCallback; expect(gateway.__testing().state.phase).toEqual('initialized'); - resolvePromiseGuaranteeingWeStayInTheCallback(); + promiseGuaranteeingWeStayInTheCallback.resolve(); await gatewayLoaded; expect(gateway.__testing().state.phase).toEqual('loaded'); }); it('moves from `initialized` to `loaded` state after calling `load()` and after user Promise resolves', async () => { - const [userPromise, resolveSupergraph] = - waitUntil<{ supergraphSdl: string }>(); + const userPromise = resolvable<{ supergraphSdl: string }>(); gateway = new ApolloGateway({ async supergraphSdl() { @@ -137,7 +131,7 @@ describe('Using supergraphSdl dynamic configuration', () => { const expectedCompositionId = createHash('sha256') .update(supergraphSdl) .digest('hex'); - resolveSupergraph({ supergraphSdl }); + userPromise.resolve({ supergraphSdl }); await loadPromise; const { state, compositionId } = gateway.__testing(); @@ -146,8 +140,7 @@ describe('Using supergraphSdl dynamic configuration', () => { }); it('updates its supergraph after user calls update function', async () => { - const [userPromise, resolveSupergraph] = - waitUntil<{ supergraphSdl: string }>(); + const userPromise = resolvable<{ supergraphSdl: string }>(); let userUpdateFn: SupergraphSdlUpdateFunction; gateway = new ApolloGateway({ @@ -159,7 +152,7 @@ describe('Using supergraphSdl dynamic configuration', () => { const supergraphSdl = getTestingSupergraphSdl(); const expectedId = createHash('sha256').update(supergraphSdl).digest('hex'); - resolveSupergraph({ supergraphSdl: getTestingSupergraphSdl() }); + userPromise.resolve({ supergraphSdl: getTestingSupergraphSdl() }); await gateway.load(); expect(gateway.__testing().compositionId).toEqual(expectedId); diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index 2edce096fb..b691f0548e 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -26,6 +26,7 @@ import { } from 'apollo-federation-integration-testsuite'; import { getTestingSupergraphSdl } from '../execution-utils'; import { nockAfterEach, nockBeforeEach } from '../nockAssertions'; +import resolvable from '@josephg/resolvable'; export interface Fixture { name: string; @@ -137,13 +138,12 @@ it('Updates Supergraph SDL from remote storage', async () => { // This test is only interested in the second time the gateway notifies of an // update, since the first happens on load. - let secondUpdateResolve: Function; - const secondUpdate = new Promise((res) => (secondUpdateResolve = res)); + const secondUpdate = resolvable(); const schemaChangeCallback = jest .fn() .mockImplementationOnce(() => {}) .mockImplementationOnce(() => { - secondUpdateResolve(); + secondUpdate.resolve(); }); gateway = new ApolloGateway({ @@ -190,9 +190,8 @@ describe('Supergraph SDL update failures', () => { mockSupergraphSdlRequestIfAfter('originalId-1234').reply(500); // Spy on logger.error so we can just await once it's been called - let errorLogged: Function; - const errorLoggedPromise = new Promise((r) => (errorLogged = r)); - logger.error = jest.fn(() => errorLogged()); + const errorLoggedPromise = resolvable(); + logger.error = jest.fn(() => errorLoggedPromise.resolve()); gateway = new ApolloGateway({ logger, @@ -224,9 +223,8 @@ describe('Supergraph SDL update failures', () => { }); // Spy on logger.error so we can just await once it's been called - let errorLogged: Function; - const errorLoggedPromise = new Promise((r) => (errorLogged = r)); - logger.error = jest.fn(() => errorLogged()); + const errorLoggedPromise = resolvable(); + logger.error = jest.fn(() => errorLoggedPromise.resolve()); gateway = new ApolloGateway({ logger, @@ -262,9 +260,8 @@ describe('Supergraph SDL update failures', () => { ); // Spy on logger.error so we can just await once it's been called - let errorLogged: Function; - const errorLoggedPromise = new Promise((r) => (errorLogged = r)); - logger.error = jest.fn(() => errorLogged()); + const errorLoggedPromise = resolvable(); + logger.error = jest.fn(() => errorLoggedPromise.resolve()); gateway = new ApolloGateway({ logger, @@ -327,18 +324,15 @@ it('Rollsback to a previous schema when triggered', async () => { ); mockSupergraphSdlRequestSuccessIfAfter('updatedId-5678'); - let firstResolve: Function; - let secondResolve: Function; - let thirdResolve: Function; - const firstSchemaChangeBlocker = new Promise((res) => (firstResolve = res)); - const secondSchemaChangeBlocker = new Promise((res) => (secondResolve = res)); - const thirdSchemaChangeBlocker = new Promise((res) => (thirdResolve = res)); + const firstSchemaChangeBlocker = resolvable(); + const secondSchemaChangeBlocker = resolvable(); + const thirdSchemaChangeBlocker = resolvable(); const onChange = jest .fn() - .mockImplementationOnce(() => firstResolve()) - .mockImplementationOnce(() => secondResolve()) - .mockImplementationOnce(() => thirdResolve()); + .mockImplementationOnce(() => firstSchemaChangeBlocker.resolve()) + .mockImplementationOnce(() => secondSchemaChangeBlocker.resolve()) + .mockImplementationOnce(() => thirdSchemaChangeBlocker.resolve()); gateway = new ApolloGateway({ logger, @@ -497,14 +491,12 @@ describe('Downstream service health checks', () => { ); mockAllServicesHealthCheckSuccess(); - let resolve1: Function; - let resolve2: Function; - const schemaChangeBlocker1 = new Promise((res) => (resolve1 = res)); - const schemaChangeBlocker2 = new Promise((res) => (resolve2 = res)); + const schemaChangeBlocker1 = resolvable(); + const schemaChangeBlocker2 = resolvable(); const onChange = jest .fn() - .mockImplementationOnce(() => resolve1()) - .mockImplementationOnce(() => resolve2()); + .mockImplementationOnce(() => schemaChangeBlocker1.resolve()) + .mockImplementationOnce(() => schemaChangeBlocker2.resolve()); gateway = new ApolloGateway({ serviceHealthCheck: true, @@ -546,8 +538,7 @@ describe('Downstream service health checks', () => { mockServiceHealthCheckSuccess(reviews); mockServiceHealthCheckSuccess(documents); - let resolve: Function; - const schemaChangeBlocker = new Promise((res) => (resolve = res)); + const schemaChangeBlocker = resolvable(); gateway = new ApolloGateway({ serviceHealthCheck: true, @@ -591,7 +582,7 @@ describe('Downstream service health checks', () => { [accounts]: 500: Internal Server Error" `); // finally resolve the promise which drives this test - resolve(); + schemaChangeBlocker.resolve(); }); // @ts-ignore for testing purposes, replace the `updateSchema` diff --git a/gateway-js/src/utilities/waitUntil.ts b/gateway-js/src/utilities/waitUntil.ts deleted file mode 100644 index 8a011efe6d..0000000000 --- a/gateway-js/src/utilities/waitUntil.ts +++ /dev/null @@ -1,18 +0,0 @@ -export function waitUntil() { - let userResolved: (value: T | PromiseLike) => void; - let userRejected: (reason?: any) => void; - const promise = new Promise( - (r) => ((userResolved = r), (userRejected = r)), - ); - return [ - promise, - // @ts-ignore - userResolved, - // @ts-ignore - userRejected, - ] as [ - Promise, - (value: T | PromiseLike) => void, - (reason?: any) => void, - ]; -} diff --git a/package-lock.json b/package-lock.json index 066bdbfd94..4f49138b02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@graphql-codegen/typescript": "2.4.2", "@graphql-codegen/typescript-operations": "2.2.2", "@iarna/toml": "2.2.5", + "@josephg/resolvable": "^1.0.1", "@opentelemetry/node": "0.24.0", "@rollup/plugin-commonjs": "21.0.1", "@rollup/plugin-json": "4.1.0", diff --git a/package.json b/package.json index 244a8f277e..ae3fdde854 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@graphql-codegen/typescript": "2.4.2", "@graphql-codegen/typescript-operations": "2.2.2", "@iarna/toml": "2.2.5", + "@josephg/resolvable": "^1.0.1", "@opentelemetry/node": "0.24.0", "@rollup/plugin-commonjs": "21.0.1", "@rollup/plugin-json": "4.1.0", From 497c1deb3a66ba0555e8e9e08d731a1ca8c68bb2 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 15:23:40 -0800 Subject: [PATCH 32/82] missed a spot --- gateway-js/src/__tests__/gateway/reporting.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/reporting.test.ts b/gateway-js/src/__tests__/gateway/reporting.test.ts index 68fa96d929..7653dc8c0f 100644 --- a/gateway-js/src/__tests__/gateway/reporting.test.ts +++ b/gateway-js/src/__tests__/gateway/reporting.test.ts @@ -13,6 +13,7 @@ import { Plugin, Config, Refs } from 'pretty-format'; import { Report, Trace } from 'apollo-reporting-protobuf'; import { fixtures } from 'apollo-federation-integration-testsuite'; import { nockAfterEach, nockBeforeEach } from '../nockAssertions'; +import resolvable, { Resolvable } from '@josephg/resolvable'; // Normalize specific fields that change often (eg timestamps) to static values, // to make snapshot testing viable. (If these helpers are more generally @@ -89,19 +90,16 @@ describe('reporting', () => { let backendServers: ApolloServer[]; let gatewayServer: ApolloServer; let gatewayUrl: string; - let reportPromise: Promise; + let reportPromise: Resolvable; beforeEach(async () => { - let reportResolver: (report: any) => void; - reportPromise = new Promise((resolve) => { - reportResolver = resolve; - }); + reportPromise = resolvable(); nockBeforeEach(); nock('https://usage-reporting.api.apollographql.com') .post('/api/ingress/traces') .reply(200, (_: any, requestBody: string) => { - reportResolver(requestBody); + reportPromise.resolve(requestBody); return 'ok'; }); From 1eb06b91fa37618c86a9454d823f1f07a55c32a7 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 15:43:19 -0800 Subject: [PATCH 33/82] Update usage recommendations --- docs/source/entities.mdx | 2 +- docs/source/gateway.mdx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 56f63b1b3d..cb7cded530 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -393,7 +393,7 @@ We're done! `Bill` now originates in a new subgraph, and it was resolvable durin -> ⚠️ We strongly recommend _against_ using `IntrospectAndCompose`. For details, see [Limitations of `IntrospectAndCompose`](./gateway/#limitations-of-introspectandcompose). +> ⚠️ We strongly recommend _against_ using `IntrospectAndCompose` in production. For details, see [Limitations of `IntrospectAndCompose`](./gateway/#limitations-of-introspectandcompose). When you provide `IntrospectAndCompose` to `ApolloGateway`, it performs composition _itself_ on startup after fetching all of your subgraph schemas. If this runtime composition fails, the gateway fails to start up, resulting in downtime. diff --git a/docs/source/gateway.mdx b/docs/source/gateway.mdx index 77d6032778..2e56615ece 100644 --- a/docs/source/gateway.mdx +++ b/docs/source/gateway.mdx @@ -153,8 +153,7 @@ Your use case may grow to have some complexity which might be better managed wit ### Composing subgraphs with `IntrospectAndCompose` -> Looking for `serviceList`? `IntrospectAndCompose` is the new drop-in replacement for the `serviceList` option. In the near future, the `serviceList` option will be removed, however `IntrospectAndCompose` will continue to be supported. Note that this is still considered an outdated workflow. Apollo recommends approaches utilize `rover` for managing local composition. -// TODO: do we have docs that demonstrate a rover-based workflow? +> Looking for `serviceList`? `IntrospectAndCompose` is the new drop-in replacement for the `serviceList` option. In the near future, the `serviceList` option will be removed, however `IntrospectAndCompose` will continue to be supported. Apollo generally recommends approaches utilize `rover` for managing local composition, however `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). From 3edaf0df59a35bef2dba586e531bca7dede66382 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 15:43:41 -0800 Subject: [PATCH 34/82] Remove buildService warning/deprecation --- .../__tests__/integration/configuration.test.ts | 16 +--------------- gateway-js/src/index.ts | 7 ------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index aa4bf75fac..241fdb9c24 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -2,7 +2,7 @@ import gql from 'graphql-tag'; import http from 'http'; import mockedEnv from 'mocked-env'; import { Logger } from 'apollo-server-types'; -import { ApolloGateway, RemoteGraphQLDataSource } from '../..'; +import { ApolloGateway } from '../..'; import { mockSdlQuerySuccess, mockSupergraphSdlRequestSuccess, @@ -413,18 +413,4 @@ describe('deprecation warnings', () => { 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); }); - - it('warns with `buildService` option set', async () => { - new ApolloGateway({ - serviceList: [{ name: 'accounts', url: 'http://localhost:4001' }], - buildService(definition) { - return new RemoteGraphQLDataSource({url: definition.url}); - }, - logger, - }); - - expect(logger.warn).toHaveBeenCalledWith( - 'The `buildService` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', - ); - }); }); diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index e1cd3b3fc5..3883074b9e 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -387,13 +387,6 @@ export class ApolloGateway implements GraphQLService { 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); } - - // TODO(trevor:removeServiceList) - if ('buildService' in this.config) { - this.logger.warn( - 'The `buildService` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', - ); - } } public async load(options?: { From 36f9dedffed846e4d48aaef30364dc4fb9af863c Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 15:55:51 -0800 Subject: [PATCH 35/82] Only cleanup user fns in appropriate states --- gateway-js/src/index.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 3883074b9e..2df3032186 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -1366,10 +1366,9 @@ export class ApolloGateway implements GraphQLService { }); } - // Stops all processes involved with the gateway (for now, just background - // schema polling). Can be called multiple times safely. Once it (async) - // returns, all gateway background activity will be finished. - public async stop() { + private async cleanupUserFunctions() { + if (this.toDispose.length === 0) return; + await Promise.all( this.toDispose.map((p) => p().catch((e) => { @@ -1381,6 +1380,12 @@ export class ApolloGateway implements GraphQLService { ), ); this.toDispose = []; + } + + // Stops all processes involved with the gateway (for now, just background + // schema polling). Can be called multiple times safely. Once it (async) + // returns, all gateway background activity will be finished. + public async stop() { switch (this.state.phase) { case 'initialized': case 'failed to load': @@ -1403,6 +1408,7 @@ export class ApolloGateway implements GraphQLService { return; case 'loaded': this.state = { phase: 'stopped' }; // nothing to do (we're not polling) + await this.cleanupUserFunctions(); return; case 'waiting to poll': { // If we're waiting to poll, we can synchronously transition to fully stopped. @@ -1411,6 +1417,7 @@ export class ApolloGateway implements GraphQLService { clearTimeout(this.state.pollWaitTimer); this.state = { phase: 'stopped' }; doneWaiting(); + await this.cleanupUserFunctions(); return; } case 'polling': { @@ -1430,6 +1437,7 @@ export class ApolloGateway implements GraphQLService { await pollingDonePromise; this.state = { phase: 'stopped' }; stoppingDone!(); + await this.cleanupUserFunctions(); return; } case 'updating schema': { From 24ae881c589d74026fc95dd0bbca865e68fdefbb Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 3 Jan 2022 18:19:16 -0800 Subject: [PATCH 36/82] Add healthCheck capability to IntrospectAndCompose --- docs/source/api/apollo-gateway.mdx | 16 ++++ .../__tests__/IntrospectAndCompose.test.ts | 96 ++++++++++++++++++- gateway-js/src/IntrospectAndCompose/index.ts | 17 +++- 3 files changed, 125 insertions(+), 4 deletions(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 8de620e882..a9d1395b18 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -688,6 +688,22 @@ Specify this option to enable supergraph updates via subgraph polling. `Introspe +###### `subgraphHealthCheck` + +`boolean` + + + +> Note: this only applies to subgraphs that are configured for polling via the `pollIntervalInMs` option. +Perform 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`, there is no need to enable `serviceHealthCheck` on your `ApolloGateway` instance.** + + + + + + ###### `logger` [`Logger`](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-types/src/index.ts#L166-L172) diff --git a/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts index 380e593b4c..fe2ff0026b 100644 --- a/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts @@ -7,7 +7,7 @@ import { nockBeforeEach, nockAfterEach } from '../../__tests__/nockAssertions'; import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; import { IntrospectAndCompose } from '..'; import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; -import { wait } from '../../__tests__/execution-utils'; +import { getTestingSupergraphSdl, wait } from '../../__tests__/execution-utils'; import resolvable from '@josephg/resolvable'; import { Logger } from 'apollo-server-types'; @@ -24,7 +24,7 @@ describe('IntrospectAndCompose', () => { ).not.toThrow(); }); - it('is instance callable (simulating the gateway calling it)', async () => { + it('has an `initialize` property which is callable (simulating the gateway calling it)', async () => { mockAllServicesSdlQuerySuccess(); const instance = new IntrospectAndCompose({ subgraphs: fixtures }); await expect( @@ -160,6 +160,41 @@ describe('IntrospectAndCompose', () => { expect(updateSpy).not.toHaveBeenCalled(); }); + it('issues subgraph health checks when enabled (and polling)', async () => { + mockAllServicesSdlQuerySuccess(); + mockAllServicesSdlQuerySuccess(fixturesWithUpdate); + + const healthCheckPromise = resolvable(); + const healthCheckSpy = jest + .fn() + .mockImplementationOnce(() => healthCheckPromise.resolve()); + + const instance = new IntrospectAndCompose({ + subgraphs: fixtures, + pollIntervalInMs: 10, + subgraphHealthCheck: true, + }); + + const { cleanup } = await instance.initialize({ + update() {}, + async healthCheck(supergraphSdl) { + healthCheckSpy(supergraphSdl); + }, + getDataSource({ url }) { + return new RemoteGraphQLDataSource({ url }); + }, + }); + + await healthCheckPromise; + + expect(healthCheckSpy).toHaveBeenCalledWith( + getTestingSupergraphSdl(fixturesWithUpdate), + ); + + // stop polling + await cleanup!(); + }); + describe('errors', () => { it('logs an error when `update` function throws', async () => { const errorLoggedPromise = resolvable(); @@ -210,5 +245,62 @@ describe('IntrospectAndCompose', () => { `IntrospectAndCompose failed to update supergraph with the following error: ${thrownErrorMessage}`, ); }); + + it('does not attempt to update when `healthCheck` function throws', async () => { + mockAllServicesSdlQuerySuccess(); + mockAllServicesSdlQuerySuccess(fixturesWithUpdate); + + const expectedErrorMsg = 'error reaching subgraph'; + const errorLoggedPromise = resolvable(); + const errorSpy = jest.fn(() => { + errorLoggedPromise.resolve(); + }); + const logger: Logger = { + error: errorSpy, + debug() {}, + info() {}, + warn() {}, + }; + + const healthCheckPromise = resolvable(); + const healthCheckSpy = jest + .fn() + .mockImplementationOnce(() => healthCheckPromise.resolve()); + const updateSpy = jest + .fn() + .mockImplementationOnce(() => healthCheckPromise.resolve()); + + const instance = new IntrospectAndCompose({ + subgraphs: fixtures, + pollIntervalInMs: 10, + subgraphHealthCheck: true, + logger, + }); + + const { cleanup } = await instance.initialize({ + update() { + updateSpy(); + }, + async healthCheck(supergraphSdl) { + healthCheckSpy(supergraphSdl); + throw new Error(expectedErrorMsg); + }, + getDataSource({ url }) { + return new RemoteGraphQLDataSource({ url }); + }, + }); + + await healthCheckPromise; + await errorLoggedPromise; + + expect(errorSpy).toHaveBeenCalledWith( + `IntrospectAndCompose failed to update supergraph with the following error: ${expectedErrorMsg}`, + ); + expect(healthCheckSpy).toHaveBeenCalledTimes(1); + expect(updateSpy).not.toHaveBeenCalled(); + + // stop polling + await cleanup!(); + }); }); }); diff --git a/gateway-js/src/IntrospectAndCompose/index.ts b/gateway-js/src/IntrospectAndCompose/index.ts index e51a20b23c..e577bb30b1 100644 --- a/gateway-js/src/IntrospectAndCompose/index.ts +++ b/gateway-js/src/IntrospectAndCompose/index.ts @@ -5,7 +5,11 @@ import { } from '@apollo/federation'; import { Logger } from 'apollo-server-types'; import { HeadersInit } from 'node-fetch'; -import { ServiceEndpointDefinition, SupergraphSdlUpdateFunction } from '..'; +import { + ServiceEndpointDefinition, + SupergraphSdlUpdateFunction, + SubgraphHealthCheckFunction, +} from '..'; import { getServiceDefinitionsFromRemoteEndpoint, Service, @@ -21,6 +25,7 @@ export interface IntrospectAndComposeOptions { ) => Promise | HeadersInit); pollIntervalInMs?: number; logger?: Logger; + subgraphHealthCheck?: boolean; } type State = @@ -31,6 +36,7 @@ type State = export class IntrospectAndCompose implements SupergraphSdlObject { private config: IntrospectAndComposeOptions; private update?: SupergraphSdlUpdateFunction; + private healthCheck?: SubgraphHealthCheckFunction; private subgraphs?: Service[]; private serviceSdlCache: Map = new Map(); private pollIntervalInMs?: number; @@ -43,8 +49,13 @@ export class IntrospectAndCompose implements SupergraphSdlObject { this.state = { phase: 'initialized' }; } - public async initialize({ update, getDataSource }: SupergraphSdlHookOptions) { + public async initialize({ update, getDataSource, healthCheck }: SupergraphSdlHookOptions) { this.update = update; + + if (this.config.subgraphHealthCheck) { + this.healthCheck = healthCheck; + } + this.subgraphs = this.config.subgraphs.map((subgraph) => ({ ...subgraph, dataSource: getDataSource(subgraph), @@ -125,6 +136,8 @@ export class IntrospectAndCompose implements SupergraphSdlObject { try { const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); if (maybeNewSupergraphSdl) { + // the healthCheck fn is only assigned if it's enabled in the config + await this.healthCheck?.(maybeNewSupergraphSdl); this.update?.(maybeNewSupergraphSdl); } } catch (e) { From ef82f49e293d61c6174985b96ff298fa1e04ce33 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 5 Jan 2022 10:39:53 -0800 Subject: [PATCH 37/82] grammar tweak --- docs/source/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/gateway.mdx b/docs/source/gateway.mdx index 2e56615ece..7499329362 100644 --- a/docs/source/gateway.mdx +++ b/docs/source/gateway.mdx @@ -153,7 +153,7 @@ Your use case may grow to have some complexity which might be better managed wit ### Composing subgraphs with `IntrospectAndCompose` -> Looking for `serviceList`? `IntrospectAndCompose` is the new drop-in replacement for the `serviceList` option. In the near future, the `serviceList` option will be removed, however `IntrospectAndCompose` will continue to be supported. Apollo generally recommends approaches utilize `rover` for managing local composition, however `IntrospectAndCompose` is still useful for various development and testing workflows. +> Looking for `serviceList`? `IntrospectAndCompose` is the new drop-in replacement for the `serviceList` option. In the near future, the `serviceList` option will be removed; however, `IntrospectAndCompose` will continue to be supported. Apollo generally recommends approaches that utilize `rover` for managing local composition, though `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). From eddf8eb2b2f8de3c34cb430b73373417e011fa76 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 5 Jan 2022 10:43:18 -0800 Subject: [PATCH 38/82] consistent utf-8 --- docs/source/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/gateway.mdx b/docs/source/gateway.mdx index 7499329362..1c2a2f5c5c 100644 --- a/docs/source/gateway.mdx +++ b/docs/source/gateway.mdx @@ -72,7 +72,7 @@ const gateway = new ApolloGateway({ // `update` is a function which we'll save for later use supergraphUpdate = update; return { - supergraphSdl: await readFile('./supergraph.graphql', 'utf8'), + supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'), } }, }); From d4fcb52d94038f28fd04addddce1f88a8559e240 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 5 Jan 2022 12:16:32 -0800 Subject: [PATCH 39/82] dont pull initialize function off of object, breaks 'this' usages internally --- gateway-js/src/IntrospectAndCompose/index.ts | 1 + gateway-js/src/index.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gateway-js/src/IntrospectAndCompose/index.ts b/gateway-js/src/IntrospectAndCompose/index.ts index e577bb30b1..741d8361df 100644 --- a/gateway-js/src/IntrospectAndCompose/index.ts +++ b/gateway-js/src/IntrospectAndCompose/index.ts @@ -50,6 +50,7 @@ export class IntrospectAndCompose implements SupergraphSdlObject { } public async initialize({ update, getDataSource, healthCheck }: SupergraphSdlHookOptions) { + console.log(this); this.update = update; if (this.config.subgraphHealthCheck) { diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 2df3032186..f3d9649824 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -516,12 +516,12 @@ export class ApolloGateway implements GraphQLService { config: ManuallyManagedSupergraphSdlGatewayConfig, ) { try { - const initFunction = - typeof config.supergraphSdl === 'function' + const supergraphSdlObject = + typeof config.supergraphSdl === 'object' ? config.supergraphSdl - : config.supergraphSdl.initialize; + : { initialize: config.supergraphSdl }; - const result = await initFunction({ + const result = await supergraphSdlObject.initialize({ update: this.externalSupergraphUpdateCallback.bind(this), healthCheck: this.externalSubgraphHealthCheckCallback.bind(this), getDataSource: this.externalGetDataSourceCallback.bind(this), From 4f0645045b8f6fc9ab4887f40ff931968738bebf Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 5 Jan 2022 12:28:14 -0800 Subject: [PATCH 40/82] use resolvable in places --- gateway-js/package.json | 1 + gateway-js/src/IntrospectAndCompose/index.ts | 8 +++----- gateway-js/src/index.ts | 18 +++++++----------- package-lock.json | 2 ++ 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/gateway-js/package.json b/gateway-js/package.json index c67f8f7344..5f3b6d88be 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -28,6 +28,7 @@ "@apollo/core-schema": "^0.2.0", "@apollo/federation": "file:../federation-js", "@apollo/query-planner": "file:../query-planner-js", + "@josephg/resolvable": "^1.0.1", "@opentelemetry/api": "^1.0.1", "@types/node-fetch": "2.5.12", "apollo-graphql": "^0.9.5", diff --git a/gateway-js/src/IntrospectAndCompose/index.ts b/gateway-js/src/IntrospectAndCompose/index.ts index 741d8361df..0221c2a81c 100644 --- a/gateway-js/src/IntrospectAndCompose/index.ts +++ b/gateway-js/src/IntrospectAndCompose/index.ts @@ -5,6 +5,7 @@ import { } from '@apollo/federation'; import { Logger } from 'apollo-server-types'; import { HeadersInit } from 'node-fetch'; +import resolvable from '@josephg/resolvable'; import { ServiceEndpointDefinition, SupergraphSdlUpdateFunction, @@ -128,10 +129,7 @@ export class IntrospectAndCompose implements SupergraphSdlObject { private poll() { this.timerRef = setTimeout(async () => { if (this.state.phase === 'polling') { - let pollingDone: () => void; - const pollingPromise = new Promise((resolve) => { - pollingDone = resolve; - }); + const pollingPromise = resolvable(); this.state.pollingPromise = pollingPromise; try { @@ -147,7 +145,7 @@ export class IntrospectAndCompose implements SupergraphSdlObject { (e.message ?? e), ); } - pollingDone!(); + pollingPromise.resolve(); } this.poll(); diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index f3d9649824..a97100fded 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -22,7 +22,7 @@ import { ServiceDefinition, } from '@apollo/federation'; import loglevel from 'loglevel'; - +import resolvable from '@josephg/resolvable'; import { buildOperationContext, OperationContext } from './operationContext'; import { executeQueryPlan, @@ -1034,12 +1034,10 @@ export class ApolloGateway implements GraphQLService { return; } - let pollingDone: () => void; + const pollingDonePromise = resolvable(); this.state = { phase: 'polling', - pollingDonePromise: new Promise((res) => { - pollingDone = res; - }), + pollingDonePromise, }; try { @@ -1056,7 +1054,7 @@ export class ApolloGateway implements GraphQLService { } // Whether we were stopped or not, let any concurrent stop() call finish. - pollingDone!(); + pollingDonePromise.resolve(); } private createAndCacheDataSource( @@ -1427,16 +1425,14 @@ export class ApolloGateway implements GraphQLService { // just wait on pollingDonePromise themselves because we want to make sure we fully // transition to state='stopped' before the other call returns.) const pollingDonePromise = this.state.pollingDonePromise; - let stoppingDone: () => void; + const stoppingDonePromise = resolvable(); this.state = { phase: 'stopping', - stoppingDonePromise: new Promise((res) => { - stoppingDone = res; - }), + stoppingDonePromise, }; await pollingDonePromise; this.state = { phase: 'stopped' }; - stoppingDone!(); + stoppingDonePromise.resolve(); await this.cleanupUserFunctions(); return; } diff --git a/package-lock.json b/package-lock.json index 4f49138b02..0ab97846e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,6 +154,7 @@ "@apollo/core-schema": "^0.2.0", "@apollo/federation": "file:../federation-js", "@apollo/query-planner": "file:../query-planner-js", + "@josephg/resolvable": "^1.0.1", "@opentelemetry/api": "^1.0.1", "@types/node-fetch": "2.5.12", "apollo-graphql": "^0.9.5", @@ -23821,6 +23822,7 @@ "@apollo/core-schema": "^0.2.0", "@apollo/federation": "file:../federation-js", "@apollo/query-planner": "file:../query-planner-js", + "@josephg/resolvable": "^1.0.1", "@opentelemetry/api": "^1.0.1", "@types/node-fetch": "2.5.12", "apollo-graphql": "^0.9.5", From 55f1b44670e480fd1462b90994db5667b0b3c2ec Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 5 Jan 2022 15:44:03 -0800 Subject: [PATCH 41/82] Use IntrospectAndCompose internally (under the hood swap for serviceList) --- .../__tests__/IntrospectAndCompose.test.ts | 94 ++++++++++++++++--- gateway-js/src/IntrospectAndCompose/index.ts | 30 ++++-- .../integration/networkRequests.test.ts | 9 +- gateway-js/src/config.ts | 24 +++-- gateway-js/src/index.ts | 84 ++++++++--------- 5 files changed, 157 insertions(+), 84 deletions(-) diff --git a/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts index fe2ff0026b..906d423ba5 100644 --- a/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts @@ -164,10 +164,13 @@ describe('IntrospectAndCompose', () => { mockAllServicesSdlQuerySuccess(); mockAllServicesSdlQuerySuccess(fixturesWithUpdate); - const healthCheckPromise = resolvable(); + const healthCheckPromiseOnLoad = resolvable(); + const healthCheckPromiseOnUpdate = resolvable(); + const healthCheckSpy = jest .fn() - .mockImplementationOnce(() => healthCheckPromise.resolve()); + .mockImplementationOnce(() => healthCheckPromiseOnLoad.resolve()) + .mockImplementationOnce(() => healthCheckPromiseOnUpdate.resolve()); const instance = new IntrospectAndCompose({ subgraphs: fixtures, @@ -185,9 +188,17 @@ describe('IntrospectAndCompose', () => { }, }); - await healthCheckPromise; + await Promise.all([ + healthCheckPromiseOnLoad, + healthCheckPromiseOnUpdate, + ]); - expect(healthCheckSpy).toHaveBeenCalledWith( + expect(healthCheckSpy).toHaveBeenNthCalledWith( + 1, + getTestingSupergraphSdl(fixtures), + ); + expect(healthCheckSpy).toHaveBeenNthCalledWith( + 2, getTestingSupergraphSdl(fixturesWithUpdate), ); @@ -246,6 +257,52 @@ describe('IntrospectAndCompose', () => { ); }); + it('fails to load when `healthCheck` function throws on startup', async () => { + mockAllServicesSdlQuerySuccess(); + + const expectedErrorMsg = 'error reaching subgraph'; + const errorLoggedPromise = resolvable(); + const errorSpy = jest.fn(() => { + errorLoggedPromise.resolve(); + }); + const logger: Logger = { + error: errorSpy, + debug() {}, + info() {}, + warn() {}, + }; + + const updateSpy = jest.fn(); + + const instance = new IntrospectAndCompose({ + subgraphs: fixtures, + pollIntervalInMs: 10, + subgraphHealthCheck: true, + logger, + }); + + await expect( + instance.initialize({ + update() { + updateSpy(); + }, + async healthCheck() { + throw new Error(expectedErrorMsg); + }, + getDataSource({ url }) { + return new RemoteGraphQLDataSource({ url }); + }, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(`"error reaching subgraph"`); + + await errorLoggedPromise; + + expect(errorSpy).toHaveBeenCalledWith( + `IntrospectAndCompose failed to update supergraph with the following error: ${expectedErrorMsg}`, + ); + expect(updateSpy).not.toHaveBeenCalled(); + }); + it('does not attempt to update when `healthCheck` function throws', async () => { mockAllServicesSdlQuerySuccess(); mockAllServicesSdlQuerySuccess(fixturesWithUpdate); @@ -262,13 +319,17 @@ describe('IntrospectAndCompose', () => { warn() {}, }; - const healthCheckPromise = resolvable(); - const healthCheckSpy = jest - .fn() - .mockImplementationOnce(() => healthCheckPromise.resolve()); - const updateSpy = jest + const healthCheckPromiseOnLoad = resolvable(); + const healthCheckPromiseOnUpdate = resolvable(); + const healthCheckSpyWhichEventuallyThrows = jest .fn() - .mockImplementationOnce(() => healthCheckPromise.resolve()); + .mockImplementationOnce(() => healthCheckPromiseOnLoad.resolve()) + .mockImplementationOnce(() => { + healthCheckPromiseOnUpdate.resolve(); + throw new Error(expectedErrorMsg); + }); + + const updateSpy = jest.fn(); const instance = new IntrospectAndCompose({ subgraphs: fixtures, @@ -282,21 +343,24 @@ describe('IntrospectAndCompose', () => { updateSpy(); }, async healthCheck(supergraphSdl) { - healthCheckSpy(supergraphSdl); - throw new Error(expectedErrorMsg); + healthCheckSpyWhichEventuallyThrows(supergraphSdl); }, getDataSource({ url }) { return new RemoteGraphQLDataSource({ url }); }, }); - await healthCheckPromise; - await errorLoggedPromise; + await Promise.all([ + healthCheckPromiseOnLoad, + healthCheckPromiseOnUpdate, + errorLoggedPromise, + ]); expect(errorSpy).toHaveBeenCalledWith( `IntrospectAndCompose failed to update supergraph with the following error: ${expectedErrorMsg}`, ); - expect(healthCheckSpy).toHaveBeenCalledTimes(1); + expect(healthCheckSpyWhichEventuallyThrows).toHaveBeenCalledTimes(2); + // update isn't called on load so this shouldn't be called even once expect(updateSpy).not.toHaveBeenCalled(); // stop polling diff --git a/gateway-js/src/IntrospectAndCompose/index.ts b/gateway-js/src/IntrospectAndCompose/index.ts index 0221c2a81c..bf29c65a86 100644 --- a/gateway-js/src/IntrospectAndCompose/index.ts +++ b/gateway-js/src/IntrospectAndCompose/index.ts @@ -51,7 +51,6 @@ export class IntrospectAndCompose implements SupergraphSdlObject { } public async initialize({ update, getDataSource, healthCheck }: SupergraphSdlHookOptions) { - console.log(this); this.update = update; if (this.config.subgraphHealthCheck) { @@ -63,7 +62,14 @@ export class IntrospectAndCompose implements SupergraphSdlObject { dataSource: getDataSource(subgraph), })); - const initialSupergraphSdl = await this.updateSupergraphSdl(); + let initialSupergraphSdl: string | null = null; + try { + initialSupergraphSdl = await this.updateSupergraphSdl(); + } catch (e) { + this.logUpdateFailure(e); + throw e; + } + // Start polling after we resolve the first supergraph if (this.pollIntervalInMs) { this.beginPolling(); @@ -103,7 +109,11 @@ export class IntrospectAndCompose implements SupergraphSdlObject { return null; } - return this.createSupergraphFromSubgraphList(result.serviceDefinitions!); + const supergraphSdl = this.createSupergraphFromSubgraphList(result.serviceDefinitions!); + // the healthCheck fn is only assigned if it's enabled in the config + await this.healthCheck?.(supergraphSdl); + + return supergraphSdl; } private createSupergraphFromSubgraphList(subgraphs: ServiceDefinition[]) { @@ -135,15 +145,10 @@ export class IntrospectAndCompose implements SupergraphSdlObject { try { const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); if (maybeNewSupergraphSdl) { - // the healthCheck fn is only assigned if it's enabled in the config - await this.healthCheck?.(maybeNewSupergraphSdl); this.update?.(maybeNewSupergraphSdl); } } catch (e) { - this.config.logger?.error( - 'IntrospectAndCompose failed to update supergraph with the following error: ' + - (e.message ?? e), - ); + this.logUpdateFailure(e); } pollingPromise.resolve(); } @@ -151,4 +156,11 @@ export class IntrospectAndCompose implements SupergraphSdlObject { this.poll(); }, this.pollIntervalInMs!); } + + private logUpdateFailure(e: any) { + this.config.logger?.error( + 'IntrospectAndCompose failed to update supergraph with the following error: ' + + (e.message ?? e), + ); + } } diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index b691f0548e..137ee5c0a6 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -168,7 +168,7 @@ describe('Supergraph SDL update failures', () => { gateway = new ApolloGateway({ logger, uplinkEndpoints: [mockCloudConfigUrl1], - uplinkMaxRetries: 0 + uplinkMaxRetries: 0, }); await expect( @@ -196,7 +196,7 @@ describe('Supergraph SDL update failures', () => { gateway = new ApolloGateway({ logger, uplinkEndpoints: [mockCloudConfigUrl1], - uplinkMaxRetries: 0 + uplinkMaxRetries: 0, }); // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here @@ -229,7 +229,7 @@ describe('Supergraph SDL update failures', () => { gateway = new ApolloGateway({ logger, uplinkEndpoints: [mockCloudConfigUrl1], - uplinkMaxRetries: 0 + uplinkMaxRetries: 0, }); // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here gateway.experimental_pollInterval = 100; @@ -396,9 +396,8 @@ describe('Downstream service health checks', () => { var err = e; } - // TODO: smell that we should be awaiting something else expect(err.message).toMatchInlineSnapshot(` - "The gateway did not update its schema due to failed service health checks. The gateway will continue to operate with the previous schema and reattempt updates. The following error occurred during the health check: + "The gateway subgraphs health check failed. Updating to the provided \`supergraphSdl\` will likely result in future request failures to subgraphs. The following error occurred during the health check: [accounts]: 500: Internal Server Error" `); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 153d1b4ab3..83a10a9c86 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -134,7 +134,7 @@ interface GatewayConfigBase { } // TODO(trevor:removeServiceList) -export interface RemoteGatewayConfig extends GatewayConfigBase { +export interface ServiceListGatewayConfig extends GatewayConfigBase { // @deprecated: use `supergraphSdl` in its function form instead serviceList: ServiceEndpointDefinition[]; // @deprecated: use `supergraphSdl` in its function form instead @@ -211,7 +211,9 @@ export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfig type ManuallyManagedGatewayConfig = | ManuallyManagedServiceDefsGatewayConfig | ExperimentalManuallyManagedSupergraphSdlGatewayConfig - | ManuallyManagedSupergraphSdlGatewayConfig; + | ManuallyManagedSupergraphSdlGatewayConfig + // TODO(trevor:removeServiceList + | ServiceListGatewayConfig; // TODO(trevor:removeServiceList) interface LocalGatewayConfig extends GatewayConfigBase { @@ -229,7 +231,6 @@ export type StaticGatewayConfig = type DynamicGatewayConfig = | ManagedGatewayConfig - | RemoteGatewayConfig | ManuallyManagedGatewayConfig; export type GatewayConfig = StaticGatewayConfig | DynamicGatewayConfig; @@ -242,9 +243,9 @@ export function isLocalConfig( } // TODO(trevor:removeServiceList) -export function isRemoteConfig( +export function isServiceListConfig( config: GatewayConfig, -): config is RemoteGatewayConfig { +): config is ServiceListGatewayConfig { return 'serviceList' in config; } @@ -262,7 +263,9 @@ export function isManuallyManagedConfig( return ( isManuallyManagedSupergraphSdlGatewayConfig(config) || 'experimental_updateServiceDefinitions' in config || - 'experimental_updateSupergraphSdl' in config + 'experimental_updateSupergraphSdl' in config || + // TODO(trevor:removeServiceList) + 'serviceList' in config ); } @@ -272,8 +275,7 @@ export function isManagedConfig( ): config is ManagedGatewayConfig { return ( 'schemaConfigDeliveryEndpoint' in config || - (!isRemoteConfig(config) && - !isLocalConfig(config) && + (!isLocalConfig(config) && !isStaticSupergraphSdlConfig(config) && !isManuallyManagedConfig(config)) ); @@ -290,9 +292,5 @@ export function isStaticConfig( export function isDynamicConfig( config: GatewayConfig, ): config is DynamicGatewayConfig { - return ( - isRemoteConfig(config) || - isManagedConfig(config) || - isManuallyManagedConfig(config) - ); + return isManagedConfig(config) || isManuallyManagedConfig(config); } diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index a97100fded..938c2ae460 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -29,8 +29,6 @@ import { ServiceMap, defaultFieldResolverWithAliasSupport, } from './executeQueryPlan'; - -import { getServiceDefinitionsFromRemoteEndpoint } from './IntrospectAndCompose/loadServicesFromRemoteEndpoint'; import { GraphQLDataSource, GraphQLDataSourceRequestKind, @@ -55,11 +53,10 @@ import { CompositionInfo, GatewayConfig, StaticGatewayConfig, - RemoteGatewayConfig, - ManagedGatewayConfig, + ServiceListGatewayConfig, isManuallyManagedConfig, isLocalConfig, - isRemoteConfig, + isServiceListConfig, isManagedConfig, isDynamicConfig, isStaticConfig, @@ -79,6 +76,7 @@ import { OpenTelemetrySpanNames, tracer } from './utilities/opentelemetry'; import { CoreSchema } from '@apollo/core-schema'; import { featureSupport } from './core'; import { createHash } from './utilities/createHash'; +import { IntrospectAndCompose } from './IntrospectAndCompose'; type DataSourceMap = { [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; @@ -180,7 +178,6 @@ export class ApolloGateway implements GraphQLService { >(); private serviceDefinitions: ServiceDefinition[] = []; private compositionMetadata?: CompositionMetadata; - private serviceSdlCache = new Map(); private warnedStates: WarnedStates = Object.create(null); private queryPlanner?: QueryPlanner; private supergraphSdl?: string; @@ -191,6 +188,10 @@ export class ApolloGateway implements GraphQLService { private errorReportingEndpoint: string | undefined = process.env.APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT ?? undefined; + // This will no longer be necessary once the `serviceList` option is removed + // TODO(trevor:removeServiceList) + private internalIntrospectAndComposeInstance?: IntrospectAndCompose; + // Observe query plan, service info, and operation info prior to execution. // The information made available here will give insight into the resulting // query plan and the inputs that generated it. @@ -270,6 +271,15 @@ export class ApolloGateway implements GraphQLService { this.config.experimental_updateServiceDefinitions; } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { // TODO: do nothing maybe? + } else if (isServiceListConfig(this.config)) { + // TODO(trevor:removeServiceList) + this.internalIntrospectAndComposeInstance = new IntrospectAndCompose({ + subgraphs: this.config.serviceList, + pollIntervalInMs: this.config.experimental_pollInterval, + logger: this.logger, + subgraphHealthCheck: this.config.serviceHealthCheck, + introspectionHeaders: this.config.introspectionHeaders, + }); } else { throw Error( 'Programming error: unexpected manual configuration provided', @@ -337,7 +347,7 @@ export class ApolloGateway implements GraphQLService { // Warn against using the pollInterval and a serviceList simultaneously // TODO(trevor:removeServiceList) - if (this.config.experimental_pollInterval && isRemoteConfig(this.config)) { + if (this.config.experimental_pollInterval && isServiceListConfig(this.config)) { this.logger.warn( 'Polling running services is dangerous and not recommended in production. ' + 'Polling should only be used against a registry. ' + @@ -375,14 +385,14 @@ export class ApolloGateway implements GraphQLService { } // TODO(trevor:removeServiceList) - if ('serviceList' in this.config) { + if (isServiceListConfig(this.config)) { this.logger.warn( 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); } // TODO(trevor:removeServiceList) - if ('localServiceList' in this.config) { + if (isLocalConfig(this.config)) { this.logger.warn( 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); @@ -451,7 +461,12 @@ export class ApolloGateway implements GraphQLService { // Handles initial assignment of `this.schema`, `this.queryPlanner` if (isStaticConfig(this.config)) { this.loadStatic(this.config); - } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { + } else if ( + isManuallyManagedSupergraphSdlGatewayConfig(this.config) || + (isServiceListConfig(this.config) && + // this setting is currently expected to override `serviceList` when they both exist + !('experimental_updateServiceDefinitions' in this.config)) + ) { await this.loadManuallyManaged(this.config); } else { await this.loadDynamic(unrefTimer); @@ -513,13 +528,16 @@ export class ApolloGateway implements GraphQLService { } private async loadManuallyManaged( - config: ManuallyManagedSupergraphSdlGatewayConfig, + config: + | ManuallyManagedSupergraphSdlGatewayConfig + | ServiceListGatewayConfig, ) { try { - const supergraphSdlObject = - typeof config.supergraphSdl === 'object' - ? config.supergraphSdl - : { initialize: config.supergraphSdl }; + const supergraphSdlObject = isServiceListConfig(config) + ? this.internalIntrospectAndComposeInstance! + : typeof config.supergraphSdl === 'object' + ? config.supergraphSdl + : { initialize: config.supergraphSdl }; const result = await supergraphSdlObject.initialize({ update: this.externalSupergraphUpdateCallback.bind(this), @@ -1098,27 +1116,7 @@ export class ApolloGateway implements GraphQLService { } } - protected async loadServiceDefinitions( - config: RemoteGatewayConfig | ManagedGatewayConfig, - ): Promise { - // TODO(trevor:removeServiceList) - if (isRemoteConfig(config)) { - const serviceList = config.serviceList.map((serviceDefinition) => ({ - ...serviceDefinition, - dataSource: this.createAndCacheDataSource(serviceDefinition), - })); - - return getServiceDefinitionsFromRemoteEndpoint({ - serviceList, - async getServiceIntrospectionHeaders(service) { - return typeof config.introspectionHeaders === 'function' - ? await config.introspectionHeaders(service) - : config.introspectionHeaders; - }, - serviceSdlCache: this.serviceSdlCache, - }); - } - + protected async loadServiceDefinitions(): Promise { const canUseManagedConfig = this.apolloConfig?.graphRef && this.apolloConfig?.keyHash; if (!canUseManagedConfig) { @@ -1405,8 +1403,13 @@ export class ApolloGateway implements GraphQLService { } return; case 'loaded': - this.state = { phase: 'stopped' }; // nothing to do (we're not polling) - await this.cleanupUserFunctions(); + const stoppingDonePromise = this.cleanupUserFunctions(); + this.state = { + phase: 'stopping', + stoppingDonePromise, + }; + await stoppingDonePromise; + this.state = { phase: 'stopped' }; return; case 'waiting to poll': { // If we're waiting to poll, we can synchronously transition to fully stopped. @@ -1415,7 +1418,6 @@ export class ApolloGateway implements GraphQLService { clearTimeout(this.state.pollWaitTimer); this.state = { phase: 'stopped' }; doneWaiting(); - await this.cleanupUserFunctions(); return; } case 'polling': { @@ -1433,7 +1435,6 @@ export class ApolloGateway implements GraphQLService { await pollingDonePromise; this.state = { phase: 'stopped' }; stoppingDonePromise.resolve(); - await this.cleanupUserFunctions(); return; } case 'updating schema': { @@ -1505,6 +1506,7 @@ export { GatewayConfig, ServiceEndpointDefinition, CompositionInfo, + IntrospectAndCompose, }; export * from './datasources'; @@ -1514,5 +1516,3 @@ export { SubgraphHealthCheckFunction, SupergraphSdlHook, } from './config'; - -export { IntrospectAndCompose } from './IntrospectAndCompose'; From c7cdf4cc00a23e52a272f0f733480420e31df018 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 6 Jan 2022 14:44:35 -0800 Subject: [PATCH 42/82] Big checkpoint, maybe just localServiceList to deal with? --- gateway-js/src/IntrospectAndCompose/index.ts | 6 +- gateway-js/src/LegacyFetcher/index.ts | 243 +++++++++ .../loadSupergraphSdlFromStorage.test.ts | 6 +- gateway-js/src/UplinkFetcher/index.ts | 129 +++++ .../loadSupergraphSdlFromStorage.ts | 6 +- .../{ => UplinkFetcher}/outOfBandReporter.ts | 4 +- .../__tests__/gateway/lifecycle-hooks.test.ts | 131 ++--- .../integration/configuration.test.ts | 22 +- .../src/__tests__/integration/logger.test.ts | 2 +- .../integration/networkRequests.test.ts | 135 ++--- .../src/__tests__/integration/nockMocks.ts | 2 +- gateway-js/src/config.ts | 2 +- gateway-js/src/index.ts | 497 ++++-------------- 13 files changed, 596 insertions(+), 589 deletions(-) create mode 100644 gateway-js/src/LegacyFetcher/index.ts rename gateway-js/src/{ => UplinkFetcher}/__tests__/loadSupergraphSdlFromStorage.test.ts (98%) create mode 100644 gateway-js/src/UplinkFetcher/index.ts rename gateway-js/src/{ => UplinkFetcher}/loadSupergraphSdlFromStorage.ts (96%) rename gateway-js/src/{ => UplinkFetcher}/outOfBandReporter.ts (96%) diff --git a/gateway-js/src/IntrospectAndCompose/index.ts b/gateway-js/src/IntrospectAndCompose/index.ts index bf29c65a86..e8a60e4dd6 100644 --- a/gateway-js/src/IntrospectAndCompose/index.ts +++ b/gateway-js/src/IntrospectAndCompose/index.ts @@ -40,13 +40,11 @@ export class IntrospectAndCompose implements SupergraphSdlObject { private healthCheck?: SubgraphHealthCheckFunction; private subgraphs?: Service[]; private serviceSdlCache: Map = new Map(); - private pollIntervalInMs?: number; private timerRef: NodeJS.Timeout | null = null; private state: State; constructor(options: IntrospectAndComposeOptions) { this.config = options; - this.pollIntervalInMs = options.pollIntervalInMs; this.state = { phase: 'initialized' }; } @@ -71,7 +69,7 @@ export class IntrospectAndCompose implements SupergraphSdlObject { } // Start polling after we resolve the first supergraph - if (this.pollIntervalInMs) { + if (this.config.pollIntervalInMs) { this.beginPolling(); } @@ -154,7 +152,7 @@ export class IntrospectAndCompose implements SupergraphSdlObject { } this.poll(); - }, this.pollIntervalInMs!); + }, this.config.pollIntervalInMs!); } private logUpdateFailure(e: any) { diff --git a/gateway-js/src/LegacyFetcher/index.ts b/gateway-js/src/LegacyFetcher/index.ts new file mode 100644 index 0000000000..5fa4a06836 --- /dev/null +++ b/gateway-js/src/LegacyFetcher/index.ts @@ -0,0 +1,243 @@ +import { Logger } from 'apollo-server-types'; +import resolvable from '@josephg/resolvable'; +import { + SupergraphSdlObject, + SupergraphSdlHookOptions, + DynamicGatewayConfig, + isSupergraphSdlUpdate, + isServiceDefinitionUpdate, + ServiceDefinitionUpdate, + GetDataSourceFunction, +} from '../config'; +import { + Experimental_UpdateComposition, + SubgraphHealthCheckFunction, + SupergraphSdlUpdateFunction, +} from '..'; +import { composeAndValidate, compositionHasErrors, ServiceDefinition } from '@apollo/federation'; +import { GraphQLSchema, isIntrospectionType, isObjectType, parse } from 'graphql'; +import { buildComposedSchema } from '@apollo/query-planner'; +import { defaultFieldResolverWithAliasSupport } from '../executeQueryPlan'; + +export interface LegacyFetcherOptions { + pollIntervalInMs?: number; + logger?: Logger; + subgraphHealthCheck?: boolean; + updateServiceDefinitions: Experimental_UpdateComposition; + gatewayConfig: DynamicGatewayConfig; +} + +type State = + | { phase: 'initialized' } + | { phase: 'polling'; pollingPromise?: Promise } + | { phase: 'stopped' }; + +export class LegacyFetcher implements SupergraphSdlObject { + private config: LegacyFetcherOptions; + private update?: SupergraphSdlUpdateFunction; + private healthCheck?: SubgraphHealthCheckFunction; + private getDataSource?: GetDataSourceFunction; + private timerRef: NodeJS.Timeout | null = null; + private state: State; + private compositionId?: string; + private serviceDefinitions?: ServiceDefinition[]; + private schema?: GraphQLSchema; + + constructor(options: LegacyFetcherOptions) { + this.config = options; + this.state = { phase: 'initialized' }; + } + + public async initialize({ + update, + healthCheck, + getDataSource, + }: SupergraphSdlHookOptions) { + this.update = update; + this.getDataSource = getDataSource; + + if (this.config.subgraphHealthCheck) { + this.healthCheck = healthCheck; + } + + let initialSupergraphSdl: string | null = null; + try { + initialSupergraphSdl = await this.updateSupergraphSdl(); + } catch (e) { + this.logUpdateFailure(e); + throw e; + } + + // Start polling after we resolve the first supergraph + if (this.config.pollIntervalInMs) { + this.beginPolling(); + } + + return { + // on init, this supergraphSdl should never actually be `null`. + // `this.updateSupergraphSdl()` will only return null if the schema hasn't + // changed over the course of an _update_. + supergraphSdl: initialSupergraphSdl!, + cleanup: async () => { + if (this.state.phase === 'polling') { + await this.state.pollingPromise; + } + this.state = { phase: 'stopped' }; + if (this.timerRef) { + this.timerRef.unref(); + clearInterval(this.timerRef); + this.timerRef = null; + } + }, + }; + } + + private async updateSupergraphSdl() { + const result = await this.config.updateServiceDefinitions( + this.config.gatewayConfig, + ); + + if (isSupergraphSdlUpdate(result)) { + // no change + if (this.compositionId === result.id) return null; + + await this.healthCheck?.(result.supergraphSdl); + this.compositionId = result.id; + return result.supergraphSdl; + } else if (isServiceDefinitionUpdate(result)) { + const supergraphSdl = this.updateByComposition(result); + if (!supergraphSdl) return null; + await this.healthCheck?.(supergraphSdl); + return supergraphSdl; + } else { + throw new Error( + 'Programming error: unexpected result type from `updateServiceDefinitions`', + ); + } + } + + private updateByComposition(result: ServiceDefinitionUpdate) { + if ( + !result.serviceDefinitions || + JSON.stringify(this.serviceDefinitions) === + JSON.stringify(result.serviceDefinitions) + ) { + this.config.logger?.debug( + 'No change in service definitions since last check.', + ); + return null; + } + + const previousSchema = this.schema; + + if (previousSchema) { + this.config.logger?.info('New service definitions were found.'); + } + + this.serviceDefinitions = result.serviceDefinitions; + + const { schema, supergraphSdl } = this.createSchemaFromServiceList( + result.serviceDefinitions, + ); + + if (!supergraphSdl) { + throw new Error( + "A valid schema couldn't be composed. Falling back to previous schema.", + ); + } else { + this.schema = schema; + return supergraphSdl; + } + } + + private createSchemaFromServiceList(serviceList: ServiceDefinition[]) { + this.config.logger?.debug( + `Composing schema from service list: \n${serviceList + .map(({ name, url }) => ` ${url || 'local'}: ${name}`) + .join('\n')}`, + ); + + const compositionResult = composeAndValidate(serviceList); + + if (compositionHasErrors(compositionResult)) { + const { errors } = compositionResult; + throw Error( + "A valid schema couldn't be composed. The following composition errors were found:\n" + + errors.map((e) => '\t' + e.message).join('\n'), + ); + } else { + const { supergraphSdl } = compositionResult; + for (const service of serviceList) { + this.getDataSource?.(service); + } + + const schema = buildComposedSchema(parse(supergraphSdl)); + + this.config.logger?.debug('Schema loaded and ready for execution'); + + // This is a workaround for automatic wrapping of all fields, which Apollo + // Server does in the case of implementing resolver wrapping for plugins. + // Here we wrap all fields with support for resolving aliases as part of the + // root value which happens because aliases are resolved by sub services and + // the shape of the root value already contains the aliased fields as + // responseNames + return { + schema: wrapSchemaWithAliasResolver(schema), + supergraphSdl, + }; + } + } + + private beginPolling() { + this.state = { phase: 'polling' }; + this.poll(); + } + + private poll() { + this.timerRef = setTimeout(async () => { + if (this.state.phase === 'polling') { + const pollingPromise = resolvable(); + + this.state.pollingPromise = pollingPromise; + try { + const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); + if (maybeNewSupergraphSdl) { + this.update?.(maybeNewSupergraphSdl); + } + } catch (e) { + this.logUpdateFailure(e); + } + pollingPromise.resolve(); + } + + this.poll(); + }, this.config.pollIntervalInMs!); + } + + private logUpdateFailure(e: any) { + this.config.logger?.error( + 'UplinkFetcher failed to update supergraph with the following error: ' + + (e.message ?? e), + ); + } +} + +// We can't use transformSchema here because the extension data for query +// planning would be lost. Instead we set a resolver for each field +// in order to counteract GraphQLExtensions preventing a defaultFieldResolver +// from doing the same job +function wrapSchemaWithAliasResolver(schema: GraphQLSchema): GraphQLSchema { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach((typeName) => { + const type = typeMap[typeName]; + + if (isObjectType(type) && !isIntrospectionType(type)) { + const fields = type.getFields(); + Object.keys(fields).forEach((fieldName) => { + const field = fields[fieldName]; + field.resolve = defaultFieldResolverWithAliasSupport; + }); + } + }); + return schema; +} diff --git a/gateway-js/src/__tests__/loadSupergraphSdlFromStorage.test.ts b/gateway-js/src/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts similarity index 98% rename from gateway-js/src/__tests__/loadSupergraphSdlFromStorage.test.ts rename to gateway-js/src/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts index efd3140e61..481e886af3 100644 --- a/gateway-js/src/__tests__/loadSupergraphSdlFromStorage.test.ts +++ b/gateway-js/src/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts @@ -14,9 +14,9 @@ import { mockSupergraphSdlRequestSuccess, mockSupergraphSdlRequestIfAfterUnchanged, mockSupergraphSdlRequestIfAfter -} from './integration/nockMocks'; -import { getTestingSupergraphSdl } from "./execution-utils"; -import { nockAfterEach, nockBeforeEach } from './nockAssertions'; +} from '../../__tests__/integration/nockMocks'; +import { getTestingSupergraphSdl } from "../../__tests__/execution-utils"; +import { nockAfterEach, nockBeforeEach } from '../../__tests__/nockAssertions'; describe('loadSupergraphSdlFromStorage', () => { beforeEach(nockBeforeEach); diff --git a/gateway-js/src/UplinkFetcher/index.ts b/gateway-js/src/UplinkFetcher/index.ts new file mode 100644 index 0000000000..314243010c --- /dev/null +++ b/gateway-js/src/UplinkFetcher/index.ts @@ -0,0 +1,129 @@ +import { fetch } from 'apollo-server-env'; +import { Logger } from 'apollo-server-types'; +import resolvable from '@josephg/resolvable'; +import { SupergraphSdlObject, SupergraphSdlHookOptions } from '../config'; +import { SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction } from '..'; +import { loadSupergraphSdlFromUplinks } from './loadSupergraphSdlFromStorage'; + +export interface UplinkFetcherOptions { + pollIntervalInMs: number; + subgraphHealthCheck?: boolean; + graphRef: string; + apiKey: string; + fetcher: typeof fetch; + maxRetries: number; + uplinkEndpoints: string[]; + logger?: Logger; +} + +type State = + | { phase: 'initialized' } + | { phase: 'polling'; pollingPromise?: Promise } + | { phase: 'stopped' }; + +export class UplinkFetcher implements SupergraphSdlObject { + private config: UplinkFetcherOptions; + private update?: SupergraphSdlUpdateFunction; + private healthCheck?: SubgraphHealthCheckFunction; + private timerRef: NodeJS.Timeout | null = null; + private state: State; + private errorReportingEndpoint: string | undefined = + process.env.APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT ?? undefined; + private compositionId?: string; + + constructor(options: UplinkFetcherOptions) { + this.config = options; + this.state = { phase: 'initialized' }; + } + + public async initialize({ update, healthCheck }: SupergraphSdlHookOptions) { + this.update = update; + + if (this.config.subgraphHealthCheck) { + this.healthCheck = healthCheck; + } + + let initialSupergraphSdl: string | null = null; + try { + initialSupergraphSdl = await this.updateSupergraphSdl(); + } catch (e) { + this.logUpdateFailure(e); + throw e; + } + + // Start polling after we resolve the first supergraph + this.beginPolling(); + + return { + // on init, this supergraphSdl should never actually be `null`. + // `this.updateSupergraphSdl()` will only return null if the schema hasn't + // changed over the course of an _update_. + supergraphSdl: initialSupergraphSdl!, + cleanup: async () => { + if (this.state.phase === 'polling') { + await this.state.pollingPromise; + } + this.state = { phase: 'stopped' }; + if (this.timerRef) { + this.timerRef.unref(); + clearInterval(this.timerRef); + this.timerRef = null; + } + }, + }; + } + + private async updateSupergraphSdl() { + const result = await loadSupergraphSdlFromUplinks({ + graphRef: this.config.graphRef, + apiKey: this.config.apiKey, + endpoints: this.config.uplinkEndpoints, + errorReportingEndpoint: this.errorReportingEndpoint, + fetcher: this.config.fetcher, + compositionId: this.compositionId ?? null, + maxRetries: this.config.maxRetries, + }); + + if (!result) { + return null; + } else { + this.compositionId = result.id; + // the healthCheck fn is only assigned if it's enabled in the config + await this.healthCheck?.(result.supergraphSdl); + return result.supergraphSdl; + } + } + + private beginPolling() { + this.state = { phase: 'polling' }; + this.poll(); + } + + private poll() { + this.timerRef = setTimeout(async () => { + if (this.state.phase === 'polling') { + const pollingPromise = resolvable(); + + this.state.pollingPromise = pollingPromise; + try { + const maybeNewSupergraphSdl = await this.updateSupergraphSdl(); + if (maybeNewSupergraphSdl) { + this.update?.(maybeNewSupergraphSdl); + } + } catch (e) { + this.logUpdateFailure(e); + } + pollingPromise.resolve(); + } + + this.poll(); + }, this.config.pollIntervalInMs); + } + + private logUpdateFailure(e: any) { + this.config.logger?.error( + 'UplinkFetcher failed to update supergraph with the following error: ' + + (e.message ?? e), + ); + } +} diff --git a/gateway-js/src/loadSupergraphSdlFromStorage.ts b/gateway-js/src/UplinkFetcher/loadSupergraphSdlFromStorage.ts similarity index 96% rename from gateway-js/src/loadSupergraphSdlFromStorage.ts rename to gateway-js/src/UplinkFetcher/loadSupergraphSdlFromStorage.ts index 9bac8a2f6d..a348cd8868 100644 --- a/gateway-js/src/loadSupergraphSdlFromStorage.ts +++ b/gateway-js/src/UplinkFetcher/loadSupergraphSdlFromStorage.ts @@ -1,8 +1,8 @@ import { fetch, Response, Request } from 'apollo-server-env'; import { GraphQLError } from 'graphql'; -import { SupergraphSdlUpdate } from './config'; +import { SupergraphSdlUpdate } from '../config'; import { submitOutOfBandReportIfConfigured } from './outOfBandReporter'; -import { SupergraphSdlQuery } from './__generated__/graphqlTypes'; +import { SupergraphSdlQuery } from '../__generated__/graphqlTypes'; // Magic /* GraphQL */ comment below is for codegen, do not remove export const SUPERGRAPH_SDL_QUERY = /* GraphQL */`#graphql @@ -35,7 +35,7 @@ interface SupergraphSdlQueryFailure { errors: GraphQLError[]; } -const { name, version } = require('../package.json'); +const { name, version } = require('../../package.json'); const fetchErrorMsg = "An error occurred while fetching your schema from Apollo: "; diff --git a/gateway-js/src/outOfBandReporter.ts b/gateway-js/src/UplinkFetcher/outOfBandReporter.ts similarity index 96% rename from gateway-js/src/outOfBandReporter.ts rename to gateway-js/src/UplinkFetcher/outOfBandReporter.ts index a9dd28ffef..747cf9b320 100644 --- a/gateway-js/src/outOfBandReporter.ts +++ b/gateway-js/src/UplinkFetcher/outOfBandReporter.ts @@ -4,7 +4,7 @@ import { ErrorCode, OobReportMutation, OobReportMutationVariables, -} from './__generated__/graphqlTypes'; +} from '../__generated__/graphqlTypes'; // Magic /* GraphQL */ comment below is for codegen, do not remove export const OUT_OF_BAND_REPORTER_QUERY = /* GraphQL */`#graphql @@ -13,7 +13,7 @@ export const OUT_OF_BAND_REPORTER_QUERY = /* GraphQL */`#graphql } `; -const { name, version } = require('../package.json'); +const { name, version } = require('../../package.json'); type OobReportMutationResult = | OobReportMutationSuccess diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 7749a89296..4ec9c108d3 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -1,8 +1,10 @@ import gql from 'graphql-tag'; import { ApolloGateway } from '../..'; import { + DynamicGatewayConfig, Experimental_DidResolveQueryPlanCallback, Experimental_UpdateServiceDefinitions, + ServiceDefinitionUpdate, } from '../../config'; import { product, @@ -11,6 +13,8 @@ import { accounts, books, documents, + fixtures, + fixturesWithUpdate, } from 'apollo-federation-integration-testsuite'; import { Logger } from 'apollo-server-types'; import resolvable from '@josephg/resolvable'; @@ -48,11 +52,10 @@ beforeEach(() => { describe('lifecycle hooks', () => { it('uses updateServiceDefinitions override', async () => { - const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions = jest.fn( - async () => { + const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions = + jest.fn(async () => { return { serviceDefinitions, isNewSchema: true }; - }, - ); + }); const gateway = new ApolloGateway({ serviceList: serviceDefinitions, @@ -68,38 +71,6 @@ describe('lifecycle hooks', () => { await gateway.stop(); }); - it('calls experimental_didFailComposition with a bad config', async () => { - const experimental_didFailComposition = jest.fn(); - - const gateway = new ApolloGateway({ - async experimental_updateServiceDefinitions() { - return { - serviceDefinitions: [serviceDefinitions[0]], - compositionMetadata: { - formatVersion: 1, - id: 'abc', - implementingServiceLocations: [], - schemaHash: 'abc', - }, - isNewSchema: true, - }; - }, - serviceList: [], - experimental_didFailComposition, - logger, - }); - - await expect(gateway.load()).rejects.toThrowError(); - - const callbackArgs = experimental_didFailComposition.mock.calls[0][0]; - expect(callbackArgs.serviceList).toHaveLength(1); - expect(callbackArgs.errors[0]).toMatchInlineSnapshot( - `[GraphQLError: [product] Book -> \`Book\` is an extension type, but \`Book\` is not defined in any service]`, - ); - expect(callbackArgs.compositionMetadata.id).toEqual('abc'); - expect(experimental_didFailComposition).toBeCalled(); - }); - it('calls experimental_didUpdateComposition on schema update', async () => { const compositionMetadata = { formatVersion: 1, @@ -108,36 +79,28 @@ describe('lifecycle hooks', () => { schemaHash: 'hash1', }; - const update: Experimental_UpdateServiceDefinitions = async () => ({ - serviceDefinitions, - isNewSchema: true, - compositionMetadata: { - ...compositionMetadata, - id: '123', - schemaHash: 'hash2', - }, - }); - - // This is the simplest way I could find to achieve mocked functions that leverage our types - const mockUpdate = jest.fn(update); - - // We want to return a different composition across two ticks, so we mock it - // slightly differenty - mockUpdate.mockImplementationOnce(async () => { - const services = serviceDefinitions.filter(s => s.name !== 'books'); - return { - serviceDefinitions: [ - ...services, - { - name: 'book', - typeDefs: books.typeDefs, - url: 'http://localhost:32542', + const mockUpdate = jest + .fn, [config: DynamicGatewayConfig]>() + .mockImplementationOnce(async () => { + return { + serviceDefinitions: fixtures, + isNewSchema: true, + compositionMetadata: { + ...compositionMetadata, + id: '123', + schemaHash: 'hash2', }, - ], - isNewSchema: true, - compositionMetadata, - }; - }); + }; + }) + // We want to return a different composition across two ticks, so we mock it + // slightly differently + .mockImplementationOnce(async () => { + return { + serviceDefinitions: fixturesWithUpdate, + isNewSchema: true, + compositionMetadata, + }; + }); const mockDidUpdate = jest.fn(); @@ -146,13 +109,13 @@ describe('lifecycle hooks', () => { experimental_didUpdateComposition: mockDidUpdate, logger, }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; const schemaChangeBlocker1 = resolvable(); const schemaChangeBlocker2 = resolvable(); - gateway.onSchemaChange( + gateway.onSchemaLoadOrUpdate( jest .fn() .mockImplementationOnce(() => schemaChangeBlocker1.resolve()) @@ -162,25 +125,27 @@ describe('lifecycle hooks', () => { await gateway.load(); await schemaChangeBlocker1; + expect(mockUpdate).toBeCalledTimes(1); expect(mockDidUpdate).toBeCalledTimes(1); await schemaChangeBlocker2; + expect(mockUpdate).toBeCalledTimes(2); expect(mockDidUpdate).toBeCalledTimes(2); const [firstCall, secondCall] = mockDidUpdate.mock.calls; - expect(firstCall[0]!.schema).toBeDefined(); - expect(firstCall[0].compositionMetadata!.schemaHash).toEqual('hash1'); + const expectedFirstId = '562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b' + expect(firstCall[0]!.compositionId).toEqual(expectedFirstId); // first call should have no second "previous" argument expect(firstCall[1]).toBeUndefined(); - expect(secondCall[0].schema).toBeDefined(); - expect(secondCall[0].compositionMetadata!.schemaHash).toEqual('hash2'); + expect(secondCall[0]!.compositionId).toEqual( + '0ced02894592ade4376276d11735b46723eb84850c32765cb78502ba5c29a563', + ); // second call should have previous info in the second arg - expect(secondCall[1]!.schema).toBeDefined(); - expect(secondCall[1]!.compositionMetadata!.schemaHash).toEqual('hash1'); + expect(secondCall[1]!.compositionId).toEqual(expectedFirstId); await gateway.stop(); }); @@ -214,11 +179,10 @@ describe('lifecycle hooks', () => { }); it('registers schema change callbacks when experimental_pollInterval is set for unmanaged configs', async () => { - const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions = jest.fn( - async (_config) => { + const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions = + jest.fn(async (_config) => { return { serviceDefinitions, isNewSchema: true }; - }, - ); + }); const gateway = new ApolloGateway({ serviceList: [{ name: 'book', url: 'http://localhost:32542' }], @@ -230,7 +194,7 @@ describe('lifecycle hooks', () => { const schemaChangeBlocker = resolvable(); const schemaChangeCallback = jest.fn(() => schemaChangeBlocker.resolve()); - gateway.onSchemaChange(schemaChangeCallback); + gateway.onSchemaLoadOrUpdate(schemaChangeCallback); await gateway.load(); await schemaChangeBlocker; @@ -240,12 +204,11 @@ describe('lifecycle hooks', () => { }); it('calls experimental_didResolveQueryPlan when executor is called', async () => { - const experimental_didResolveQueryPlan: Experimental_DidResolveQueryPlanCallback = jest.fn() + const experimental_didResolveQueryPlan: Experimental_DidResolveQueryPlanCallback = + jest.fn(); const gateway = new ApolloGateway({ - localServiceList: [ - books - ], + localServiceList: [books], experimental_didResolveQueryPlan, }); @@ -255,7 +218,7 @@ describe('lifecycle hooks', () => { { book(isbn: "0262510871") { year } } `; - // @ts-ignore + // @ts-ignore await executor({ source, document: gql(source), diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 241fdb9c24..6dccd3de0a 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -327,14 +327,17 @@ describe('gateway config / env behavior', () => { APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT: 'env-config', }); - gateway = new ApolloGateway({ + const config = { logger, uplinkEndpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2, mockCloudConfigUrl3], - }); + }; + gateway = new ApolloGateway(config); - expect(gateway['uplinkEndpoints']).toEqual( - [mockCloudConfigUrl1, mockCloudConfigUrl2, mockCloudConfigUrl3], - ); + expect(gateway['getUplinkEndpoints'](config)).toEqual([ + mockCloudConfigUrl1, + mockCloudConfigUrl2, + mockCloudConfigUrl3, + ]); gateway = null; }); @@ -346,14 +349,13 @@ describe('gateway config / env behavior', () => { APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT: 'env-config', }); - gateway = new ApolloGateway({ + const config = { logger, schemaConfigDeliveryEndpoint: 'code-config', - }); + }; + gateway = new ApolloGateway(config); - expect(gateway['uplinkEndpoints']).toEqual( - ['code-config'], - ); + expect(gateway['getUplinkEndpoints'](config)).toEqual(['code-config']); gateway = null; }); diff --git a/gateway-js/src/__tests__/integration/logger.test.ts b/gateway-js/src/__tests__/integration/logger.test.ts index 67c287b767..9d49bea54b 100644 --- a/gateway-js/src/__tests__/integration/logger.test.ts +++ b/gateway-js/src/__tests__/integration/logger.test.ts @@ -10,7 +10,7 @@ import * as log4js from "log4js"; const LOWEST_LOG_LEVEL = "debug"; -const KNOWN_DEBUG_MESSAGE = "Checking for composition updates..."; +const KNOWN_DEBUG_MESSAGE = "Gateway successfully initialized (but not yet loaded)"; async function triggerKnownDebugMessage(logger: Logger) { // Trigger a known error. diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index 137ee5c0a6..3020de3b61 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -139,26 +139,38 @@ it('Updates Supergraph SDL from remote storage', async () => { // This test is only interested in the second time the gateway notifies of an // update, since the first happens on load. const secondUpdate = resolvable(); - const schemaChangeCallback = jest - .fn() - .mockImplementationOnce(() => {}) - .mockImplementationOnce(() => { - secondUpdate.resolve(); - }); gateway = new ApolloGateway({ logger, uplinkEndpoints: [mockCloudConfigUrl1], }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; - gateway.onSchemaLoadOrUpdate(schemaChangeCallback); + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; + + const schemas: GraphQLSchema[] = []; + gateway.onSchemaLoadOrUpdate(({apiSchema}) => { + schemas.push(apiSchema); + }); + gateway.onSchemaLoadOrUpdate( + jest + .fn() + .mockImplementationOnce(() => {}) + .mockImplementationOnce(() => secondUpdate.resolve()), + ); await gateway.load(mockApolloConfig); - expect(gateway['compositionId']).toMatchInlineSnapshot(`"originalId-1234"`); await secondUpdate; - expect(gateway['compositionId']).toMatchInlineSnapshot(`"updatedId-5678"`); + + // First schema has no 'review' field on the 'Query' type + expect( + (schemas[0].getType('Query') as GraphQLObjectType).getFields()['review'], + ).toBeFalsy(); + + // Updated schema adds 'review' field on the 'Query' type + expect( + (schemas[1].getType('Query') as GraphQLObjectType).getFields()['review'], + ).toBeTruthy(); }); describe('Supergraph SDL update failures', () => { @@ -199,14 +211,14 @@ describe('Supergraph SDL update failures', () => { uplinkMaxRetries: 0, }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; await gateway.load(mockApolloConfig); await errorLoggedPromise; expect(logger.error).toHaveBeenCalledWith( - 'An error occurred while fetching your schema from Apollo: 500 Internal Server Error', + 'UplinkFetcher failed to update supergraph with the following error: An error occurred while fetching your schema from Apollo: 500 Internal Server Error', ); }); @@ -231,16 +243,15 @@ describe('Supergraph SDL update failures', () => { uplinkEndpoints: [mockCloudConfigUrl1], uplinkMaxRetries: 0, }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; await gateway.load(mockApolloConfig); await errorLoggedPromise; + // @ts-ignore expect(logger.error).toHaveBeenCalledWith( - 'An error occurred while fetching your schema from Apollo: ' + - '\n' + - 'Cannot query field "fail" on type "Query".', + `UplinkFetcher failed to update supergraph with the following error: An error occurred while fetching your schema from Apollo: \nCannot query field "fail" on type "Query".`, ); }); @@ -267,14 +278,14 @@ describe('Supergraph SDL update failures', () => { logger, uplinkEndpoints: [mockCloudConfigUrl1], }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; await gateway.load(mockApolloConfig); await errorLoggedPromise; expect(logger.error).toHaveBeenCalledWith( - 'Syntax Error: Unexpected Name "Syntax".', + 'UplinkFetcher failed to update supergraph with the following error: Syntax Error: Unexpected Name "Syntax".', ); expect(gateway.schema).toBeTruthy(); }); @@ -297,8 +308,8 @@ describe('Supergraph SDL update failures', () => { logger, uplinkEndpoints: [mockCloudConfigUrl1], }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; await expect( gateway.load(mockApolloConfig), @@ -338,8 +349,8 @@ it('Rollsback to a previous schema when triggered', async () => { logger, uplinkEndpoints: [mockCloudConfigUrl1], }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; gateway.onSchemaChange(onChange); await gateway.load(mockApolloConfig); @@ -421,8 +432,8 @@ describe('Downstream service health checks', () => { logger, uplinkEndpoints: [mockCloudConfigUrl1], }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; await gateway.load(mockApolloConfig); await gateway.stop(); @@ -461,7 +472,7 @@ describe('Downstream service health checks', () => { // TODO: smell that we should be awaiting something else expect(err.message).toMatchInlineSnapshot(` - "The gateway did not update its schema due to failed service health checks. The gateway will continue to operate with the previous schema and reattempt updates. The following error occurred during the health check: + "The gateway subgraphs health check failed. Updating to the provided \`supergraphSdl\` will likely result in future request failures to subgraphs. The following error occurred during the health check: [accounts]: 500: Internal Server Error" `); @@ -502,8 +513,8 @@ describe('Downstream service health checks', () => { logger, uplinkEndpoints: [mockCloudConfigUrl1], }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; gateway.onSchemaChange(onChange); await gateway.load(mockApolloConfig); @@ -521,6 +532,10 @@ describe('Downstream service health checks', () => { }); it('Preserves original schema when health check fails', async () => { + const errorLoggedPromise = resolvable(); + const errorSpy = jest.fn(() => errorLoggedPromise.resolve()); + logger.error = errorSpy; + mockSupergraphSdlRequestSuccess(); mockAllServicesHealthCheckSuccess(); @@ -537,56 +552,16 @@ describe('Downstream service health checks', () => { mockServiceHealthCheckSuccess(reviews); mockServiceHealthCheckSuccess(documents); - const schemaChangeBlocker = resolvable(); - gateway = new ApolloGateway({ serviceHealthCheck: true, logger, uplinkEndpoints: [mockCloudConfigUrl1], }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; - - // @ts-ignore for testing purposes, we'll call the original `updateSchema` - // function from our mock. The first call should mimic original behavior, - // but the second call needs to handle the PromiseRejection. Typically for tests - // like these we would leverage the `gateway.onSchemaChange` callback to drive - // the test, but in this case, that callback isn't triggered when the update - // fails (as expected) so we get creative with the second mock as seen below. - const original = gateway.updateSchema; - const mockUpdateSchema = jest - .fn() - .mockImplementationOnce(async () => { - await original.apply(gateway); - }) - .mockImplementationOnce(async () => { - // mock the first poll and handle the error which would otherwise be caught - // and logged from within the `pollServices` class method - - // This is the ideal, but our version of Jest has a bug with printing error snapshots. - // See: https://github.com/facebook/jest/pull/10217 (fixed in v26.2.0) - // expect(original.apply(gateway)).rejects.toThrowErrorMatchingInlineSnapshot(` - // The gateway did not update its schema due to failed service health checks. The gateway will continue to operate with the previous schema and reattempt updates. The following error occurred during the health check: - // [accounts]: 500: Internal Server Error" - // `); - // Instead we'll just use the regular snapshot matcher... - try { - await original.apply(gateway); - } catch (e) { - var err = e; - } - - expect(err.message).toMatchInlineSnapshot(` - "The gateway did not update its schema due to failed service health checks. The gateway will continue to operate with the previous schema and reattempt updates. The following error occurred during the health check: - [accounts]: 500: Internal Server Error" - `); - // finally resolve the promise which drives this test - schemaChangeBlocker.resolve(); - }); - - // @ts-ignore for testing purposes, replace the `updateSchema` - // function on the gateway with our mock - gateway.updateSchema = mockUpdateSchema; + // for testing purposes, a short pollInterval is ideal so we'll override here + gateway['pollIntervalInMs'] = 100; + + const updateSpy = jest.fn(); + gateway.onSchemaLoadOrUpdate(() => updateSpy()); // load the gateway as usual await gateway.load(mockApolloConfig); @@ -595,11 +570,15 @@ describe('Downstream service health checks', () => { expect(getRootQueryFields(gateway.schema)).toContain('topReviews'); expect(getRootQueryFields(gateway.schema)).not.toContain('review'); - await schemaChangeBlocker; + await errorLoggedPromise; + expect(logger.error).toHaveBeenCalledWith( + `UplinkFetcher failed to update supergraph with the following error: The gateway subgraphs health check failed. Updating to the provided \`supergraphSdl\` will likely result in future request failures to subgraphs. The following error occurred during the health check:\n[accounts]: 500: Internal Server Error`, + ); // At this point, the mock update should have been called but the schema // should still be the original. - expect(mockUpdateSchema).toHaveBeenCalledTimes(2); + expect(updateSpy).toHaveBeenCalledTimes(1); + expect(getRootQueryFields(gateway.schema)).toContain('topReviews'); expect(getRootQueryFields(gateway.schema)).not.toContain('review'); }); diff --git a/gateway-js/src/__tests__/integration/nockMocks.ts b/gateway-js/src/__tests__/integration/nockMocks.ts index 5d93dd31f2..5a115de914 100644 --- a/gateway-js/src/__tests__/integration/nockMocks.ts +++ b/gateway-js/src/__tests__/integration/nockMocks.ts @@ -1,6 +1,6 @@ import nock from 'nock'; import { HEALTH_CHECK_QUERY, SERVICE_DEFINITION_QUERY } from '../..'; -import { SUPERGRAPH_SDL_QUERY } from '../../loadSupergraphSdlFromStorage'; +import { SUPERGRAPH_SDL_QUERY } from '../../UplinkFetcher/loadSupergraphSdlFromStorage'; import { getTestingSupergraphSdl } from '../../__tests__/execution-utils'; import { print } from 'graphql'; import { Fixture, fixtures as testingFixtures } from 'apollo-federation-integration-testsuite'; diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 83a10a9c86..5b0620042a 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -229,7 +229,7 @@ export type StaticGatewayConfig = | LocalGatewayConfig | StaticSupergraphSdlGatewayConfig; -type DynamicGatewayConfig = +export type DynamicGatewayConfig = | ManagedGatewayConfig | ManuallyManagedGatewayConfig; diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 938c2ae460..b6efb0d57e 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -22,7 +22,6 @@ import { ServiceDefinition, } from '@apollo/federation'; import loglevel from 'loglevel'; -import resolvable from '@josephg/resolvable'; import { buildOperationContext, OperationContext } from './operationContext'; import { executeQueryPlan, @@ -53,7 +52,6 @@ import { CompositionInfo, GatewayConfig, StaticGatewayConfig, - ServiceListGatewayConfig, isManuallyManagedConfig, isLocalConfig, isServiceListConfig, @@ -61,22 +59,20 @@ import { isDynamicConfig, isStaticConfig, CompositionMetadata, - isSupergraphSdlUpdate, - isServiceDefinitionUpdate, - ServiceDefinitionUpdate, SupergraphSdlUpdate, - CompositionUpdate, + SupergraphSdlHook, isManuallyManagedSupergraphSdlGatewayConfig, - ManuallyManagedSupergraphSdlGatewayConfig, + ManagedGatewayConfig, } from './config'; import { buildComposedSchema } from '@apollo/query-planner'; -import { loadSupergraphSdlFromUplinks } from './loadSupergraphSdlFromStorage'; import { SpanStatusCode } from '@opentelemetry/api'; import { OpenTelemetrySpanNames, tracer } from './utilities/opentelemetry'; import { CoreSchema } from '@apollo/core-schema'; import { featureSupport } from './core'; import { createHash } from './utilities/createHash'; import { IntrospectAndCompose } from './IntrospectAndCompose'; +import { UplinkFetcher } from './UplinkFetcher'; +import { LegacyFetcher } from './LegacyFetcher'; type DataSourceMap = { [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; @@ -126,12 +122,6 @@ type GatewayState = | { phase: 'loaded' } | { phase: 'stopping'; stoppingDonePromise: Promise } | { phase: 'stopped' } - | { - phase: 'waiting to poll'; - pollWaitTimer: NodeJS.Timer; - doneWaiting: () => void; - } - | { phase: 'polling'; pollingDonePromise: Promise } | { phase: 'updating schema' }; // We want to be compatible with `load()` as called by both AS2 and AS3, so we @@ -176,7 +166,6 @@ export class ApolloGateway implements GraphQLService { coreSupergraphSdl: string; }) => void >(); - private serviceDefinitions: ServiceDefinition[] = []; private compositionMetadata?: CompositionMetadata; private warnedStates: WarnedStates = Object.create(null); private queryPlanner?: QueryPlanner; @@ -185,12 +174,6 @@ export class ApolloGateway implements GraphQLService { private fetcher: typeof fetch; private compositionId?: string; private state: GatewayState; - private errorReportingEndpoint: string | undefined = - process.env.APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT ?? undefined; - - // This will no longer be necessary once the `serviceList` option is removed - // TODO(trevor:removeServiceList) - private internalIntrospectAndComposeInstance?: IntrospectAndCompose; // Observe query plan, service info, and operation info prior to execution. // The information made available here will give insight into the resulting @@ -203,16 +186,8 @@ export class ApolloGateway implements GraphQLService { // Used to communicated composition changes, and what definitions caused // those updates private experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; - // Used for overriding the default service list fetcher. This should return - // an array of ServiceDefinition. *This function must be awaited.* - private updateServiceDefinitions?: Experimental_UpdateComposition; - // how often service defs should be loaded/updated (in ms) - private experimental_pollInterval?: number; - // Configure the endpoints by which gateway will access its precomposed schema. - // * An array of URLs means use these endpoints to obtain schema, if one is unavailable then try the next. - // * `undefined` means the gateway is not using managed federation - private uplinkEndpoints?: string[]; - private uplinkMaxRetries?: number; + // how often service defs should be loaded/updated + private pollIntervalInMs?: number; // Functions to call during gateway cleanup (when stop() is called) private toDispose: (() => Promise)[] = []; @@ -239,61 +214,14 @@ export class ApolloGateway implements GraphQLService { this.experimental_didUpdateComposition = config?.experimental_didUpdateComposition; - this.experimental_pollInterval = config?.experimental_pollInterval; - - // 1. If config is set to a `string`, use it - // 2. If the env var is set, use that - // 3. If config is `undefined`, use the default uplink URLs - if (isManagedConfig(this.config)) { - const rawEndpointsString = process.env.APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT; - const envEndpoints = rawEndpointsString?.split(",") ?? null; - - if (this.config.schemaConfigDeliveryEndpoint && !this.config.uplinkEndpoints) { - this.uplinkEndpoints = [this.config.schemaConfigDeliveryEndpoint]; - } else { - this.uplinkEndpoints = this.config.uplinkEndpoints ?? - envEndpoints ?? [ - 'https://uplink.api.apollographql.com/', - 'https://aws.uplink.api.apollographql.com/' - ]; - } - - this.uplinkMaxRetries = this.config.uplinkMaxRetries ?? this.uplinkEndpoints.length * 3; - } - - if (isManuallyManagedConfig(this.config)) { - // Use the provided updater function if provided by the user, else default - if ('experimental_updateSupergraphSdl' in this.config) { - this.updateServiceDefinitions = - this.config.experimental_updateSupergraphSdl; - } else if ('experimental_updateServiceDefinitions' in this.config) { - this.updateServiceDefinitions = - this.config.experimental_updateServiceDefinitions; - } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { - // TODO: do nothing maybe? - } else if (isServiceListConfig(this.config)) { - // TODO(trevor:removeServiceList) - this.internalIntrospectAndComposeInstance = new IntrospectAndCompose({ - subgraphs: this.config.serviceList, - pollIntervalInMs: this.config.experimental_pollInterval, - logger: this.logger, - subgraphHealthCheck: this.config.serviceHealthCheck, - introspectionHeaders: this.config.introspectionHeaders, - }); - } else { - throw Error( - 'Programming error: unexpected manual configuration provided', - ); - } - } else { - this.updateServiceDefinitions = this.loadServiceDefinitions; - } + this.pollIntervalInMs = config?.experimental_pollInterval; this.issueDeprecationWarningsIfApplicable(); if (isDynamicConfig(this.config)) { this.issueDynamicWarningsIfApplicable(); } + this.logger.debug('Gateway successfully initialized (but not yet loaded)'); this.state = { phase: 'initialized' }; } @@ -335,7 +263,7 @@ export class ApolloGateway implements GraphQLService { this.config.experimental_pollInterval && this.config.experimental_pollInterval < 10000 ) { - this.experimental_pollInterval = 10000; + this.pollIntervalInMs = 10000; this.logger.warn( 'Polling Apollo services at a frequency of less than once per 10 ' + 'seconds (10000) is disallowed. Instead, the minimum allowed ' + @@ -403,6 +331,8 @@ export class ApolloGateway implements GraphQLService { apollo?: ApolloConfigFromAS2Or3; engine?: GraphQLServiceEngineConfig; }) { + this.logger.debug('Loading gateway...'); + if (this.state.phase !== 'initialized') { throw Error( `ApolloGateway.load called in surprising state ${this.state.phase}`, @@ -428,48 +358,71 @@ export class ApolloGateway implements GraphQLService { }; } - // Before @apollo/gateway v0.23, ApolloGateway didn't expect stop() to be - // called after it started. The only thing that stop() did at that point was - // cancel the poll timer, and so to prevent that timer from keeping an - // otherwise-finished Node process alive, ApolloGateway unconditionally - // called unref() on that timeout. As part of making the ApolloGateway - // lifecycle more predictable and concrete (and to allow for a future where - // there are other reasons to make sure to explicitly stop your gateway), - // v0.23 tries to avoid calling unref(). - // - // Apollo Server v2.20 and newer calls gateway.stop() from its stop() - // method, so as long as you're using v2.20, ApolloGateway won't keep - // running after you stop your server, and your Node process can shut down. - // To make this change a bit less backwards-incompatible, we detect if it - // looks like you're using an older version of Apollo Server; if so, we - // still call unref(). Specifically: Apollo Server has always passed an - // options object to load(), and before v2.18 it did not pass the `apollo` - // key on it. So if we detect that particular pattern, we assume we're with - // pre-v2.18 Apollo Server and we still call unref(). So this will be a - // behavior change only for: - // - non-Apollo-Server uses of ApolloGateway (where you can add your own - // call to gateway.stop()) - // - Apollo Server v2.18 and v2.19 (where you can either do the small - // compatible upgrade or add your own call to gateway.stop()) - // - if you don't call stop() on your ApolloServer (but in that case other - // things like usage reporting will also stop shutdown, so you should fix - // that) - const unrefTimer = !!options && !options.apollo; - this.maybeWarnOnConflictingConfig(); // Handles initial assignment of `this.schema`, `this.queryPlanner` if (isStaticConfig(this.config)) { this.loadStatic(this.config); + } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { + const supergraphSdlFetcher = typeof this.config.supergraphSdl === 'object' + ? this.config.supergraphSdl + : { initialize: this.config.supergraphSdl }; + await this.loadSupergraphSdl(supergraphSdlFetcher); } else if ( - isManuallyManagedSupergraphSdlGatewayConfig(this.config) || - (isServiceListConfig(this.config) && - // this setting is currently expected to override `serviceList` when they both exist - !('experimental_updateServiceDefinitions' in this.config)) + isServiceListConfig(this.config) && + // this setting is currently expected to override `serviceList` when they both exist + !('experimental_updateServiceDefinitions' in this.config) ) { - await this.loadManuallyManaged(this.config); + await this.loadSupergraphSdl( + new IntrospectAndCompose({ + subgraphs: this.config.serviceList, + pollIntervalInMs: this.config.experimental_pollInterval, + logger: this.logger, + subgraphHealthCheck: this.config.serviceHealthCheck, + introspectionHeaders: this.config.introspectionHeaders, + }), + ); + } else if (isManagedConfig(this.config)) { + const canUseManagedConfig = + this.apolloConfig?.graphRef && this.apolloConfig?.keyHash; + if (!canUseManagedConfig) { + throw new Error( + 'When a manual configuration is not provided, gateway requires an Apollo ' + + 'configuration. See https://www.apollographql.com/docs/apollo-server/federation/managed-federation/ ' + + 'for more information. Manual configuration options include: ' + + '`serviceList`, `supergraphSdl`, and `experimental_updateServiceDefinitions`.', + ); + } + const uplinkEndpoints = this.getUplinkEndpoints(this.config); + + await this.loadSupergraphSdl( + new UplinkFetcher({ + graphRef: this.apolloConfig!.graphRef!, + apiKey: this.apolloConfig!.key!, + uplinkEndpoints, + maxRetries: + this.config.uplinkMaxRetries ?? uplinkEndpoints.length * 3, + subgraphHealthCheck: this.config.serviceHealthCheck, + fetcher: this.fetcher, + logger: this.logger, + pollIntervalInMs: this.pollIntervalInMs ?? 10000, + }), + ); } else { - await this.loadDynamic(unrefTimer); + const updateServiceDefinitions = + 'experimental_updateServiceDefinitions' in this.config + ? this.config.experimental_updateServiceDefinitions + : this.config.experimental_updateSupergraphSdl; + + const legacyFetcher = new LegacyFetcher({ + logger: this.logger, + gatewayConfig: this.config, + updateServiceDefinitions, + pollIntervalInMs: this.pollIntervalInMs, + subgraphHealthCheck: this.config.serviceHealthCheck, + }); + + await this.loadSupergraphSdl(legacyFetcher); } const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; @@ -487,6 +440,23 @@ export class ApolloGateway implements GraphQLService { }; } + private getUplinkEndpoints(config: ManagedGatewayConfig) { + // 1. If config is set to a `string`, use it + // 2. If the env var is set, use that + // 3. If config is `undefined`, use the default uplink URLs + const rawEndpointsString = + process.env.APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT; + const envEndpoints = rawEndpointsString?.split(',') ?? null; + return config.uplinkEndpoints ?? + (config.schemaConfigDeliveryEndpoint + ? [config.schemaConfigDeliveryEndpoint] + : null) ?? + envEndpoints ?? [ + 'https://uplink.api.apollographql.com/', + 'https://aws.uplink.api.apollographql.com/', + ]; + } + // Synchronously load a statically configured schema, update class instance's // schema and query planner. private loadStatic(config: StaticGatewayConfig) { @@ -507,46 +477,22 @@ export class ApolloGateway implements GraphQLService { this.state = { phase: 'loaded' }; } - // Asynchronously load a dynamically configured schema. `this.updateSchema` - // is responsible for updating the class instance's schema and query planner. - private async loadDynamic(unrefTimer: boolean) { - try { - await this.updateSchema(); - } catch (e) { - this.state = { phase: 'failed to load' }; - throw e; - } - - this.state = { phase: 'loaded' }; - if (this.shouldBeginPolling()) { - this.pollServices(unrefTimer); - } - } - private getIdForSupergraphSdl(supergraphSdl: string) { return createHash('sha256').update(supergraphSdl).digest('hex'); } - private async loadManuallyManaged( - config: - | ManuallyManagedSupergraphSdlGatewayConfig - | ServiceListGatewayConfig, + private async loadSupergraphSdl( + supergraphSdlFetcher: T, ) { try { - const supergraphSdlObject = isServiceListConfig(config) - ? this.internalIntrospectAndComposeInstance! - : typeof config.supergraphSdl === 'object' - ? config.supergraphSdl - : { initialize: config.supergraphSdl }; - - const result = await supergraphSdlObject.initialize({ + const result = await supergraphSdlFetcher.initialize({ update: this.externalSupergraphUpdateCallback.bind(this), healthCheck: this.externalSubgraphHealthCheckCallback.bind(this), getDataSource: this.externalGetDataSourceCallback.bind(this), }); if (!result?.supergraphSdl) { throw new Error( - 'User provided `supergraphSdl` function did not return an object containing a `supergraphSdl` property', + 'Provided `supergraphSdl` function did not return an object containing a `supergraphSdl` property', ); } if (result?.cleanup) { @@ -555,94 +501,15 @@ export class ApolloGateway implements GraphQLService { this.externalSupergraphUpdateCallback(result.supergraphSdl); } catch (e) { - this.logger.error(e.message ?? e); this.state = { phase: 'failed to load' }; + await this.performCleanup(); + this.logger.error(e.message ?? e); throw e; } this.state = { phase: 'loaded' }; } - private shouldBeginPolling() { - return isManagedConfig(this.config) || this.experimental_pollInterval; - } - - private async updateSchema(): Promise { - this.logger.debug('Checking for composition updates...'); - - if (!this.updateServiceDefinitions) { - throw new Error( - 'Programming error: `updateSchema` was called unexpectedly.', - ); - } - // This may throw, but an error here is caught and logged upstream - const result = await this.updateServiceDefinitions(this.config); - - await this.maybePerformServiceHealthCheck(result); - if (isSupergraphSdlUpdate(result)) { - this.updateWithSupergraphSdl(result); - } else if (isServiceDefinitionUpdate(result)) { - this.updateByComposition(result); - } else { - throw new Error( - 'Programming error: unexpected result type from `updateServiceDefinitions`', - ); - } - } - - private updateByComposition(result: ServiceDefinitionUpdate) { - if ( - !result.serviceDefinitions || - JSON.stringify(this.serviceDefinitions) === - JSON.stringify(result.serviceDefinitions) - ) { - this.logger.debug('No change in service definitions since last check.'); - return; - } - - const previousSchema = this.schema; - const previousServiceDefinitions = this.serviceDefinitions; - const previousCompositionMetadata = this.compositionMetadata; - - if (previousSchema) { - this.logger.info('New service definitions were found.'); - } - this.compositionMetadata = result.compositionMetadata; - this.serviceDefinitions = result.serviceDefinitions; - - const { schema, supergraphSdl } = this.createSchemaFromServiceList( - result.serviceDefinitions, - ); - - if (!supergraphSdl) { - this.logger.error( - "A valid schema couldn't be composed. Falling back to previous schema.", - ); - } else { - this.updateWithSchemaAndNotify(schema, supergraphSdl); - - if (this.experimental_didUpdateComposition) { - this.experimental_didUpdateComposition( - { - serviceDefinitions: result.serviceDefinitions, - schema, - ...(this.compositionMetadata && { - compositionMetadata: this.compositionMetadata, - }), - }, - previousServiceDefinitions && - previousSchema && { - serviceDefinitions: previousServiceDefinitions, - schema: previousSchema, - ...(previousCompositionMetadata && { - compositionMetadata: previousCompositionMetadata, - }), - }, - ); - } - } - } - /** * @throws Error * when called from a state other than `loaded` @@ -817,40 +684,6 @@ export class ApolloGateway implements GraphQLService { }); } - private async maybePerformServiceHealthCheck(update: CompositionUpdate) { - // Run service health checks before we commit and update the new schema. - // This is the last chance to bail out of a schema update. - if (!this.config.serviceHealthCheck) return; - - const serviceList = isSupergraphSdlUpdate(update) - ? // TODO(trevor): #580 redundant parse - // Parsing could technically fail and throw here, but parseability has - // already been confirmed slightly earlier in the code path - this.serviceListFromSupergraphSdl(parse(update.supergraphSdl)) - : // Existence of this is determined in advance with an early return otherwise - update.serviceDefinitions!; - // Here we need to construct new datasources based on the new schema info - // so we can check the health of the services we're _updating to_. - const serviceMap = serviceList.reduce((serviceMap, serviceDef) => { - serviceMap[serviceDef.name] = { - url: serviceDef.url, - dataSource: this.createDataSource(serviceDef), - }; - return serviceMap; - }, Object.create(null) as DataSourceMap); - - try { - await this.serviceHealthCheck(serviceMap); - } catch (e) { - throw new Error( - 'The gateway did not update its schema due to failed service health checks. ' + - 'The gateway will continue to operate with the previous schema and reattempt updates. ' + - 'The following error occurred during the health check:\n' + - e.message, - ); - } - } - /** * This can be used without an argument in order to perform an ad-hoc health check * of the downstream services like so: @@ -992,89 +825,6 @@ export class ApolloGateway implements GraphQLService { }; } - // TODO(trevor:removeServiceList): gateway shouldn't be responsible for polling - // in the future. - // This function waits an appropriate amount, updates composition, and calls itself - // again. Note that it is an async function whose Promise is not actually awaited; - // it should never throw itself other than due to a bug in its state machine. - private async pollServices(unrefTimer: boolean) { - switch (this.state.phase) { - case 'stopping': - case 'stopped': - case 'failed to load': - return; - case 'initialized': - throw Error('pollServices should not be called before load!'); - case 'polling': - throw Error( - 'pollServices should not be called while in the middle of polling!', - ); - case 'waiting to poll': - throw Error( - 'pollServices should not be called while already waiting to poll!', - ); - case 'loaded': - // This is the normal case. - break; - case 'updating schema': - // This should never happen - throw Error( - "ApolloGateway.pollServices called from an unexpected state 'updating schema'", - ); - default: - throw new UnreachableCaseError(this.state); - } - - // Transition into 'waiting to poll' and set a timer. The timer resolves the - // Promise we're awaiting here; note that calling stop() also can resolve - // that Promise. - await new Promise((doneWaiting) => { - this.state = { - phase: 'waiting to poll', - doneWaiting, - pollWaitTimer: setTimeout(() => { - // Note that we might be in 'stopped', in which case we just do - // nothing. - if (this.state.phase == 'waiting to poll') { - this.state.doneWaiting(); - } - }, this.experimental_pollInterval || 10000), - }; - if (unrefTimer) { - this.state.pollWaitTimer.unref(); - } - }); - - // If we've been stopped, then we're done. The cast here is because TS - // doesn't understand that this.state can change during the await - // (https://github.com/microsoft/TypeScript/issues/9998). - if ((this.state as GatewayState).phase !== 'waiting to poll') { - return; - } - - const pollingDonePromise = resolvable(); - this.state = { - phase: 'polling', - pollingDonePromise, - }; - - try { - await this.updateSchema(); - } catch (err) { - this.logger.error((err && err.message) || err); - } - - if (this.state.phase === 'polling') { - // If we weren't stopped, we should transition back to the initial 'loading' state and trigger - // another call to itself. (Do that in a setImmediate to avoid unbounded stack sizes.) - this.state = { phase: 'loaded' }; - setImmediate(() => this.pollServices(unrefTimer)); - } - - // Whether we were stopped or not, let any concurrent stop() call finish. - pollingDonePromise.resolve(); - } - private createAndCacheDataSource( serviceDef: ServiceEndpointDefinition, ): GraphQLDataSource { @@ -1116,36 +866,6 @@ export class ApolloGateway implements GraphQLService { } } - protected async loadServiceDefinitions(): Promise { - const canUseManagedConfig = - this.apolloConfig?.graphRef && this.apolloConfig?.keyHash; - if (!canUseManagedConfig) { - throw new Error( - 'When a manual configuration is not provided, gateway requires an Apollo ' + - 'configuration. See https://www.apollographql.com/docs/apollo-server/federation/managed-federation/ ' + - 'for more information. Manual configuration options include: ' + - '`serviceList`, `supergraphSdl`, and `experimental_updateServiceDefinitions`.', - ); - } - - const result = await loadSupergraphSdlFromUplinks({ - graphRef: this.apolloConfig!.graphRef!, - apiKey: this.apolloConfig!.key!, - endpoints: this.uplinkEndpoints!, - errorReportingEndpoint: this.errorReportingEndpoint, - fetcher: this.fetcher, - compositionId: this.compositionId ?? null, - maxRetries: this.uplinkMaxRetries!, - }); - - return ( - result ?? { - id: this.compositionId!, - supergraphSdl: this.supergraphSdl!, - } - ); - } - private maybeWarnOnConflictingConfig() { const canUseManagedConfig = this.apolloConfig?.graphRef && this.apolloConfig?.keyHash; @@ -1362,7 +1082,7 @@ export class ApolloGateway implements GraphQLService { }); } - private async cleanupUserFunctions() { + private async performCleanup() { if (this.toDispose.length === 0) return; await Promise.all( @@ -1378,9 +1098,8 @@ export class ApolloGateway implements GraphQLService { this.toDispose = []; } - // Stops all processes involved with the gateway (for now, just background - // schema polling). Can be called multiple times safely. Once it (async) - // returns, all gateway background activity will be finished. + // Stops all processes involved with the gateway. Can be called multiple times + // safely. Once it (async) returns, all gateway background activity will be finished. public async stop() { switch (this.state.phase) { case 'initialized': @@ -1403,7 +1122,7 @@ export class ApolloGateway implements GraphQLService { } return; case 'loaded': - const stoppingDonePromise = this.cleanupUserFunctions(); + const stoppingDonePromise = this.performCleanup(); this.state = { phase: 'stopping', stoppingDonePromise, @@ -1411,32 +1130,6 @@ export class ApolloGateway implements GraphQLService { await stoppingDonePromise; this.state = { phase: 'stopped' }; return; - case 'waiting to poll': { - // If we're waiting to poll, we can synchronously transition to fully stopped. - // We will terminate the current pollServices call and it will succeed quickly. - const doneWaiting = this.state.doneWaiting; - clearTimeout(this.state.pollWaitTimer); - this.state = { phase: 'stopped' }; - doneWaiting(); - return; - } - case 'polling': { - // We're in the middle of running updateSchema. We need to go into 'stopping' - // mode and let this run complete. First we set things up so that any concurrent - // calls to stop() will wait until we let them finish. (Those concurrent calls shouldn't - // just wait on pollingDonePromise themselves because we want to make sure we fully - // transition to state='stopped' before the other call returns.) - const pollingDonePromise = this.state.pollingDonePromise; - const stoppingDonePromise = resolvable(); - this.state = { - phase: 'stopping', - stoppingDonePromise, - }; - await pollingDonePromise; - this.state = { phase: 'stopped' }; - stoppingDonePromise.resolve(); - return; - } case 'updating schema': { // This should never happen throw Error( From 143f03a2f20ae87acf7bb75c4aa1f7ddfe835bcf Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 6 Jan 2022 15:53:33 -0800 Subject: [PATCH 43/82] Remove composition from gateway, more cleanup, local compose in a separate file --- docs/source/api/apollo-gateway.mdx | 4 +- gateway-js/src/IntrospectAndCompose/index.ts | 4 +- gateway-js/src/LegacyFetcher/index.ts | 20 ++- gateway-js/src/LocalCompose/index.ts | 122 +++++++++++++++ gateway-js/src/UplinkFetcher/index.ts | 4 +- .../__tests__/gateway/opentelemetry.test.ts | 11 +- .../__tests__/gateway/queryPlanCache.test.ts | 34 ++-- .../src/__tests__/integration/aliases.test.ts | 12 +- .../integration/configuration.test.ts | 26 +++- gateway-js/src/config.ts | 4 +- gateway-js/src/index.ts | 147 ++++-------------- 11 files changed, 239 insertions(+), 149 deletions(-) create mode 100644 gateway-js/src/LocalCompose/index.ts diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index a9d1395b18..9e900cf90b 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -100,7 +100,7 @@ const gateway = new ApolloGateway({ ###### `supergraphSdl` -`string | SupergraphSdlHook | SupergraphSdlObject` +`string | SupergraphSdlHook | SupergraphSdlManager` @@ -111,7 +111,7 @@ When `supergraphSdl` is a `SupergraphSdlHook`: An `async` function that returns 2. `healthCheck`: A function which issues a health check against the subgraphs 3. `getDataSource`: A function which gets a data source for a particular subgraph from the gateway -When `supergraphSdl` is a `SupergraphSdlObject`: An object containing an `initialize` property. `initialize` is an `async` function of the `SupergraphSdlHook` type mentioned directly above. +When `supergraphSdl` is a `SupergraphSdlManager`: An object containing an `initialize` property. `initialize` is an `async` function of the `SupergraphSdlHook` type mentioned directly above. **If you are using managed federation,** do not provide this field. diff --git a/gateway-js/src/IntrospectAndCompose/index.ts b/gateway-js/src/IntrospectAndCompose/index.ts index e8a60e4dd6..7148dd2482 100644 --- a/gateway-js/src/IntrospectAndCompose/index.ts +++ b/gateway-js/src/IntrospectAndCompose/index.ts @@ -15,7 +15,7 @@ import { getServiceDefinitionsFromRemoteEndpoint, Service, } from './loadServicesFromRemoteEndpoint'; -import { SupergraphSdlObject, SupergraphSdlHookOptions } from '../config'; +import { SupergraphSdlManager, SupergraphSdlHookOptions } from '../config'; export interface IntrospectAndComposeOptions { subgraphs: ServiceEndpointDefinition[]; @@ -34,7 +34,7 @@ type State = | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; -export class IntrospectAndCompose implements SupergraphSdlObject { +export class IntrospectAndCompose implements SupergraphSdlManager { private config: IntrospectAndComposeOptions; private update?: SupergraphSdlUpdateFunction; private healthCheck?: SubgraphHealthCheckFunction; diff --git a/gateway-js/src/LegacyFetcher/index.ts b/gateway-js/src/LegacyFetcher/index.ts index 5fa4a06836..269ecaf97d 100644 --- a/gateway-js/src/LegacyFetcher/index.ts +++ b/gateway-js/src/LegacyFetcher/index.ts @@ -1,7 +1,8 @@ +// TODO(trevor:removeServiceList) the whole file goes away import { Logger } from 'apollo-server-types'; import resolvable from '@josephg/resolvable'; import { - SupergraphSdlObject, + SupergraphSdlManager, SupergraphSdlHookOptions, DynamicGatewayConfig, isSupergraphSdlUpdate, @@ -32,7 +33,7 @@ type State = | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; -export class LegacyFetcher implements SupergraphSdlObject { +export class LegacyFetcher implements SupergraphSdlManager { private config: LegacyFetcherOptions; private update?: SupergraphSdlUpdateFunction; private healthCheck?: SubgraphHealthCheckFunction; @@ -46,6 +47,21 @@ export class LegacyFetcher implements SupergraphSdlObject { constructor(options: LegacyFetcherOptions) { this.config = options; this.state = { phase: 'initialized' }; + this.issueDeprecationWarnings(); + } + + private issueDeprecationWarnings() { + if ('experimental_updateSupergraphSdl' in this.config.gatewayConfig) { + this.config.logger?.warn( + 'The `experimental_updateSupergraphSdl` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + } + + if ('experimental_updateServiceDefinitions' in this.config.gatewayConfig) { + this.config.logger?.warn( + 'The `experimental_updateServiceDefinitions` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + } } public async initialize({ diff --git a/gateway-js/src/LocalCompose/index.ts b/gateway-js/src/LocalCompose/index.ts new file mode 100644 index 0000000000..cd6ad79be7 --- /dev/null +++ b/gateway-js/src/LocalCompose/index.ts @@ -0,0 +1,122 @@ +// TODO(trevor:removeServiceList) the whole file goes away +import { Logger } from 'apollo-server-types'; +import { + GetDataSourceFunction, + SupergraphSdlHookOptions, + SupergraphSdlManager, +} from '../config'; +import { + composeAndValidate, + compositionHasErrors, + ServiceDefinition, +} from '@apollo/federation'; +import { + GraphQLSchema, + isIntrospectionType, + isObjectType, + parse, +} from 'graphql'; +import { buildComposedSchema } from '@apollo/query-planner'; +import { defaultFieldResolverWithAliasSupport } from '../executeQueryPlan'; + +export interface LocalComposeOptions { + logger?: Logger; + localServiceList: ServiceDefinition[]; +} + +export class LocalCompose implements SupergraphSdlManager { + private config: LocalComposeOptions; + private getDataSource?: GetDataSourceFunction; + + constructor(options: LocalComposeOptions) { + this.config = options; + this.issueDeprecationWarnings(); + } + + private issueDeprecationWarnings() { + this.config.logger?.warn( + 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + } + + public async initialize({ getDataSource }: SupergraphSdlHookOptions) { + this.getDataSource = getDataSource; + let supergraphSdl: string | null = null; + try { + ({ supergraphSdl } = this.createSchemaFromServiceList( + this.config.localServiceList, + )); + } catch (e) { + this.logUpdateFailure(e); + throw e; + } + return { + supergraphSdl, + }; + } + + private createSchemaFromServiceList(serviceList: ServiceDefinition[]) { + this.config.logger?.debug( + `Composing schema from service list: \n${serviceList + .map(({ name, url }) => ` ${url || 'local'}: ${name}`) + .join('\n')}`, + ); + + const compositionResult = composeAndValidate(serviceList); + + if (compositionHasErrors(compositionResult)) { + const { errors } = compositionResult; + throw Error( + "A valid schema couldn't be composed. The following composition errors were found:\n" + + errors.map((e) => '\t' + e.message).join('\n'), + ); + } else { + const { supergraphSdl } = compositionResult; + for (const service of serviceList) { + this.getDataSource?.(service); + } + + const schema = buildComposedSchema(parse(supergraphSdl)); + + this.config.logger?.debug('Schema loaded and ready for execution'); + + // This is a workaround for automatic wrapping of all fields, which Apollo + // Server does in the case of implementing resolver wrapping for plugins. + // Here we wrap all fields with support for resolving aliases as part of the + // root value which happens because aliases are resolved by sub services and + // the shape of the root value already contains the aliased fields as + // responseNames + return { + schema: wrapSchemaWithAliasResolver(schema), + supergraphSdl, + }; + } + } + + private logUpdateFailure(e: any) { + this.config.logger?.error( + 'UplinkFetcher failed to update supergraph with the following error: ' + + (e.message ?? e), + ); + } +} + +// We can't use transformSchema here because the extension data for query +// planning would be lost. Instead we set a resolver for each field +// in order to counteract GraphQLExtensions preventing a defaultFieldResolver +// from doing the same job +function wrapSchemaWithAliasResolver(schema: GraphQLSchema): GraphQLSchema { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach((typeName) => { + const type = typeMap[typeName]; + + if (isObjectType(type) && !isIntrospectionType(type)) { + const fields = type.getFields(); + Object.keys(fields).forEach((fieldName) => { + const field = fields[fieldName]; + field.resolve = defaultFieldResolverWithAliasSupport; + }); + } + }); + return schema; +} diff --git a/gateway-js/src/UplinkFetcher/index.ts b/gateway-js/src/UplinkFetcher/index.ts index 314243010c..085157748c 100644 --- a/gateway-js/src/UplinkFetcher/index.ts +++ b/gateway-js/src/UplinkFetcher/index.ts @@ -1,7 +1,7 @@ import { fetch } from 'apollo-server-env'; import { Logger } from 'apollo-server-types'; import resolvable from '@josephg/resolvable'; -import { SupergraphSdlObject, SupergraphSdlHookOptions } from '../config'; +import { SupergraphSdlManager, SupergraphSdlHookOptions } from '../config'; import { SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction } from '..'; import { loadSupergraphSdlFromUplinks } from './loadSupergraphSdlFromStorage'; @@ -21,7 +21,7 @@ type State = | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; -export class UplinkFetcher implements SupergraphSdlObject { +export class UplinkFetcher implements SupergraphSdlManager { private config: UplinkFetcherOptions; private update?: SupergraphSdlUpdateFunction; private healthCheck?: SubgraphHealthCheckFunction; diff --git a/gateway-js/src/__tests__/gateway/opentelemetry.test.ts b/gateway-js/src/__tests__/gateway/opentelemetry.test.ts index d1e40dcf76..bdca057f5b 100644 --- a/gateway-js/src/__tests__/gateway/opentelemetry.test.ts +++ b/gateway-js/src/__tests__/gateway/opentelemetry.test.ts @@ -36,11 +36,16 @@ describe('opentelemetry', () => { describe('with local data', () => { async function gateway() { + const localDataSources = Object.fromEntries( + fixtures.map((f) => [ + f.name, + new LocalGraphQLDataSource(buildSubgraphSchema(f)), + ]), + ); const gateway = new ApolloGateway({ localServiceList: fixtures, - buildService: service => { - // @ts-ignore - return new LocalGraphQLDataSource(buildSubgraphSchema([service])); + buildService(service) { + return localDataSources[service.name]; }, }); diff --git a/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts b/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts index b8ef499d3c..f0ef311591 100644 --- a/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts +++ b/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts @@ -6,14 +6,20 @@ import { LocalGraphQLDataSource } from '../../datasources/LocalGraphQLDataSource import { ApolloGateway } from '../../'; import { fixtures } from 'apollo-federation-integration-testsuite'; import { QueryPlanner } from '@apollo/query-planner'; + it('caches the query plan for a request', async () => { const buildQueryPlanSpy = jest.spyOn(QueryPlanner.prototype, 'buildQueryPlan'); + const localDataSources = Object.fromEntries( + fixtures.map((f) => [ + f.name, + new LocalGraphQLDataSource(buildSubgraphSchema(f)), + ]), + ); const gateway = new ApolloGateway({ localServiceList: fixtures, - buildService: service => { - // @ts-ignore - return new LocalGraphQLDataSource(buildSubgraphSchema([service])); + buildService(service) { + return localDataSources[service.name]; }, }); @@ -65,11 +71,17 @@ it('supports multiple operations and operationName', async () => { } `; + const localDataSources = Object.fromEntries( + fixtures.map((f) => [ + f.name, + new LocalGraphQLDataSource(buildSubgraphSchema(f)), + ]), + ); + const gateway = new ApolloGateway({ localServiceList: fixtures, - buildService: service => { - // @ts-ignore - return new LocalGraphQLDataSource(buildSubgraphSchema([service])); + buildService(service) { + return localDataSources[service.name]; }, }); @@ -170,11 +182,15 @@ it('does not corrupt cached queryplan data across requests', async () => { }, }; + const dataSources: Record = { + a: new LocalGraphQLDataSource(buildSubgraphSchema(serviceA)), + b: new LocalGraphQLDataSource(buildSubgraphSchema(serviceB)), + }; + const gateway = new ApolloGateway({ localServiceList: [serviceA, serviceB], - buildService: service => { - // @ts-ignore - return new LocalGraphQLDataSource(buildSubgraphSchema([service])); + buildService(service) { + return dataSources[service.name]; }, }); diff --git a/gateway-js/src/__tests__/integration/aliases.test.ts b/gateway-js/src/__tests__/integration/aliases.test.ts index 354d61f8a1..956c527f1f 100644 --- a/gateway-js/src/__tests__/integration/aliases.test.ts +++ b/gateway-js/src/__tests__/integration/aliases.test.ts @@ -141,11 +141,17 @@ it('supports aliases of nested fields on subservices', async () => { // TODO after we remove GraphQLExtensions from ApolloServer, this can go away it('supports aliases when using ApolloServer', async () => { + const localDataSources = Object.fromEntries( + fixtures.map((f) => [ + f.name, + new LocalGraphQLDataSource(buildSubgraphSchema(f)), + ]), + ); + const gateway = new ApolloGateway({ localServiceList: fixtures, - buildService: service => { - // @ts-ignore - return new LocalGraphQLDataSource(buildSubgraphSchema([service])); + buildService(service) { + return localDataSources[service.name]; }, }); diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 6dccd3de0a..a29d1113dc 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -363,8 +363,8 @@ describe('gateway config / env behavior', () => { }); describe('deprecation warnings', () => { - it('warns with `experimental_updateSupergraphSdl` option set', async () => { - new ApolloGateway({ + fit('warns with `experimental_updateSupergraphSdl` option set', async () => { + const gateway = new ApolloGateway({ async experimental_updateSupergraphSdl() { return { id: 'supergraph', @@ -374,13 +374,17 @@ describe('deprecation warnings', () => { logger, }); + await gateway.load(); + expect(logger.warn).toHaveBeenCalledWith( 'The `experimental_updateSupergraphSdl` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); + + await gateway.stop(); }); it('warns with `experimental_updateServiceDefinitions` option set', async () => { - new ApolloGateway({ + const gateway = new ApolloGateway({ async experimental_updateServiceDefinitions() { return { isNewSchema: false, @@ -389,30 +393,42 @@ describe('deprecation warnings', () => { logger, }); + await gateway.load(); + expect(logger.warn).toHaveBeenCalledWith( 'The `experimental_updateServiceDefinitions` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); + + await gateway.stop(); }); it('warns with `serviceList` option set', async () => { - new ApolloGateway({ + const gateway = new ApolloGateway({ serviceList: [{ name: 'accounts', url: 'http://localhost:4001' }], logger, }); + await gateway.load(); + expect(logger.warn).toHaveBeenCalledWith( 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); + + await gateway.stop(); }); it('warns with `localServiceList` option set', async () => { - new ApolloGateway({ + const gateway = new ApolloGateway({ localServiceList: fixtures, logger, }); + await gateway.load(); + expect(logger.warn).toHaveBeenCalledWith( 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); + + await gateway.stop(); }); }); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 5b0620042a..dded48f1e0 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -200,12 +200,12 @@ export interface SupergraphSdlHook { }>; } -export interface SupergraphSdlObject { +export interface SupergraphSdlManager { initialize: SupergraphSdlHook } export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { - supergraphSdl: SupergraphSdlHook | SupergraphSdlObject; + supergraphSdl: SupergraphSdlHook | SupergraphSdlManager; } type ManuallyManagedGatewayConfig = diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index b6efb0d57e..897437c2c0 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -16,11 +16,7 @@ import { parse, Source, } from 'graphql'; -import { - composeAndValidate, - compositionHasErrors, - ServiceDefinition, -} from '@apollo/federation'; +import { ServiceDefinition } from '@apollo/federation'; import loglevel from 'loglevel'; import { buildOperationContext, OperationContext } from './operationContext'; import { @@ -51,18 +47,16 @@ import { Experimental_UpdateComposition, CompositionInfo, GatewayConfig, - StaticGatewayConfig, isManuallyManagedConfig, isLocalConfig, isServiceListConfig, isManagedConfig, isDynamicConfig, - isStaticConfig, - CompositionMetadata, SupergraphSdlUpdate, - SupergraphSdlHook, isManuallyManagedSupergraphSdlGatewayConfig, ManagedGatewayConfig, + isStaticSupergraphSdlConfig, + SupergraphSdlManager, } from './config'; import { buildComposedSchema } from '@apollo/query-planner'; import { SpanStatusCode } from '@opentelemetry/api'; @@ -73,6 +67,7 @@ import { createHash } from './utilities/createHash'; import { IntrospectAndCompose } from './IntrospectAndCompose'; import { UplinkFetcher } from './UplinkFetcher'; import { LegacyFetcher } from './LegacyFetcher'; +import { LocalCompose } from './LocalCompose'; type DataSourceMap = { [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; @@ -166,7 +161,6 @@ export class ApolloGateway implements GraphQLService { coreSupergraphSdl: string; }) => void >(); - private compositionMetadata?: CompositionMetadata; private warnedStates: WarnedStates = Object.create(null); private queryPlanner?: QueryPlanner; private supergraphSdl?: string; @@ -179,10 +173,6 @@ export class ApolloGateway implements GraphQLService { // The information made available here will give insight into the resulting // query plan and the inputs that generated it. private experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; - // Observe composition failures and the ServiceList that caused them. This - // enables reporting any issues that occur during composition. Implementors - // will be interested in addressing these immediately. - private experimental_didFailComposition?: Experimental_DidFailCompositionCallback; // Used to communicated composition changes, and what definitions caused // those updates private experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; @@ -209,14 +199,11 @@ export class ApolloGateway implements GraphQLService { // set up experimental observability callbacks and config settings this.experimental_didResolveQueryPlan = config?.experimental_didResolveQueryPlan; - this.experimental_didFailComposition = - config?.experimental_didFailComposition; this.experimental_didUpdateComposition = config?.experimental_didUpdateComposition; this.pollIntervalInMs = config?.experimental_pollInterval; - this.issueDeprecationWarningsIfApplicable(); if (isDynamicConfig(this.config)) { this.issueDynamicWarningsIfApplicable(); } @@ -297,36 +284,6 @@ export class ApolloGateway implements GraphQLService { } } - private issueDeprecationWarningsIfApplicable() { - // TODO(trevor:removeServiceList) - if ('experimental_updateSupergraphSdl' in this.config) { - this.logger.warn( - 'The `experimental_updateSupergraphSdl` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', - ); - } - - // TODO(trevor:removeServiceList) - if ('experimental_updateServiceDefinitions' in this.config) { - this.logger.warn( - 'The `experimental_updateServiceDefinitions` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', - ); - } - - // TODO(trevor:removeServiceList) - if (isServiceListConfig(this.config)) { - this.logger.warn( - 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', - ); - } - - // TODO(trevor:removeServiceList) - if (isLocalConfig(this.config)) { - this.logger.warn( - 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', - ); - } - } - public async load(options?: { apollo?: ApolloConfigFromAS2Or3; engine?: GraphQLServiceEngineConfig; @@ -361,19 +318,36 @@ export class ApolloGateway implements GraphQLService { this.maybeWarnOnConflictingConfig(); // Handles initial assignment of `this.schema`, `this.queryPlanner` - if (isStaticConfig(this.config)) { - this.loadStatic(this.config); + if (isStaticSupergraphSdlConfig(this.config)) { + const supergraphSdl = this.config.supergraphSdl; + await this.initializeSupergraphSdlManager({ + initialize: async () => { + return { + supergraphSdl, + }; + }, + }); + } else if (isLocalConfig(this.config)) { + // TODO(trevor:removeServiceList) + await this.initializeSupergraphSdlManager(new LocalCompose({ + localServiceList: this.config.localServiceList, + logger: this.logger, + })); } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { const supergraphSdlFetcher = typeof this.config.supergraphSdl === 'object' ? this.config.supergraphSdl : { initialize: this.config.supergraphSdl }; - await this.loadSupergraphSdl(supergraphSdlFetcher); + await this.initializeSupergraphSdlManager(supergraphSdlFetcher); } else if ( isServiceListConfig(this.config) && // this setting is currently expected to override `serviceList` when they both exist !('experimental_updateServiceDefinitions' in this.config) ) { - await this.loadSupergraphSdl( + // TODO(trevor:removeServiceList) + this.logger.warn( + 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + ); + await this.initializeSupergraphSdlManager( new IntrospectAndCompose({ subgraphs: this.config.serviceList, pollIntervalInMs: this.config.experimental_pollInterval, @@ -395,7 +369,7 @@ export class ApolloGateway implements GraphQLService { } const uplinkEndpoints = this.getUplinkEndpoints(this.config); - await this.loadSupergraphSdl( + await this.initializeSupergraphSdlManager( new UplinkFetcher({ graphRef: this.apolloConfig!.graphRef!, apiKey: this.apolloConfig!.key!, @@ -422,7 +396,7 @@ export class ApolloGateway implements GraphQLService { subgraphHealthCheck: this.config.serviceHealthCheck, }); - await this.loadSupergraphSdl(legacyFetcher); + await this.initializeSupergraphSdlManager(legacyFetcher); } const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; @@ -457,31 +431,11 @@ export class ApolloGateway implements GraphQLService { ]; } - // Synchronously load a statically configured schema, update class instance's - // schema and query planner. - private loadStatic(config: StaticGatewayConfig) { - let schema: GraphQLSchema; - let supergraphSdl: string; - try { - ({ schema, supergraphSdl } = isLocalConfig(config) - ? this.createSchemaFromServiceList(config.localServiceList) - : this.createSchemaFromSupergraphSdl(config.supergraphSdl)); - // TODO(trevor): #580 redundant parse - this.parsedSupergraphSdl = parse(supergraphSdl); - this.supergraphSdl = supergraphSdl; - this.updateWithSchemaAndNotify(schema, supergraphSdl, true); - } catch (e) { - this.state = { phase: 'failed to load' }; - throw e; - } - this.state = { phase: 'loaded' }; - } - private getIdForSupergraphSdl(supergraphSdl: string) { return createHash('sha256').update(supergraphSdl).digest('hex'); } - private async loadSupergraphSdl( + private async initializeSupergraphSdlManager( supergraphSdlFetcher: T, ) { try { @@ -716,51 +670,6 @@ export class ApolloGateway implements GraphQLService { ); } - private createSchemaFromServiceList(serviceList: ServiceDefinition[]) { - this.logger.debug( - `Composing schema from service list: \n${serviceList - .map(({ name, url }) => ` ${url || 'local'}: ${name}`) - .join('\n')}`, - ); - - const compositionResult = composeAndValidate(serviceList); - - if (compositionHasErrors(compositionResult)) { - const { errors } = compositionResult; - if (this.experimental_didFailComposition) { - this.experimental_didFailComposition({ - errors, - serviceList, - ...(this.compositionMetadata && { - compositionMetadata: this.compositionMetadata, - }), - }); - } - throw Error( - "A valid schema couldn't be composed. The following composition errors were found:\n" + - errors.map((e) => '\t' + e.message).join('\n'), - ); - } else { - const { supergraphSdl } = compositionResult; - this.createServices(serviceList); - - const schema = buildComposedSchema(parse(supergraphSdl)); - - this.logger.debug('Schema loaded and ready for execution'); - - // This is a workaround for automatic wrapping of all fields, which Apollo - // Server does in the case of implementing resolver wrapping for plugins. - // Here we wrap all fields with support for resolving aliases as part of the - // root value which happens because aliases are resolved by sub services and - // the shape of the root value already contains the aliased fields as - // responseNames - return { - schema: wrapSchemaWithAliasResolver(schema), - supergraphSdl, - }; - } - } - private serviceListFromSupergraphSdl( supergraphSdl: DocumentNode, ): Omit[] { From c13919861b88f5dd4a27b552575cfc88c4c4660e Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 6 Jan 2022 16:12:36 -0800 Subject: [PATCH 44/82] cleanup, move things around --- docs/source/api/apollo-gateway.mdx | 4 +-- .../__tests__/tsconfig.json | 8 ------ gateway-js/src/config.ts | 4 +-- gateway-js/src/index.ts | 26 ++++++++++--------- .../__tests__/IntrospectAndCompose.test.ts | 2 +- .../loadServicesFromRemoteEndpoint.test.ts | 0 .../__tests__/tsconfig.json | 8 ++++++ .../IntrospectAndCompose/index.ts | 6 ++--- .../loadServicesFromRemoteEndpoint.ts | 6 ++--- .../LegacyFetcher/index.ts | 10 +++---- .../LocalCompose/index.ts | 14 +++++----- .../loadSupergraphSdlFromStorage.test.ts | 8 +++--- .../UplinkFetcher/__tests__/tsconfig.json | 8 ++++++ .../UplinkFetcher/index.ts | 6 ++--- .../loadSupergraphSdlFromStorage.ts | 4 +-- .../UplinkFetcher/outOfBandReporter.ts | 2 +- gateway-js/src/supergraphManagers/index.ts | 4 +++ 17 files changed, 67 insertions(+), 53 deletions(-) delete mode 100644 gateway-js/src/IntrospectAndCompose/__tests__/tsconfig.json rename gateway-js/src/{ => supergraphManagers}/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts (99%) rename gateway-js/src/{ => supergraphManagers}/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts (100%) create mode 100644 gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/tsconfig.json rename gateway-js/src/{ => supergraphManagers}/IntrospectAndCompose/index.ts (97%) rename gateway-js/src/{ => supergraphManagers}/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts (96%) rename gateway-js/src/{ => supergraphManagers}/LegacyFetcher/index.ts (97%) rename gateway-js/src/{ => supergraphManagers}/LocalCompose/index.ts (95%) rename gateway-js/src/{ => supergraphManagers}/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts (97%) create mode 100644 gateway-js/src/supergraphManagers/UplinkFetcher/__tests__/tsconfig.json rename gateway-js/src/{ => supergraphManagers}/UplinkFetcher/index.ts (96%) rename gateway-js/src/{ => supergraphManagers}/UplinkFetcher/loadSupergraphSdlFromStorage.ts (97%) rename gateway-js/src/{ => supergraphManagers}/UplinkFetcher/outOfBandReporter.ts (98%) create mode 100644 gateway-js/src/supergraphManagers/index.ts diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 9e900cf90b..6d53cbb3fa 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -100,7 +100,7 @@ const gateway = new ApolloGateway({ ###### `supergraphSdl` -`string | SupergraphSdlHook | SupergraphSdlManager` +`string | SupergraphSdlHook | SupergraphManager` @@ -111,7 +111,7 @@ When `supergraphSdl` is a `SupergraphSdlHook`: An `async` function that returns 2. `healthCheck`: A function which issues a health check against the subgraphs 3. `getDataSource`: A function which gets a data source for a particular subgraph from the gateway -When `supergraphSdl` is a `SupergraphSdlManager`: An object containing an `initialize` property. `initialize` is an `async` function of the `SupergraphSdlHook` type mentioned directly above. +When `supergraphSdl` is a `SupergraphManager`: An object containing an `initialize` property. `initialize` is an `async` function of the `SupergraphSdlHook` type mentioned directly above. **If you are using managed federation,** do not provide this field. diff --git a/gateway-js/src/IntrospectAndCompose/__tests__/tsconfig.json b/gateway-js/src/IntrospectAndCompose/__tests__/tsconfig.json deleted file mode 100644 index 0a2bbf99d9..0000000000 --- a/gateway-js/src/IntrospectAndCompose/__tests__/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.test", - "include": ["**/*"], - "references": [ - { "path": "../../../" }, - { "path": "../../../../federation-integration-testsuite-js" }, - ] -} diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index dded48f1e0..795d6b6702 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -200,12 +200,12 @@ export interface SupergraphSdlHook { }>; } -export interface SupergraphSdlManager { +export interface SupergraphManager { initialize: SupergraphSdlHook } export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { - supergraphSdl: SupergraphSdlHook | SupergraphSdlManager; + supergraphSdl: SupergraphSdlHook | SupergraphManager; } type ManuallyManagedGatewayConfig = diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 897437c2c0..287587046b 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -56,7 +56,7 @@ import { isManuallyManagedSupergraphSdlGatewayConfig, ManagedGatewayConfig, isStaticSupergraphSdlConfig, - SupergraphSdlManager, + SupergraphManager, } from './config'; import { buildComposedSchema } from '@apollo/query-planner'; import { SpanStatusCode } from '@opentelemetry/api'; @@ -64,10 +64,12 @@ import { OpenTelemetrySpanNames, tracer } from './utilities/opentelemetry'; import { CoreSchema } from '@apollo/core-schema'; import { featureSupport } from './core'; import { createHash } from './utilities/createHash'; -import { IntrospectAndCompose } from './IntrospectAndCompose'; -import { UplinkFetcher } from './UplinkFetcher'; -import { LegacyFetcher } from './LegacyFetcher'; -import { LocalCompose } from './LocalCompose'; +import { + IntrospectAndCompose, + UplinkFetcher, + LegacyFetcher, + LocalCompose, +} from './supergraphManagers'; type DataSourceMap = { [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; @@ -320,7 +322,7 @@ export class ApolloGateway implements GraphQLService { // Handles initial assignment of `this.schema`, `this.queryPlanner` if (isStaticSupergraphSdlConfig(this.config)) { const supergraphSdl = this.config.supergraphSdl; - await this.initializeSupergraphSdlManager({ + await this.initializeSupergraphManager({ initialize: async () => { return { supergraphSdl, @@ -329,7 +331,7 @@ export class ApolloGateway implements GraphQLService { }); } else if (isLocalConfig(this.config)) { // TODO(trevor:removeServiceList) - await this.initializeSupergraphSdlManager(new LocalCompose({ + await this.initializeSupergraphManager(new LocalCompose({ localServiceList: this.config.localServiceList, logger: this.logger, })); @@ -337,7 +339,7 @@ export class ApolloGateway implements GraphQLService { const supergraphSdlFetcher = typeof this.config.supergraphSdl === 'object' ? this.config.supergraphSdl : { initialize: this.config.supergraphSdl }; - await this.initializeSupergraphSdlManager(supergraphSdlFetcher); + await this.initializeSupergraphManager(supergraphSdlFetcher); } else if ( isServiceListConfig(this.config) && // this setting is currently expected to override `serviceList` when they both exist @@ -347,7 +349,7 @@ export class ApolloGateway implements GraphQLService { this.logger.warn( 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); - await this.initializeSupergraphSdlManager( + await this.initializeSupergraphManager( new IntrospectAndCompose({ subgraphs: this.config.serviceList, pollIntervalInMs: this.config.experimental_pollInterval, @@ -369,7 +371,7 @@ export class ApolloGateway implements GraphQLService { } const uplinkEndpoints = this.getUplinkEndpoints(this.config); - await this.initializeSupergraphSdlManager( + await this.initializeSupergraphManager( new UplinkFetcher({ graphRef: this.apolloConfig!.graphRef!, apiKey: this.apolloConfig!.key!, @@ -396,7 +398,7 @@ export class ApolloGateway implements GraphQLService { subgraphHealthCheck: this.config.serviceHealthCheck, }); - await this.initializeSupergraphSdlManager(legacyFetcher); + await this.initializeSupergraphManager(legacyFetcher); } const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; @@ -435,7 +437,7 @@ export class ApolloGateway implements GraphQLService { return createHash('sha256').update(supergraphSdl).digest('hex'); } - private async initializeSupergraphSdlManager( + private async initializeSupergraphManager( supergraphSdlFetcher: T, ) { try { diff --git a/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts similarity index 99% rename from gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts rename to gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts index 906d423ba5..01e4eb1f24 100644 --- a/gateway-js/src/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts @@ -4,7 +4,7 @@ import { fixturesWithUpdate, } from 'apollo-federation-integration-testsuite'; import { nockBeforeEach, nockAfterEach } from '../../__tests__/nockAssertions'; -import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../..'; +import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../../..'; import { IntrospectAndCompose } from '..'; import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; import { getTestingSupergraphSdl, wait } from '../../__tests__/execution-utils'; diff --git a/gateway-js/src/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts similarity index 100% rename from gateway-js/src/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts rename to gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/tsconfig.json b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/tsconfig.json new file mode 100644 index 0000000000..12ae429d6b --- /dev/null +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.test", + "include": ["**/*"], + "references": [ + { "path": "../../../../" }, + { "path": "../../../../../federation-integration-testsuite-js" }, + ] +} diff --git a/gateway-js/src/IntrospectAndCompose/index.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts similarity index 97% rename from gateway-js/src/IntrospectAndCompose/index.ts rename to gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts index 7148dd2482..fb66de093e 100644 --- a/gateway-js/src/IntrospectAndCompose/index.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts @@ -10,12 +10,12 @@ import { ServiceEndpointDefinition, SupergraphSdlUpdateFunction, SubgraphHealthCheckFunction, -} from '..'; +} from '../..'; import { getServiceDefinitionsFromRemoteEndpoint, Service, } from './loadServicesFromRemoteEndpoint'; -import { SupergraphSdlManager, SupergraphSdlHookOptions } from '../config'; +import { SupergraphManager, SupergraphSdlHookOptions } from '../../config'; export interface IntrospectAndComposeOptions { subgraphs: ServiceEndpointDefinition[]; @@ -34,7 +34,7 @@ type State = | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; -export class IntrospectAndCompose implements SupergraphSdlManager { +export class IntrospectAndCompose implements SupergraphManager { private config: IntrospectAndComposeOptions; private update?: SupergraphSdlUpdateFunction; private healthCheck?: SubgraphHealthCheckFunction; diff --git a/gateway-js/src/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts similarity index 96% rename from gateway-js/src/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts rename to gateway-js/src/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts index 97a338cfbf..4caede3a02 100644 --- a/gateway-js/src/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts @@ -1,9 +1,9 @@ import { GraphQLRequest } from 'apollo-server-types'; import { parse } from 'graphql'; import { Headers, HeadersInit } from 'node-fetch'; -import { GraphQLDataSource, GraphQLDataSourceRequestKind } from '../datasources/types'; -import { SERVICE_DEFINITION_QUERY } from '../'; -import { ServiceDefinitionUpdate, ServiceEndpointDefinition } from '../config'; +import { GraphQLDataSource, GraphQLDataSourceRequestKind } from '../../datasources/types'; +import { SERVICE_DEFINITION_QUERY } from '../..'; +import { ServiceDefinitionUpdate, ServiceEndpointDefinition } from '../../config'; import { ServiceDefinition } from '@apollo/federation'; export type Service = ServiceEndpointDefinition & { diff --git a/gateway-js/src/LegacyFetcher/index.ts b/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts similarity index 97% rename from gateway-js/src/LegacyFetcher/index.ts rename to gateway-js/src/supergraphManagers/LegacyFetcher/index.ts index 269ecaf97d..2873aea85c 100644 --- a/gateway-js/src/LegacyFetcher/index.ts +++ b/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts @@ -2,23 +2,23 @@ import { Logger } from 'apollo-server-types'; import resolvable from '@josephg/resolvable'; import { - SupergraphSdlManager, + SupergraphManager, SupergraphSdlHookOptions, DynamicGatewayConfig, isSupergraphSdlUpdate, isServiceDefinitionUpdate, ServiceDefinitionUpdate, GetDataSourceFunction, -} from '../config'; +} from '../../config'; import { Experimental_UpdateComposition, SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction, -} from '..'; +} from '../..'; import { composeAndValidate, compositionHasErrors, ServiceDefinition } from '@apollo/federation'; import { GraphQLSchema, isIntrospectionType, isObjectType, parse } from 'graphql'; import { buildComposedSchema } from '@apollo/query-planner'; -import { defaultFieldResolverWithAliasSupport } from '../executeQueryPlan'; +import { defaultFieldResolverWithAliasSupport } from '../../executeQueryPlan'; export interface LegacyFetcherOptions { pollIntervalInMs?: number; @@ -33,7 +33,7 @@ type State = | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; -export class LegacyFetcher implements SupergraphSdlManager { +export class LegacyFetcher implements SupergraphManager { private config: LegacyFetcherOptions; private update?: SupergraphSdlUpdateFunction; private healthCheck?: SubgraphHealthCheckFunction; diff --git a/gateway-js/src/LocalCompose/index.ts b/gateway-js/src/supergraphManagers/LocalCompose/index.ts similarity index 95% rename from gateway-js/src/LocalCompose/index.ts rename to gateway-js/src/supergraphManagers/LocalCompose/index.ts index cd6ad79be7..1fb9b26293 100644 --- a/gateway-js/src/LocalCompose/index.ts +++ b/gateway-js/src/supergraphManagers/LocalCompose/index.ts @@ -1,10 +1,5 @@ // TODO(trevor:removeServiceList) the whole file goes away import { Logger } from 'apollo-server-types'; -import { - GetDataSourceFunction, - SupergraphSdlHookOptions, - SupergraphSdlManager, -} from '../config'; import { composeAndValidate, compositionHasErrors, @@ -17,14 +12,19 @@ import { parse, } from 'graphql'; import { buildComposedSchema } from '@apollo/query-planner'; -import { defaultFieldResolverWithAliasSupport } from '../executeQueryPlan'; +import { + GetDataSourceFunction, + SupergraphSdlHookOptions, + SupergraphManager, +} from '../../config'; +import { defaultFieldResolverWithAliasSupport } from '../../executeQueryPlan'; export interface LocalComposeOptions { logger?: Logger; localServiceList: ServiceDefinition[]; } -export class LocalCompose implements SupergraphSdlManager { +export class LocalCompose implements SupergraphManager { private config: LocalComposeOptions; private getDataSource?: GetDataSourceFunction; diff --git a/gateway-js/src/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts b/gateway-js/src/supergraphManagers/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts similarity index 97% rename from gateway-js/src/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts rename to gateway-js/src/supergraphManagers/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts index 481e886af3..77d4e27a2d 100644 --- a/gateway-js/src/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts +++ b/gateway-js/src/supergraphManagers/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts @@ -2,7 +2,7 @@ import { loadSupergraphSdlFromStorage, loadSupergraphSdlFromUplinks } from '../loadSupergraphSdlFromStorage'; -import { getDefaultFetcher } from '../..'; +import { getDefaultFetcher } from '../../..'; import { graphRef, apiKey, @@ -14,9 +14,9 @@ import { mockSupergraphSdlRequestSuccess, mockSupergraphSdlRequestIfAfterUnchanged, mockSupergraphSdlRequestIfAfter -} from '../../__tests__/integration/nockMocks'; -import { getTestingSupergraphSdl } from "../../__tests__/execution-utils"; -import { nockAfterEach, nockBeforeEach } from '../../__tests__/nockAssertions'; +} from '../../../__tests__/integration/nockMocks'; +import { getTestingSupergraphSdl } from "../../../__tests__/execution-utils"; +import { nockAfterEach, nockBeforeEach } from '../../../__tests__/nockAssertions'; describe('loadSupergraphSdlFromStorage', () => { beforeEach(nockBeforeEach); diff --git a/gateway-js/src/supergraphManagers/UplinkFetcher/__tests__/tsconfig.json b/gateway-js/src/supergraphManagers/UplinkFetcher/__tests__/tsconfig.json new file mode 100644 index 0000000000..12ae429d6b --- /dev/null +++ b/gateway-js/src/supergraphManagers/UplinkFetcher/__tests__/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.test", + "include": ["**/*"], + "references": [ + { "path": "../../../../" }, + { "path": "../../../../../federation-integration-testsuite-js" }, + ] +} diff --git a/gateway-js/src/UplinkFetcher/index.ts b/gateway-js/src/supergraphManagers/UplinkFetcher/index.ts similarity index 96% rename from gateway-js/src/UplinkFetcher/index.ts rename to gateway-js/src/supergraphManagers/UplinkFetcher/index.ts index 085157748c..f118fdd80b 100644 --- a/gateway-js/src/UplinkFetcher/index.ts +++ b/gateway-js/src/supergraphManagers/UplinkFetcher/index.ts @@ -1,8 +1,8 @@ import { fetch } from 'apollo-server-env'; import { Logger } from 'apollo-server-types'; import resolvable from '@josephg/resolvable'; -import { SupergraphSdlManager, SupergraphSdlHookOptions } from '../config'; -import { SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction } from '..'; +import { SupergraphManager, SupergraphSdlHookOptions } from '../../config'; +import { SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction } from '../..'; import { loadSupergraphSdlFromUplinks } from './loadSupergraphSdlFromStorage'; export interface UplinkFetcherOptions { @@ -21,7 +21,7 @@ type State = | { phase: 'polling'; pollingPromise?: Promise } | { phase: 'stopped' }; -export class UplinkFetcher implements SupergraphSdlManager { +export class UplinkFetcher implements SupergraphManager { private config: UplinkFetcherOptions; private update?: SupergraphSdlUpdateFunction; private healthCheck?: SubgraphHealthCheckFunction; diff --git a/gateway-js/src/UplinkFetcher/loadSupergraphSdlFromStorage.ts b/gateway-js/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts similarity index 97% rename from gateway-js/src/UplinkFetcher/loadSupergraphSdlFromStorage.ts rename to gateway-js/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts index a348cd8868..dca6a4c170 100644 --- a/gateway-js/src/UplinkFetcher/loadSupergraphSdlFromStorage.ts +++ b/gateway-js/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts @@ -1,8 +1,8 @@ import { fetch, Response, Request } from 'apollo-server-env'; import { GraphQLError } from 'graphql'; -import { SupergraphSdlUpdate } from '../config'; +import { SupergraphSdlUpdate } from '../../config'; import { submitOutOfBandReportIfConfigured } from './outOfBandReporter'; -import { SupergraphSdlQuery } from '../__generated__/graphqlTypes'; +import { SupergraphSdlQuery } from '../../__generated__/graphqlTypes'; // Magic /* GraphQL */ comment below is for codegen, do not remove export const SUPERGRAPH_SDL_QUERY = /* GraphQL */`#graphql diff --git a/gateway-js/src/UplinkFetcher/outOfBandReporter.ts b/gateway-js/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts similarity index 98% rename from gateway-js/src/UplinkFetcher/outOfBandReporter.ts rename to gateway-js/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts index 747cf9b320..7b822cc3dd 100644 --- a/gateway-js/src/UplinkFetcher/outOfBandReporter.ts +++ b/gateway-js/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts @@ -4,7 +4,7 @@ import { ErrorCode, OobReportMutation, OobReportMutationVariables, -} from '../__generated__/graphqlTypes'; +} from '../../__generated__/graphqlTypes'; // Magic /* GraphQL */ comment below is for codegen, do not remove export const OUT_OF_BAND_REPORTER_QUERY = /* GraphQL */`#graphql diff --git a/gateway-js/src/supergraphManagers/index.ts b/gateway-js/src/supergraphManagers/index.ts new file mode 100644 index 0000000000..da7ee9c16b --- /dev/null +++ b/gateway-js/src/supergraphManagers/index.ts @@ -0,0 +1,4 @@ +export { LocalCompose } from './LocalCompose'; +export { LegacyFetcher } from './LegacyFetcher'; +export { IntrospectAndCompose } from './IntrospectAndCompose'; +export { UplinkFetcher } from './UplinkFetcher'; From 484d182131b3d2f739099dd5fc161a35727892b7 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 6 Jan 2022 16:18:10 -0800 Subject: [PATCH 45/82] missed a few spots --- gateway-js/src/__tests__/integration/nockMocks.ts | 2 +- .../__tests__/IntrospectAndCompose.test.ts | 6 +++--- .../__tests__/loadServicesFromRemoteEndpoint.test.ts | 2 +- .../UplinkFetcher/loadSupergraphSdlFromStorage.ts | 2 +- .../supergraphManagers/UplinkFetcher/outOfBandReporter.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gateway-js/src/__tests__/integration/nockMocks.ts b/gateway-js/src/__tests__/integration/nockMocks.ts index 5a115de914..6c5c17dc04 100644 --- a/gateway-js/src/__tests__/integration/nockMocks.ts +++ b/gateway-js/src/__tests__/integration/nockMocks.ts @@ -1,6 +1,6 @@ import nock from 'nock'; import { HEALTH_CHECK_QUERY, SERVICE_DEFINITION_QUERY } from '../..'; -import { SUPERGRAPH_SDL_QUERY } from '../../UplinkFetcher/loadSupergraphSdlFromStorage'; +import { SUPERGRAPH_SDL_QUERY } from '../../supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage'; import { getTestingSupergraphSdl } from '../../__tests__/execution-utils'; import { print } from 'graphql'; import { Fixture, fixtures as testingFixtures } from 'apollo-federation-integration-testsuite'; diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts index 01e4eb1f24..e135c660fa 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts @@ -3,11 +3,11 @@ import { fixtures, fixturesWithUpdate, } from 'apollo-federation-integration-testsuite'; -import { nockBeforeEach, nockAfterEach } from '../../__tests__/nockAssertions'; +import { nockBeforeEach, nockAfterEach } from '../../../__tests__/nockAssertions'; import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../../..'; import { IntrospectAndCompose } from '..'; -import { mockAllServicesSdlQuerySuccess } from '../../__tests__/integration/nockMocks'; -import { getTestingSupergraphSdl, wait } from '../../__tests__/execution-utils'; +import { mockAllServicesSdlQuerySuccess } from '../../../__tests__/integration/nockMocks'; +import { getTestingSupergraphSdl, wait } from '../../../__tests__/execution-utils'; import resolvable from '@josephg/resolvable'; import { Logger } from 'apollo-server-types'; diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts index be5b3c80c3..00dadd6e58 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts @@ -1,5 +1,5 @@ import { getServiceDefinitionsFromRemoteEndpoint } from '../loadServicesFromRemoteEndpoint'; -import { RemoteGraphQLDataSource } from '../../datasources'; +import { RemoteGraphQLDataSource } from '../../../datasources'; describe('getServiceDefinitionsFromRemoteEndpoint', () => { it('errors when no URL was specified', async () => { diff --git a/gateway-js/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts b/gateway-js/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts index dca6a4c170..e12352ea7b 100644 --- a/gateway-js/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts +++ b/gateway-js/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts @@ -35,7 +35,7 @@ interface SupergraphSdlQueryFailure { errors: GraphQLError[]; } -const { name, version } = require('../../package.json'); +const { name, version } = require('../../../package.json'); const fetchErrorMsg = "An error occurred while fetching your schema from Apollo: "; diff --git a/gateway-js/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts b/gateway-js/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts index 7b822cc3dd..b751964642 100644 --- a/gateway-js/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts +++ b/gateway-js/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts @@ -13,7 +13,7 @@ export const OUT_OF_BAND_REPORTER_QUERY = /* GraphQL */`#graphql } `; -const { name, version } = require('../../package.json'); +const { name, version } = require('../../../package.json'); type OobReportMutationResult = | OobReportMutationSuccess From 29305ca81e10576a3482a5b7aea6c08e4d53cb26 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 6 Jan 2022 17:31:15 -0800 Subject: [PATCH 46/82] docs updates --- docs/source/api/apollo-gateway.mdx | 41 +++++++++++++++++------------- docs/source/gateway.mdx | 24 ++++++++--------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 6d53cbb3fa..63c9319764 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -104,14 +104,16 @@ const gateway = new ApolloGateway({ -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. +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 `SupergraphSdlHook`: An `async` function that returns an object containing a `supergraphSdl` string as well as a `cleanup` function. The hook accepts an object containing 3 properties: -1. `update`: A function which updates the supergraph schema -2. `healthCheck`: A function which issues a health check against the subgraphs -3. `getDataSource`: A function which gets a data source for a particular subgraph from the gateway +**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 `SupergraphManager`: An object containing an `initialize` property. `initialize` is an `async` function of the `SupergraphSdlHook` type mentioned directly above. +**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 mentioned directly above. **If you are using managed federation,** do not provide this field. @@ -150,12 +152,12 @@ You can specify any string value for the `name` field, which is used for identif ###### `introspectionHeaders` -**This option is deprecated in favor of the drop-in replacement, [`IntrospectAndCompose`](#class-introspectandcompose).** - `Object | (service: ServiceEndpointDefinition) => (Promise | Object)` +**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. @@ -569,13 +571,13 @@ The details of the `fetch` response sent by the subgraph. ## `class IntrospectAndCompose` -The drop-in replacement for `serviceList`. This class is meant to be passed directly to `ApolloGateway`'s `supergraphSdl` constructor option. +The drop-in replacement for `serviceList`. You pass an instance of this class directly to `ApolloGateway`'s `supergraphSdl` constructor option. ### Methods #### `constructor` -Returns an initialized `IntrospectAndCompose` instance, which you can then pass to the `supergraphSdl` configuration option to the `ApolloGateway` constructor, like so: +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({ @@ -611,7 +613,7 @@ const gateway = new ApolloGateway({ ###### Configuring the subgraph fetcher -`IntrospectAndCompose` will use the data sources constructed by `ApolloGateway`. In order to customize the gateway's data sources, you can proved a [`buildService`](#buildservice) function to the `ApolloGateway` constructor. In the example below, `IntrospectAndCompose` will make authenticated requests to the subgraphs +`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 @@ -651,8 +653,11 @@ const gateway = new ApolloGateway({ 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 which 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 also satisfy the following regex when publishing: `^[a-zA-Z][a-zA-Z0-9_-]{0,63}$`. +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 @@ -666,7 +671,7 @@ The `name` field is a string which should be treated as a subgraph's unique iden -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. +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. @@ -681,7 +686,7 @@ An object, or an (optionally) async function returning an object, containing the -Specify this option to enable supergraph updates via subgraph polling. `IntrospectAndCompose` will poll each subgraph at the given interval. +Specify this option to enable supergraph updates via subgraph polling. `IntrospectAndCompose` polls each subgraph at the given interval. @@ -694,10 +699,10 @@ Specify this option to enable supergraph updates via subgraph polling. `Introspe -> Note: this only applies to subgraphs that are configured for polling via the `pollIntervalInMs` option. -Perform 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 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`, there is no need to enable `serviceHealthCheck` on your `ApolloGateway` instance.** +**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.** diff --git a/docs/source/gateway.mdx b/docs/source/gateway.mdx index 1c2a2f5c5c..7d11d35510 100644 --- a/docs/source/gateway.mdx +++ b/docs/source/gateway.mdx @@ -59,7 +59,7 @@ On startup, the gateway processes your `supergraphSdl`, which includes routing i ### Updating the supergraph schema -In the above example, we provide a _static_ supergraph schema to the gateway. This approach requires the gateway to be restarted in order to update the supergraph schema. This is less than ideal for many applications, so we also provide the ability to update the supergraph schema dynamically. +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'); @@ -87,15 +87,15 @@ server.listen().then(({ url }) => { }); ``` -There are a few things happening here; let's take a look at each of them individually. +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 only called once when `ApolloServer` initializes the gateway, and has two primary responsibilities: -1. Receive the `update` function, which we'll use to update the supergraph schema. -2. Return the initial supergraph schema which the gateway will use at startup. +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, we'll assume `rover` is updating the `supergraphSdl.graphql` file. +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'); @@ -139,25 +139,25 @@ server.listen().then(({ url }) => { }); ``` -This example is a bit more fleshed out. Let's take a look at what we've added. +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 allows 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. +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. -You might've also noticed we're returning a `cleanup` function. This is a callback that is called when the gateway is stopped. This allows 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 will `await` it before shutting down. +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 -Your use case may grow to have some complexity which might be better managed within a class that handles the `update` and `healthCheck` functions as well as any additional state. In this case, you may instead provide an object (or class) with an `initialize` function. This function is called just the same as the `supergraphSdl` function discussed above. For a good example of this, check out the [`IntrospectAndCompose` source code](https://github.com/apollographql/federation/blob/main/packages/apollo-gateway/src/IntrospectAndCompose/index.ts). +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`? `IntrospectAndCompose` is the new drop-in replacement for the `serviceList` option. In the near future, the `serviceList` option will be removed; however, `IntrospectAndCompose` will continue to be supported. Apollo generally recommends approaches that utilize `rover` for managing local composition, though `IntrospectAndCompose` is still useful for various development and testing workflows. +> 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 `serviceList` array to the `ApolloGateway` constructor, like so: +Alternatively, you can provide a `subgraph` array to the `IntrospectAndCompose` constructor, like so: ```js:title=index.js const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway'); From e999f2a366aa0f7545de87aefd77f4a30a4ffbb9 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 6 Jan 2022 17:36:19 -0800 Subject: [PATCH 47/82] missed a docs suggestion --- docs/source/api/apollo-gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 63c9319764..d20bd69455 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -113,7 +113,7 @@ You provide your supergraph schema to the gateway with this option. You can prov - `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 mentioned directly above. +**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. From 64b8bd59e3acc094607f4c1344fdacaf580a9216 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 6 Jan 2022 17:39:12 -0800 Subject: [PATCH 48/82] update codegen config --- codegen.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codegen.yml b/codegen.yml index 0fd158d697..c28c1dcc61 100644 --- a/codegen.yml +++ b/codegen.yml @@ -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: From 2eaaab9cb9281a3cc6934e48cc53ec7a54d0f580 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:07:06 -0800 Subject: [PATCH 49/82] only need one Fixture def --- gateway-js/src/__tests__/integration/configuration.test.ts | 3 +-- .../src/__tests__/integration/networkRequests.test.ts | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index a29d1113dc..057c8ca28a 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -12,8 +12,7 @@ import { mockCloudConfigUrl3, } from './nockMocks'; import { getTestingSupergraphSdl } from '../execution-utils'; -import { Fixture } from './networkRequests.test'; -import { fixtures } from 'apollo-federation-integration-testsuite'; +import { fixtures, Fixture } from 'apollo-federation-integration-testsuite'; let logger: Logger; diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index 3020de3b61..ddb163a603 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -19,6 +19,7 @@ import { accounts, books, documents, + Fixture, fixturesWithUpdate, inventory, product, @@ -28,12 +29,6 @@ import { getTestingSupergraphSdl } from '../execution-utils'; import { nockAfterEach, nockBeforeEach } from '../nockAssertions'; import resolvable from '@josephg/resolvable'; -export interface Fixture { - name: string; - url: string; - typeDefs: DocumentNode; -} - const simpleService: Fixture = { name: 'accounts', url: 'http://localhost:4001', From 97b6561e217500c96f557381eda399848964a002 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:16:50 -0800 Subject: [PATCH 50/82] unfocus test and address failures --- .../__tests__/integration/configuration.test.ts | 16 +++++++++------- .../integration/networkRequests.test.ts | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 057c8ca28a..87abcd3725 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -362,7 +362,7 @@ describe('gateway config / env behavior', () => { }); describe('deprecation warnings', () => { - fit('warns with `experimental_updateSupergraphSdl` option set', async () => { + it('warns with `experimental_updateSupergraphSdl` option set', async () => { const gateway = new ApolloGateway({ async experimental_updateSupergraphSdl() { return { @@ -392,13 +392,14 @@ describe('deprecation warnings', () => { logger, }); - await gateway.load(); + try { + await gateway.load(); + // gateway will throw since we're not providing an actual service list, disregard + } catch {} expect(logger.warn).toHaveBeenCalledWith( 'The `experimental_updateServiceDefinitions` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); - - await gateway.stop(); }); it('warns with `serviceList` option set', async () => { @@ -407,13 +408,14 @@ describe('deprecation warnings', () => { logger, }); - await gateway.load(); + try { + await gateway.load(); + // gateway will throw since we haven't mocked these requests, unimportant for this test + } catch {} expect(logger.warn).toHaveBeenCalledWith( 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', ); - - await gateway.stop(); }); it('warns with `localServiceList` option set', async () => { diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index ddb163a603..77c8fb0d0a 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -1,5 +1,5 @@ import gql from 'graphql-tag'; -import { DocumentNode, GraphQLObjectType, GraphQLSchema } from 'graphql'; +import { GraphQLObjectType, GraphQLSchema } from 'graphql'; import mockedEnv from 'mocked-env'; import { Logger } from 'apollo-server-types'; import { ApolloGateway } from '../..'; From 556a4e176f8d4f7ce62a712d6255784e556e92f1 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:22:45 -0800 Subject: [PATCH 51/82] fix tsdoc --- gateway-js/src/config.ts | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 795d6b6702..68e60da012 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -135,9 +135,13 @@ interface GatewayConfigBase { // TODO(trevor:removeServiceList) export interface ServiceListGatewayConfig extends GatewayConfigBase { - // @deprecated: use `supergraphSdl` in its function form instead + /** + * @deprecated: use `supergraphSdl` in its function form instead + */ serviceList: ServiceEndpointDefinition[]; - // @deprecated: use `supergraphSdl` in its function form instead + /** + * @deprecated: use `supergraphSdl` in its function form instead + */ introspectionHeaders?: | HeadersInit | (( @@ -149,22 +153,28 @@ export interface ManagedGatewayConfig extends GatewayConfigBase { /** * This configuration option shouldn't be used unless by recommendation from * Apollo staff. + * + * @deprecated: use `uplinkEndpoints` instead */ - schemaConfigDeliveryEndpoint?: string; // deprecated + schemaConfigDeliveryEndpoint?: string; uplinkEndpoints?: string[]; uplinkMaxRetries?: number; } // TODO(trevor:removeServiceList): migrate users to `supergraphSdl` function option interface ManuallyManagedServiceDefsGatewayConfig extends GatewayConfigBase { - // @deprecated: use `supergraphSdl` in its function form instead + /** + * @deprecated: use `supergraphSdl` in its function form instead + */ experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions; } // TODO(trevor:removeServiceList): migrate users to `supergraphSdl` function option interface ExperimentalManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { - // @deprecated: use `supergraphSdl` in its function form instead + /** + * @deprecated: use `supergraphSdl` in its function form instead + */ experimental_updateSupergraphSdl: Experimental_UpdateSupergraphSdl; } @@ -179,9 +189,13 @@ export function isManuallyManagedSupergraphSdlGatewayConfig( ); } -export type SupergraphSdlUpdateFunction = (updatedSupergraphSdl: string) => void; +export type SupergraphSdlUpdateFunction = ( + updatedSupergraphSdl: string, +) => void; -export type SubgraphHealthCheckFunction = (supergraphSdl: string) => Promise; +export type SubgraphHealthCheckFunction = ( + supergraphSdl: string, +) => Promise; export type GetDataSourceFunction = ({ name, @@ -201,7 +215,7 @@ export interface SupergraphSdlHook { } export interface SupergraphManager { - initialize: SupergraphSdlHook + initialize: SupergraphSdlHook; } export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { @@ -217,7 +231,9 @@ type ManuallyManagedGatewayConfig = // TODO(trevor:removeServiceList) interface LocalGatewayConfig extends GatewayConfigBase { - // @deprecated: use `supergraphSdl` in its function form instead + /** + * @deprecated: use `supergraphSdl` in its function form instead + */ localServiceList: ServiceDefinition[]; } From 4666916ff4b304e7c3364762887dc1e66c422232 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:29:28 -0800 Subject: [PATCH 52/82] update and use predicates --- gateway-js/src/config.ts | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 68e60da012..8d834f0b84 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -181,12 +181,7 @@ interface ExperimentalManuallyManagedSupergraphSdlGatewayConfig export function isManuallyManagedSupergraphSdlGatewayConfig( config: GatewayConfig, ): config is ManuallyManagedSupergraphSdlGatewayConfig { - return ( - 'supergraphSdl' in config && - (typeof config.supergraphSdl === 'function' || - (typeof config.supergraphSdl === 'object' && - 'initialize' in config.supergraphSdl)) - ); + return isSupergraphSdlHookConfig(config) || isSupergraphManagerConfig(config); } export type SupergraphSdlUpdateFunction = ( @@ -218,8 +213,16 @@ export interface SupergraphManager { initialize: SupergraphSdlHook; } -export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { - supergraphSdl: SupergraphSdlHook | SupergraphManager; +type ManuallyManagedSupergraphSdlGatewayConfig = + | SupergraphSdlHookGatewayConfig + | SupergraphManagerGatewayConfig; + +export interface SupergraphSdlHookGatewayConfig extends GatewayConfigBase { + supergraphSdl: SupergraphSdlHook; +} + +export interface SupergraphManagerGatewayConfig extends GatewayConfigBase { + supergraphSdl: SupergraphManager; } type ManuallyManagedGatewayConfig = @@ -271,6 +274,24 @@ export function isStaticSupergraphSdlConfig( return 'supergraphSdl' in config && typeof config.supergraphSdl === 'string'; } +export function isSupergraphSdlHookConfig( + config: GatewayConfig, +): config is SupergraphSdlHookGatewayConfig { + return ( + 'supergraphSdl' in config && typeof config.supergraphSdl === 'function' + ); +} + +export function isSupergraphManagerConfig( + config: GatewayConfig, +): config is SupergraphManagerGatewayConfig { + return ( + 'supergraphSdl' in config && + typeof config.supergraphSdl === 'object' && + 'initialize' in config.supergraphSdl + ); +} + // A manually managed config means the user has provided a function which // handles providing service definitions to the gateway. export function isManuallyManagedConfig( @@ -281,7 +302,7 @@ export function isManuallyManagedConfig( 'experimental_updateServiceDefinitions' in config || 'experimental_updateSupergraphSdl' in config || // TODO(trevor:removeServiceList) - 'serviceList' in config + isServiceListConfig(config) ); } @@ -291,6 +312,7 @@ export function isManagedConfig( ): config is ManagedGatewayConfig { return ( 'schemaConfigDeliveryEndpoint' in config || + 'uplinkEndpoints' in config || (!isLocalConfig(config) && !isStaticSupergraphSdlConfig(config) && !isManuallyManagedConfig(config)) From 4dffaab92e44e8a119edf5ac8345f96c991db5c6 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:38:05 -0800 Subject: [PATCH 53/82] rename / cleanup --- gateway-js/src/index.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 287587046b..14c65ee4ca 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -336,10 +336,10 @@ export class ApolloGateway implements GraphQLService { logger: this.logger, })); } else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) { - const supergraphSdlFetcher = typeof this.config.supergraphSdl === 'object' + const supergraphManager = typeof this.config.supergraphSdl === 'object' ? this.config.supergraphSdl : { initialize: this.config.supergraphSdl }; - await this.initializeSupergraphManager(supergraphSdlFetcher); + await this.initializeSupergraphManager(supergraphManager); } else if ( isServiceListConfig(this.config) && // this setting is currently expected to override `serviceList` when they both exist @@ -390,15 +390,15 @@ export class ApolloGateway implements GraphQLService { ? this.config.experimental_updateServiceDefinitions : this.config.experimental_updateSupergraphSdl; - const legacyFetcher = new LegacyFetcher({ - logger: this.logger, - gatewayConfig: this.config, - updateServiceDefinitions, - pollIntervalInMs: this.pollIntervalInMs, - subgraphHealthCheck: this.config.serviceHealthCheck, - }); - - await this.initializeSupergraphManager(legacyFetcher); + await this.initializeSupergraphManager( + new LegacyFetcher({ + logger: this.logger, + gatewayConfig: this.config, + updateServiceDefinitions, + pollIntervalInMs: this.pollIntervalInMs, + subgraphHealthCheck: this.config.serviceHealthCheck, + }), + ); } const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; @@ -438,10 +438,10 @@ export class ApolloGateway implements GraphQLService { } private async initializeSupergraphManager( - supergraphSdlFetcher: T, + supergraphManager: T, ) { try { - const result = await supergraphSdlFetcher.initialize({ + const result = await supergraphManager.initialize({ update: this.externalSupergraphUpdateCallback.bind(this), healthCheck: this.externalSubgraphHealthCheckCallback.bind(this), getDataSource: this.externalGetDataSourceCallback.bind(this), From 53b00d2bbe234ead372d8e8c53ea84cf8ad27cc8 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:42:21 -0800 Subject: [PATCH 54/82] reorder if elses for simplicity --- gateway-js/src/index.ts | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 14c65ee4ca..6f4415d46c 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -341,10 +341,23 @@ export class ApolloGateway implements GraphQLService { : { initialize: this.config.supergraphSdl }; await this.initializeSupergraphManager(supergraphManager); } else if ( - isServiceListConfig(this.config) && - // this setting is currently expected to override `serviceList` when they both exist - !('experimental_updateServiceDefinitions' in this.config) + 'experimental_updateServiceDefinitions' in this.config || 'experimental_updateSupergraphSdl' in this.config ) { + const updateServiceDefinitions = + 'experimental_updateServiceDefinitions' in this.config + ? this.config.experimental_updateServiceDefinitions + : this.config.experimental_updateSupergraphSdl; + + await this.initializeSupergraphManager( + new LegacyFetcher({ + logger: this.logger, + gatewayConfig: this.config, + updateServiceDefinitions, + pollIntervalInMs: this.pollIntervalInMs, + subgraphHealthCheck: this.config.serviceHealthCheck, + }), + ); + } else if (isServiceListConfig(this.config)) { // TODO(trevor:removeServiceList) this.logger.warn( 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', @@ -358,7 +371,8 @@ export class ApolloGateway implements GraphQLService { introspectionHeaders: this.config.introspectionHeaders, }), ); - } else if (isManagedConfig(this.config)) { + } else { + // isManagedConfig(this.config) const canUseManagedConfig = this.apolloConfig?.graphRef && this.apolloConfig?.keyHash; if (!canUseManagedConfig) { @@ -384,21 +398,6 @@ export class ApolloGateway implements GraphQLService { pollIntervalInMs: this.pollIntervalInMs ?? 10000, }), ); - } else { - const updateServiceDefinitions = - 'experimental_updateServiceDefinitions' in this.config - ? this.config.experimental_updateServiceDefinitions - : this.config.experimental_updateSupergraphSdl; - - await this.initializeSupergraphManager( - new LegacyFetcher({ - logger: this.logger, - gatewayConfig: this.config, - updateServiceDefinitions, - pollIntervalInMs: this.pollIntervalInMs, - subgraphHealthCheck: this.config.serviceHealthCheck, - }), - ); } const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; From 8df89e777e385d7b9ff976ae231de5d67c5c12a4 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:45:04 -0800 Subject: [PATCH 55/82] protect against invalid cleanup property --- gateway-js/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 6f4415d46c..48c3c8dd39 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -451,7 +451,13 @@ export class ApolloGateway implements GraphQLService { ); } if (result?.cleanup) { + if (typeof result.cleanup === 'function') { this.toDispose.push(result.cleanup); + } else { + this.logger.error( + 'Provided `supergraphSdl` function returned an invalid `cleanup` property (must be a function)', + ); + } } this.externalSupergraphUpdateCallback(result.supergraphSdl); From f2a0e2f6371e73d6c0571ebda0720877601efcc6 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:46:24 -0800 Subject: [PATCH 56/82] update comments on external hook functions --- gateway-js/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 48c3c8dd39..93170b7c2f 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -473,7 +473,7 @@ export class ApolloGateway implements GraphQLService { /** * @throws Error - * when called from a state other than `loaded` + * when called from a state other than `loaded` or `intialized` * * @throws Error * when the provided supergraphSdl is invalid @@ -515,6 +515,10 @@ export class ApolloGateway implements GraphQLService { } } + /** + * @throws Error + * when any subgraph fails the health check + */ private async externalSubgraphHealthCheckCallback(supergraphSdl: string) { const parsedSupergraphSdl = supergraphSdl === this.supergraphSdl From 437edd6fd5658fcdd735a2326dc1dc7fb6395427 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:47:02 -0800 Subject: [PATCH 57/82] rename createAndCacheDataSource --- gateway-js/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 93170b7c2f..392247d64e 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -552,7 +552,7 @@ export class ApolloGateway implements GraphQLService { name, url, }: ServiceEndpointDefinition) { - return this.createAndCacheDataSource({ name, url }); + return this.getOrCreateDataSource({ name, url }); } private updateWithSupergraphSdl({ supergraphSdl, id }: SupergraphSdlUpdate) { @@ -745,7 +745,7 @@ export class ApolloGateway implements GraphQLService { }; } - private createAndCacheDataSource( + private getOrCreateDataSource( serviceDef: ServiceEndpointDefinition, ): GraphQLDataSource { // If the DataSource has already been created, early return @@ -782,7 +782,7 @@ export class ApolloGateway implements GraphQLService { private createServices(services: ServiceEndpointDefinition[]) { for (const serviceDef of services) { - this.createAndCacheDataSource(serviceDef); + this.getOrCreateDataSource(serviceDef); } } From 83f5d34850c426c487c0b5f456caa2dba9a6723c Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:48:12 -0800 Subject: [PATCH 58/82] update comment --- gateway-js/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 392247d64e..b308313d27 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -1051,7 +1051,8 @@ export class ApolloGateway implements GraphQLService { this.state = { phase: 'stopped' }; return; case 'updating schema': { - // This should never happen + // This should never happen since schema updates are sync and `stop` + // shouldn't be called mid-update throw Error( '`ApolloGateway.stop` called from an unexpected state `updating schema`', ); From c546ce50e7fea2f6c7855805419c44f558ee4028 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 09:48:37 -0800 Subject: [PATCH 59/82] export useful types --- gateway-js/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index b308313d27..07d77b4cd6 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -1128,5 +1128,7 @@ export * from './datasources'; export { SupergraphSdlUpdateFunction, SubgraphHealthCheckFunction, + GetDataSourceFunction, SupergraphSdlHook, + SupergraphManager } from './config'; From 0c471c541b281f5c1ebc6bb24a8b5c99a1950359 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 10:16:31 -0800 Subject: [PATCH 60/82] Add deprecation warning for schemaConfigDeliveryEndpoint option (unrelated to PR) --- .../src/__tests__/integration/configuration.test.ts | 11 +++++++++++ gateway-js/src/config.ts | 7 ------- gateway-js/src/index.ts | 13 ++++++++----- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 87abcd3725..83784adec3 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -432,4 +432,15 @@ describe('deprecation warnings', () => { await gateway.stop(); }); + + it('warns with `schemaConfigDeliveryEndpoint` option set', async () => { + new ApolloGateway({ + schemaConfigDeliveryEndpoint: 'test', + logger, + }); + + expect(logger.warn).toHaveBeenCalledWith( + 'The `schemaConfigDeliveryEndpoint` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent (array form) `uplinkEndpoints` configuration option.', + ); + }); }); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 8d834f0b84..3d72e0716b 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -325,10 +325,3 @@ export function isStaticConfig( ): config is StaticGatewayConfig { return isLocalConfig(config) || isStaticSupergraphSdlConfig(config); } - -// A dynamic config is one which loads asynchronously and (can) update via polling -export function isDynamicConfig( - config: GatewayConfig, -): config is DynamicGatewayConfig { - return isManagedConfig(config) || isManuallyManagedConfig(config); -} diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 07d77b4cd6..598776150f 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -51,7 +51,6 @@ import { isLocalConfig, isServiceListConfig, isManagedConfig, - isDynamicConfig, SupergraphSdlUpdate, isManuallyManagedSupergraphSdlGatewayConfig, ManagedGatewayConfig, @@ -206,9 +205,7 @@ export class ApolloGateway implements GraphQLService { this.pollIntervalInMs = config?.experimental_pollInterval; - if (isDynamicConfig(this.config)) { - this.issueDynamicWarningsIfApplicable(); - } + this.issueConfigurationWarningsIfApplicable(); this.logger.debug('Gateway successfully initialized (but not yet loaded)'); this.state = { phase: 'initialized' }; @@ -245,7 +242,7 @@ export class ApolloGateway implements GraphQLService { }); } - private issueDynamicWarningsIfApplicable() { + private issueConfigurationWarningsIfApplicable() { // Warn against a pollInterval of < 10s in managed mode and reset it to 10s if ( isManagedConfig(this.config) && @@ -284,6 +281,12 @@ export class ApolloGateway implements GraphQLService { 'are provided.', ); } + + if ('schemaConfigDeliveryEndpoint' in this.config) { + this.logger.warn( + 'The `schemaConfigDeliveryEndpoint` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent (array form) `uplinkEndpoints` configuration option.', + ); + } } public async load(options?: { From 4e6049ca06004d403f4bedbbeaa2572bc14c3c3f Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 10:29:51 -0800 Subject: [PATCH 61/82] deprecate experimental_pollInterval --- .../__tests__/gateway/lifecycle-hooks.test.ts | 4 ++-- .../integration/configuration.test.ts | 11 +++++++++++ gateway-js/src/config.ts | 4 ++++ gateway-js/src/index.ts | 19 +++++++++++++------ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 4ec9c108d3..16305064e1 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -170,7 +170,7 @@ describe('lifecycle hooks', () => { it('warns when polling on the default fetcher', async () => { new ApolloGateway({ serviceList: serviceDefinitions, - experimental_pollInterval: 10, + pollIntervalInMs: 10, logger, }); expect(logger.warn).toHaveBeenCalledWith( @@ -187,7 +187,7 @@ describe('lifecycle hooks', () => { const gateway = new ApolloGateway({ serviceList: [{ name: 'book', url: 'http://localhost:32542' }], experimental_updateServiceDefinitions, - experimental_pollInterval: 100, + pollIntervalInMs: 100, logger, }); diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 83784adec3..55fcf59a2e 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -443,4 +443,15 @@ describe('deprecation warnings', () => { 'The `schemaConfigDeliveryEndpoint` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent (array form) `uplinkEndpoints` configuration option.', ); }); + + it('warns with `experimental_pollInterval` option set', async () => { + new ApolloGateway({ + experimental_pollInterval: 10000, + logger, + }); + + expect(logger.warn).toHaveBeenCalledWith( + 'The `experimental_pollInterval` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent `pollIntervalInMs` configuration option.', + ); + }); }); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 3d72e0716b..e94b067932 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -126,7 +126,11 @@ interface GatewayConfigBase { experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; experimental_didFailComposition?: Experimental_DidFailCompositionCallback; experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; + /** + * @deprecated use `pollIntervalInMs` instead + */ experimental_pollInterval?: number; + pollIntervalInMs?: number; experimental_approximateQueryPlanStoreMiB?: number; experimental_autoFragmentization?: boolean; fetcher?: typeof fetch; diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 598776150f..06c7615fbd 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -203,7 +203,8 @@ export class ApolloGateway implements GraphQLService { this.experimental_didUpdateComposition = config?.experimental_didUpdateComposition; - this.pollIntervalInMs = config?.experimental_pollInterval; + this.pollIntervalInMs = + config?.pollIntervalInMs ?? config?.experimental_pollInterval; this.issueConfigurationWarningsIfApplicable(); @@ -246,22 +247,22 @@ export class ApolloGateway implements GraphQLService { // Warn against a pollInterval of < 10s in managed mode and reset it to 10s if ( isManagedConfig(this.config) && - this.config.experimental_pollInterval && - this.config.experimental_pollInterval < 10000 + this.pollIntervalInMs && + this.pollIntervalInMs < 10000 ) { this.pollIntervalInMs = 10000; this.logger.warn( 'Polling Apollo services at a frequency of less than once per 10 ' + 'seconds (10000) is disallowed. Instead, the minimum allowed ' + 'pollInterval of 10000 will be used. Please reconfigure your ' + - 'experimental_pollInterval accordingly. If this is problematic for ' + + '`pollIntervalInMs` accordingly. If this is problematic for ' + 'your team, please contact support.', ); } // Warn against using the pollInterval and a serviceList simultaneously // TODO(trevor:removeServiceList) - if (this.config.experimental_pollInterval && isServiceListConfig(this.config)) { + if (this.pollIntervalInMs && isServiceListConfig(this.config)) { this.logger.warn( 'Polling running services is dangerous and not recommended in production. ' + 'Polling should only be used against a registry. ' + @@ -287,6 +288,12 @@ export class ApolloGateway implements GraphQLService { 'The `schemaConfigDeliveryEndpoint` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent (array form) `uplinkEndpoints` configuration option.', ); } + + if ('experimental_pollInterval' in this.config) { + this.logger.warn( + 'The `experimental_pollInterval` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent `pollIntervalInMs` configuration option.', + ); + } } public async load(options?: { @@ -368,7 +375,7 @@ export class ApolloGateway implements GraphQLService { await this.initializeSupergraphManager( new IntrospectAndCompose({ subgraphs: this.config.serviceList, - pollIntervalInMs: this.config.experimental_pollInterval, + pollIntervalInMs: this.pollIntervalInMs, logger: this.logger, subgraphHealthCheck: this.config.serviceHealthCheck, introspectionHeaders: this.config.introspectionHeaders, From 3b03867f445b373ca1e70203f1c1a9420fb1c3bb Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 10:32:35 -0800 Subject: [PATCH 62/82] rename fn to match filename --- .../__tests__/loadServicesFromRemoteEndpoint.test.ts | 8 ++++---- .../src/supergraphManagers/IntrospectAndCompose/index.ts | 4 ++-- .../loadServicesFromRemoteEndpoint.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts index 00dadd6e58..6f9fceb0a4 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts @@ -1,13 +1,13 @@ -import { getServiceDefinitionsFromRemoteEndpoint } from '../loadServicesFromRemoteEndpoint'; +import { loadServicesFromRemoteEndpoint } from '../loadServicesFromRemoteEndpoint'; import { RemoteGraphQLDataSource } from '../../../datasources'; -describe('getServiceDefinitionsFromRemoteEndpoint', () => { +describe('loadServicesFromRemoteEndpoint', () => { it('errors when no URL was specified', async () => { const serviceSdlCache = new Map(); const dataSource = new RemoteGraphQLDataSource({ url: '' }); const serviceList = [{ name: 'test', dataSource }]; await expect( - getServiceDefinitionsFromRemoteEndpoint({ + loadServicesFromRemoteEndpoint({ serviceList, serviceSdlCache, getServiceIntrospectionHeaders: async () => ({}) @@ -28,7 +28,7 @@ describe('getServiceDefinitionsFromRemoteEndpoint', () => { // of `EAI_AGAIN` or `ENOTFOUND`. This `toThrowError` uses a Regex // to match either case. await expect( - getServiceDefinitionsFromRemoteEndpoint({ + loadServicesFromRemoteEndpoint({ serviceList, serviceSdlCache, getServiceIntrospectionHeaders: async () => ({}), diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts index fb66de093e..4a2c47f6ee 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts @@ -12,7 +12,7 @@ import { SubgraphHealthCheckFunction, } from '../..'; import { - getServiceDefinitionsFromRemoteEndpoint, + loadServicesFromRemoteEndpoint, Service, } from './loadServicesFromRemoteEndpoint'; import { SupergraphManager, SupergraphSdlHookOptions } from '../../config'; @@ -93,7 +93,7 @@ export class IntrospectAndCompose implements SupergraphManager { } private async updateSupergraphSdl() { - const result = await getServiceDefinitionsFromRemoteEndpoint({ + const result = await loadServicesFromRemoteEndpoint({ serviceList: this.subgraphs!, getServiceIntrospectionHeaders: async (service) => { return typeof this.config.introspectionHeaders === 'function' diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts index 4caede3a02..4f946add46 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts @@ -10,7 +10,7 @@ export type Service = ServiceEndpointDefinition & { dataSource: GraphQLDataSource; }; -export async function getServiceDefinitionsFromRemoteEndpoint({ +export async function loadServicesFromRemoteEndpoint({ serviceList, getServiceIntrospectionHeaders, serviceSdlCache, From 45c9cdaa652334efb5226495048f5345f50b0108 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 10:43:13 -0800 Subject: [PATCH 63/82] reference new supergraph managers in warnings. export LocalCompose --- gateway-js/src/__tests__/integration/configuration.test.ts | 4 ++-- gateway-js/src/index.ts | 3 ++- gateway-js/src/supergraphManagers/LocalCompose/index.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 55fcf59a2e..79562da448 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -414,7 +414,7 @@ describe('deprecation warnings', () => { } catch {} expect(logger.warn).toHaveBeenCalledWith( - 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to its replacement `IntrospectAndCompose`. More information on `IntrospectAndCompose` can be found in the documentation.', ); }); @@ -427,7 +427,7 @@ describe('deprecation warnings', () => { await gateway.load(); expect(logger.warn).toHaveBeenCalledWith( - 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the `LocalCompose` supergraph manager exported by `@apollo/gateway`.', ); await gateway.stop(); diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 06c7615fbd..eed2b50902 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -370,7 +370,7 @@ export class ApolloGateway implements GraphQLService { } else if (isServiceListConfig(this.config)) { // TODO(trevor:removeServiceList) this.logger.warn( - 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + 'The `serviceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to its replacement `IntrospectAndCompose`. More information on `IntrospectAndCompose` can be found in the documentation.', ); await this.initializeSupergraphManager( new IntrospectAndCompose({ @@ -1131,6 +1131,7 @@ export { ServiceEndpointDefinition, CompositionInfo, IntrospectAndCompose, + LocalCompose, }; export * from './datasources'; diff --git a/gateway-js/src/supergraphManagers/LocalCompose/index.ts b/gateway-js/src/supergraphManagers/LocalCompose/index.ts index 1fb9b26293..d21f330f8e 100644 --- a/gateway-js/src/supergraphManagers/LocalCompose/index.ts +++ b/gateway-js/src/supergraphManagers/LocalCompose/index.ts @@ -35,7 +35,7 @@ export class LocalCompose implements SupergraphManager { private issueDeprecationWarnings() { this.config.logger?.warn( - 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.', + 'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the `LocalCompose` supergraph manager exported by `@apollo/gateway`.', ); } From 547d02c6d04ae8aa6fec7cf63bae0e22abf016f1 Mon Sep 17 00:00:00 2001 From: Stephen Barlow Date: Thu, 6 Jan 2022 13:04:25 -0800 Subject: [PATCH 64/82] Document setting gateway poll interval --- docs/source/managed-federation/uplink.mdx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/source/managed-federation/uplink.mdx b/docs/source/managed-federation/uplink.mdx index b6393a7b47..5601c1c36e 100644 --- a/docs/source/managed-federation/uplink.mdx +++ b/docs/source/managed-federation/uplink.mdx @@ -1,6 +1,7 @@ --- title: Apollo Uplink sidebar_title: Uplink +description: Fetch your managed gateway's configuration --- When using [managed federation](./overview/), your federated gateway regularly polls an endpoint called **Apollo Uplink** for its latest supergraph schema and other configuration: @@ -36,7 +37,11 @@ Even if a particular poll request fails _all_ of its retries, the gateway contin ## Configuring polling behavior -You can configure the number of retries your gateway performs for a failed poll request, along with the list of Uplink URLs your gateway uses. +You can configure the following aspects of your gateway's polling behavior: + +* The number of retries your gateway performs for a failed poll request +* The interval at which your gateway polls +* The list of Uplink URLs your gateway uses ### Retry limit @@ -56,6 +61,21 @@ By default, the gateway retries a single poll request a number of times equal to > Even if a particular poll request fails _all_ of its retries, the gateway continues polling as usual at the next interval (with its own set of retries if needed). In the meantime, the gateway continues using its most recently obtained configuration. +### Poll interval + +You can configure the interval at which your gateway polls Apollo Uplink like so: + +```js +const { ApolloGateway } = require('@apollo/gateway'); + +// ... + +const gateway = new ApolloGateway({ + pollIntervalInMs: 15000 // 15 seconds +}); +``` + +The `pollIntervalInMs` option specifies the polling interval in milliseconds. This value must be at least `10000` (which is also the default value). ### Uplink URLs (advanced) From 3a301d3bbfcb91454a1e21c0d1193fc40e5878a9 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 7 Jan 2022 18:01:19 -0800 Subject: [PATCH 65/82] Update gateway-js/src/__tests__/integration/networkRequests.test.ts --- gateway-js/src/__tests__/integration/networkRequests.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index 77c8fb0d0a..9e61264ae7 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -244,7 +244,6 @@ describe('Supergraph SDL update failures', () => { await gateway.load(mockApolloConfig); await errorLoggedPromise; - // @ts-ignore expect(logger.error).toHaveBeenCalledWith( `UplinkFetcher failed to update supergraph with the following error: An error occurred while fetching your schema from Apollo: \nCannot query field "fail" on type "Query".`, ); From 49200f6b68a3576eabbdb0fb121fd026ffa82219 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 08:46:31 -0800 Subject: [PATCH 66/82] expand on IntrospectAndCompose description --- docs/source/api/apollo-gateway.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/api/apollo-gateway.mdx b/docs/source/api/apollo-gateway.mdx index 71bbbde0e3..c6d14cfbe0 100644 --- a/docs/source/api/apollo-gateway.mdx +++ b/docs/source/api/apollo-gateway.mdx @@ -571,7 +571,9 @@ The details of the `fetch` response sent by the subgraph. ## `class IntrospectAndCompose` -The drop-in replacement for `serviceList`. You pass an instance of this class directly to `ApolloGateway`'s `supergraphSdl` constructor option. +`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 From 95e6caaf5af120adcdbce0fed46df969d4730088 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 08:47:12 -0800 Subject: [PATCH 67/82] remove failure hook from config base --- gateway-js/src/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index e94b067932..06cbb64055 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -124,7 +124,6 @@ interface GatewayConfigBase { // experimental observability callbacks experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; - experimental_didFailComposition?: Experimental_DidFailCompositionCallback; experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; /** * @deprecated use `pollIntervalInMs` instead From 5158be797cc614b18a61197a0b3ccc4278b4a9d5 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 08:51:53 -0800 Subject: [PATCH 68/82] update deprecation comments --- gateway-js/src/config.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 06cbb64055..495b2bfbfb 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -139,11 +139,11 @@ interface GatewayConfigBase { // TODO(trevor:removeServiceList) export interface ServiceListGatewayConfig extends GatewayConfigBase { /** - * @deprecated: use `supergraphSdl` in its function form instead + * @deprecated: use `supergraphSdl: new IntrospectAndCompose(...)` instead */ serviceList: ServiceEndpointDefinition[]; /** - * @deprecated: use `supergraphSdl` in its function form instead + * @deprecated: use `supergraphSdl: new IntrospectAndCompose(...)` instead */ introspectionHeaders?: | HeadersInit @@ -167,7 +167,7 @@ export interface ManagedGatewayConfig extends GatewayConfigBase { // TODO(trevor:removeServiceList): migrate users to `supergraphSdl` function option interface ManuallyManagedServiceDefsGatewayConfig extends GatewayConfigBase { /** - * @deprecated: use `supergraphSdl` in its function form instead + * @deprecated: use `supergraphSdl` instead (either as a `SupergraphSdlHook` or `SupergraphManager`) */ experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions; } @@ -176,7 +176,7 @@ interface ManuallyManagedServiceDefsGatewayConfig extends GatewayConfigBase { interface ExperimentalManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase { /** - * @deprecated: use `supergraphSdl` in its function form instead + * @deprecated: use `supergraphSdl` instead (either as a `SupergraphSdlHook` or `SupergraphManager`) */ experimental_updateSupergraphSdl: Experimental_UpdateSupergraphSdl; } @@ -238,7 +238,7 @@ type ManuallyManagedGatewayConfig = // TODO(trevor:removeServiceList) interface LocalGatewayConfig extends GatewayConfigBase { /** - * @deprecated: use `supergraphSdl` in its function form instead + * @deprecated: use `supergraphSdl: new LocalCompose(...)` instead */ localServiceList: ServiceDefinition[]; } From c5dcc7a3f16c145f901749ca85e35a9971e1cfb1 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 09:01:29 -0800 Subject: [PATCH 69/82] Add clarifying comment to uplinkEndpoints option --- gateway-js/src/config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 495b2bfbfb..baf467e922 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -160,6 +160,12 @@ export interface ManagedGatewayConfig extends GatewayConfigBase { * @deprecated: use `uplinkEndpoints` instead */ schemaConfigDeliveryEndpoint?: string; + /** + * This defaults to: + * ['https://uplink.api.apollographql.com/', 'https://aws.uplink.api.apollographql.com/'] + * The first URL points to GCP, the second to AWS. This option should most likely + * be left to default unless you have a specific reason to change it. + */ uplinkEndpoints?: string[]; uplinkMaxRetries?: number; } From fa7dc256c83382f0ace6a8d0645c72a8d7b6d710 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 09:05:23 -0800 Subject: [PATCH 70/82] clarify endpoints comment --- gateway-js/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index eed2b50902..9a04b213ea 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -426,9 +426,13 @@ export class ApolloGateway implements GraphQLService { } private getUplinkEndpoints(config: ManagedGatewayConfig) { - // 1. If config is set to a `string`, use it - // 2. If the env var is set, use that - // 3. If config is `undefined`, use the default uplink URLs + /** + * Configuration priority order: + * 1. `uplinkEndpoints` configuration option + * 2. (deprecated) `schemaConfigDeliveryEndpoint` configuration option + * 3. APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT environment variable + * 4. default (GCP and AWS) + */ const rawEndpointsString = process.env.APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT; const envEndpoints = rawEndpointsString?.split(',') ?? null; From adcab86d5b640c0598e4b159dd9cadcd8e7d6418 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 09:37:29 -0800 Subject: [PATCH 71/82] No need to log and rethrow on init failure, just throw --- gateway-js/src/__tests__/gateway/supergraphSdl.test.ts | 4 +--- gateway-js/src/index.ts | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 94cb9efd15..970f6eda41 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -231,7 +231,7 @@ describe('Using supergraphSdl dynamic configuration', () => { }); describe('errors', () => { - it('fails to load if user-provided `supergraphSdl` function throws', async () => { + it('fails to load if `SupergraphManager` throws on initialization', async () => { const failureMessage = 'Error from supergraphSdl function'; gateway = new ApolloGateway({ async supergraphSdl() { @@ -243,8 +243,6 @@ describe('Using supergraphSdl dynamic configuration', () => { await expect(gateway.load()).rejects.toThrowError(failureMessage); expect(gateway.__testing().state.phase).toEqual('failed to load'); - expect(logger.error).toHaveBeenCalledWith(failureMessage); - // we don't want the `afterEach` to call `gateway.stop()` in this case // since it would throw an error due to the gateway's failed to load state gateway = null; diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 9a04b213ea..f2a41fad3d 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -478,7 +478,6 @@ export class ApolloGateway implements GraphQLService { } catch (e) { this.state = { phase: 'failed to load' }; await this.performCleanup(); - this.logger.error(e.message ?? e); throw e; } From 9d321f84ef4348e24f210481264fa994407f1634 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 09:40:08 -0800 Subject: [PATCH 72/82] rename performCleanup fn --- gateway-js/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index f2a41fad3d..f9cb5ab54f 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -477,7 +477,7 @@ export class ApolloGateway implements GraphQLService { this.externalSupergraphUpdateCallback(result.supergraphSdl); } catch (e) { this.state = { phase: 'failed to load' }; - await this.performCleanup(); + await this.performCleanupAndLogErrors(); throw e; } @@ -1015,7 +1015,7 @@ export class ApolloGateway implements GraphQLService { }); } - private async performCleanup() { + private async performCleanupAndLogErrors() { if (this.toDispose.length === 0) return; await Promise.all( @@ -1055,7 +1055,7 @@ export class ApolloGateway implements GraphQLService { } return; case 'loaded': - const stoppingDonePromise = this.performCleanup(); + const stoppingDonePromise = this.performCleanupAndLogErrors(); this.state = { phase: 'stopping', stoppingDonePromise, From 2198c4253d65cd61d9ab4c90d05608da4fd1e145 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 09:45:02 -0800 Subject: [PATCH 73/82] handle stopping case --- gateway-js/src/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index f9cb5ab54f..348d0e603c 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -505,15 +505,16 @@ export class ApolloGateway implements GraphQLService { throw new Error( "Can't call `update` callback after gateway has been stopped.", ); + case 'stopping': + throw new Error( + "Can't call `update` callback while gateway is stopping.", + ); case 'loaded': case 'initialized': // typical case break; default: - // this should never happen - throw new Error( - `Called \`update\` callback from unexpected state: "${this.state.phase}". This is a bug.`, - ); + throw new UnreachableCaseError(this.state); } this.state = { phase: 'updating schema' }; From 7b7101d79a0bf8e6d6892794b40ce9a95342f1e0 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 09:52:24 -0800 Subject: [PATCH 74/82] update error message on bad call to stop --- gateway-js/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 348d0e603c..60dd4730b2 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -1065,10 +1065,8 @@ export class ApolloGateway implements GraphQLService { this.state = { phase: 'stopped' }; return; case 'updating schema': { - // This should never happen since schema updates are sync and `stop` - // shouldn't be called mid-update throw Error( - '`ApolloGateway.stop` called from an unexpected state `updating schema`', + '`ApolloGateway.stop` shouldn\'t be called from inside a schema change listener', ); } default: From 2a63f07e166f05ce20717cb26052c2db507cfbd3 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 09:54:13 -0800 Subject: [PATCH 75/82] Remove unrefs and use clearTimeout instead of clearInterval --- .../src/supergraphManagers/IntrospectAndCompose/index.ts | 3 +-- gateway-js/src/supergraphManagers/LegacyFetcher/index.ts | 3 +-- gateway-js/src/supergraphManagers/UplinkFetcher/index.ts | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts index 4a2c47f6ee..3b1f4bc0d2 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/index.ts @@ -84,8 +84,7 @@ export class IntrospectAndCompose implements SupergraphManager { } this.state = { phase: 'stopped' }; if (this.timerRef) { - this.timerRef.unref(); - clearInterval(this.timerRef); + clearTimeout(this.timerRef); this.timerRef = null; } }, diff --git a/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts b/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts index 2873aea85c..eba86b8012 100644 --- a/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts +++ b/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts @@ -100,8 +100,7 @@ export class LegacyFetcher implements SupergraphManager { } this.state = { phase: 'stopped' }; if (this.timerRef) { - this.timerRef.unref(); - clearInterval(this.timerRef); + clearTimeout(this.timerRef); this.timerRef = null; } }, diff --git a/gateway-js/src/supergraphManagers/UplinkFetcher/index.ts b/gateway-js/src/supergraphManagers/UplinkFetcher/index.ts index f118fdd80b..cd079e58c5 100644 --- a/gateway-js/src/supergraphManagers/UplinkFetcher/index.ts +++ b/gateway-js/src/supergraphManagers/UplinkFetcher/index.ts @@ -65,8 +65,7 @@ export class UplinkFetcher implements SupergraphManager { } this.state = { phase: 'stopped' }; if (this.timerRef) { - this.timerRef.unref(); - clearInterval(this.timerRef); + clearTimeout(this.timerRef); this.timerRef = null; } }, From 335553fcbec3537faf614871018317f89297b265 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 10:04:33 -0800 Subject: [PATCH 76/82] Update comment for LegacyFetcher --- gateway-js/src/supergraphManagers/LegacyFetcher/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts b/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts index eba86b8012..0787367635 100644 --- a/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts +++ b/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts @@ -1,4 +1,9 @@ -// TODO(trevor:removeServiceList) the whole file goes away +/** + * Similar in concept to `IntrospectAndCompose`, but this handles + * the `experimental_updateComposition` and `experimental_updateSupergraphSdl` + * configuration options of the gateway and will be removed in a future release + * along with those options. + */ import { Logger } from 'apollo-server-types'; import resolvable from '@josephg/resolvable'; import { From 2ba31fe188a156fee07b0d306db6ab4bff4cc06b Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 11:16:03 -0800 Subject: [PATCH 77/82] rename experimental_didUpdateComposition to didUpdateSupergraph --- .../src/__tests__/gateway/buildService.test.ts | 4 ++-- .../src/__tests__/gateway/lifecycle-hooks.test.ts | 5 ++--- .../src/__tests__/gateway/supergraphSdl.test.ts | 2 +- gateway-js/src/config.ts | 2 +- gateway-js/src/index.ts | 13 ++++++------- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/gateway-js/src/__tests__/gateway/buildService.test.ts b/gateway-js/src/__tests__/gateway/buildService.test.ts index 6b6e309949..63dfd3efb7 100644 --- a/gateway-js/src/__tests__/gateway/buildService.test.ts +++ b/gateway-js/src/__tests__/gateway/buildService.test.ts @@ -218,7 +218,7 @@ it('does not share service definition cache between gateways', async () => { url: 'https://api.example.com/repeat', }, ], - experimental_didUpdateComposition: updateObserver, + didUpdateSupergraph: updateObserver, }); await gateway.load(); @@ -237,7 +237,7 @@ it('does not share service definition cache between gateways', async () => { url: 'https://api.example.com/repeat', }, ], - experimental_didUpdateComposition: updateObserver, + didUpdateSupergraph: updateObserver, }); await gateway.load(); diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 16305064e1..780d2fd4a3 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -60,7 +60,6 @@ describe('lifecycle hooks', () => { const gateway = new ApolloGateway({ serviceList: serviceDefinitions, experimental_updateServiceDefinitions, - experimental_didUpdateComposition: jest.fn(), logger, }); @@ -71,7 +70,7 @@ describe('lifecycle hooks', () => { await gateway.stop(); }); - it('calls experimental_didUpdateComposition on schema update', async () => { + it('calls didUpdateSupergraph on schema update', async () => { const compositionMetadata = { formatVersion: 1, id: 'abc', @@ -106,7 +105,7 @@ describe('lifecycle hooks', () => { const gateway = new ApolloGateway({ experimental_updateServiceDefinitions: mockUpdate, - experimental_didUpdateComposition: mockDidUpdate, + didUpdateSupergraph: mockDidUpdate, logger, }); // for testing purposes, a short pollInterval is ideal so we'll override here diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 970f6eda41..a94f570333 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -331,7 +331,7 @@ describe('Using supergraphSdl dynamic configuration', () => { supergraphSdl, }; }, - experimental_didUpdateComposition() { + didUpdateSupergraph() { updateCallback(getTestingSupergraphSdl(fixturesWithUpdate)); }, }); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index baf467e922..e8fa0f25c3 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -124,7 +124,7 @@ interface GatewayConfigBase { // experimental observability callbacks experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; - experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; + didUpdateSupergraph?: Experimental_DidUpdateCompositionCallback; /** * @deprecated use `pollIntervalInMs` instead */ diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 60dd4730b2..a5dcd76bef 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -174,9 +174,8 @@ export class ApolloGateway implements GraphQLService { // The information made available here will give insight into the resulting // query plan and the inputs that generated it. private experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; - // Used to communicated composition changes, and what definitions caused - // those updates - private experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; + // Used to communicate supergraph updates + private didUpdateSupergraph?: Experimental_DidUpdateCompositionCallback; // how often service defs should be loaded/updated private pollIntervalInMs?: number; // Functions to call during gateway cleanup (when stop() is called) @@ -200,8 +199,8 @@ export class ApolloGateway implements GraphQLService { // set up experimental observability callbacks and config settings this.experimental_didResolveQueryPlan = config?.experimental_didResolveQueryPlan; - this.experimental_didUpdateComposition = - config?.experimental_didUpdateComposition; + this.didUpdateSupergraph = + config?.didUpdateSupergraph; this.pollIntervalInMs = config?.pollIntervalInMs ?? config?.experimental_pollInterval; @@ -599,8 +598,8 @@ export class ApolloGateway implements GraphQLService { } else { this.updateWithSchemaAndNotify(schema, generatedSupergraphSdl); - if (this.experimental_didUpdateComposition) { - this.experimental_didUpdateComposition( + if (this.didUpdateSupergraph) { + this.didUpdateSupergraph( { compositionId: id, supergraphSdl, From 01761a0be950c8e4650e9748b91bf5c18fbba633 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 11:18:33 -0800 Subject: [PATCH 78/82] update types --- gateway-js/src/config.ts | 4 ++-- gateway-js/src/index.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index e8fa0f25c3..74be9aefcf 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -65,7 +65,7 @@ export type CompositionInfo = | ServiceDefinitionCompositionInfo | SupergraphSdlCompositionInfo; -export type Experimental_DidUpdateCompositionCallback = ( +export type DidUpdateSupergraphCallback = ( currentConfig: CompositionInfo, previousConfig?: CompositionInfo, ) => void; @@ -124,7 +124,7 @@ interface GatewayConfigBase { // experimental observability callbacks experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; - didUpdateSupergraph?: Experimental_DidUpdateCompositionCallback; + didUpdateSupergraph?: DidUpdateSupergraphCallback; /** * @deprecated use `pollIntervalInMs` instead */ diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index a5dcd76bef..55f6abbb7b 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -43,7 +43,7 @@ import { ServiceEndpointDefinition, Experimental_DidFailCompositionCallback, Experimental_DidResolveQueryPlanCallback, - Experimental_DidUpdateCompositionCallback, + DidUpdateSupergraphCallback, Experimental_UpdateComposition, CompositionInfo, GatewayConfig, @@ -175,7 +175,7 @@ export class ApolloGateway implements GraphQLService { // query plan and the inputs that generated it. private experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; // Used to communicate supergraph updates - private didUpdateSupergraph?: Experimental_DidUpdateCompositionCallback; + private didUpdateSupergraph?: DidUpdateSupergraphCallback; // how often service defs should be loaded/updated private pollIntervalInMs?: number; // Functions to call during gateway cleanup (when stop() is called) @@ -1126,7 +1126,7 @@ export { ServiceMap, Experimental_DidFailCompositionCallback, Experimental_DidResolveQueryPlanCallback, - Experimental_DidUpdateCompositionCallback, + DidUpdateSupergraphCallback, Experimental_UpdateComposition, GatewayConfig, ServiceEndpointDefinition, From 7ad0ac1e2c074f9f32ad273e821cef4a6322c2a2 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 11:38:32 -0800 Subject: [PATCH 79/82] changelog entry --- gateway-js/CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index 5edca3c509..bdcda6451e 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -4,7 +4,24 @@ > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. -- _Nothing yet! Stay tuned._ +- __BREAKING__: Improve `supergraphSdl` configuration option, 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 `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` <=v2.18. If you are still running one of these versions, server shutdown may not happen as smoothly. + +[#1246](https://github.com/apollographql/federation/pull/1246) ## v0.45.1 From 59e9f5570da1292f428a362dbc2e811b121e601c Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 11:41:18 -0800 Subject: [PATCH 80/82] keep it experimental --- gateway-js/CHANGELOG.md | 2 +- .../src/__tests__/gateway/buildService.test.ts | 4 ++-- .../src/__tests__/gateway/lifecycle-hooks.test.ts | 4 ++-- .../src/__tests__/gateway/supergraphSdl.test.ts | 2 +- gateway-js/src/config.ts | 4 ++-- gateway-js/src/index.ts | 14 +++++++------- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index bdcda6451e..e2d654f5aa 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -14,7 +14,7 @@ For now: all of the mentioned deprecated configurations will still continue to w * `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 `didUpdateSupergraph` (no signature change) +* `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`. diff --git a/gateway-js/src/__tests__/gateway/buildService.test.ts b/gateway-js/src/__tests__/gateway/buildService.test.ts index 63dfd3efb7..189f57ded8 100644 --- a/gateway-js/src/__tests__/gateway/buildService.test.ts +++ b/gateway-js/src/__tests__/gateway/buildService.test.ts @@ -218,7 +218,7 @@ it('does not share service definition cache between gateways', async () => { url: 'https://api.example.com/repeat', }, ], - didUpdateSupergraph: updateObserver, + experimental_didUpdateSupergraph: updateObserver, }); await gateway.load(); @@ -237,7 +237,7 @@ it('does not share service definition cache between gateways', async () => { url: 'https://api.example.com/repeat', }, ], - didUpdateSupergraph: updateObserver, + experimental_didUpdateSupergraph: updateObserver, }); await gateway.load(); diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 780d2fd4a3..c8ede8e642 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -70,7 +70,7 @@ describe('lifecycle hooks', () => { await gateway.stop(); }); - it('calls didUpdateSupergraph on schema update', async () => { + it('calls experimental_didUpdateSupergraph on schema update', async () => { const compositionMetadata = { formatVersion: 1, id: 'abc', @@ -105,7 +105,7 @@ describe('lifecycle hooks', () => { const gateway = new ApolloGateway({ experimental_updateServiceDefinitions: mockUpdate, - didUpdateSupergraph: mockDidUpdate, + experimental_didUpdateSupergraph: mockDidUpdate, logger, }); // for testing purposes, a short pollInterval is ideal so we'll override here diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index a94f570333..e24b267734 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -331,7 +331,7 @@ describe('Using supergraphSdl dynamic configuration', () => { supergraphSdl, }; }, - didUpdateSupergraph() { + experimental_didUpdateSupergraph() { updateCallback(getTestingSupergraphSdl(fixturesWithUpdate)); }, }); diff --git a/gateway-js/src/config.ts b/gateway-js/src/config.ts index 74be9aefcf..10766bea7d 100644 --- a/gateway-js/src/config.ts +++ b/gateway-js/src/config.ts @@ -65,7 +65,7 @@ export type CompositionInfo = | ServiceDefinitionCompositionInfo | SupergraphSdlCompositionInfo; -export type DidUpdateSupergraphCallback = ( +export type Experimental_DidUpdateSupergraphCallback = ( currentConfig: CompositionInfo, previousConfig?: CompositionInfo, ) => void; @@ -124,7 +124,7 @@ interface GatewayConfigBase { // experimental observability callbacks experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; - didUpdateSupergraph?: DidUpdateSupergraphCallback; + experimental_didUpdateSupergraph?: Experimental_DidUpdateSupergraphCallback; /** * @deprecated use `pollIntervalInMs` instead */ diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 55f6abbb7b..02e5b46e0b 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -43,7 +43,7 @@ import { ServiceEndpointDefinition, Experimental_DidFailCompositionCallback, Experimental_DidResolveQueryPlanCallback, - DidUpdateSupergraphCallback, + Experimental_DidUpdateSupergraphCallback, Experimental_UpdateComposition, CompositionInfo, GatewayConfig, @@ -175,7 +175,7 @@ export class ApolloGateway implements GraphQLService { // query plan and the inputs that generated it. private experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; // Used to communicate supergraph updates - private didUpdateSupergraph?: DidUpdateSupergraphCallback; + private experimental_didUpdateSupergraph?: Experimental_DidUpdateSupergraphCallback; // how often service defs should be loaded/updated private pollIntervalInMs?: number; // Functions to call during gateway cleanup (when stop() is called) @@ -199,8 +199,8 @@ export class ApolloGateway implements GraphQLService { // set up experimental observability callbacks and config settings this.experimental_didResolveQueryPlan = config?.experimental_didResolveQueryPlan; - this.didUpdateSupergraph = - config?.didUpdateSupergraph; + this.experimental_didUpdateSupergraph = + config?.experimental_didUpdateSupergraph; this.pollIntervalInMs = config?.pollIntervalInMs ?? config?.experimental_pollInterval; @@ -598,8 +598,8 @@ export class ApolloGateway implements GraphQLService { } else { this.updateWithSchemaAndNotify(schema, generatedSupergraphSdl); - if (this.didUpdateSupergraph) { - this.didUpdateSupergraph( + if (this.experimental_didUpdateSupergraph) { + this.experimental_didUpdateSupergraph( { compositionId: id, supergraphSdl, @@ -1126,7 +1126,7 @@ export { ServiceMap, Experimental_DidFailCompositionCallback, Experimental_DidResolveQueryPlanCallback, - DidUpdateSupergraphCallback, + Experimental_DidUpdateSupergraphCallback, Experimental_UpdateComposition, GatewayConfig, ServiceEndpointDefinition, From d49dcf9bd6c6e44621b74dc69e63400227daf682 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 11:51:50 -0800 Subject: [PATCH 81/82] remove extraneous work being done around schema objects, no longer needed --- .../supergraphManagers/LegacyFetcher/index.ts | 52 ++++--------------- .../supergraphManagers/LocalCompose/index.ts | 47 ++--------------- 2 files changed, 13 insertions(+), 86 deletions(-) diff --git a/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts b/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts index 0787367635..cd7c1cf9c0 100644 --- a/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts +++ b/gateway-js/src/supergraphManagers/LegacyFetcher/index.ts @@ -20,10 +20,11 @@ import { SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction, } from '../..'; -import { composeAndValidate, compositionHasErrors, ServiceDefinition } from '@apollo/federation'; -import { GraphQLSchema, isIntrospectionType, isObjectType, parse } from 'graphql'; -import { buildComposedSchema } from '@apollo/query-planner'; -import { defaultFieldResolverWithAliasSupport } from '../../executeQueryPlan'; +import { + composeAndValidate, + compositionHasErrors, + ServiceDefinition, +} from '@apollo/federation'; export interface LegacyFetcherOptions { pollIntervalInMs?: number; @@ -47,7 +48,6 @@ export class LegacyFetcher implements SupergraphManager { private state: State; private compositionId?: string; private serviceDefinitions?: ServiceDefinition[]; - private schema?: GraphQLSchema; constructor(options: LegacyFetcherOptions) { this.config = options; @@ -148,15 +148,13 @@ export class LegacyFetcher implements SupergraphManager { return null; } - const previousSchema = this.schema; - - if (previousSchema) { + if (this.serviceDefinitions) { this.config.logger?.info('New service definitions were found.'); } this.serviceDefinitions = result.serviceDefinitions; - const { schema, supergraphSdl } = this.createSchemaFromServiceList( + const supergraphSdl = this.createSupergraphFromServiceList( result.serviceDefinitions, ); @@ -165,12 +163,11 @@ export class LegacyFetcher implements SupergraphManager { "A valid schema couldn't be composed. Falling back to previous schema.", ); } else { - this.schema = schema; return supergraphSdl; } } - private createSchemaFromServiceList(serviceList: ServiceDefinition[]) { + private createSupergraphFromServiceList(serviceList: ServiceDefinition[]) { this.config.logger?.debug( `Composing schema from service list: \n${serviceList .map(({ name, url }) => ` ${url || 'local'}: ${name}`) @@ -191,20 +188,9 @@ export class LegacyFetcher implements SupergraphManager { this.getDataSource?.(service); } - const schema = buildComposedSchema(parse(supergraphSdl)); - this.config.logger?.debug('Schema loaded and ready for execution'); - // This is a workaround for automatic wrapping of all fields, which Apollo - // Server does in the case of implementing resolver wrapping for plugins. - // Here we wrap all fields with support for resolving aliases as part of the - // root value which happens because aliases are resolved by sub services and - // the shape of the root value already contains the aliased fields as - // responseNames - return { - schema: wrapSchemaWithAliasResolver(schema), - supergraphSdl, - }; + return supergraphSdl; } } @@ -241,23 +227,3 @@ export class LegacyFetcher implements SupergraphManager { ); } } - -// We can't use transformSchema here because the extension data for query -// planning would be lost. Instead we set a resolver for each field -// in order to counteract GraphQLExtensions preventing a defaultFieldResolver -// from doing the same job -function wrapSchemaWithAliasResolver(schema: GraphQLSchema): GraphQLSchema { - const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach((typeName) => { - const type = typeMap[typeName]; - - if (isObjectType(type) && !isIntrospectionType(type)) { - const fields = type.getFields(); - Object.keys(fields).forEach((fieldName) => { - const field = fields[fieldName]; - field.resolve = defaultFieldResolverWithAliasSupport; - }); - } - }); - return schema; -} diff --git a/gateway-js/src/supergraphManagers/LocalCompose/index.ts b/gateway-js/src/supergraphManagers/LocalCompose/index.ts index d21f330f8e..6150bc97a1 100644 --- a/gateway-js/src/supergraphManagers/LocalCompose/index.ts +++ b/gateway-js/src/supergraphManagers/LocalCompose/index.ts @@ -5,19 +5,11 @@ import { compositionHasErrors, ServiceDefinition, } from '@apollo/federation'; -import { - GraphQLSchema, - isIntrospectionType, - isObjectType, - parse, -} from 'graphql'; -import { buildComposedSchema } from '@apollo/query-planner'; import { GetDataSourceFunction, SupergraphSdlHookOptions, SupergraphManager, } from '../../config'; -import { defaultFieldResolverWithAliasSupport } from '../../executeQueryPlan'; export interface LocalComposeOptions { logger?: Logger; @@ -43,9 +35,9 @@ export class LocalCompose implements SupergraphManager { this.getDataSource = getDataSource; let supergraphSdl: string | null = null; try { - ({ supergraphSdl } = this.createSchemaFromServiceList( + supergraphSdl = this.createSupergraphFromServiceList( this.config.localServiceList, - )); + ); } catch (e) { this.logUpdateFailure(e); throw e; @@ -55,7 +47,7 @@ export class LocalCompose implements SupergraphManager { }; } - private createSchemaFromServiceList(serviceList: ServiceDefinition[]) { + private createSupergraphFromServiceList(serviceList: ServiceDefinition[]) { this.config.logger?.debug( `Composing schema from service list: \n${serviceList .map(({ name, url }) => ` ${url || 'local'}: ${name}`) @@ -76,20 +68,9 @@ export class LocalCompose implements SupergraphManager { this.getDataSource?.(service); } - const schema = buildComposedSchema(parse(supergraphSdl)); - this.config.logger?.debug('Schema loaded and ready for execution'); - // This is a workaround for automatic wrapping of all fields, which Apollo - // Server does in the case of implementing resolver wrapping for plugins. - // Here we wrap all fields with support for resolving aliases as part of the - // root value which happens because aliases are resolved by sub services and - // the shape of the root value already contains the aliased fields as - // responseNames - return { - schema: wrapSchemaWithAliasResolver(schema), - supergraphSdl, - }; + return supergraphSdl; } } @@ -100,23 +81,3 @@ export class LocalCompose implements SupergraphManager { ); } } - -// We can't use transformSchema here because the extension data for query -// planning would be lost. Instead we set a resolver for each field -// in order to counteract GraphQLExtensions preventing a defaultFieldResolver -// from doing the same job -function wrapSchemaWithAliasResolver(schema: GraphQLSchema): GraphQLSchema { - const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach((typeName) => { - const type = typeMap[typeName]; - - if (isObjectType(type) && !isIntrospectionType(type)) { - const fields = type.getFields(); - Object.keys(fields).forEach((fieldName) => { - const field = fields[fieldName]; - field.resolve = defaultFieldResolverWithAliasSupport; - }); - } - }); - return schema; -} From fd995bbc0052bacd8f4066abeaa0746d1fdd82cd Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 Jan 2022 12:07:03 -0800 Subject: [PATCH 82/82] changelog tweaks --- gateway-js/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index e2d654f5aa..eff1f589f3 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -4,7 +4,7 @@ > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. -- __BREAKING__: Improve `supergraphSdl` configuration option, 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. +- __BREAKING__: 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`). @@ -19,7 +19,7 @@ Since the gateway itself is no longer responsible for composition: `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` <=v2.18. If you are still running one of these versions, server shutdown may not happen as smoothly. +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. [#1246](https://github.com/apollographql/federation/pull/1246)