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

Add cache option to config to expose ApolloGateway's queryPlanStore #2385

Merged
merged 7 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 38 additions & 0 deletions .changeset/forty-gifts-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"@apollo/query-planner": minor
"@apollo/gateway": minor
---

This change introduces a configurable query plan cache. This option allows
developers to provide their own query plan cache like so:

```
new ApolloGateway({
queryPlannerConfig: {
cache: new MyCustomQueryPlanCache(),
},
});
```

The current default implementation is effectively as follows:
```
import { InMemoryLRUCache } from "@apollo/utils.keyvaluecache";

const cache = new InMemoryLRUCache<string>({
maxSize: Math.pow(2, 20) * 30,
sizeCalculation<T>(obj: T): number {
return Buffer.byteLength(JSON.stringify(obj), "utf8");
},
});
```

TypeScript users should implement the `QueryPlanCache` type which is now
exported by `@apollo/query-planner`:
```
import { QueryPlanCache } from '@apollo/query-planner';

class MyCustomQueryPlanCache implements QueryPlanCache {
// ...
}
```

1 change: 0 additions & 1 deletion gateway-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"@types/node-fetch": "^2.6.2",
"async-retry": "^1.3.3",
"loglevel": "^1.6.1",
"lru-cache": "^7.13.1",
"make-fetch-happen": "^11.0.0",
"node-abort-controller": "^3.0.1",
"node-fetch": "^2.6.7"
Expand Down
40 changes: 37 additions & 3 deletions gateway-js/src/__tests__/gateway/endToEnd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import { GraphQLSchemaModule } from '@apollo/subgraph/src/schema-helper';
import { buildSchema, ObjectType, ServiceDefinition } from '@apollo/federation-internals';
import gql from 'graphql-tag';
import { printSchema } from 'graphql';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
import { QueryPlan } from '@apollo/query-planner';
import { createHash } from '@apollo/utils.createhash';
import { QueryPlanCache } from '@apollo/query-planner';

function approximateObjectSize<T>(obj: T): number {
return Buffer.byteLength(JSON.stringify(obj), 'utf8');
}

async function startFederatedServer(modules: GraphQLSchemaModule[]) {
const schema = buildSubgraphSchema(modules);
Expand All @@ -25,7 +33,7 @@ let gateway: ApolloGateway;
let gatewayServer: ApolloServer;
let gatewayUrl: string;

async function startServicesAndGateway(servicesDefs: ServiceDefinition[]) {
async function startServicesAndGateway(servicesDefs: ServiceDefinition[], cache?: QueryPlanCache) {
backendServers = [];
const serviceList = [];
for (const serviceDef of servicesDefs) {
Expand All @@ -34,7 +42,11 @@ async function startServicesAndGateway(servicesDefs: ServiceDefinition[]) {
serviceList.push({ name: serviceDef.name, url });
}

gateway = new ApolloGateway({ serviceList });
gateway = new ApolloGateway({
serviceList,
queryPlannerConfig: cache ? { cache } : undefined,
});

gatewayServer = new ApolloServer({
gateway,
});
Expand All @@ -60,9 +72,31 @@ afterEach(async () => {
}
});


describe('caching', () => {
const cache = new InMemoryLRUCache<QueryPlan>({maxSize: Math.pow(2, 20) * (30), sizeCalculation: approximateObjectSize});
beforeEach(async () => {
await startServicesAndGateway(fixtures);
await startServicesAndGateway(fixtures, cache);
});

it(`cached query plan`, async () => {
const query = `
query {
me {
name {
first
last
}
}
topProducts {
name
}
}
`;

await queryGateway(query);
const queryHash:string = createHash('sha256').update(query).digest('hex');
expect(await cache.get(queryHash)).toBeTruthy();
});

it(`cache control`, async () => {
Expand Down
14 changes: 9 additions & 5 deletions gateway-js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { deprecate } from 'util';
import { createHash } from '@apollo/utils.createhash';
import type { Logger } from '@apollo/utils.logger';
import LRUCache from 'lru-cache';
import { QueryPlanCache } from '@apollo/query-planner'
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
import {
GraphQLSchema,
VariableDefinitionNode,
Expand Down Expand Up @@ -123,7 +124,7 @@ export class ApolloGateway implements GatewayInterface {
private serviceMap: DataSourceMap = Object.create(null);
private config: GatewayConfig;
private logger: Logger;
private queryPlanStore: LRUCache<string, QueryPlan>;
private queryPlanStore: QueryPlanCache;
private apolloConfig?: ApolloConfigFromAS3;
private onSchemaChangeListeners = new Set<(schema: GraphQLSchema) => void>();
private onSchemaLoadOrUpdateListeners = new Set<
Expand Down Expand Up @@ -189,14 +190,17 @@ export class ApolloGateway implements GatewayInterface {
}

private initQueryPlanStore(approximateQueryPlanStoreMiB?: number) {
if(this.config.queryPlannerConfig?.cache){
return this.config.queryPlannerConfig?.cache
}
// Create ~about~ a 30MiB InMemoryLRUCache (or 50MiB if the full operation ASTs are
// enabled in query plans as this requires plans to use more memory). 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.
const defaultSize = this.config.queryPlannerConfig?.exposeDocumentNodeInFetchNode ? 50 : 30;
return new LRUCache<string, QueryPlan>({
return new InMemoryLRUCache<QueryPlan>({
maxSize: Math.pow(2, 20) * (approximateQueryPlanStoreMiB || defaultSize),
sizeCalculation: approximateObjectSize,
});
Expand Down Expand Up @@ -768,7 +772,7 @@ export class ApolloGateway implements GatewayInterface {
span.setStatus({ code: SpanStatusCode.ERROR });
return { errors: validationErrors };
}
let queryPlan = this.queryPlanStore.get(queryPlanStoreKey);
let queryPlan = await this.queryPlanStore.get(queryPlanStoreKey);

if (!queryPlan) {
queryPlan = tracer.startActiveSpan(
Expand All @@ -792,7 +796,7 @@ export class ApolloGateway implements GatewayInterface {
);

try {
this.queryPlanStore.set(queryPlanStoreKey, queryPlan);
await this.queryPlanStore.set(queryPlanStoreKey, queryPlan);
} catch (err) {
this.logger.warn(
'Could not store queryPlan' + ((err && err.message) || err),
Expand Down
2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions query-planner-js/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Concrete } from "@apollo/federation-internals";
import { QueryPlan } from ".";
import { InMemoryLRUCache, KeyValueCache } from '@apollo/utils.keyvaluecache';

export type QueryPlanCache = KeyValueCache<QueryPlan> & { clear: () => void }

export type QueryPlannerConfig = {
/**
Expand Down Expand Up @@ -45,6 +49,7 @@ export type QueryPlannerConfig = {
*/
enableDefer?: boolean,
}
cache?: QueryPlanCache,
}

export function enforceQueryPlannerConfigDefaults(
Expand All @@ -56,6 +61,7 @@ export function enforceQueryPlannerConfigDefaults(
incrementalDelivery: {
enableDefer: false,
},
cache: new InMemoryLRUCache<QueryPlan>({maxSize: Math.pow(2, 20) * 50 }),
...config,
};
}
2 changes: 1 addition & 1 deletion query-planner-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ export { prettyFormatQueryPlan } from './prettyFormatQueryPlan';

export * from './QueryPlan';
export { QueryPlanner } from './buildPlan';
export { QueryPlannerConfig } from './config';
export { QueryPlanCache, QueryPlannerConfig } from './config';