Repository navigation
Better Async Effect Cleanup #956
Description
Activity
- addedflag-triageNot prioritized.Not prioritized.priority-2-moderateShould be resolved on a reasonable timeline.Should be resolved on a reasonable timeline.type-revisionAbout a change in functionality or behaviorAbout a change in functionality or behaviorrelease-patchWarrents a patch releaseWarrents a patch releaseand removedflag-triageNot prioritized.Not prioritized.
on Mar 16, 2023 @Archmonger I'm realizing that replicating the prior cancellation behavior is quite tricky to do with the interupt event - it would almost always be better to just write a sync event in that case. Perhaps we should preserve the original behavior where we cancel the effect if it does not not accept any arguments?
That was my original justification for the
async_scheduleparameter (name TBD). It simplifies awkward situations like that.I think it's less ambiguous than just assuming cancellation on argless functions. With this, it might cover enough to where we don't need the interrupt parameter.
- cancel-oldest # The current behavior
- queue-all # Best for something that needs to run in the background every time, no matter what
- no-requeue # Best for long polling functions
The interrupt event is definitely necessary since that's what allows the user to implement cleanup logic after the event has been set. I'm also realizing that effects must run one after the other - that is, we must wait for the the prior effect to complete before kicking off the next one. Otherwise you could end up in a weird situation where the cleanup logic for the last effect might be running while the setup logic for the next one does.
Otherwise you could end up in a weird situation where the cleanup logic for the last effect might be running while the setup logic for the next one does.
I believe race conditions are expected with asyncio, and should be a problem for the user to solve.
I'm not a huge fan of an implict cancellation policy on argless functions...
Maybe we have a
cancellableparameter in theuse_effectcall?@use_effect(cancellable=True) async def do_something(interrupt): ...
But then that kinda makes
interruptuseless in this context anyways... Maybe we really do have to just support argless async effects. Either that or supportasync_schedule, which isn't ideal either.I believe race conditions are expected with asyncio, and should be a problem for the user to solve.
The problem is that, with the proposed API, the user doesn't actually have enough control to manage this themselves. We would need to pass in a reference to the prior task so they could await it if they wanted:
@use_effect def my_effect(prior: Task | None, interupt: Event): if prior is not None: await prior # do stuff await interupt.wait() # clean up
This may actually be the interface that provides the necessary level of control. With a reference to the prior task you could cancel it if you wanted.
If we think we might need even more options in the future, we can consider a
dataclasscalled something likeEffectHandlercontaining both of these args.- linked a pull request that will close this issueasync effects get prior task and interupt event #957
on Apr 3, 2023 Should this be closed in favor of
Deprecate async effectsissue?Probably
Cancellation shielding may be the answer to this interface question. In short, the current behavior would be maintained. That is, async effects are cancelled before the next one is executed. Users can then leverage cancellation shielding to protect important tasks from cancellation if necessary.
With that said, we should, but currently don't, wait for the completion of old cancelled effects.
Fundamentally, work being done by an effect as a result of an old render should be stopped. The main issue for users is doing so gracefully since dealing with the unpredictable timing of cancellations is challenging. Thus, the remedy may simply be good documentation that explains this.
Current Situation
Presently, the "cleanup" behavior for async effects is that they are cancelled before the next effect takes place or the component is unmounted. While this behavior may be desirable in many cases, it does not give the user the ability to gracefully deal with cleanup logic in the way one can with a sync event.
For example, consider a long running async effect:
The problem with this current behavior is that handling the cancellation is messy. You could do the following:
However, this may lead to some confusing behavior since it's not possible to know whether
something()orsomething_else()will receive the cancellation. You might have to do a lot of extra work to figure out what state your program was left in after the cancellation.Proposed Actions
Thankfully the above only impacts async effects. With a synchronous event, you would be able to handle async task cleanup using something like the following:
Given this, our proposed solution to the problem is to allow async effects to accept a similar interupt
asyncio.Event: