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

Added integrity checks on operations and selection sets #5502

Draft
wants to merge 6 commits into
base: dev
Choose a base branch
from

Conversation

duckki
Copy link
Contributor

@duckki duckki commented Jun 21, 2024

Operations and selection/selection-sets has internal invariants (before considering its correctness or any other constraints).

I call it "well-formedness" of operation/selection/-set structs. For example:

  • Every node has the same schema.
  • Every selection set must have a type that is dictated by their parent selection (namely, field's base type and inline fragment's "casted" type).
  • All fields must be defined in the schema.
  • All fragment spreads must be defined in the NamedFragments.
  • Every inline fragment spread's parent_type_position must be the type of its parent selection set.

I implemented the check and placed it in several places where we build operations. Those checks are only executed in debug builds.

I found a bug in rebase_on and fixed it in this PR. Though, that fix didn't change the status of existing tests.


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

Note any exceptions here

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.

@router-perf
Copy link

router-perf bot commented Jun 21, 2024

CI performance tests

  • reload - Reload test over a long period of time at a constant rate of users
  • no-tracing - Basic stress test, no tracing
  • events_callback - Stress test for events with a lot of users and deduplication ENABLED in callback mode
  • xlarge-request - Stress test with 10 MB request payload
  • events_big_cap_high_rate_callback - Stress test for events with a lot of users, deduplication enabled and high rate event with a big queue capacity using callback mode
  • events_without_dedup - Stress test for events with a lot of users and deduplication DISABLED
  • events_without_dedup_callback - Stress test for events with a lot of users and deduplication DISABLED using callback mode
  • step - Basic stress test that steps up the number of users over time
  • step-jemalloc-tuning - Clone of the basic stress test for jemalloc tuning
  • events_big_cap_high_rate - Stress test for events with a lot of users, deduplication enabled and high rate event with a big queue capacity
  • step-with-prometheus - A copy of the step test with the Prometheus metrics exporter enabled
  • demand-control-instrumented - A copy of the step test, but with demand control monitoring and metrics enabled
  • events - Stress test for events with a lot of users and deduplication ENABLED
  • xxlarge-request - Stress test with 100 MB request payload
  • large-request - Stress test with a 1 MB request payload
  • demand-control-uninstrumented - A copy of the step test, but with demand control monitoring enabled
  • const - Basic stress test that runs with a constant number of users

@TylerBloom
Copy link
Contributor

I really, really like the listing of invariants and checking them. We should start a doc that lists these things out.

Copy link
Contributor

@TylerBloom TylerBloom left a comment

Choose a reason for hiding this comment

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

It seems we don't want to call the is_well_formed method in production, but it might be something we want to enable based on log level as it would provide never helpful information. This isn't something you need to change in this PR, but I wanted to note it.

// `debug_check`: a debug-only sanity check.
// - Executes an expression `$result` that returns a `Result<(), E>` and returns the error if the
// result is an Err.
macro_rules! debug_check {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason we would want to bubble this error up instead of just panicking while debugging?


//================================================================================================
// Well-formedness checks
// - structural invariant checks for operations.
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to call out here that we're specifically not looking to check GraphQL validations, and instead looking to check that data/fields introduced by our operation data structures are self-consistent.

)?;
}
Ok(())
// Note: fragment_data.type_condition_position and the parent type do not have to have
Copy link
Contributor

Choose a reason for hiding this comment

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

This is really a GraphQL validation, so as long as we have the explanation mentioned above about how we're not doing those, I think it's fine to not have this note. (Or rather, if we included this note, we could also e.g. include notes about how we skip argument value validation, or directive application validation. I think it'd be less noisy to declare we're generally skipping GraphQL validation near the top.)

inline_fragment
.selection_set
.is_well_formed(schema, named_fragments, option)
// Note: fragment_data.type_condition_position and the parent type do not have to have
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar feedback for this note as above.


// FragmentSpreadSelection holds a copy of fragment definition's selection set.
// This option controls whether or not to check that copy or skip it.
// Note that `Operation::optimize()` returns a selection set that may have outdated fragment
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that Operation::optimize() returns a selection set that may have outdated fragment selection sets.

This strikes me as a bug. For rebasing, the JS codebase ensures that fragment data is updated in FragmentSpreadSelection.rebaseOn(), specifically here. The pre-requisite is that the passed-in named fragments have been rebased correctly, but this should be handled provided we've rebased the fragments in dependency order (which is what the JS code does here).

}

impl Operation {
pub fn is_well_formed(
Copy link
Contributor

@sachindshinde sachindshinde Jun 26, 2024

Choose a reason for hiding this comment

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

I'm noticing that:

  1. The well-formedness check for fragment spread selections recurses into the fragment's selection sets, each time the fragment spread is referenced (assuming they're not skipped).
  2. We don't check the well-formedness of Operation.named_fragments here. I'm guessing because we assume they should be exactly the same as the fragments referenced above (although we're not checking this here).

Looking at the codebase, the FragmentSpreadSelection.selection_set field, when directly set, comes from existing Fragments. That means that it should always match the corresponding Fragment's selection set in the operation. And not just Eq here for SelectionSet, but more specifically SelectionSet.schema and SelectionSet.type_position should be Eq, and SelectionSet.selections should compare the same using Arc::ptr_eq().

This would suggest that we change the above validation to:

  1. Validate the fragments in Operation.named_fragments (recursing into their selection sets, but also e.g. checking that Fragment.type_condition_position matches the one in the selection set).
  2. When validating a fragment spread selection, we should check that the FragmentSpreadSelection.selection_set matches in the way described above.

There's a similar case to be made for FragmentSpreadSelection.spread, where it always comes from feeding a Fragment to FragmentSpreadData::from_fragment(). This means we should similarly check matching there, i.e. we should check for Eq for FragmentSpreadData.schema, FragmentSpreadData.fragment_name, and FragmentSpreadData.type_condition_position, and check Arc::ptr_eq() for FragmentSpreadData::fragment_directives().

  • While looking here, I noticed a bug in FragmentSpreadSelection::new() where it re-fetches its fragment selection set from NamedFragments, but doesn't update the spread data. Glancing at the code, this looks to be used only for SelectionSet::make_selection(), specifically in Selection::from_operation_element(). The latter function appears to be a rename of selectionOfElement() from JS, but the JS code converts fragment spreads to inline fragments, while the Rust code appears to be keeping them as fragment spreads. It would be better I think here to copy the JS code's behavior (or error when fragment spreads are present, if we believe they shouldn't occur here).

@abernix abernix changed the title FED-246: added integrity checks on operations and selection sets Added integrity checks on operations and selection sets Sep 10, 2024
@goto-bus-stop goto-bus-stop marked this pull request as draft October 24, 2024 09:04
@goto-bus-stop
Copy link
Member

Changing to draft so we don't get pinged to rereview it every day in slack 😇

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.

5 participants