Skip to content

bpo-32250: Implement asyncio.current_task() and asyncio.all_tasks()#4799

Merged
asvetlov merged 28 commits intopython:masterfrom
asvetlov:current_task2
Dec 16, 2017
Merged

bpo-32250: Implement asyncio.current_task() and asyncio.all_tasks()#4799
asvetlov merged 28 commits intopython:masterfrom
asvetlov:current_task2

Conversation

@asvetlov
Copy link
Copy Markdown
Contributor

@asvetlov asvetlov commented Dec 11, 2017

Second version.
Proof of concept, no tests, C Accelerator is not updated.

@1st1 @bdarnell please take a look

https://bugs.python.org/issue32250

Comment thread Lib/asyncio/base_tasks.py Outdated

from . import base_futures
from . import coroutines
from .events import get_running_loop
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Import the module, not the function.

Comment thread Lib/asyncio/tasks.py
By default all tasks for the current event loop are returned.
"""
if loop is None:
loop = events.get_event_loop()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You should keep get_event_loop() here for backwards compatibility. So that code that calls Task.all_tasks() before a loop is created continues to work in 3.7 (although it will raise a warning).

asyncio.all_tasks() should use get_running_loop().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure about asyncio.all_tasks() -- calling the function outside of coroutine context might make sense.
RuntimeError from asyncio.current_task() is more reasonable but I not sure again.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure about asyncio.all_tasks() -- calling the function outside of coroutine context might make sense.

Then pass a loop object explicitly. The problem with get_event_loop() is that it can create a new loop object implicitly. Or it may fail at creating it -- that function is weird.

@1st1
Copy link
Copy Markdown
Member

1st1 commented Dec 11, 2017

Overall it looks good. I think we should keep using a weak-mapping for _all_tasks.

_unregister_task exists for the purpose of forcefully removing the task object from _all_tasks for task implementations that need it, but I don't want to make it mandatory to be called.

@asvetlov
Copy link
Copy Markdown
Contributor Author

Implementation is finished (I hope), test coverage added.
Please review again.

If everything is ok -- I'll start documenting the feature.

P.S.
AbstractTask/AbstractFuture are not implemented, let's do it in separate PR.
This one is large enough already.

Comment thread Lib/asyncio/base_tasks.py Outdated


# WeakKeyDictionary of {Task: EventLoop} containing all tasks alive.
_all_tasks = weakref.WeakKeyDictionary()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why weakref.WeakKeyDictionary and not weakref.WeakValueDictionary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A task should be weak, on task garbage collection an item from _all_tasks should disappear. asyncio on master branch uses WeakSet of tasks.
But task._loop is a private property, custom task implementation can not expose it. That's why storing a loop along with task itself is required.

Mapping like {loop: WeakSet[task]} is not an option: there is no signal for loop deletion and _all_tasks is the singleton. In case of many loops creations/deletions (unit tests) the singleton will blow up with closed loops. I want to avoid it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright. This deserves to be put in a comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok

Comment thread Lib/asyncio/base_tasks.py Outdated
current_task = _current_tasks.get(loop)
if current_task is not None:
raise RuntimeError(f"Entering into task {task!r} "
f"when other task {current_task!r} is executed.")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Cannot enter into task {task!r} while another
task {current_task!r} is being executed.

Comment thread Lib/asyncio/base_tasks.py Outdated
current_task = _current_tasks.get(loop)
if current_task is not task:
raise RuntimeError(f"Leaving task {task!r} "
f"is not current {current_task!r}.")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Leaving task {task!r} does not match
the current task {current_task!r}.

Comment thread Lib/asyncio/tasks.py Outdated
from . import events
from . import futures
from .coroutines import coroutine
from .base_tasks import (all_tasks, current_task, _register_task,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please import the module, not functions from it.

Copy link
Copy Markdown
Contributor Author

@asvetlov asvetlov Dec 12, 2017

Choose a reason for hiding this comment

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

In this case base_tasks should have __all__ = ('all_tasks', 'current_task') to export names into asyncio namespace. _register_task cannot be exposed by base_tasks, see comment below.

Comment thread Lib/asyncio/tasks.py
"use asyncio.current_task() instead",
PendingDeprecationWarning,
stacklevel=2)
return current_task(loop)
Copy link
Copy Markdown
Member

@1st1 1st1 Dec 12, 2017

Choose a reason for hiding this comment

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

So we want to use asyncio.get_running_loop() in asyncio.current_task(). And we want to keep using asyncio.get_event_loop() in Task.current_task() to maintain backwards compat.

@classmethod
def current_task(cls, loop=None):
    if loop is None:
        loop = events.get_event_loop()
    warnings.warn(..)
    return current_task(loop)

Comment thread Lib/asyncio/base_tasks.py Outdated

def current_task(loop=None):
if loop is None:
loop = events.get_event_loop()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use events.get_running_loop() here -- we want to use the new API here. It doesn't make sense to call asyncio.current_task() and accidentally create a new event loop because get_event_loop() thinks so.

Comment thread Lib/asyncio/tasks.py Outdated
_PyTask = Task
_py_register_task = _register_task
_py_enter_task = _enter_task
_py_leave_task = _leave_task
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You should do this logic in base_tasks.py.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not so easy: asyncio._asyncio imports asyncio.base_tasks. C Accelerators becomes available only in asyncio.tasks, pushing them into base_tasks leads to circular imports.

That's why I've decided to have both _py_register_task and _c_register_task in the same asyncio.tasks module.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Then define all these functions (and _all_tasks and _current_tasks mappings) in _asynciomodule.c and try to import them in tasks.py. If that fails, define them in Python. Let's not touch base_tasks.py at all in this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If we define _all_tasks in _asynciomodule.c -- C Accelerator becomes mandatory.
Let's define _all_tasks and _current_tasks in base_tasks.py but move _register_task and family into tasks.py.

Copy link
Copy Markdown
Member

@1st1 1st1 Dec 12, 2017

Choose a reason for hiding this comment

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

C Accelerator becomes mandatory.

No, why?

_asynciomodule.c:

// defines _all_tasks, _current_tasks

asyncio/tasks.py:

try:
    from _asyncio import _all_tasks, _current_tasks
except ImportError:
    _all_tasks = ...
    _current_tasks = ...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I actually wanted to make a separate PR

I can make that PR myself now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

working on it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Please do.
I'm going to sleep soon anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please do.

Almost there.

Thus asyncio.current_loop() should be in base_tasks.py.

Yeah... I get what you're saying now. OK, let it be in base_tasks.py. I'm not blocking you from working on this PR, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are not blocking.

Comment thread Lib/asyncio/tasks.py Outdated
Task = _CTask = _asyncio.Task
_register_task = _c_register_task = _asyncio._register_task
_enter_task = _c_enter_task = _asyncio._enter_task
_leave_task = _c_leave_task = _asyncio._leave_task
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You should do this logic in base_tasks.py.

Comment thread Modules/_asynciomodule.c

static PyObject *
_asyncio__enter_task_impl(PyObject *module, PyObject *loop, PyObject *task)
/*[clinic end generated code: output=a22611c858035b73 input=de1b06dca70d8737]*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I prefer not to use _impl functions in C code. Better to implement int enter_task(loop, task) function and use it in Task implementation directly. Then you call that function in _asyncio__enter_task_impl and return NULL/None based on its return value. This way you also don't need to decref None in Task code, and the code is overall cleaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok

@1st1
Copy link
Copy Markdown
Member

1st1 commented Dec 12, 2017

AbstractTask/AbstractFuture are not implemented, let's do it in separate PR.

Absolutely, a separate PR is the way to go.

@asvetlov
Copy link
Copy Markdown
Contributor Author

Do you agree with using get_event_loop() in asyncio.all_tasks()? asyncio.current_task() will be switched to get_running_loop().

@1st1
Copy link
Copy Markdown
Member

1st1 commented Dec 14, 2017

overall LGTM.

Please apply this diff to fix tests in refleak mode:

diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py
index 31d4ed649c..f6a1ac6f31 100644
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -2202,6 +2202,8 @@ class BaseTaskIntrospectionTests:
         self.assertEqual(asyncio.all_tasks(loop), set())
         self._register_task(loop, task)
         self.assertEqual(asyncio.all_tasks(loop), {task})
+        self._unregister_task(loop, task)
+        self.assertEqual(asyncio.all_tasks(loop), set())

     def test__enter_task(self):
         task = mock.Mock()
@@ -2209,6 +2211,7 @@ class BaseTaskIntrospectionTests:
         self.assertIsNone(asyncio.current_task(loop))
         self._enter_task(loop, task)
         self.assertIs(asyncio.current_task(loop), task)
+        self._leave_task(loop, task)

     def test__enter_task_failure(self):
         task1 = mock.Mock()
@@ -2218,6 +2221,7 @@ class BaseTaskIntrospectionTests:
         with self.assertRaises(RuntimeError):
             self._enter_task(loop, task2)
         self.assertIs(asyncio.current_task(loop), task1)
+        self._leave_task(loop, task1)

     def test__leave_task(self):
         task = mock.Mock()
@@ -2234,6 +2238,7 @@ class BaseTaskIntrospectionTests:
         with self.assertRaises(RuntimeError):
             self._leave_task(loop, task2)
         self.assertIs(asyncio.current_task(loop), task1)
+        self._leave_task(loop, task1)

     def test__leave_task_failure2(self):
         task = mock.Mock()
@@ -2246,13 +2251,13 @@ class BaseTaskIntrospectionTests:
         task = mock.Mock()
         loop = mock.Mock()
         self._register_task(loop, task)
-        self._unregister_task(loop, task)
+        asyncio._unregister_task(loop, task)
         self.assertEqual(asyncio.all_tasks(loop), set())

     def test__unregister_task_not_registered(self):
         task = mock.Mock()
         loop = mock.Mock()
-        self._unregister_task(loop, task)
+        asyncio._unregister_task(loop, task)
         self.assertEqual(asyncio.all_tasks(loop), set())

Comment thread Doc/library/asyncio-task.rst Outdated

.. function:: current_task(loop=None):

Return a currently executed task or ``None`` if no task is running.
Copy link
Copy Markdown
Member

@1st1 1st1 Dec 15, 2017

Choose a reason for hiding this comment

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

Please change to "Return the current running task or None, if no task is running."

Comment thread Doc/library/asyncio-task.rst Outdated
Return a currently executed task or ``None`` if no task is running.

If *loop* is ``None`` :func:`get_running_loop` is used for gettung
current loop.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

gettung -> getting

Please change to "If loop is None, :func:get_running_loop is used to get the current loop."

Comment thread Doc/library/asyncio-task.rst Outdated
.. function:: all_tasks(loop=None):

Return a set of tasks created for the *loop* (the set can be empty
if there is no task exists).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Change to: "Return a set of :class:Task objects created for the loop."

I don't think "the set can be empty if there is no task exists" is needed, I think people will figure it out.

@1st1
Copy link
Copy Markdown
Member

1st1 commented Dec 15, 2017

This is ready to be merged. Just please fix two doc nits.

Comment thread Modules/_asynciomodule.c Outdated
enter_task(PyObject *loop, PyObject *task)
{
PyObject *item;
item = PyDict_GetItem(current_tasks, loop);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe we can further optimize this by using _PyDict_GetItem_KnownHash and _PyDict_SetItem_KnownHash?

@asvetlov
Copy link
Copy Markdown
Contributor Author

Notes are fixed.
The only thing bothers me: current_task(loop) accepts a loop as regular parameter but asyncio usually accepts it as keyword-only.
Should I change it?

@1st1
Copy link
Copy Markdown
Member

1st1 commented Dec 16, 2017

I think it's ok for loop to be a positional parameter for our new functions. The reason it was a keyword-only parameter was that those functions were methods on Task and that's the convention for methods.

Comment thread Modules/_asynciomodule.c Outdated
{
PyObject *item;
item = PyDict_GetItem(current_tasks, loop);
long hash;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Py_hash_t

Comment thread Modules/_asynciomodule.c Outdated
{
PyObject *item;
item = PyDict_GetItem(current_tasks, loop);
long hash;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Py_hash_t

Comment thread Modules/_asynciomodule.c Outdated
item = PyDict_GetItem(current_tasks, loop);
long hash;
hash = PyObject_Hash(loop);
if (hash == NULL) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

== -1

Comment thread Modules/_asynciomodule.c Outdated
item = PyDict_GetItem(current_tasks, loop);
long hash;
hash = PyObject_Hash(loop);
if (hash == NULL) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

== -1

@1st1
Copy link
Copy Markdown
Member

1st1 commented Dec 16, 2017

Looks like there's some compile/sphinx issue in the updated documentation. Otherwise LGTM. Let's merge this.

@asvetlov asvetlov merged commit 44d1a59 into python:master Dec 16, 2017
@asvetlov asvetlov deleted the current_task2 branch December 16, 2017 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants