Skip to content
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

Add Task.eventual_parent_nursery introspection attribute #1558

Merged
merged 4 commits into from
May 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/source/reference-lowlevel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,11 @@ Windows-specific API

.. function:: WaitForSingleObject(handle)
:async:

Async and cancellable variant of `WaitForSingleObject
<https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx>`__.
Windows only.

:arg handle:
A Win32 object handle, as a Python integer.
:raises OSError:
Expand Down Expand Up @@ -517,6 +517,8 @@ Task API

.. autoattribute:: parent_nursery

.. autoattribute:: eventual_parent_nursery

.. autoattribute:: child_nurseries

.. attribute:: custom_sleep_data
Expand Down
11 changes: 11 additions & 0 deletions newsfragments/1558.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Tasks spawned with `nursery.start() <trio.Nursery.start>` aren't treated as
direct children of their nursery until they call ``task_status.started()``.
This is visible through the task tree introspection attributes such as
`Task.parent_nursery <trio.lowlevel.Task.parent_nursery>`. Sometimes, though,
you want to know where the task is going to wind up, even if it hasn't finished
initializing yet. To support this, we added a new attribute
`Task.eventual_parent_nursery <trio.lowlevel.Task.eventual_parent_nursery>`.
For a task spawned with :meth:`~trio.Nursery.start` that hasn't yet called
``started()``, this is the nursery that the task was nominally started in,
where it will be running once it finishes starting up. In all other cases,
it is ``None``.
23 changes: 21 additions & 2 deletions trio/_core/_run.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# coding: utf-8

import functools
import itertools
import logging
Expand Down Expand Up @@ -659,6 +661,7 @@ def started(self, value=None):
self._old_nursery._children = set()
for task in tasks:
task._parent_nursery = self._new_nursery
task._eventual_parent_nursery = None
self._new_nursery._children.add(task)

# Move all children of the old nursery's cancel status object
Expand Down Expand Up @@ -862,7 +865,7 @@ def start_soon(self, async_fn, *args, name=None):
If you want to run a function and immediately wait for its result,
then you don't need a nursery; just use ``await async_fn(*args)``.
If you want to wait for the task to initialize itself before
continuing, see :meth:`start()`.
continuing, see :meth:`start`.

It's possible to pass a nursery object into another task, which
allows that task to start new child tasks in the first task's
Expand Down Expand Up @@ -942,7 +945,10 @@ async def async_fn(arg1, arg2, \*, task_status=trio.TASK_STATUS_IGNORED):
async with open_nursery() as old_nursery:
task_status = _TaskStatus(old_nursery, self)
thunk = functools.partial(async_fn, task_status=task_status)
old_nursery.start_soon(thunk, *args, name=name)
task = GLOBAL_RUN_CONTEXT.runner.spawn_impl(
thunk, args, old_nursery, name
)
task._eventual_parent_nursery = self
# Wait for either _TaskStatus.started or an exception to
# cancel this nursery:
# If we get here, then the child either got reparented or exited
Expand Down Expand Up @@ -992,6 +998,7 @@ class Task(metaclass=NoPublicConstructor):

# For introspection and nursery.start()
_child_nurseries = attr.ib(factory=list)
_eventual_parent_nursery = attr.ib(default=None)

# these are counts of how many cancel/schedule points this task has
# executed, for assert{_no,}_checkpoints
Expand All @@ -1013,6 +1020,18 @@ def parent_nursery(self):
"""
return self._parent_nursery

@property
def eventual_parent_nursery(self):
"""The nursery this task will be inside after it calls
``task_status.started()``.

If this task has already called ``started()``, or if it was not
spawned using `nursery.start() <trio.Nursery.start>`, then
its `eventual_parent_nursery` is ``None``.

"""
return self._eventual_parent_nursery

@property
def child_nurseries(self):
"""The nurseries this task contains.
Expand Down
26 changes: 21 additions & 5 deletions trio/_core/tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ async def child():

async def test_root_task():
root = _core.current_root_task()
assert root.parent_nursery is None
assert root.parent_nursery is root.eventual_parent_nursery is None


def test_out_of_context():
Expand Down Expand Up @@ -1588,7 +1588,7 @@ async def test_task_tree_introspection():
tasks = {}
nurseries = {}

async def parent():
async def parent(task_status=_core.TASK_STATUS_IGNORED):
tasks["parent"] = _core.current_task()

assert tasks["parent"].child_nurseries == []
Expand All @@ -1601,7 +1601,7 @@ async def parent():

async with _core.open_nursery() as nursery:
nurseries["parent"] = nursery
nursery.start_soon(child1)
await nursery.start(child1)

# Upward links survive after tasks/nurseries exit
assert nurseries["parent"].parent_task is tasks["parent"]
Expand All @@ -1624,15 +1624,31 @@ async def child2():
assert nurseries["child1"].child_tasks == frozenset({tasks["child2"]})
assert tasks["child2"].child_nurseries == []

async def child1():
tasks["child1"] = _core.current_task()
async def child1(task_status=_core.TASK_STATUS_IGNORED):
me = tasks["child1"] = _core.current_task()
assert me.parent_nursery.parent_task is tasks["parent"]
assert me.parent_nursery is not nurseries["parent"]
assert me.eventual_parent_nursery is nurseries["parent"]
task_status.started()
assert me.parent_nursery is nurseries["parent"]
assert me.eventual_parent_nursery is None

# Wait for the start() call to return and close its internal nursery, to
# ensure consistent results in child2:
await _core.wait_all_tasks_blocked()

async with _core.open_nursery() as nursery:
nurseries["child1"] = nursery
nursery.start_soon(child2)

async with _core.open_nursery() as nursery:
nursery.start_soon(parent)

# There are no pending starts, so no one should have a non-None
# eventual_parent_nursery
for task in tasks.values():
assert task.eventual_parent_nursery is None


async def test_nursery_closure():
async def child1(nursery):
Expand Down