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

FED-191: [Operation Processing] port operation.optimize #5207

Merged
merged 16 commits into from
May 29, 2024
Merged

Conversation

duckki
Copy link
Contributor

@duckki duckki commented May 21, 2024

implemented the rest of Operation::optimize method

https://apollographql.atlassian.net/browse/FED-191

Depends on:


Checklist

Complete the checklist (and note appropriate exceptions) before the PR is marked ready-for-review.

  • Changes are compatible1
  • Documentation2 completed
  • Performance impact assessed and acceptable
  • Tests added and passing3
    • Unit Tests
    • Integration Tests
    • Manual Tests

Exceptions

Have a few simple tests. More tests shall be done by poring existing federation tests.

Notes

Footnotes

  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.

Copy link
Contributor

@duckki, please consider creating a changeset entry in /.changesets/. These instructions describe the process and tooling.

@duckki duckki changed the base branch from dev to duckki/FED-32 May 21, 2024 00:06
@lrlna lrlna self-assigned this May 21, 2024
@duckki duckki mentioned this pull request May 21, 2024
6 tasks
@duckki duckki force-pushed the duckki/FED-191 branch 2 times, most recently from f364f53 to f7486ef Compare May 22, 2024 17:01
Base automatically changed from duckki/FED-32 to dev May 22, 2024 17:17
Comment on lines 1218 to 1220
let Some(fragments) = fragments else {
return Ok(());
};
Copy link
Member

Choose a reason for hiding this comment

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

I feel it makes more sense to do this at the usage site, and require passing in a fragments: &NamedFragments.

if fragments.is_empty() {
return Ok(());
}
assert!(
Copy link
Member

@goto-bus-stop goto-bus-stop May 23, 2024

Choose a reason for hiding this comment

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

We can avoid this assert either by taking a Option<NonZeroU32> parameter, or maybe even removing it entirely and hardcoding the value to 2 (AFAIK JS only used it in tests)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. I'll remove the parameter altogether.

@lrlna lrlna assigned goto-bus-stop and unassigned lrlna May 23, 2024
Copy link
Member

@lrlna lrlna left a comment

Choose a reason for hiding this comment

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

This is one is complicated too 😱 I think I'll have to do another pass on Monday to make sure I didn't miss anything. The biggest thing for me is more tests. I know the JS repo's got a ton of optimize tests that we can borrow.

Comment on lines 1293 to 1296
pub(crate) fn output_ast_type(&self) -> Result<&ast::Type, FederationError> {
Ok(&self.field_position.get(self.schema.schema())?.ty)
}

Copy link
Member

Choose a reason for hiding this comment

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

this probably also needs to live on an impl outside of this internal mod. perhaps on Field?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, it's no longer used. Let's make it private and then decide where to put it later.

@@ -2616,7 +2600,7 @@ impl SelectionSet {
// version, but its Rust version doesn't use `lazy_map`.
// PORT_NOTE #2: Taking ownership of `self` in this method was considered. However, calling
// `Arc::make_mut` on the `Arc` fields of `self` didn't seem better than cloning Arc instances.
fn lazy_map(
pub(crate) fn lazy_map(
Copy link
Member

Choose a reason for hiding this comment

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

don't particularly love that we are exposing more of these methods outside of the operation file, but I suppose we will deal with once we start fixing up this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree.

/// Compute the reduced set of NamedFragments that are used in the selection set at least
/// `min_usage_to_optimize` times. Also, computes the new selection set that uses only the
/// reduced set of fragments by expanding the other ones.
fn reduce_named_fragments(
Copy link
Member

Choose a reason for hiding this comment

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

just reduce? we are already on the NamedFragments impl. or just call it optimize like on the other impls?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point.

self.named_fragments = final_fragments;
Ok(())
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I know you've got some tests below already. There are also some additional tests in the JS repo that specifically look at fragment optimization. It'd be good to add those here as well. This functionality is complicated, and we are not doing exactly the same thing as the JS QP, so it'd be really good to port these along as well. https://github.com/apollographql/federation/blob/28850ffba17d4201321dc6de0edc24cae48ec5e5/internals-js/src/__tests__/operations.test.ts#L37-L1033

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I didn't get to that today. I'll tackle this next week.

BTW, I was wondering if I would step on Simon's foot, porting tests. Let's synchronize on this next week. @SimonSapin

impl Operation {
// PORT_NOTE: The JS version of `optimize` takes an optional `minUsagesToOptimize` argument.
// However, it's only used in tests. So, it's removed in the Rust version.
const DEFAULT_MIN_USAGES_TO_OPTIMIZE: u32 = 2;
Copy link
Member

Choose a reason for hiding this comment

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

should we just remove this const entirely then? or do you want to keep it the argument to pass to tests as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It didn't seem necessary to test different values. However, I think literals would better be declared as const for clarity. Otherwise, they can become magic numbers that no one knows what they are.

Comment on lines 1149 to 1152
pub(crate) fn optimize_at_root(
&self,
fragments: &NamedFragments,
) -> Result<SelectionSet, FederationError> {
Copy link
Member

Choose a reason for hiding this comment

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

I am wondering why we are not mutating the existing structs with all these optimize functions, but are instead creating a brand new selection set/inline fragment selection/etc. I feel like we are doing a lot of cloning (and I know it's an Arc), only to use that cloned item to create the exact same modified type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea.

And I noticed a bug in this function and fixed it.

Comment on lines 1015 to 1019
fn optimize(
&self,
fragments: &NamedFragments,
validator: &mut FieldsConflictMultiBranchValidator,
) -> Result<Selection, FederationError> {
Copy link
Member

@lrlna lrlna May 27, 2024

Choose a reason for hiding this comment

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

Do you think it'll be easier to reason about if this returns a FieldSelection instead? Something like this:

Suggested change
fn optimize(
&self,
fragments: &NamedFragments,
validator: &mut FieldsConflictMultiBranchValidator,
) -> Result<Selection, FederationError> {
fn optimize(
&mut self,
fragments: &NamedFragments,
validator: &mut FieldsConflictMultiBranchValidator,
) -> Result<Self, FederationError> {

I am thinking we are walking down the tree from the SelectionSet root, modifying and optimising it's potential children. It might be easier if the results bubble up as their own respective types, and we handle converting them into a Selection at one of the top level nodes?

Copy link
Member

Choose a reason for hiding this comment

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

Potentially might not work due to borrow checker. But I guess what I am trying to say is that I am finding it difficult to follow that FieldSelection/InlineFragmentSelection/etc all optimize, theoretically their own values, but they don't actually always modify their own selection_set field and also don't return the selection_set but a selection

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. In the JS version, optimze() function was a virtual function with the same return type (Selection). But, in Rust, we don't have do that. The wrinkle here is that InlineFragmentSelection can be optimized into FragmentSpreadSelection. So, it makes sense to add a new enum InlineOrFragmentSelection, which helps to eliminate one unreachable!() branch case.

Comment on lines 1058 to 1063
impl InlineFragmentSelection {
fn optimize(
&self,
fragments: &NamedFragments,
validator: &mut FieldsConflictMultiBranchValidator,
) -> Result<Selection, FederationError> {
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as above.

Copy link
Member

@lrlna lrlna left a comment

Choose a reason for hiding this comment

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

Ok, I did a second pass, and nothing really jumps out at me. I think the main thing to do is to port those optimisation tests. It'll be a huge help here!

@lrlna lrlna assigned lrlna and unassigned goto-bus-stop May 28, 2024
@duckki
Copy link
Contributor Author

duckki commented May 29, 2024

I ported a few tests. That detected a bug in optimization and in a few other places. I'd like to merge this PR and hunt the rest (if any) together as we port tests.

Copy link
Member

@lrlna lrlna left a comment

Choose a reason for hiding this comment

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

Glad you already found some bugs! Let's merge and find some more.

@lrlna lrlna enabled auto-merge (squash) May 29, 2024 10:17
Comment on lines +5356 to +5365
normalized_operation.named_fragments = Default::default();
insta::assert_snapshot!(normalized_operation, @r###"
query NamedFragmentQuery {
foo {
id
bar
baz
}
}
"###);
Copy link
Member

Choose a reason for hiding this comment

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

do we want to have snapshots inside mods? have we done this before? i don't want to us to introduce a new pattern for mod tests

@lrlna
Copy link
Member

lrlna commented May 29, 2024

There are some snapshot tests changes, and the new changes produced by these snapshots genuinely look incorrect - spacing for fragment definitions looks off 🤔

@duckki
Copy link
Contributor Author

duckki commented May 29, 2024

I fixed the spacing issue and a bug in reuse_query_fragments option handling.
I reviewed the status of failing tests and annotated appropriately (some improved and some failed showing different symptoms).
I think it's ok to merge into dev.

@lrlna lrlna merged commit a9b8d0e into dev May 29, 2024
13 of 14 checks passed
@lrlna lrlna deleted the duckki/FED-191 branch May 29, 2024 21:19
lrlna added a commit that referenced this pull request Jun 3, 2024
Co-authored-by: Iryna Shestak <shestak.irina@gmail.com>
theJC pushed a commit to theJC/router that referenced this pull request Jun 10, 2024
…hql#5207)

Co-authored-by: Iryna Shestak <shestak.irina@gmail.com>
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.

3 participants