Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gateway: introduce make-fetch-happen #3783

Merged
merged 13 commits into from
Feb 20, 2020
Merged
431 changes: 356 additions & 75 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions packages/apollo-federation/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
const config = require('../../jest.config.base');

module.exports = Object.assign(Object.create(null), config, {
const NODE_MAJOR_VERSION = parseInt(
process.versions.node.split('.', 1)[0],
10
);

const additionalConfig = {
setupFiles: [
'core-js/features/array/flat',
'core-js/features/array/flat-map',
],
});
testPathIgnorePatterns: [
...config.testPathIgnorePatterns,
...NODE_MAJOR_VERSION === 6 ? ["<rootDir>"] : []
]
};

module.exports = Object.assign(Object.create(null), config, additionalConfig);
1 change: 1 addition & 0 deletions packages/apollo-gateway/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- __BREAKING__: The behavior and signature of `RemoteGraphQLDataSource`'s `didReceiveResponse` method has been changed. No changes are necessary _unless_ your implementation has overridden the default behavior of this method by either extending the class and overriding the method or by providing `didReceiveResponse` as a parameter to the `RemoteGraphQLDataSource`'s constructor options. Implementations which have provided their own `didReceiveResponse` using either of these methods should view the PR linked here for details on what has changed. [PR #3743](https://github.com/apollographql/apollo-server/pull/3743)
- __NEW__: Setting the `apq` option to `true` on the `RemoteGraphQLDataSource` will enable the use of [automated persisted queries (APQ)](https://www.apollographql.com/docs/apollo-server/performance/apq/) when sending queries to downstream services. Depending on the complexity of queries sent to downstream services, this technique can greatly reduce the size of the payloads being transmitted over the network. Downstream implementing services must also support APQ functionality to participate in this feature (Apollo Server does by default unless it has been explicitly disabled). As with normal APQ behavior, a downstream server must have received and registered a query once before it will be able to serve an APQ request. [#3744](https://github.com/apollographql/apollo-server/pull/3744)
- __NEW__: Experimental feature: compress downstream requests via generated fragments [#3791](https://github.com/apollographql/apollo-server/pull/3791) This feature enables the gateway to generate fragments for queries to downstream services in order to minimize bytes over the wire and parse time. This can be enabled via the gateway config by setting `experimental_autoFragmentization: true`. It is currently disabled by default.
- Introduce `make-fetch-happen` package. Remove `cachedFetcher` in favor of the caching implementation provided by this package. [#3783](https://github.com/apollographql/apollo-server/pull/3783/files)

## v0.12.1

Expand Down
12 changes: 11 additions & 1 deletion packages/apollo-gateway/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
const path = require('path');
const config = require('../../jest.config.base');

const NODE_MAJOR_VERSION = parseInt(
process.versions.node.split('.', 1)[0],
10
);

const additionalConfig = {
setupFilesAfterEnv: [path.resolve(__dirname, './src/__tests__/testSetup.ts')],
testPathIgnorePatterns: [
...config.testPathIgnorePatterns,
...NODE_MAJOR_VERSION === 6 ? ["<rootDir>"] : []
]
};
module.exports = Object.assign(Object.create(null), additionalConfig, config);

module.exports = Object.assign(Object.create(null), config, additionalConfig);
1 change: 1 addition & 0 deletions packages/apollo-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"graphql-extensions": "file:../graphql-extensions",
"loglevel": "^1.6.1",
"loglevel-debug": "^0.0.1",
"make-fetch-happen": "^7.1.1",
"pretty-format": "^24.7.0"
},
"peerDependencies": {
Expand Down
55 changes: 55 additions & 0 deletions packages/apollo-gateway/src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { CacheManager } from 'make-fetch-happen';
import { Request, Response, Headers } from 'apollo-server-env';
import { InMemoryLRUCache } from 'apollo-server-caching';

const MAX_SIZE = 5 * 1024 * 1024; // 5MB

function cacheKey(request: Request) {
return `gateway:request-cache:${request.method}:${request.url}`;
}

interface CachedRequest {
body: string;
status: number;
statusText: string;
headers: Headers;
}

export class HttpRequestCache implements CacheManager {
constructor(
public cache: InMemoryLRUCache<CachedRequest> = new InMemoryLRUCache({
maxSize: MAX_SIZE,
}),
) {}

// Return true if entry exists, else false
async delete(request: Request) {
const key = cacheKey(request);
const entry = await this.cache.get(key);
await this.cache.delete(key);
return Boolean(entry);
}
Comment on lines +26 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need to implement a delete method? When does it get called?

Copy link
Member Author

@trevor-scheer trevor-scheer Feb 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ours is never actually called (at least within our tests), however I do want our exported interface to comply with make-fetch-happen's requirements listed here: https://github.com/npm/make-fetch-happen#--optscachemanager

On that note, I'm unsure if I should export the CacheManager interface from the gateway package or if I can leave it in the make-fetch-happen definition file. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trevor-scheer I think we should leave it in make-fetch-happen, and avoid exposing this directly to the user, for now.


async put(request: Request, response: Response) {
let body = await response.text();

this.cache.set(cacheKey(request), {
body,
status: response.status,
statusText: response.statusText,
headers: response.headers,
});

return new Response(body, response);
}

async match(request: Request) {
return this.cache.get(cacheKey(request)).then(response => {
if (response) {
const { body, ...requestInit } = response;
return new Response(body, requestInit);
}
return;
});
}
}
69 changes: 0 additions & 69 deletions packages/apollo-gateway/src/cachedFetcher.ts

This file was deleted.

13 changes: 11 additions & 2 deletions packages/apollo-gateway/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import { GraphQLDataSource } from './datasources/types';
import { RemoteGraphQLDataSource } from './datasources/RemoteGraphQLDataSource';
import { HeadersInit } from 'node-fetch';
import { getVariableValues } from 'graphql/execution/values';
import { CachedFetcher } from './cachedFetcher';
import fetcher, { Fetcher } from 'make-fetch-happen';
import { HttpRequestCache } from './cache';

export type ServiceEndpointDefinition = Pick<ServiceDefinition, 'name' | 'url'>;

Expand All @@ -60,6 +61,7 @@ interface GatewayConfigBase {
experimental_pollInterval?: number;
experimental_approximateQueryPlanStoreMiB?: number;
experimental_autoFragmentization?: boolean;
fetcher?: Fetcher;
}

interface RemoteGatewayConfig extends GatewayConfigBase {
Expand Down Expand Up @@ -161,7 +163,10 @@ export class ApolloGateway implements GraphQLService {
private serviceDefinitions: ServiceDefinition[] = [];
private compositionMetadata?: CompositionMetadata;
private serviceSdlCache = new Map<string, string>();
private fetcher = new CachedFetcher();

private fetcher: Fetcher = fetcher.defaults({
trevor-scheer marked this conversation as resolved.
Show resolved Hide resolved
cacheManager: new HttpRequestCache()
});

// Observe query plan, service info, and operation info prior to execution.
// The information made available here will give insight into the resulting
Expand Down Expand Up @@ -247,6 +252,10 @@ export class ApolloGateway implements GraphQLService {
'If you are polling running services, use with caution.',
);
}

if (config.fetcher) {
this.fetcher = config.fetcher;
}
}
}

Expand Down
46 changes: 19 additions & 27 deletions packages/apollo-gateway/src/loadServicesFromStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CachedFetcher } from './cachedFetcher';
import { Fetcher } from 'make-fetch-happen';
import { parse } from 'graphql';
import { Experimental_UpdateServiceDefinitions } from '.';

Expand Down Expand Up @@ -61,54 +61,46 @@ export async function getServiceDefinitionsFromStorage({
apiKeyHash: string;
graphVariant?: string;
federationVersion: number;
fetcher: CachedFetcher;
fetcher: Fetcher;
}): ReturnType<Experimental_UpdateServiceDefinitions> {
// fetch the storage secret
const storageSecretUrl = getStorageSecretUrl(graphId, apiKeyHash);
const response = await fetcher.fetch(storageSecretUrl);
const secret = JSON.parse(response.result);

const secret: string = await fetcher(storageSecretUrl).then(response =>
response.json(),
);

if (!graphVariant) {
graphVariant = 'current';
}

const baseUrl = `${urlPartialSchemaBase}/${secret}/${graphVariant}/v${federationVersion}`;

const {
isCacheHit: linkFileCacheHit,
result: linkFileResult,
} = await fetcher.fetch(`${baseUrl}/composition-config-link`);

// If the link file is a cache hit, no further work is needed
if (linkFileCacheHit) return { isNewSchema: false };
const response = await fetcher(`${baseUrl}/composition-config-link`);

const parsedLink = JSON.parse(linkFileResult) as LinkFileResult;
if (response.status === 304) {
return { isNewSchema: false };
}

const { result: configFileResult } = await fetcher.fetch(
`${urlPartialSchemaBase}/${parsedLink.configPath}`,
);
const linkFileResult: LinkFileResult = await response.json();

const compositionMetadata = JSON.parse(
configFileResult,
) as CompositionMetadata;
const compositionMetadata: CompositionMetadata = await fetcher(
`${urlPartialSchemaBase}/${linkFileResult.configPath}`,
).then(response => response.json());

// It's important to maintain the original order here
const serviceDefinitions = await Promise.all(
compositionMetadata.implementingServiceLocations.map(
async ({ name, path }) => {
const serviceLocation = await fetcher.fetch(
const { url, partialSchemaPath }: ImplementingService = await fetcher(
`${urlPartialSchemaBase}/${path}`,
);

const { url, partialSchemaPath } = JSON.parse(
serviceLocation.result,
) as ImplementingService;
).then(response => response.json());

const { result } = await fetcher.fetch(
const sdl = await fetcher(
`${urlPartialSchemaBase}/${partialSchemaPath}`,
);
).then(response => response.text());

return { name, url, typeDefs: parse(result) };
return { name, url, typeDefs: parse(sdl) };
},
),
);
Expand Down
53 changes: 53 additions & 0 deletions packages/apollo-gateway/src/make-fetch-happen.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
declare module 'make-fetch-happen' {
import {
Response,
Request,
RequestInfo,
RequestInit,
} from 'apollo-server-env';

// If adding to these options, they should mirror those from `make-fetch-happen`
// @see: https://github.com/npm/make-fetch-happen/#extra-options
export interface FetcherOptions {
abernix marked this conversation as resolved.
Show resolved Hide resolved
trevor-scheer marked this conversation as resolved.
Show resolved Hide resolved
cacheManager?: string | CacheManager;
// @see: https://www.npmjs.com/package/retry#retrytimeoutsoptions
retry?:
| boolean
| number
| {
// The maximum amount of times to retry the operation. Default is 10. Seting this to 1 means do it once, then retry it once
retries?: number;
// The exponential factor to use. Default is 2.
factor?: number;
// The number of milliseconds before starting the first retry. Default is 1000.
minTimeout?: number;
// The maximum number of milliseconds between two retries. Default is Infinity.
maxTimeout?: number;
// Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false.
randomize?: boolean;
};
onRetry?(): void;
}

export interface CacheManager {
delete(req: Request): Promise<Boolean>;
put(req: Request, res: Response): Promise<Response>;
match(req: Request): Promise<Response | undefined>;
}

/**
* This is an augmentation of the fetch function types provided by `apollo-server-env`
* @see: https://git.io/JvBwX
*/
export interface Fetcher {
(input?: RequestInfo, init?: RequestInit & FetcherOptions): Promise<
Response
>;
}

let fetch: Fetcher & {
defaults(opts?: FetcherOptions): Fetcher;
};

export default fetch;
}