Skip to content

Commit

Permalink
Adds debug query planner option to skip query planning for a single s…
Browse files Browse the repository at this point in the history
…ubgraph (#2441)

This commit adds a new option to the query planner config that, when
used and when the supergraph is built from only a single subgraph, skip
the query planning algorithm and instead generates a single fetch (to
the one subgraph) that directly uses the gateway "input" query.

This option is disabled by default and marked as a "debug" option as it
is a bit of a hack and so we're not suggesting committing to supporting
it forever. But it is easy enough to have now and can be convenient at
least for testing/debugging.

Note in particular that, when used, some of the features enabled by
the query planner (for instance `@defer`) cannot be enabled. The option
will also stop having any effect as soon as another subgraph is added,
not matter what that 2nd subgraph contains.
  • Loading branch information
Sylvain Lebresne committed Mar 8, 2023
1 parent 2a35f55 commit ade7ceb
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 118 deletions.
9 changes: 9 additions & 0 deletions .changeset/shaggy-knives-exercise.md
@@ -0,0 +1,9 @@
---
"@apollo/query-planner": minor
"@apollo/gateway": minor
---

Adds debug/testing query planner options (`debug.bypassPlannerForSingleSubgraph`) to bypass the query planning
process for federated supergraph having only a single subgraph. The option is disabled by default, is not recommended
for production, and is not supported (it may be removed later). It is meant for debugging/testing purposes.

91 changes: 19 additions & 72 deletions gateway-js/src/__tests__/gateway/endToEnd.test.ts
@@ -1,82 +1,29 @@
import { buildSubgraphSchema } from '@apollo/subgraph';
import { ApolloServer } from 'apollo-server';
import fetch, { Response } from 'node-fetch';
import { ApolloGateway } from '../..';
import { fixtures } from 'apollo-federation-integration-testsuite';
import { ApolloServerPluginInlineTrace } from 'apollo-server-core';
import { GraphQLSchemaModule } from '@apollo/subgraph/src/schema-helper';
import { buildSchema, ObjectType, ServiceDefinition } from '@apollo/federation-internals';
import { buildSchema, ObjectType } from '@apollo/federation-internals';
import gql from 'graphql-tag';
import { printSchema } from 'graphql';
import { startSubgraphsAndGateway, Services } from './testUtils'
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);
const server = new ApolloServer({
schema,
// Manually installing the inline trace plugin means it doesn't log a message.
plugins: [ApolloServerPluginInlineTrace()],
});
const { url } = await server.listen({ port: 0 });
return { url, server };
}

let backendServers: ApolloServer[];
let gateway: ApolloGateway;
let gatewayServer: ApolloServer;
let gatewayUrl: string;

async function startServicesAndGateway(servicesDefs: ServiceDefinition[], cache?: QueryPlanCache) {
backendServers = [];
const serviceList = [];
for (const serviceDef of servicesDefs) {
const { server, url } = await startFederatedServer([serviceDef]);
backendServers.push(server);
serviceList.push({ name: serviceDef.name, url });
}

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

gatewayServer = new ApolloServer({
gateway,
});
({ url: gatewayUrl } = await gatewayServer.listen({ port: 0 }));
}

async function queryGateway(query: string): Promise<Response> {
return fetch(gatewayUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
});
}
let services: Services;

afterEach(async () => {
for (const server of backendServers) {
await server.stop();
}
if (gatewayServer) {
await gatewayServer.stop();
if (services) {
await services.stop();
}
});


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

it(`cached query plan`, async () => {
Expand All @@ -94,7 +41,7 @@ describe('caching', () => {
}
`;

await queryGateway(query);
await services.queryGateway(query);
const queryHash:string = createHash('sha256').update(query).digest('hex');
expect(await cache.get(queryHash)).toBeTruthy();
});
Expand All @@ -114,7 +61,7 @@ describe('caching', () => {
}
`;

const response = await queryGateway(query);
const response = await services.queryGateway(query);
const result = await response.json();
expect(result).toMatchInlineSnapshot(`
Object {
Expand Down Expand Up @@ -168,7 +115,7 @@ describe('caching', () => {
}
`;

const response = await queryGateway(query);
const response = await services.queryGateway(query);
const result = await response.json();
expect(result).toMatchInlineSnapshot(`
Object {
Expand Down Expand Up @@ -266,7 +213,7 @@ describe('end-to-end features', () => {
}
};

await startServicesAndGateway([subgraphA, subgraphB]);
services = await startSubgraphsAndGateway([subgraphA, subgraphB]);

const query = `
{
Expand All @@ -277,7 +224,7 @@ describe('end-to-end features', () => {
}
`;

const response = await queryGateway(query);
const response = await services.queryGateway(query);
const result = await response.json();
expect(result).toMatchInlineSnapshot(`
Object {
Expand All @@ -290,7 +237,7 @@ describe('end-to-end features', () => {
}
`);

const supergraphSdl = gateway.__testing().supergraphSdl;
const supergraphSdl = services.gateway.__testing().supergraphSdl;
expect(supergraphSdl).toBeDefined();
const supergraph = buildSchema(supergraphSdl!);
const typeT = supergraph.type('T') as ObjectType;
Expand Down Expand Up @@ -340,7 +287,7 @@ describe('end-to-end features', () => {
}
};

await startServicesAndGateway([subgraphA, subgraphB]);
services = await startSubgraphsAndGateway([subgraphA, subgraphB]);

const query = `
{
Expand All @@ -351,7 +298,7 @@ describe('end-to-end features', () => {
}
`;

const response = await queryGateway(query);
const response = await services.queryGateway(query);
const result = await response.json();
expect(result).toMatchInlineSnapshot(`
Object {
Expand Down Expand Up @@ -433,7 +380,7 @@ describe('end-to-end features', () => {
}
};

await startServicesAndGateway([subgraphA, subgraphB]);
services = await startSubgraphsAndGateway([subgraphA, subgraphB]);

const q1 = `
{
Expand All @@ -445,7 +392,7 @@ describe('end-to-end features', () => {
}
`;

const resp1 = await queryGateway(q1);
const resp1 = await services.queryGateway(q1);
const res1 = await resp1.json();
expect(res1).toMatchInlineSnapshot(`
Object {
Expand All @@ -460,7 +407,7 @@ describe('end-to-end features', () => {
`);

// Make sure the exposed API doesn't have any @inaccessible elements.
expect(printSchema(gateway.schema!)).toMatchInlineSnapshot(`
expect(printSchema(services.gateway.schema!)).toMatchInlineSnapshot(`
"enum E {
FOO
}
Expand All @@ -483,7 +430,7 @@ describe('end-to-end features', () => {
f(e: BAR)
}
`;
const resp2 = await queryGateway(q2);
const resp2 = await services.queryGateway(q2);
const res2 = await resp2.json();
expect(res2).toMatchInlineSnapshot(`
Object {
Expand All @@ -505,7 +452,7 @@ describe('end-to-end features', () => {
}
}
`;
const resp3 = await queryGateway(q3);
const resp3 = await services.queryGateway(q3);
const res3 = await resp3.json();
expect(res3).toMatchInlineSnapshot(`
Object {
Expand Down
101 changes: 101 additions & 0 deletions gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts
@@ -0,0 +1,101 @@
import gql from 'graphql-tag';
import { startSubgraphsAndGateway, Services } from './testUtils'

let services: Services;

afterEach(async () => {
if (services) {
await services.stop();
}
});

describe('`debug.bypassPlannerForSingleSubgraph` config', () => {
const subgraph = {
name: 'A',
url: 'https://A',
typeDefs: gql`
type Query {
a: A
}
type A {
b: B
}
type B {
x: Int
y: String
}
`,
resolvers: {
Query: {
a: () => ({
b: {
x: 1,
y: 'foo',
}
}),
}
}
};

const query = `
{
a {
b {
x
y
}
}
}
`;

const expectedResult = `
Object {
"data": Object {
"a": Object {
"b": Object {
"x": 1,
"y": "foo",
},
},
},
}
`;

it('is disabled by default', async () => {
services = await startSubgraphsAndGateway([subgraph]);

const response = await services.queryGateway(query);
const result = await response.json();
expect(result).toMatchInlineSnapshot(expectedResult);

const queryPlanner = services.gateway.__testing().queryPlanner!;
// If the query planner is genuinely used, we shoud have evaluated 1 plan.
expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1);
});

it('works when enabled', async () => {
services = await startSubgraphsAndGateway(
[subgraph],
{
gatewayConfig: {
queryPlannerConfig: {
debug: {
bypassPlannerForSingleSubgraph: true,
}
}
}
}
);

const response = await services.queryGateway(query);
const result = await response.json();
expect(result).toMatchInlineSnapshot(expectedResult);

const queryPlanner = services.gateway.__testing().queryPlanner!;
// The `bypassPlannerForSingleSubgraph` doesn't evaluate anything. It's use is the only case where `evaluatedPlanCount` can be 0.
expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(0);
});
});

0 comments on commit ade7ceb

Please sign in to comment.