-
-
Notifications
You must be signed in to change notification settings - Fork 317
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
Better Async Effects #1093
Better Async Effects #1093
Conversation
This interface should be relatively similar to the Notes
import asyncio
from reactpy import component, use_effect
@component
def example():
async def teardown():
...
@use_effect(timeout=2, teardown=teardown)
async def example_effect(cancel: asyncio.Event):
while True:
await something("my-message-type")
...
if cancel.is_set():
break |
I think this is a slight simplification - teardown should just happen after the stop event is triggered. @use_effect
async def effect(stop):
# do effect
await stop.wait()
# cleanup
I thought about this for a bit and realized that a simple await asyncio.wait_for(task(), timeout...) |
This can't always occur though. For example, in the case of the user closing their browser window. We need a timeout to cover edge cases like this.
We can call it
What you proposed isn't equivalent to a |
Both comments seem to address the fact that we might want to enforce a timeout when a connection is closed. That seems reasonable, but in that case, it seems better to introduce that timeout here when a
Though, even without this, the user could enforce a "stop timeout" themselves if they wanted: await wait_for(create_effect(), timeout=...) # creation timeout
await stop.wait()
await wait_for(cleanup_effect(), timeout=...) # cleanup timeout |
f3d4405
to
be5cf27
Compare
So thinking through our interface some more I'm realizing that: @use_effect
async def my_effect(stop):
task = asyncio.create_task(do_something())
await stop.wait()
task.cancel()
await finalize_it() Is not correct since simply cancelling a task does not mean that it will have exited by the time @use_effect
async def my_effect(stop):
task = asyncio.create_task(do_something())
await stop.wait()
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
await finalize_it() This is far too complicated for users to understand. As such, I've done some thinking and realized that in 3.11 @use_effect
async def my_effect(effect):
async with effect:
await do_something()
await finalize_it() The behavior is that awaitables within the The implementation of the class AsyncEffect:
_task: asyncio.Task | None = None
def __init__(self) -> None:
self._stop = asyncio.Event()
self._cancel_count = 0
def stop(self) -> None:
if self._task is not None:
self._cancel_task()
self._stop.set()
async def __aenter__(self) -> None:
self._task = asyncio.current_task()
self._cancel_count = self._task.cancelling()
if self._stop.is_set():
self._cancel_task()
return None
async def __aexit__(self, exc_type: type[BaseException], *exc: Any) -> Any:
if exc_type is not asyncio.CancelledError:
# propagate non-cancellation exceptions
return None
if self._task.cancelling() > self._cancel_count:
# Task has been cancelled by something else - propagate it
return None
await self._stop.wait()
return True
def _cancel_task(self) -> None:
assert self._task is not None
self._task.cancel()
self._cancel_count += 1 |
Is there any way for us to use Python 3.11 asyncio features in older versions? I'm pretty sure the answer is no, and if so what do we want to do in the interim while we wait for 3.11 to become our minimum version? |
I'll have to play around with whether this can be achieved with |
If we're going to introduce async specific Such as |
3550cbf
to
3d81311
Compare
00c5830
to
72d4d66
Compare
72d4d66
to
4ee124c
Compare
Limiting the default python version to 3.11 seems to have fixed it. |
8ee3e6a
to
b389b2b
Compare
f546794
to
3fe82fa
Compare
I'm going to close this and break up the changes into two parts:
|
By submitting this pull request you agree that all contributions to this project are made under the MIT license.
Issues
Closes: #956
Solution
Async effects now accept a "stop"
Event
that is set when an effect needs to be re-run or a component is unmounting. The next effect will only run when the last effect has exited.Implementing this same behavior using sync effects is quite challenging:
To achieve this without requiring an implementation similar to the above, we've asyncified the internals of
Layout
. This has the side-effect of allowing other things to happen while theLayout
is rendering (e.g. receive events, or allowing the server to respond to other requests). This potentially comes at the cost of rendering speed. For example, if a user is spamming the server with events renders may be interrupted in order to respond to them.Checklist
changelog.rst
has been updated with any significant changes.