Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### v2.4.0-alpha

- Implement an in-memory cache store to save parsed and validated documents and provide performance benefits for successful executions of the same document. [PR #2111](https://github.com/apollographql/apollo-server/pull/2111)

### v2.3.1

- Provide types for `graphql-upload` in a location where they can be accessed by TypeScript consumers of `apollo-server` packages. [ccf935f9](https://github.com/apollographql/apollo-server/commit/ccf935f9) [Issue #2092](https://github.com/apollographql/apollo-server/issues/2092)
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-cache-control/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-cache-control",
"version": "0.4.0",
"version": "0.5.0-alpha.0",
"description": "A GraphQL extension for cache control",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-datasource-rest/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-datasource-rest",
"version": "0.2.1",
"version": "0.3.0-alpha.0",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-datasource/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-datasource",
"version": "0.2.1",
"version": "0.3.0-alpha.0",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-engine-reporting/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-engine-reporting",
"version": "0.2.0",
"version": "0.3.0-alpha.0",
"description": "Send reports about your GraphQL services to Apollo Engine",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-azure-functions/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-server-azure-functions",
"version": "2.3.1",
"version": "2.4.0-alpha.0",
"description": "Production-ready Node.js GraphQL server for Azure Functions",
"keywords": [
"GraphQL",
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-cache-memcached/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-server-cache-memcached",
"version": "0.2.1",
"version": "0.3.0-alpha.0",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-cache-redis/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-server-cache-redis",
"version": "0.2.1",
"version": "0.3.0-alpha.0",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-caching/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-server-caching",
"version": "0.2.1",
"version": "0.3.0-alpha.0",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-cloud-functions/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-server-cloud-functions",
"version": "2.3.1",
"version": "2.4.0-alpha.0",
"description": "Production-ready Node.js GraphQL server for Google Cloud Functions",
"keywords": [
"GraphQL",
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-cloudflare/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-server-cloudflare",
"version": "2.3.1",
"version": "2.4.0-alpha.0",
"description": "Production-ready Node.js GraphQL server for Cloudflare workers",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-server-core",
"version": "2.3.1",
"version": "2.4.0-alpha.0",
"description": "Core engine for Apollo GraphQL server",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
21 changes: 21 additions & 0 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
GraphQLFieldResolver,
ValidationContext,
FieldDefinitionNode,
DocumentNode,
} from 'graphql';
import { GraphQLExtension } from 'graphql-extensions';
import { EngineReportingAgent } from 'apollo-engine-reporting';
Expand Down Expand Up @@ -114,6 +115,11 @@ export class ApolloServerBase {
// the default version is specified in playground.ts
protected playgroundOptions?: PlaygroundRenderPageOptions;

// An optionally defined store that, when enabled (default), will store the
// parsed and validated versions of operations in-memory, allowing subsequent
// parse and validates on the same operation to be executed immediately.
private documentStore?: InMemoryLRUCache<DocumentNode>;
Comment thread
abernix marked this conversation as resolved.

// The constructor should be universal across all environments. All environment specific behavior should be set by adding or overriding methods
constructor(config: Config) {
if (!config) throw new Error('ApolloServer requires options.');
Expand All @@ -136,6 +142,9 @@ export class ApolloServerBase {
...requestOptions
} = config;

// Initialize the document store. This cannot currently be disabled.
this.initializeDocumentStore();

// Plugins will be instantiated if they aren't already, and this.plugins
// is populated accordingly.
this.ensurePluginInstantiation(plugins);
Expand Down Expand Up @@ -486,6 +495,17 @@ export class ApolloServerBase {
});
}

private initializeDocumentStore(): void {
this.documentStore = new InMemoryLRUCache({
// Create ~about~ a 30MiB InMemoryLRUCache. This is less than precise
// since the technique to calculate the size of a DocumentNode is
// only using JSON.stringify on the DocumentNode (and thus doesn't account
// for unicode characters, etc.), but it should do a reasonable job at
// providing a caching document store for most operations.
maxSize: Math.pow(2, 20) * 30,
});
}

// This function is used by the integrations to generate the graphQLOptions
// from an object containing the request and other integration specific
// options
Expand All @@ -509,6 +529,7 @@ export class ApolloServerBase {
return {
schema: this.schema,
plugins: this.plugins,
documentStore: this.documentStore,
extensions: this.extensions,
context,
// Allow overrides from options. Be explicit about a couple of them to
Expand Down
164 changes: 164 additions & 0 deletions packages/apollo-server-core/src/__tests__/runQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import {
import { processGraphQLRequest, GraphQLRequest } from '../requestPipeline';
import { Request } from 'apollo-server-env';
import { GraphQLOptions, Context as GraphQLContext } from 'apollo-server-core';
import {
ApolloServerPlugin,
GraphQLRequestListener,
} from 'apollo-server-plugin-base';
import { InMemoryLRUCache } from 'apollo-server-caching';

// This is a temporary kludge to ensure we preserve runQuery behavior with the
// GraphQLRequestProcessor refactoring.
Expand Down Expand Up @@ -49,10 +54,12 @@ interface QueryOptions
| 'cacheControl'
| 'context'
| 'debug'
| 'documentStore'
| 'extensions'
| 'fieldResolver'
| 'formatError'
| 'formatResponse'
| 'plugins'
| 'rootValue'
| 'schema'
| 'tracing'
Expand Down Expand Up @@ -444,6 +451,163 @@ describe('runQuery', () => {
});
});

describe('parsing and validation cache', () => {
function createLifecyclePluginMocks() {
const validationDidStart = jest.fn();
const parsingDidStart = jest.fn();

const plugins: ApolloServerPlugin[] = [
{
requestDidStart() {
return {
validationDidStart,
parsingDidStart,
} as GraphQLRequestListener;
},
},
];

return {
plugins,
events: { validationDidStart, parsingDidStart },
};
}

function runRequest({
queryString = '{ testString }',
plugins = [],
documentStore,
}: {
queryString?: string;
plugins?: ApolloServerPlugin[];
documentStore?: QueryOptions['documentStore'];
}) {
return runQuery({
schema,
documentStore,
queryString,
plugins,
request: new MockReq(),
});
}

function forgeLargerTestQuery(
count: number,
prefix: string = 'prefix',
): string {
if (count <= 0) {
count = 1;
}

let query: string = '';

for (let q = 0; q < count; q++) {
query += ` ${prefix}_${count}: testString\n`;
}

return '{\n' + query + '}';
}

it('validates each time when the documentStore is not present', async () => {
expect.assertions(4);

const {
plugins,
events: { parsingDidStart, validationDidStart },
} = createLifecyclePluginMocks();

// The first request will do a parse and validate. (1/1)
await runRequest({ plugins });
expect(parsingDidStart.mock.calls.length).toBe(1);
expect(validationDidStart.mock.calls.length).toBe(1);

// The second request should ALSO do a parse and validate. (2/2)
await runRequest({ plugins });
expect(parsingDidStart.mock.calls.length).toBe(2);
expect(validationDidStart.mock.calls.length).toBe(2);
});

it('caches the DocumentNode in the documentStore when instrumented', async () => {
expect.assertions(4);
const documentStore = new InMemoryLRUCache<DocumentNode>();

const {
plugins,
events: { parsingDidStart, validationDidStart },
} = createLifecyclePluginMocks();

// An uncached request will have 1 parse and 1 validate call.
await runRequest({ plugins, documentStore });
expect(parsingDidStart.mock.calls.length).toBe(1);
expect(validationDidStart.mock.calls.length).toBe(1);

// The second request should still only have a 1 validate and 1 parse.
await runRequest({ plugins, documentStore });
expect(parsingDidStart.mock.calls.length).toBe(1);
expect(validationDidStart.mock.calls.length).toBe(1);

console.log(documentStore);
});

it("the documentStore calculates the DocumentNode's length by its JSON.stringify'd representation", async () => {
expect.assertions(14);
const {
plugins,
events: { parsingDidStart, validationDidStart },
} = createLifecyclePluginMocks();

const queryLarge = forgeLargerTestQuery(3, 'large');
const querySmall1 = forgeLargerTestQuery(1, 'small1');
const querySmall2 = forgeLargerTestQuery(1, 'small2');

// We're going to create a smaller-than-default cache which will be the
// size of the two smaller queries. All three of these queries will never
// fit into this cache, so we'll roll through them all.
const maxSize =
JSON.stringify(parse(querySmall1)).length +
JSON.stringify(parse(querySmall2)).length;

const documentStore = new InMemoryLRUCache<DocumentNode>({ maxSize });

await runRequest({ plugins, documentStore, queryString: querySmall1 });
expect(parsingDidStart.mock.calls.length).toBe(1);
expect(validationDidStart.mock.calls.length).toBe(1);

await runRequest({ plugins, documentStore, queryString: querySmall2 });
expect(parsingDidStart.mock.calls.length).toBe(2);
expect(validationDidStart.mock.calls.length).toBe(2);

// This query should be large enough to evict both of the previous
// from the LRU cache since it's larger than the TOTAL limit of the cache
// (which is capped at the length of small1 + small2) — though this will
// still fit (barely).
await runRequest({ plugins, documentStore, queryString: queryLarge });
expect(parsingDidStart.mock.calls.length).toBe(3);
expect(validationDidStart.mock.calls.length).toBe(3);

// Make sure the large query is still cached (No incr. to parse/validate.)
await runRequest({ plugins, documentStore, queryString: queryLarge });
expect(parsingDidStart.mock.calls.length).toBe(3);
expect(validationDidStart.mock.calls.length).toBe(3);

// This small (and the other) should both trigger parse/validate since
// the cache had to have evicted them both after accommodating the larger.
await runRequest({ plugins, documentStore, queryString: querySmall1 });
expect(parsingDidStart.mock.calls.length).toBe(4);
expect(validationDidStart.mock.calls.length).toBe(4);

await runRequest({ plugins, documentStore, queryString: querySmall2 });
expect(parsingDidStart.mock.calls.length).toBe(5);
expect(validationDidStart.mock.calls.length).toBe(5);

// Finally, make sure that the large query is gone (it should be, after
// the last two have take its spot again.)
await runRequest({ plugins, documentStore, queryString: queryLarge });
expect(parsingDidStart.mock.calls.length).toBe(6);
expect(validationDidStart.mock.calls.length).toBe(6);
});
});

describe('async_hooks', () => {
let asyncHooks: typeof import('async_hooks');
let asyncHook: import('async_hooks').AsyncHook;
Expand Down
3 changes: 2 additions & 1 deletion packages/apollo-server-core/src/graphqlOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from 'graphql';
import { GraphQLExtension } from 'graphql-extensions';
import { CacheControlExtensionOptions } from 'apollo-cache-control';
import { KeyValueCache } from 'apollo-server-caching';
import { KeyValueCache, InMemoryLRUCache } from 'apollo-server-caching';
import { DataSource } from 'apollo-datasource';
import { ApolloServerPlugin } from 'apollo-server-plugin-base';

Expand Down Expand Up @@ -43,6 +43,7 @@ export interface GraphQLServerOptions<
cache?: KeyValueCache;
persistedQueries?: PersistedQueryOptions;
plugins?: ApolloServerPlugin[];
documentStore?: InMemoryLRUCache<DocumentNode>;
}

export type DataSources<TContext> = {
Expand Down
Loading