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

Support subscriptions extensions #3554

Merged

Conversation

nrbnlulu
Copy link
Member

@nrbnlulu nrbnlulu commented Jul 2, 2024

Description

Support extensions in subscriptions operations.
Thing to note is that GraphQL core ~3.2 doesn't support middlewares (which are used by our schema extension resolve) on subscription. Support for this was added in 876aef6.
We can initially remove resolve support until we upgrade to 3.3 Please LMK what you guys think.

PS: There are few tests that are not passing since I upgraded GraphQL-core (for the midleware support) and there are some incompatibilities (mostly due to spaces in the SDL).

Types of Changes

  • Core
  • Bugfix
  • New feature
  • Enhancement/optimization
  • Documentation

Issues Fixed or Closed by This PR

fix #2097
closes #3598

TODO

  • fix integrations tests
  • test extensions hooks for many yields
  • test first yield is an error
  • test that extensions results are cleaned between yields
  • remove deprecation's about execution context. we'll do that in another pr
  • resolve optimization TODO's after review is accepted.

Things I am not certain about

  1. on_execute was chosen the mitigate the normal on_execute per subscription message.
    The thing is after the last yield of the subscription we still wrap with on_execution causing a last on_execution lifetime although the async gen raised StopAsyncIteration I am not sure this is the wanted behavior.
  2. Currently I clear ExecutionContext.extensions_result on every yield do you think this is the wanted behaviour?

Summary by Sourcery

This pull request introduces support for schema extensions in subscription operations, allowing hooks to be executed for each yield in a subscription. It also includes significant refactoring to improve error handling and performance, along with comprehensive tests and updated documentation.

  • New Features:
    • Added support for schema extensions in subscription operations, allowing hooks like on_execute to be called for each subscription yield.
  • Enhancements:
    • Refactored the execution and subscription handling to use internal types instead of graphql-core result types.
    • Improved error handling in subscription operations to differentiate between immediate errors and execution errors.
    • Introduced caching for extensions and middleware manager in the schema to optimize performance.
  • Documentation:
    • Updated documentation to include examples and explanations for using schema extensions with subscriptions.
  • Tests:
    • Added comprehensive tests for subscription extensions, including tests for immediate errors, errors after yields, and extension results clearing between yields.
    • Refactored existing tests to align with the new subscription handling and error processing logic.

Copy link

codecov bot commented Jul 2, 2024

Codecov Report

Attention: Patch coverage is 98.70130% with 7 lines in your changes missing coverage. Please review.

Project coverage is 96.77%. Comparing base (fc25e04) to head (56e0a6f).
Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3554      +/-   ##
==========================================
- Coverage   96.79%   96.77%   -0.03%     
==========================================
  Files         517      521       +4     
  Lines       33517    33731     +214     
  Branches     5570     5627      +57     
==========================================
+ Hits        32444    32644     +200     
- Misses        845      855      +10     
- Partials      228      232       +4     

Copy link

codspeed-hq bot commented Jul 2, 2024

CodSpeed Performance Report

Merging #3554 will degrade performances by 80%

Comparing nrbnlulu:support_extensions_on_subscriptions (56e0a6f) with main (fc25e04)

Summary

❌ 1 regressions
✅ 14 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Benchmarks breakdown

Benchmark main nrbnlulu:support_extensions_on_subscriptions Change
test_subscription 43.3 ms 216.3 ms -80%

@DoctorJohn DoctorJohn self-requested a review July 4, 2024 21:17
Copy link
Member

@DoctorJohn DoctorJohn left a comment

Choose a reason for hiding this comment

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

Just finished reviewing the websocket protocol parts. Looks like readability went up in some places and we still follow the protocol specs 🎉 I left some minor comments and my approval because at its core this PR is pretty much good to go.

strawberry/subscriptions/protocols/graphql_ws/handlers.py Outdated Show resolved Hide resolved
Comment on lines +67 to +68
if extensions is not None:
assert response["payload"]["extensions"] == extensions
Copy link
Member

Choose a reason for hiding this comment

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

Would it be assert response["payload"]["extensions"] == extensions or {} because extensions, if set, would be a map? I would prefer including the or {}, because that should technically be a valid value as well.

tests/websockets/test_graphql_transport_ws.py Outdated Show resolved Hide resolved
Copy link
Member

@bellini666 bellini666 left a comment

Choose a reason for hiding this comment

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

One last review round. After this one I'm ready to actually approve this

strawberry/schema/schema.py Outdated Show resolved Hide resolved

if self.directives:
extensions.append(DirectivesExtensionSync if sync else DirectivesExtension)
def _get_middleware_manager(
Copy link
Member

Choose a reason for hiding this comment

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

The same suggestion as @patrick91 made applies for this. You can probably move this to a cached_property called middleware_manager, then you don't need to handle caching by yourself

Copy link
Member Author

Choose a reason for hiding this comment

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

This one is kinda trickier because extensions are not just a boolean argument. I think the current approuch suffice.
LMK if thats a blocker, I'll think of something better.

Comment on lines +162 to +164
yield first
async for result in asyncgen:
yield result
Copy link
Member

Choose a reason for hiding this comment

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

Wondering what happens if this gets cancelled, specially with asyncio.CancelledError, maybe the asyncgen would not be closed?

Maybe:

Suggested change
yield first
async for result in asyncgen:
yield result
try:
yield first
async for result in asyncgen:
yield result
finally:
with suppress(RuntimeError):
await asyncgen.aclose()

or to do that specifically for cancellederror:

Suggested change
yield first
async for result in asyncgen:
yield result
try:
yield first
async for result in asyncgen:
yield result
except asyncio.CancelledError:
with suppress(RuntimeError):
await asyncgen.aclose()
raise

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you think of a cenario that this would happend? I'll add a test for it.

Copy link
Member

Choose a reason for hiding this comment

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

Wondering what happens if this gets cancelled, specially with asyncio.CancelledError, maybe the asyncgen would not be closed?

I'm pretty sure the async generator would also live in the canceled task and, therefore, be automatically canceled as well.

# To overcome this while maintaining the extension contexts we do this trick.
first = await asyncgen.__anext__()
if isinstance(first, PreExecutionError):
await asyncgen.aclose()
Copy link
Member

Choose a reason for hiding this comment

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

This is going to raise RuntimeError in case the generator is already closed

Suggested change
await asyncgen.aclose()
with suppress(RuntimeError):
await asyncgen.aclose()

Copy link
Member Author

Choose a reason for hiding this comment

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

Why would it be though?

Comment on lines +67 to +68
if extensions is not None:
assert response["payload"]["extensions"] == extensions
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I understand. how would that differ from what it does RN?

What you have right now will not check what response["payload"]["extensions"] is at all

My suggestion (and I think @DoctorJohn is saying the same) is to also check it when extensions is None, to make sure that it contains an empty list/dict/whatever would be returned in that case

tests/websockets/test_graphql_ws.py Outdated Show resolved Hide resolved
Copy link
Member

@patrick91 patrick91 left a comment

Choose a reason for hiding this comment

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

let's go!

patrick91

This comment was marked as duplicate.

patrick91

This comment was marked as duplicate.

@patrick91 patrick91 merged commit 0dcf23d into strawberry-graphql:main Sep 10, 2024
103 of 105 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GraphQL Core 3.3 tests appear to be running 3.2 instead Extenstions support for subscriptions.
6 participants