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 new generateQueryFragments option to query planner config #2958

Merged
merged 18 commits into from
Mar 18, 2024

Conversation

trevor-scheer
Copy link
Member

@trevor-scheer trevor-scheer commented Mar 14, 2024

If enabled, the query planner will extract inline fragments into fragment definitions before sending queries to subgraphs. This can significantly reduce the size of the query sent to subgraphs, but may increase the time it takes to plan the query.

This is a reimplementation of #2893 that applies transformations on the selection set instead of AST.

Credit to @samuelAndalon for suggesting the new approach. Resolves #2892

Copy link

netlify bot commented Mar 14, 2024

👷 Deploy request for apollo-federation-docs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit e8560f8

Copy link

changeset-bot bot commented Mar 14, 2024

🦋 Changeset detected

Latest commit: e8560f8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
apollo-federation-integration-testsuite Minor
@apollo/query-planner Minor
@apollo/federation-internals Minor
@apollo/gateway Minor
@apollo/composition Minor
@apollo/query-graphs Minor
@apollo/subgraph Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

codesandbox-ci bot commented Mar 14, 2024

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@trevor-scheer trevor-scheer force-pushed the trevor/auto-frag-no-ast branch 3 times, most recently from 56ddc4a to b511cf6 Compare March 14, 2024 23:05
@trevor-scheer trevor-scheer changed the title Add option for query plans to generate and reuse fragments automatically (alternate impl) Add new generateQueryFragments option to query planner config Mar 14, 2024
@trevor-scheer trevor-scheer marked this pull request as ready for review March 14, 2024 23:13
@trevor-scheer trevor-scheer requested a review from a team as a code owner March 14, 2024 23:13
internals-js/src/operations.ts Outdated Show resolved Hide resolved
internals-js/src/operations.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@sachindshinde sachindshinde left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spoke to Trevor about the PR, had some suggestions there but will summarize them here:

  • The approach we're taking where "every eligible inline fragment gets replaced with a fragment spread" is okay for now, given that (1) this is feature flagged, and (2) there are some users who just flat-out need the compression above all else, and soon. But it's a terrible choice for readability, and there's extra overhead from the unnecessary fragment indirection (for processing and validation), so I'm going to write out a design for a separate algorithm some time next week. We can take that as a follow-up.
  • Using the stringified selection set doesn't quite work, since we don't care about the order of various things (selections, directives, arguments, object value fields). We could implement an actual .hashCode() function, but I think we're too time-constrained here for that. The suggestion I've made there is to instead use the length of the stringified selection set as a hash function (as it's unaffected by order changes). Collisions will mostly happen at low sizes, but those should be fast to compare via .equals() anyway, so it's fine in the short-term.
    • This means changing the map of stringified selection sets to named fragment definitions to instead become a map of hash codes (numbers) to lists of pairs, where each pair consists of a SelectionSet object, along with the named fragment definition. Lookup involves computing the hash of the SelectionSet, and if an entry is found, iterating through the pairs in the list, comparing the SelectionSet to the ones in the pairs via .equals(). If a pair is equal, then the SelectionSet is "in" the map, and the corresponding named fragment definition is the value. If no pair is equal (or no entry was found), then the SelectionSet is not "in" the map, and we'd append to that list a new pair with that SelectionSet and the new named fragment definition (or if no entry was found, add an entry with the hash code and a new list containing just that pair).

@dariuszkuc
Copy link
Member

dariuszkuc commented Mar 15, 2024

The suggestion I've made there is to instead use the length of the stringified selection set as a hash function (as it's unaffected by order changes)

I think "stringifying" selection set is going to be rather slow for computing the keys. Why not just go with something simple as selection.key() + selection.selectionSet.selections().length instead? We are going to be comparing the selection sets on hits anyway, so first usingselection.key() we will limit to the same type condition + directives and then we would just need to quickly compare the size of selection set size to achieve the same thing.

If you are going to be pointing to an array of NamedFragmentDefinition then we still need to account for the additional index in order to generate unique fragment name -> you could simply incorporate that index with the key and avoid nested loop.

@sachindshinde
Copy link
Contributor

The suggestion I've made there is to instead use the length of the stringified selection set as a hash function (as it's unaffected by order changes)

I think "stringifying" selection set is going to be rather slow for computing the keys. Why not just go with something simple as selection.key() + selection.selectionSet.selections().length instead? We are going to be comparing the selection sets on hits anyway, so first usingselection.key() we will limit to the same type condition + directives and then we would just need to quickly compare the size of selection set size to achieve the same thing.

It's a tradeoff between hash function speed and collision rate. You can indeed use the top-level field info of the selection set for the hash function instead of the entire selection set (it's like using the prefix of a string for a string's hash function instead of the entire string). The cost is that you may encounter more collisions, which means more spurious SelectionSet.equals() checks (asymptotically it means the algorithm leans more quadratically than linearly, though practically it's probably fine).

In any case this is a short-term fix under a feature flag, so I'm fine with whatever hash function we choose here as long as the collision rate isn't too high.

Comment on lines 1584 to 1589
if (!equivalentSelectionSetCandidates) {
seenSelections.set(mockHashCode, [[minimizedSelectionSet, fragmentDefinition]]);
namedFragments.add(fragmentDefinition);
} else {
equivalentSelectionSetCandidates.push([minimizedSelectionSet, fragmentDefinition]);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the comparison is against the pre-minified selection set, I think you'll want the pre-minified selection set in the pair instead of the minified selection set. (The NamedFragmentDefinition should still use the minified selection set though.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dariuszkuc
Copy link
Member

It's a tradeoff between hash function speed and collision rate. You can indeed use the top-level field info of the selection set for the hash function instead of the entire selection set (it's like using the prefix of a string for a string's hash function instead of the entire string). The cost is that you may encounter more collisions, which means more spurious SelectionSet.equals() checks (asymptotically it means the algorithm leans more quadratically than linearly, though practically it's probably fine).

Indeed there are way more complex hashing algos that we should look into in the future. Using the prefix just narrows down the selection sets to the same type condition and then we just check for number of selections there. I'd imagine instead doing the concatenating selection set into a string to get the final length we could simply recursively iterate over the selection sets and just aggregate the final length value (but unsure whether in practice it would be any better than recursively going through selection sets to calculate total number of selected fields).

@dariuszkuc dariuszkuc merged commit d7189a8 into apollographql:main Mar 18, 2024
12 of 13 checks passed
@trevor-scheer trevor-scheer deleted the trevor/auto-frag-no-ast branch March 19, 2024 00:21
Copy link
Contributor

@sachindshinde sachindshinde left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor nit to fix in a followup

@@ -2413,7 +2478,7 @@ export function selectionOfElement(element: OperationElement, subSelection?: Sel
return element.kind === 'Field' ? new FieldSelection(element, subSelection) : new InlineFragmentSelection(element, subSelection!);
}

export type Selection = FieldSelection | FragmentSelection;
export type Selection = FieldSelection | FragmentSelection | FragmentSpreadSelection;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FragmentSelection is an abstract class implemented by InlineFragmentSelection and FragmentSpreadSelection, so I don't think you'll need to change the definition here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@duckki
Copy link
Contributor

duckki commented Mar 19, 2024

I noticed that reuseQueryFragments option does not reuse fragments of size 1 (having only one field selection). It's probably because we don't get any compression by reusing those. Would the same logic apply with generateQueryFragments? I see tests are generating size-1 fragments. (Sorry I joined the party too late)

@dariuszkuc
Copy link
Member

Indeed when reusing fragments we apply a logic to ensure we have selection set > 1. For fragment generation logic we decided to keep it simple for now (we could definitely improve it in the future and provide some better optimizations, e.g. join all sibling inline fragments into one named fragment etc).

duckki added a commit to duckki/federation that referenced this pull request Mar 20, 2024
dariuszkuc pushed a commit that referenced this pull request Mar 20, 2024
…ts at least 2 selections (#2962)

This is a follow-up of #2958.

I've changed `SelectionSet.minimizeSelectionSet` to create a new fragment only when the selection set has at least 2 items.

This behavior is inspired by the behavior of `reuseQueryFragments` option. `NamedFragments.selectionSetIsWorthUsing` was moved to `SelectionSet.isWorthUsing` and is now used by both `NamedFragments` and fragment generation logic.
clenfest pushed a commit that referenced this pull request Mar 21, 2024
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @apollo/[email protected]

### Patch Changes

- When a linked directive requires a federation version higher than the
linked federation spec, upgrade to the implied version and issue a hint
([#2929](#2929))

- Stop emitting "inconsistent value type" hints against definitions
where the type is marked `@external` or all fields are marked
`@external`.
([#2951](#2951))

- Introduce a new composition hint pertaining specifically to
progressive `@override` usage (when a `label` argument is present).
([#2922](#2922))

- Updated dependencies
\[[`33b937b18d3c7ca6af14b904696b536399e597d1`](33b937b),
[`09cd3e55e810ee513127b7440f5b11af7540c9b0`](09cd3e5),
[`d7189a86c27891af408d3d0184db6133d3342967`](d7189a8)]:
    -   @apollo/[email protected]
    -   @apollo/[email protected]

## @apollo/[email protected]

### Patch Changes

- Remove out-of-band reporting in the gateway and provide a warning for
users who have the endpoint configured.
([#2946](#2946))

- Updated dependencies
\[[`33b937b18d3c7ca6af14b904696b536399e597d1`](33b937b),
[`09cd3e55e810ee513127b7440f5b11af7540c9b0`](09cd3e5),
[`d7189a86c27891af408d3d0184db6133d3342967`](d7189a8),
[`33506bef6d755c58400081824167711c1747ee40`](33506be),
[`1f72f2a361a83ebaaf15ae052f5ca9a93fc18bfc`](1f72f2a)]:
    -   @apollo/[email protected]
    -   @apollo/[email protected]
    -   @apollo/[email protected]

## @apollo/[email protected]

### Patch Changes

- When a linked directive requires a federation version higher than the
linked federation spec, upgrade to the implied version and issue a hint
([#2929](#2929))

- When auto-upgrading a subgraph (i.e. one that does not explicitly
@link the federation spec) do not go past v2.4. This is so that
subgraphs will not inadvertently require the latest join spec (which
cause the router or gateway not to start if running an older version).
([#2933](#2933))

- Add new `generateQueryFragments` option to query planner config
([#2958](#2958))

If enabled, the query planner will extract inline fragments into
fragment definitions before sending queries to subgraphs. This can
significantly reduce the size of the query sent to subgraphs, but may
increase the time it takes to plan the query.

## @apollo/[email protected]

### Patch Changes

- Updated dependencies
\[[`33b937b18d3c7ca6af14b904696b536399e597d1`](33b937b),
[`09cd3e55e810ee513127b7440f5b11af7540c9b0`](09cd3e5),
[`d7189a86c27891af408d3d0184db6133d3342967`](d7189a8)]:
    -   @apollo/[email protected]

## @apollo/[email protected]

### Patch Changes

- When auto-upgrading a subgraph (i.e. one that does not explicitly
@link the federation spec) do not go past v2.4. This is so that
subgraphs will not inadvertently require the latest join spec (which
cause the router or gateway not to start if running an older version).
([#2933](#2933))

- Add new `generateQueryFragments` option to query planner config
([#2958](#2958))

If enabled, the query planner will extract inline fragments into
fragment definitions before sending queries to subgraphs. This can
significantly reduce the size of the query sent to subgraphs, but may
increase the time it takes to plan the query.

- Updated dependencies
\[[`33b937b18d3c7ca6af14b904696b536399e597d1`](33b937b),
[`09cd3e55e810ee513127b7440f5b11af7540c9b0`](09cd3e5),
[`d7189a86c27891af408d3d0184db6133d3342967`](d7189a8)]:
    -   @apollo/[email protected]
    -   @apollo/[email protected]

## @apollo/[email protected]

### Patch Changes

- Updated dependencies
\[[`33b937b18d3c7ca6af14b904696b536399e597d1`](33b937b),
[`09cd3e55e810ee513127b7440f5b11af7540c9b0`](09cd3e5),
[`d7189a86c27891af408d3d0184db6133d3342967`](d7189a8)]:
    -   @apollo/[email protected]

## [email protected]

### Patch Changes

- Add new `generateQueryFragments` option to query planner config
([#2958](#2958))

If enabled, the query planner will extract inline fragments into
fragment definitions before sending queries to subgraphs. This can
significantly reduce the size of the query sent to subgraphs, but may
increase the time it takes to plan the query.

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
trevor-scheer added a commit to apollographql/federation-rs that referenced this pull request Mar 21, 2024
Ref: apollographql/federation#2958

Adopting this in router should take into consideration that both
`reuse_query_fragments` and `generate_query_fragments` should not both
be set to true. Since `reuse_` currently defaults to true, it should be
disabled when `generate_` is enabled.

---------

Co-authored-by: apollo-bot2 <[email protected]>
nicholascioli pushed a commit to apollographql/router that referenced this pull request Apr 5, 2024
Closing the loop on
apollographql/federation#2958

- [ ] I'm unsure if override + log warning is the appropriate action, do
we have a similar pattern in config elsewhere?
- [x] Is there a place where the yaml config options are all documented?
I don't see anything existing for `reuse_query_fragments`.
- [ ] How to test the log warning was emitted?

---------

Co-authored-by: Bryn Cooke <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

An alternative to reuseFragments
4 participants