Skip to content

Commit f42bc56

Browse files
committed
munet: simplify async child watcher setting for old pythons
Older pytest-asyncio did things with event_loops that we tried to work with, but in the end this didn't work and pytest-asyncio was improved to not require it anymore. Now we just have to make sure we have a non-threaded child watcher for asyncio when we are unsharing inline with a new pid namespace as we are not allowed to create threads in the main process under these conditions. Signed-off-by: Christian Hopps <chopps@labn.net>
1 parent a68da7e commit f42bc56

File tree

5 files changed

+28
-49
lines changed

5 files changed

+28
-49
lines changed

munet/__main__.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from . import cli
1818
from . import parser
1919
from .args import add_launch_args
20-
from .base import get_event_loop
2120
from .cleanup import cleanup_previous
2221
from .cleanup import is_running_in_rundir
2322
from .compat import PytestConfig
@@ -200,22 +199,16 @@ def main(*args):
200199
logger.critical("No nodes defined in config file")
201200
return 1
202201

203-
loop = None
204202
status = 4
205203
try:
206204
parser.validate_config(config, logger, args)
207205
if args.validate_only:
208206
return 0
209-
# Executes the cmd for each node.
210-
loop = get_event_loop()
211-
status = loop.run_until_complete(async_main(args, config))
207+
status = asyncio.get_event_loop().run_until_complete(async_main(args, config))
212208
except KeyboardInterrupt:
213209
logger.info("Exiting, received KeyboardInterrupt in main")
214210
except Exception as error:
215211
logger.info("Exiting, unexpected exception %s", error, exc_info=True)
216-
finally:
217-
if loop:
218-
loop.close()
219212

220213
return status
221214

munet/base.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -268,34 +268,34 @@ def _get_exec_path(binary, cmdf, cache):
268268
return None
269269

270270

271-
def get_event_loop():
272-
"""Configure and return our non-thread using event loop.
271+
def ensure_non_threaded_event_watcher():
272+
# Skip all this if >= python 3.12
273+
if sys.version_info >= (3, 12):
274+
return
273275

274-
This function configures a new child watcher to not use threads.
275-
Threads cannot be used when we inline unshare a PID namespace.
276-
"""
277276
policy = asyncio.get_event_loop_policy()
278-
loop = policy.get_event_loop()
279-
if not hasattr(os, "pidfd_open"):
280-
return loop
281-
282277
owatcher = policy.get_child_watcher()
278+
try:
279+
want_class = asyncio.PidfdChildWatcher # pylint: disable=no-member
280+
except Exception:
281+
want_class = asyncio.SafeChildWatcher # pylint: disable=deprecated-class
282+
283283
logging.debug(
284-
"event_loop_fixture: global policy %s, current loop %s, current watcher %s",
284+
"ensure_non_threaded_event_watcher: global policy %s, current watcher %s",
285285
policy,
286-
loop,
287286
owatcher,
288287
)
289288

289+
# It's already non-threaded
290+
if isinstance(owatcher, (want_class, asyncio.SafeChildWatcher)):
291+
return
292+
290293
policy.set_child_watcher(None)
291294
owatcher.close()
292295

293-
try:
294-
watcher = asyncio.PidfdChildWatcher() # pylint: disable=no-member
295-
except Exception:
296-
watcher = asyncio.SafeChildWatcher() # pylint: disable=deprecated-class
296+
watcher = want_class()
297+
# Reget the loop (why, does it change with the watcher.close()?)
297298
loop = policy.get_event_loop()
298-
299299
logging.debug(
300300
"event_loop_fixture: attaching new watcher %s to loop and setting in policy",
301301
watcher,
@@ -305,8 +305,6 @@ def get_event_loop():
305305
policy.set_event_loop(loop)
306306
assert asyncio.get_event_loop_policy().get_child_watcher() is watcher
307307

308-
return loop
309-
310308

311309
class Commander: # pylint: disable=R0904
312310
"""An object that can execute commands."""
@@ -2188,11 +2186,16 @@ def __init__(
21882186

21892187
linux.unshare(uflags)
21902188

2191-
if not pid:
2189+
if not self.pid_ns:
21922190
p = None
21932191
self.pid = None
21942192
self.nsenter_fork = False
21952193
else:
2194+
# If we are unsharing inline and creating a new pid namespace we need to
2195+
# ensure the default event loop watcher is a non-threaded version.
2196+
if not kvok or sys.version_info < (3, 12):
2197+
ensure_non_threaded_event_watcher()
2198+
21962199
# Need to fork to create the PID namespace, but we need to continue
21972200
# running from the parent so that things like pytest work. We'll execute
21982201
# a mutini process to manage the child init 1 duties.
@@ -2331,7 +2334,7 @@ def __init__(
23312334
# process using self.pid
23322335
#
23332336

2334-
if pid:
2337+
if self.pid_ns:
23352338
nsenter_fork = True
23362339
elif unet and unet.nsenter_fork:
23372340
# if unet created a pid namespace we need to enter it since we aren't
@@ -2376,7 +2379,7 @@ def __init__(
23762379

23772380
# We need to remount the procfs for the new PID namespace, since we aren't using
23782381
# unshare(1) which does that for us.
2379-
if pid and unshare_inline:
2382+
if self.pid_ns and unshare_inline:
23802383
assert mount
23812384
self.cmd_raises_nsonly("mount -t proc proc /proc")
23822385

@@ -2443,7 +2446,7 @@ def __init__(
24432446
self.bind_mount(s[0], s[1])
24442447

24452448
# this will fail if running inside the namespace with PID
2446-
if pid:
2449+
if self.pid_ns:
24472450
o = self.cmd_nostatus_nsonly("ls -l /proc/1/ns")
24482451
else:
24492452
o = self.cmd_nostatus_nsonly("ls -l /proc/self/ns")

munet/mutest/__main__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from munet import parser
2525
from munet.args import add_testing_args
2626
from munet.base import Bridge
27-
from munet.base import get_event_loop
2827
from munet.cli import async_cli
2928
from munet.compat import PytestConfig
3029
from munet.mutest import userapi as uapi
@@ -473,8 +472,7 @@ def main():
473472
loop = None
474473
status = 4
475474
try:
476-
loop = get_event_loop()
477-
status = loop.run_until_complete(async_main(args))
475+
status = asyncio.get_event_loop().run_until_complete(async_main(args))
478476
except KeyboardInterrupt:
479477
logging.info("Exiting (main), received KeyboardInterrupt in main")
480478
except Exception as error:

munet/testing/fixtures.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
from ..base import BaseMunet
2525
from ..base import Bridge
26-
from ..base import get_event_loop
2726
from ..cleanup import cleanup_current
2827
from ..native import L3NodeMixin
2928
from ..parser import async_build_topology
@@ -164,17 +163,6 @@ def module_autouse(request):
164163
raise Exception("Base Munet was not cleaned up/deleted")
165164

166165

167-
@pytest.fixture(scope="session")
168-
def event_loop():
169-
"""Create an instance of the default event loop for the session."""
170-
loop = get_event_loop()
171-
try:
172-
logging.info("event_loop_fixture: yielding with new event loop watcher")
173-
yield loop
174-
finally:
175-
loop.close()
176-
177-
178166
@pytest.fixture(scope="module")
179167
def rundir_module():
180168
root_path = os.environ.get("MUNET_RUNDIR", "/tmp/unet-test")

tests/control/test_cmds.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@
1313
import pytest
1414

1515

16-
# All tests are coroutines
17-
pytestmark = pytest.mark.asyncio
18-
19-
# How does this double assignment work, what's going on?
16+
# Run tests with unshare_inline and not.
2017
pytestmark = pytest.mark.parametrize("unet", [True, False], indirect=["unet"])
2118

2219

0 commit comments

Comments
 (0)