Skip to content

Commit

Permalink
Gateway(experimental): compress downstream requests via generat… (apo…
Browse files Browse the repository at this point in the history
…llographql/apollo-server#3791)

For large queries (esp. ones that leverage many nested fragments),
queries to downstream services can blow up astronomically because
the gateway expands everything during a downstream request.

i.e. we've seen a ~700 line query become over 200k lines due to
this expansion.

This commit has the gateway build fragments on the fly for the
selection sets that it encounters and reuses them whenever possible.
This is an initial and simple implementation, but with tangible wins.

This is currently being landed as an experimental feature that is
disabled by default. To enable, simply add to your gateway config:
experimental_autoFragmentization: true

Apollo-Orig-Commit-AS: apollographql/apollo-server@c48c0a3
  • Loading branch information
trevor-scheer authored Feb 15, 2020
1 parent e481999 commit 19c83bb
Show file tree
Hide file tree
Showing 9 changed files with 525 additions and 72 deletions.
3 changes: 3 additions & 0 deletions gateway-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

## 0.13.1 (pre-release; `@next` tag)

> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section.
- __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:/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:/apollographql/apollo-server/pull/3744)
- __NEW__: Experimental feature: compress downstream requests via generated fragments [#3791](https:/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.

## v0.12.1

Expand Down
1 change: 1 addition & 0 deletions gateway-js/src/QueryPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface FetchNode {
selectionSet: SelectionSetNode;
variableUsages?: { [name: string]: VariableDefinitionNode };
requires?: SelectionSetNode;
internalFragments: Set<FragmentDefinitionNode>;
}
export interface FlattenNode {
kind: 'Flatten';
Expand Down
333 changes: 322 additions & 11 deletions gateway-js/src/__tests__/buildQueryPlan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,25 +856,336 @@ describe('buildQueryPlan', () => {
const queryPlan = buildQueryPlan(buildOperationContext(schema, query));

expect(queryPlan).toMatchInlineSnapshot(`
QueryPlan {
Fetch(service: "product") {
{
product(upc: "") {
__typename
... on Book {
QueryPlan {
Fetch(service: "product") {
{
product(upc: "") {
__typename
... on Book {
details {
country
}
}
... on Furniture {
details {
country
}
}
}
}
},
}
`);
});

describe(`experimental compression to downstream services`, () => {
it(`should generate fragments internally to downstream requests`, () => {
const query = gql`
query {
topReviews {
body
author
product {
name
price
details {
country
}
}
... on Furniture {
}
}
`;

const queryPlan = buildQueryPlan(
buildOperationContext(schema, query, undefined),
{ autoFragmentization: true },
);

expect(queryPlan).toMatchInlineSnapshot(`
QueryPlan {
Sequence {
Fetch(service: "reviews") {
{
topReviews {
...__QueryPlanFragment_1__
}
}
fragment __QueryPlanFragment_1__ on Review {
body
author
product {
...__QueryPlanFragment_0__
}
}
fragment __QueryPlanFragment_0__ on Product {
__typename
... on Book {
__typename
isbn
}
... on Furniture {
__typename
upc
}
}
},
Parallel {
Sequence {
Flatten(path: "[email protected]") {
Fetch(service: "books") {
{
... on Book {
__typename
isbn
}
} =>
{
... on Book {
__typename
isbn
title
year
}
}
},
},
Flatten(path: "[email protected]") {
Fetch(service: "product") {
{
... on Book {
__typename
isbn
title
year
}
} =>
{
... on Book {
name
}
}
},
},
},
Flatten(path: "[email protected]") {
Fetch(service: "product") {
{
... on Furniture {
__typename
upc
}
... on Book {
__typename
isbn
}
} =>
{
... on Furniture {
name
price
details {
country
}
}
... on Book {
price
details {
country
}
}
}
},
},
},
},
}
`);
});

it(`shouldn't generate fragments for selection sets of length 2 or less`, () => {
const query = gql`
query {
topReviews {
body
author
}
}
`;

const queryPlan = buildQueryPlan(
buildOperationContext(schema, query, undefined),
{ autoFragmentization: true },
);

expect(queryPlan).toMatchInlineSnapshot(`
QueryPlan {
Fetch(service: "reviews") {
{
topReviews {
body
author
}
}
},
}
`);
});

it(`should generate fragments for selection sets of length 3 or greater`, () => {
const query = gql`
query {
topReviews {
id
body
author
}
}
`;

const queryPlan = buildQueryPlan(
buildOperationContext(schema, query, undefined),
{ autoFragmentization: true },
);

expect(queryPlan).toMatchInlineSnapshot(`
QueryPlan {
Fetch(service: "reviews") {
{
topReviews {
...__QueryPlanFragment_0__
}
}
fragment __QueryPlanFragment_0__ on Review {
id
body
author
}
},
}
`);
});

it(`should generate fragments correctly when aliases are used`, () => {
const query = gql`
query {
reviews: topReviews {
content: body
author
product {
name
cost: price
details {
country
origin: country
}
}
}
}
},
}
`);
`;

const queryPlan = buildQueryPlan(
buildOperationContext(schema, query, undefined),
{ autoFragmentization: true },
);

expect(queryPlan).toMatchInlineSnapshot(`
QueryPlan {
Sequence {
Fetch(service: "reviews") {
{
reviews: topReviews {
...__QueryPlanFragment_1__
}
}
fragment __QueryPlanFragment_1__ on Review {
content: body
author
product {
...__QueryPlanFragment_0__
}
}
fragment __QueryPlanFragment_0__ on Product {
__typename
... on Book {
__typename
isbn
}
... on Furniture {
__typename
upc
}
}
},
Parallel {
Sequence {
Flatten(path: "[email protected]") {
Fetch(service: "books") {
{
... on Book {
__typename
isbn
}
} =>
{
... on Book {
__typename
isbn
title
year
}
}
},
},
Flatten(path: "[email protected]") {
Fetch(service: "product") {
{
... on Book {
__typename
isbn
title
year
}
} =>
{
... on Book {
name
}
}
},
},
},
Flatten(path: "[email protected]") {
Fetch(service: "product") {
{
... on Furniture {
__typename
upc
}
... on Book {
__typename
isbn
}
} =>
{
... on Furniture {
name
cost: price
details {
origin: country
}
}
... on Book {
cost: price
details {
origin: country
}
}
}
},
},
},
},
}
`);
});
});
});
Loading

0 comments on commit 19c83bb

Please sign in to comment.