-
-
Notifications
You must be signed in to change notification settings - Fork 511
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
Reimplement channel_listen as context manager #2856
Reimplement channel_listen as context manager #2856
Conversation
|
Addresses issue #2799 |
|
Thanks for adding the Here's a preview of the changelog: This release updates the API to listen to Django Channels to avoid race conditions Deprecations: This release contains a deprecation for the Channels integration. The An example of migrating existing code is given below: # Existing code
@strawberry.type
class MyDataType:
name: str
@strawberry.type
class Subscription:
@strawberry.subscription
async def my_data_subscription(
self, info: Info, groups: list[str]
) -> AsyncGenerator[MyDataType | None, None]:
yield None
async for message in info.context["ws"].channel_listen("my_data", groups=groups):
yield MyDataType(name=message["payload"])# New code
@strawberry.type
class Subscription:
@strawberry.subscription
async def my_data_subscription(
self, info: Info, groups: list[str]
) -> AsyncGenerator[MyDataType | None, None]:
async with info.context["ws"].listen_to_channel("my_data", groups=groups) as cm:
yield None
async for message in cm:
yield MyDataType(name=message["payload"])Here's the preview release card for twitter: Here's the tweet text: |
|
This is the simplest implementation possibility. However, it has the problem that a leak can occur if the async generator is never started. Will create a second approach that uses a context manager to ensure that the cleanup will be done. |
|
Added a second option of starting channel_listen with a context manager to ensure that group_discard is called after it is started. |
|
@DoctorJohn @patrick91 Wanted to ask which approach you'd prefer? ContextManager or the simple approach |
Hi @moritz89 thanks for the PR. I prefer the ContextManager approach. It's the one I did my quick proof of concept with. Main argument for it is the cleanup ability you already mentionened in your comment. Feel free to remove the "simple approach". |
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #2856 +/- ##
==========================================
- Coverage 96.36% 96.18% -0.18%
==========================================
Files 207 211 +4
Lines 8948 9212 +264
Branches 1645 1701 +56
==========================================
+ Hits 8623 8861 +238
- Misses 206 226 +20
- Partials 119 125 +6 |
Why: - Allow user code to be run after Channels subscription - Avoids race condition when confirming GQL subscriptions This change addresses the need by: - Returning the async generator instead of runnning it
Why: - Ensure no leak of group_add / group_discard This change addresses the need by: - Encapsulating channel_listen with a context manager
Why: - Only support code path without resource leak This change addresses the need by: - Removing naive channel_listen methods
bf1762e
to
b14dd0b
Compare
for more information, see https://pre-commit.ci
|
Isn't this fun, adding breaking changes 😅 Anything else that should be updated? |
|
@moritz89 it is a lot of work to make it backwards compatibile with a deprecation? 😊 |
Not at all. The question is only how this should be handled. It would result in two functions in the API and the question is how they should be named. One option is to append |
|
@patrick91 I've had a look at unifying the calls in the following style, but also that did not work and going down that line of thought makes the library code itself more and more tricky. If you have any suggestions, would be glad to take them up. class ChannelListen:
def __init__(
self, type: str, *, timeout: Optional[float] = None, groups: Sequence[str] = ()
) -> None:
...
async def __call__(
self, type: str, *, timeout: Optional[float] = None, groups: Sequence[str] = ()
) -> Any:
...
async def __aenter__(self) -> Awaitable[AsyncGenerator[Any, None]]:
...
async def __aexit__(self) -> None:
...
async def _channel_listen_generator(
self, queue: asyncio.Queue, timeout: Optional[float]
) -> AsyncGenerator[Any, None]:
...
class ChannelsConsumer(AsyncConsumer):
...
channel_listen = ChannelListen |
Why: - Ensure context manager channel_listen works This change addresses the need by: - Updating existing tests - Adding new test to show yield after channel subscription
|
Fixed mypy issues and updated existing tests and added a new one, but don't know why one is failing:
Would appreciate help in fixing it EDIT: Seems to be passing in the CI 😅 |
|
Methods don't know whether they're called with or without
I would recommend using two methods and naming the newer one something like Using the second approach would be akward IMHO since it would require a second migration after we end the deprecation period and remove the parameter deciding the return type. |
|
@DoctorJohn If the plan is to deprecate the existing approach, then I'll create a commit assuming you meant the former (deprecate the existing approach). |
|
How should the deprecation be communicated to the library users? |
Yup, that's what I meant. I think the new approach is superior and covers all usecases of the old one.
Using the strawberry/strawberry/types/info.py Lines 52 to 56 in 19be575
|
|
@patrick91 The MR won't break compatibility but adds a deprecation warning |
Thank you so much! <3 |
Co-authored-by: Jonathan Ehwald <github@ehwald.info>
Co-authored-by: Jonathan Ehwald <github@ehwald.info>
Downgraded from breaking change to deprecation
|
@DoctorJohn @patrick91 Does this MR require anything else or can it be merged? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry for the late review, it looks good to me! I only left a minor comment on the release note, maybe we can simplify the snippet there?
| async def my_data_subscription( | ||
| self, info: Info, groups: list[str] | ||
| ) -> AsyncGenerator[MyDataType | None, None]: | ||
| yield None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any reason why there's this yield none here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's to show how GQL subscription confirmations (with null data) should be migrated, i.e., that confirmation messages should be sent before the async for loop. However, for every else not confirming their GQL subscriptions the yield None can be omitted in both new and old code variants.
How should the doc be updated to reflect that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh I see, let's leave it there.
For the docs, maybe we could add a paragraph about confirming subscriptions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Added a section in the channels.md doc
|
Thanks for contributing to Strawberry! 🎉 You've been invited to join You can also request a free sticker by filling this form: https://forms.gle/dmnfQUPoY5gZbVT67 And don't forget to join our discord server: https://strawberry.rocks/discord 🔥 |


Why:
This change addresses the need by:
Description
Types of Changes
Issues Fixed or Closed by This PR
Checklist