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

Optimize views #1439

Merged
merged 11 commits into from Oct 29, 2023
Merged

Optimize views #1439

merged 11 commits into from Oct 29, 2023

Conversation

danthewildcat
Copy link
Contributor

@danthewildcat danthewildcat commented Aug 3, 2023

This PR is a no-op optimization:

  1. Makes it so that the expensive parse call for parsing the request body into valid graphql is not called redundantly
  2. Fails faster if the request does not have a valid operation_ast
  3. Removes passing (and handling) the show_graphiql kwarg between the view methods

@danthewildcat danthewildcat reopened this Aug 3, 2023
@danthewildcat danthewildcat force-pushed the optimize-views branch 2 times, most recently from 861c50a to 09556b6 Compare August 3, 2023 14:12
if operation_ast and operation_ast.operation != OperationType.QUERY:
if show_graphiql:
return None
operation_ast = get_operation_ast(document, operation_name)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All of the old logic would at some point call get_operation_ast so I hoisted it up

@danthewildcat danthewildcat force-pushed the optimize-views branch 2 times, most recently from 9c5bca6 to 10793be Compare August 3, 2023 16:36
Comment on lines 300 to 317
if not operation_ast:
ops_count = len(
[
x
for x in document.definitions
if isinstance(x, OperationDefinitionNode)
]
)
if ops_count > 1:
op_error = (
"Must provide operation name if query contains multiple operations."
)
try:
extra_options = {}
if self.execution_context_class:
extra_options["execution_context_class"] = self.execution_context_class
elif operation_name:
op_error = f"Unknown operation named '{operation_name}'."
else:
op_error = "Must provide a valid operation."

return ExecutionResult(errors=[GraphQLError(op_error)])
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If operation_ast is not found it will fail eventually anyway; throw it here to simplify other validation checks like for the GET request and for the transaction checks for mutation ops.

Comment on lines 333 to 336
execute_args = (self.schema.graphql_schema, document)
validation_errors = validate(*execute_args)

if validation_errors:
return ExecutionResult(data=None, errors=validation_errors)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The existing version called self.schema.execute which is basically just a wrapper to calling graphql_impl which performs the following steps:

  1. validate_schema -> we are doing this here
  2. parse -> we already did this operation a few lines up. This operation is surprisingly expensive to we get a nice optimization by not having to redundantly call it
  3. validate -> This actually also calls validate_schema (though schema validation is cached so that particular piece isn't done redundantly)

So rather than calling self.schema.execute I am replacing the validate call and then calling execute rather than redundantly parsing and validating the request.

@@ -186,7 +186,7 @@ def dispatch(self, request, *args, **kwargs):
or 200
)
else:
result, status_code = self.get_response(request, data, show_graphiql)
result, status_code = self.get_response(request, data)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The preceding code does not allow show_graphiql to ever be True here:

show_graphiql = self.graphiql and self.can_display_graphiql(request, data)
if show_graphiql:
return self.render_graphiql(

Ergo, there is no reason to pass it around or pivot any other behavior on whether show_graphiql is True or not so I have removed this pivot to simplify downstream logic.

execute_args = (self.schema.graphql_schema, document)

if validation_errors := validate(*execute_args):
return ExecutionResult(data=None, errors=validation_errors)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The existing version called self.schema.execute which is basically just a wrapper to calling graphql_impl which performs the following steps:

  1. validate_schema -> we are doing this here
  2. parse -> we already did this operation a few lines up. This operation is surprisingly expensive to we get a nice optimization by not having to redundantly call it
  3. validate -> This actually also calls validate_schema (though schema validation is cached so that particular piece isn't done redundantly)

Rather than calling self.schema.execute I am reimplementing the validate call and then calling execute directly rather than redundantly parsing and validating the request.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you put this part in a separate PR? I can review and merge this part now.

@kiendang
Copy link
Collaborator

kiendang commented Aug 3, 2023

@danthewildcat is this related to #1393 (comment)?

@danthewildcat
Copy link
Contributor Author

@danthewildcat is this related to #1393 (comment)?

Yes, this comment addresses the redundant parse logic mentioned by @erikwrede

This was referenced Oct 25, 2023
@kiendang
Copy link
Collaborator

Pushed 2 commits to add some small changes

  • Added the missing schema validation step in graphql_impl
  • Keep the show_graphiql arg. Agree that this arg is somewhat redundant but removing it is a non-backward compatible change.

Thanks @danthewildcat this will speed up execute_graphql_request and also paves the way for implementing support for validations rules (#1342, #1472)

Copy link
Collaborator

@sjdemartini sjdemartini left a comment

Choose a reason for hiding this comment

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

Overall this looks really good! Thanks both of you for putting this together! 🙌 Basically just one question and one minor code suggestion.

Comment on lines 352 to 353
execute_args = (self.schema.graphql_schema, document)
validation_errors = validate(*execute_args)
Copy link
Collaborator

Choose a reason for hiding this comment

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

minor: it seems slightly awkward/unnecessary to be using the execute_args when calling validate() here, since validate doesn't have the same signature as execute; they just happen to share the same first two args. How about just removing the execute_args var, passing those two arguments directly here to validate, and adding the schema and document as part of execute_options dictionary below?

Copy link
Collaborator

Choose a reason for hiding this comment

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

agree

else:
op_error = "Must provide a valid operation."

return ExecutionResult(errors=[GraphQLError(op_error)])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Took me a minute to realize why this new op_error logic above is included, and I see it's trying to provide a helpful error message to the user for the specific cases that get_operation_ast ends up returning None, which seems nice.

Question though: is it always true that execute will necessarily error in the cases that get_operation_ast returns None? If not, I worry that this new logic will introduce errors in some cases where we didn't used to error. Before, POST requests seemed to still call execute even if the AST result was None. Based on a search in the graphql-core repo, it seems that get_operation_ast is just a utility and not used internally, so I can't easily detect if there are similar scenarios that raise errors.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this logic is ported from here https://github.com/graphql-python/graphql-core/blob/0c93b8452eed38d4f800c7e71cf6f3f3758cd1c6/src/graphql/execution/execute.py#L708-L727. This is called during execute.

We also have a test

def test_errors_when_missing_operation_name(client):
response = client.get(
url_string(
query="""
query TestQuery { test }
mutation TestMutation { writeTest { test } }
"""
)
)
assert response.status_code == 400
assert response_json(response) == {
"errors": [
{
"message": "Must provide operation name if query contains multiple operations.",
}
]
}

Thus I don't think this will change the code behavior (throwing errors it didn't before). Though I do agree that not doing it ourselves is better since the checked is run inside execute anyway so this is basically unnecessary duplicated code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ahh, good finds, agreed then that it doesn't make sense to reimplement here. Thanks!

Copy link
Collaborator

@sjdemartini sjdemartini left a comment

Choose a reason for hiding this comment

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

Sweet, LGTM!

@kiendang kiendang merged commit e735f5d into graphql-python:main Oct 29, 2023
12 checks passed
superlevure pushed a commit to loft-orbital/graphene-django that referenced this pull request Dec 6, 2023
* Optimize execute_graphql_request

* Require operation_ast to be found by view handler

* Remove unused show_graphiql kwarg

* Old style if syntax

* Revert "Remove unused show_graphiql kwarg"

This reverts commit 33b3426.

* Add missing schema validation step

* Pass args directly to improve clarity

* Remove duplicated operation_ast not None check

---------

Co-authored-by: Firas Kafri <3097061+firaskafri@users.noreply.github.com>
Co-authored-by: Kien Dang <mail@kien.ai>
superlevure pushed a commit to loft-orbital/graphene-django that referenced this pull request Jan 30, 2024
* Optimize execute_graphql_request

* Require operation_ast to be found by view handler

* Remove unused show_graphiql kwarg

* Old style if syntax

* Revert "Remove unused show_graphiql kwarg"

This reverts commit 33b3426.

* Add missing schema validation step

* Pass args directly to improve clarity

* Remove duplicated operation_ast not None check

---------

Co-authored-by: Firas Kafri <3097061+firaskafri@users.noreply.github.com>
Co-authored-by: Kien Dang <mail@kien.ai>
superlevure pushed a commit to loft-orbital/graphene-django that referenced this pull request Jan 30, 2024
* Optimize execute_graphql_request

* Require operation_ast to be found by view handler

* Remove unused show_graphiql kwarg

* Old style if syntax

* Revert "Remove unused show_graphiql kwarg"

This reverts commit 33b3426.

* Add missing schema validation step

* Pass args directly to improve clarity

* Remove duplicated operation_ast not None check

---------

Co-authored-by: Firas Kafri <3097061+firaskafri@users.noreply.github.com>
Co-authored-by: Kien Dang <mail@kien.ai>
superlevure pushed a commit to loft-orbital/graphene-django that referenced this pull request Jan 30, 2024
* Optimize execute_graphql_request

* Require operation_ast to be found by view handler

* Remove unused show_graphiql kwarg

* Old style if syntax

* Revert "Remove unused show_graphiql kwarg"

This reverts commit 33b3426.

* Add missing schema validation step

* Pass args directly to improve clarity

* Remove duplicated operation_ast not None check

---------

Co-authored-by: Firas Kafri <3097061+firaskafri@users.noreply.github.com>
Co-authored-by: Kien Dang <mail@kien.ai>
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.

None yet

4 participants