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

Reuse named fragment in subgraphs even if they only apply partially #2639

Merged
merged 2 commits into from
Jun 27, 2023

Conversation

pcmanus
Copy link
Contributor

@pcmanus pcmanus commented Jun 23, 2023

The focus of the code reusing fragment has been thus far on making that if a given named fragment can be used in a given selection set, then the code finds it. But in practice, the named fragments we try to reuse are those of the input query, so defined against the supergraph API schema, while we're trying to reuse them on subgraph fetches, so the fragment may be defined on a type that a subgraph does know, or request fields the subgraph does not define.

Currently, when asking if a given named fragment can be used against a given subgraph, we only keep fragment that fully apply to that subgraph (that is, only if everything the fragment queries exists in the subgraph).

This does limit the usefulness of named fragment reuse however, as one of the point of federation is that each subgraph defines a subset of the full supergraph API.

This commit improves this by instead computing the subset of each named fragment that do apply to a given subgraph, and define the fragment as only that fragment for the subgraph.

Note that it does so, this commit do not try reusing a named fragment against a subgraph if the subset of the fragment on the subgraph comes to only a single leaf field. This avoids spending time trying to reuse a fragment that ends up being just:

fragment MySuperFragment on X {
  __typename
}

or even:

fragment MySuperFragment on X {
 id
}

as those aren't productive (the goal of fragment reuse is to try to make the subgraph fetches smaller, but it is small to just request id, even 10 times, than to request 10 times ...MySuperFragment).

@pcmanus pcmanus requested a review from a team as a code owner June 23, 2023 08:16
@netlify
Copy link

netlify bot commented Jun 23, 2023

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

Visit the deploys page to approve it

Name Link
🔨 Latest commit f4172d4

@changeset-bot
Copy link

changeset-bot bot commented Jun 23, 2023

🦋 Changeset detected

Latest commit: f4172d4

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

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

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

The focus of the code reusing fragment has been thus far on making
that if a given named fragment can be used in a given selection set,
then the code finds it. But in practice, the named fragments we try
to reuse are those of the input query, so defined against the
supergraph API schema, while we're trying to reuse them on subgraph
fetches, so the fragment may be defined on a type that a subgraph
does know, or request fields the subgraph does not define.

Currently, when asking if a given named fragment can be used against
a given subgraph, we only keep fragment that _fully_ apply to that
subgraph (that is, only if everything the fragment queries exists
in the subgraph).

This does limit the usefulness of named fragment reuse however, as one
of the point of federation is that each subgraph defines a subset
of the full supergraph API.

This commit improves this by instead computing the subset of each
named fragment that do apply to a given subgraph, and define the
fragment as only that fragment for the subgraph.

Note that it does so, this commit do not try reusing a named fragment
against a subgraph if the subset of the fragment on the subgraph
comes to only a single leaf field. This avoids spending time trying
to reuse a fragment that ends up being just:
```graphql
fragment MySuperFragment on X {
  __typename
}
```
or even:
```graphql
fragment MySuperFragment on X {
 id
}
```
as those aren't productive (the goal of fragment reuse is to try to
make the subgraph fetches smaller, but it is small to just request
`id`, even 10 times, than to request 10 times `...MySuperFragment`).
@codesandbox-ci
Copy link

codesandbox-ci bot commented Jun 23, 2023

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.

@korinne korinne added the P3 An issue that should be addressed when able. label Jun 23, 2023
Copy link
Member

@dariuszkuc dariuszkuc left a comment

Choose a reason for hiding this comment

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

Looks good. Left few nits related to comments.

}
`);

// F1 reduces to nothing, and F2 reduces to just __typename so we should keep them.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// F1 reduces to nothing, and F2 reduces to just __typename so we should keep them.
// F1 reduces to nothing, and F2 reduces to just __typename so we shouldn't keep them.

@@ -1347,21 +1358,39 @@ export class NamedFragments {
});
}

// When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong
// to that particular subgraph. And there is a few sub-cases where that subset is such that we basically need or
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// to that particular subgraph. And there is a few sub-cases where that subset is such that we basically need or
// to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or

Comment on lines 2543 to 2553
// As we check inclusion, we ignore the case where the fragment queries __typename but the subSelection does not.
// The rational is that querying `__typename` unecessarily is mostly harmess (it always works and it's super cheap)
// so we don't want to not use a fragment just to save querying a `__typename` in a few case. But the underlying
// context of why this matter is that the query planner always request __typename for abstract type, and will do
// so in fragments too, but we can have a field that _does_ return an abstract type within a fragment, but that
// _do not_ end up returning an abstract type when applied in a "more specific" context (think a fragment on
// an interface I1 where a inside field returns another interface I2, but applied in the context of a implementation
// type of I1 where that particular field returns an implentation of I2 rather than I2 directly; we would have
// added __typename to the fragment (because it's all interfaces), but the selection itself, which only deals
// with object type, may not have __typename requested; using the fragment might still be a good idea, and
// querying __typename needlessly is a very small price to pay for that).
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// As we check inclusion, we ignore the case where the fragment queries __typename but the subSelection does not.
// The rational is that querying `__typename` unecessarily is mostly harmess (it always works and it's super cheap)
// so we don't want to not use a fragment just to save querying a `__typename` in a few case. But the underlying
// context of why this matter is that the query planner always request __typename for abstract type, and will do
// so in fragments too, but we can have a field that _does_ return an abstract type within a fragment, but that
// _do not_ end up returning an abstract type when applied in a "more specific" context (think a fragment on
// an interface I1 where a inside field returns another interface I2, but applied in the context of a implementation
// type of I1 where that particular field returns an implentation of I2 rather than I2 directly; we would have
// added __typename to the fragment (because it's all interfaces), but the selection itself, which only deals
// with object type, may not have __typename requested; using the fragment might still be a good idea, and
// querying __typename needlessly is a very small price to pay for that).
// As we check inclusion, we ignore the case where the fragment queries __typename but the subSelection does not.
// The rational is that querying `__typename` unecessarily is mostly harmless (it always works and it's super cheap)
// so we don't want to not use a fragment just to save querying a `__typename` in a few cases. But the underlying
// context of why this matters is that the query planner always requests __typename for abstract type, and will do
// so in fragments too, but we can have a field that _does_ return an abstract type within a fragment, but that
// _does not_ end up returning an abstract type when applied in a "more specific" context (think a fragment on
// an interface I1 where a inside field returns another interface I2, but applied in the context of a implementation
// type of I1 where that particular field returns an implementation of I2 rather than I2 directly; we would have
// added __typename to the fragment (because it's all interfaces), but the selection itself, which only deals
// with an object type, may not have __typename requested; using the fragment might still be a good idea, and
// querying __typename needlessly is a very small price to pay for that).

@pcmanus pcmanus merged commit d60349b into apollographql:main Jun 27, 2023
This was referenced Jun 27, 2023
trevor-scheer added a commit that referenced this pull request Jul 17, 2023
BrynCooke added a commit to apollographql/router that referenced this pull request Jul 18, 2023
### Update router bridge and add option for `reuse_query_fragments`
([Issue #3452](#3452))

Federation v2.4.9 enabled a new feature for [query fragment
reuse](apollographql/federation#2639) that is
causing issues for some users.

A new option has been added to he router config file to opt into this
feature:
```yaml
supergraph:
  experimental_reuse_query_fragments: true
```

The default is disabled.
Fixes #3452

<!-- start metadata -->

**Checklist**

Complete the checklist (and note appropriate exceptions) before a final
PR is raised.

- [ ] Changes are compatible[^1]
- [ ] Documentation[^2] completed
- [ ] Performance impact assessed and acceptable
- Tests added and passing[^3]
    - [ ] Unit Tests
    - [ ] Integration Tests
    - [ ] Manual Tests

**Exceptions**

*Note any exceptions here*

**Notes**

[^1]. It may be appropriate to bring upcoming changes to the attention
of other (impacted) groups. Please endeavour to do this before seeking
PR approval. The mechanism for doing this will vary considerably, so use
your judgement as to how and when to do this.
[^2]. Configuration is an important part of many changes. Where
applicable please try to document configuration examples.
[^3]. Tick whichever testing boxes are applicable. If you are adding
Manual Tests:
- please document the manual testing (extensively) in the Exceptions.
- please raise a separate issue to automate the test and label it (or
ask for it to be labeled) as `manual test`

---------

Co-authored-by: bryn <[email protected]>
trevor-scheer added a commit that referenced this pull request Jul 18, 2023
trevor-scheer added a commit that referenced this pull request Jul 19, 2023
This commit reapplies #2639 with an additional fix related to
reproducing and resolving #2680.

The bug existed in the SelectionSet.contains logic in the final
check to provide a ContainsResult. Here the lengths of the
compared selection sets are used to determine subset or
strictly equal. In the case that the __typename field was
ignored up above, the comparison becomes invalid unless we
offset the comparison by 1 to account for the non-existent field.
trevor-scheer added a commit that referenced this pull request Jul 19, 2023
This commit reapplies #2639 with an additional fix related to
reproducing and resolving #2680.

The bug existed in the SelectionSet.contains logic in the final
check to provide a ContainsResult. Here the lengths of the
compared selection sets are used to determine subset or
strictly equal. In the case that the __typename field was
ignored up above, the comparison becomes invalid unless we
offset the comparison by 1 to account for the non-existent field.
trevor-scheer added a commit that referenced this pull request Jul 19, 2023
This commit reapplies #2639 with an additional fix related to
reproducing and resolving #2680.

The bug existed in the SelectionSet.contains logic in the final
check to provide a ContainsResult. Here the lengths of the
compared selection sets are used to determine subset or
strictly equal. In the case that the __typename field was
ignored up above, the comparison becomes invalid unless we
offset the comparison by 1 to account for the non-existent field.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
P3 An issue that should be addressed when able.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants