This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Hooking into pause/resume of iterators/coroutines
Type: behavior Stage: resolved
Components: asyncio Versions: Python 3.8, Python 3.7, Python 3.6
process
Status: closed Resolution: rejected
Dependencies: Superseder:
Assigned To: Nosy List: Liran Nuna, asvetlov, yselivanov
Priority: normal Keywords:

Created on 2018-06-20 18:24 by Liran Nuna, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Files
File name Uploaded Description Edit
async_context_managers.py Liran Nuna, 2018-06-20 18:24
Messages (7)
msg320101 - (view) Author: Liran Nuna (Liran Nuna) * Date: 2018-06-20 18:24
An interesting property of async programming is that execution order is nondeterministic and async function "pause" and "resume" execution as events come in.

This can play havok with context managers, especially ones that wrap a global state change. I can best explain it with code - see attached file.

If you were to run this, you'd notice that "Should be logged" does not get logged - this is because the execution order runs the context manager immediately and that affects the entire batch (created by asyncio.gather).

Is there a way to hook into a pause/resume handling of coroutines so this kind of thing could be done correctly? I'm aware that this particular problem could be solved with the new context variables introduced with python3.7, however it is just a simplification of our actual issue.

Iterators also suffer from this issue, as `yield` pauses and resumes execution.
msg320105 - (view) Author: Liran Nuna (Liran Nuna) * Date: 2018-06-20 18:58
I would like to stress this issue happens with iterators as well, and this isn't a unique issue to asyncio only. 

I would like to propose four new magic methods for context managers to solve this: __pause__, __resume__, __apause__ and __aresume__ which will be called before/after a pause/resume happen before the coroutine/iterator continues.

I'm not sure however if this is the correct venue for such discussion.
msg320110 - (view) Author: Yury Selivanov (yselivanov) * (Python committer) Date: 2018-06-20 20:53
You should try to use the contextvars module that was specifically created to handle local context state (for tasks & coroutines).
msg320115 - (view) Author: Liran Nuna (Liran Nuna) * Date: 2018-06-20 21:10
> You should try to use the contextvars module that was specifically created to handle local context state (for tasks & coroutines).

Yury, from my original report:

> I'm aware that this particular problem could be solved with the new context variables introduced with python3.7, however it is just a simplification of our actual issue.

Not everything can use context managers. Imagine the context manager is mock.patch used in testing and you want to run two tests in "parallel", each with a different mocked method. mock.patch isn't aware of `await` so patching will be incorrect.

Those are just some behaviors where context variables don't solve the issue I'm describing.
msg320116 - (view) Author: Yury Selivanov (yselivanov) * (Python committer) Date: 2018-06-20 21:25
> Imagine the context manager is mock.patch used in testing and you want to run two tests in "parallel", each with a different mocked method. mock.patch isn't aware of `await` so patching will be incorrect.

That's still doable with contextvars. You just need a custom mock-like object (or library) that stores its settings/state in a context variable.

Now, the "mock" module doesn't provide this functionality out of the box, but I hope that somebody will come up with a new mock library that will work that way (or with a new mock primitive) after 3.7.0 is released.

Adding __pause__ and __resume__ was considered in PEP 521, and it was decided that the actual implementation will be too slow and complex.  It's very unlikely that PEP 521 is ever accepted.
msg320546 - (view) Author: Liran Nuna (Liran Nuna) * Date: 2018-06-27 06:04
> That's still doable with contextvars. You just need a custom mock-like object (or library) that stores its settings/state in a context variable.

contextvars only work with asyncio, what about the iterator case?

In addition, you can't possibly expect authors to re-implement a library just because it may or may not be used with asyncio. In my example, re-implementing mock/patch is quite a task just to get such basic functionality.

In other words, contextvars don't solve this issue, it just creates new issues to solve and causes code duplication.
msg343885 - (view) Author: Andrew Svetlov (asvetlov) * (Python committer) Date: 2019-05-29 14:29
decimal was changed from threading.local to contextvar usage.
The module is "safe" not only for asyncio but for threading, trio etc.

unittest.mock doesn't use explicit context all for patching.
It changes global interpreter-wide objects instead.

So, mock.patch fails not only if two async tasks are executed in parallel but two threads also.

I doubt if thread-local (or contextvar) can be applied to mock because it changes the current behavior -- but this is a different story.

*Any* library that needs to modify a global state, e.g. your MyLogger.enabled can use contextvars for handling it.

Say again, contextvars is not for asyncio-only but a generic instrument for handling context-aware variables.

I'm going to close the issue.
History
Date User Action Args
2022-04-11 14:59:02adminsetgithub: 78099
2019-05-29 14:29:57asvetlovsetstatus: open -> closed
resolution: rejected
stage: resolved
2019-05-29 14:29:34asvetlovsetmessages: + msg343885
2018-06-27 06:04:55Liran Nunasetmessages: + msg320546
2018-06-20 21:25:50yselivanovsetmessages: + msg320116
2018-06-20 21:10:14Liran Nunasetmessages: + msg320115
2018-06-20 20:53:04yselivanovsetmessages: + msg320110
2018-06-20 18:58:27Liran Nunasetmessages: + msg320105
2018-06-20 18:44:28ned.deilysetnosy: + asvetlov, yselivanov

components: + asyncio, - Interpreter Core
versions: - Python 3.4, Python 3.5
2018-06-20 18:24:16Liran Nunacreate