Skip to content

Commit

Permalink
Gateway: introduce make-fetch-happen (apollographql/apollo-server#3783
Browse files Browse the repository at this point in the history
)

* Introduce `make-fetch-happen` for GCS requests
* Implement in memory cache manager for make-fetch-happen
* Provide local typings for `make-fetch-happen`
* Skip node v6 tests for federation and gateway packages
Apollo-Orig-Commit-AS: apollographql/apollo-server@9420aa0
  • Loading branch information
trevor-scheer authored Feb 20, 2020
1 parent c19a13b commit e77a0cb
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 101 deletions.
15 changes: 13 additions & 2 deletions federation-js/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 gateway-js/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 gateway-js/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 gateway-js/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 gateway-js/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);
}

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 gateway-js/src/cachedFetcher.ts

This file was deleted.

13 changes: 11 additions & 2 deletions gateway-js/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({
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 gateway-js/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 gateway-js/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 {
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;
}

0 comments on commit e77a0cb

Please sign in to comment.