Skip to content

Commit 3255d5d

Browse files
[3.15] gh-159963: Don't hold parent package import locks while a submodule executes (GH-159965) (#160630)
gh-159963: Don't hold parent package import locks while a submodule executes (GH-159965) Since gh-83065 (3.15.0), the first import of pkg.mod kept pkg's module lock until pkg/mod.py had finished executing. Threads that needed only the finished pkg stayed blocked for that long, and concurrent first imports could raise _DeadlockError or a spurious "partially initialized module" ImportError, or hang, with no import cycle involved. Parent packages are now imported, or waited for if another thread is initializing them, before the submodule's module lock is taken, so only that lock is held while the submodule executes. The lock ordering deadlock fixed by gh-83065 stays fixed. importlib.import_module() now returns the partially initialized module in a circular import split across threads, as the import statement already did, instead of raising _DeadlockError. That was long-standing behavior rather than part of the 3.15.0 regression. (cherry picked from commit 7159b15) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent 1196c34 commit 3255d5d

5 files changed

Lines changed: 684 additions & 85 deletions

File tree

‎Doc/whatsnew/3.15.rst‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2912,6 +2912,18 @@ that may require changes to your code.
29122912
Notable changes in 3.15.2
29132913
=========================
29142914

2915+
importlib
2916+
---------
2917+
2918+
* Threads that simultaneously perform first-time ``import package.submodule``
2919+
imports no longer risk a deadlock or spurious import errors, a regression
2920+
introduced in 3.15.0 by the fix for :gh:`83065`. A package's import lock is
2921+
now released as soon as its ``__init__`` returns.
2922+
:func:`importlib.import_module` now returns the partially initialized
2923+
module in a circular import split across threads, as the :keyword:`import`
2924+
statement already did, instead of raising ``_DeadlockError``.
2925+
(Contributed by Gregory P. Smith in :gh:`159963`.)
2926+
29152927
ssl
29162928
---
29172929

‎Lib/importlib/_bootstrap.py‎

Lines changed: 84 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -424,64 +424,6 @@ def __exit__(self, *args, **kwargs):
424424
self._lock.release()
425425

426426

427-
def _get_module_chain(name):
428-
"""Return the chain of dotted-name prefixes from root to leaf.
429-
430-
For example: 'a.b.c' -> ['a', 'a.b', 'a.b.c']
431-
"""
432-
parts = name.split('.')
433-
return ['.'.join(parts[:i+1]) for i in range(len(parts))]
434-
435-
436-
class _HierarchicalLockManager:
437-
"""Manages acquisition of multiple module locks in hierarchical order.
438-
439-
This prevents deadlocks by ensuring all threads acquire locks in the
440-
same order (parent modules before child modules).
441-
"""
442-
443-
def __init__(self, name):
444-
self._name = name
445-
self._module_chain = _get_module_chain(name)
446-
self._locks = []
447-
448-
def __enter__(self):
449-
try:
450-
for module_name in self._module_chain:
451-
# Only acquire lock if module is not already fully loaded
452-
module = sys.modules.get(module_name)
453-
if (module is None or
454-
getattr(getattr(module, "__spec__", None),
455-
"_initializing", False)):
456-
lock = _get_module_lock(module_name)
457-
try:
458-
lock.acquire()
459-
except _DeadlockError:
460-
if module_name == self._name:
461-
raise
462-
# The parent is being initialised by a thread that
463-
# is (transitively) waiting on a lock we hold.
464-
# Apply the same policy as _lock_unlock_module():
465-
# accept a partially-initialised parent for circular
466-
# imports rather than failing the whole chain.
467-
continue
468-
self._locks.append((module_name, lock))
469-
except:
470-
# __exit__ is not called when __enter__ raises (e.g. _DeadlockError
471-
# on the leaf lock, or KeyboardInterrupt), so release whatever we
472-
# already hold to avoid permanently leaking held module locks.
473-
for module_name, lock in reversed(self._locks):
474-
lock.release()
475-
self._locks.clear()
476-
raise
477-
return self
478-
479-
def __exit__(self, *args, **kwargs):
480-
for module_name, lock in reversed(self._locks):
481-
lock.release()
482-
self._locks.clear()
483-
484-
485427
# The following two functions are for consumption by Python/import.c.
486428

487429
def _get_module_lock(name):
@@ -1262,17 +1204,15 @@ def _sanity_check(name, package, level):
12621204

12631205
def _find_and_load_unlocked(name, import_, *, lazy_submodule=False):
12641206
path = None
1265-
sys.audit(
1266-
"import",
1267-
name,
1268-
path,
1269-
getattr(sys, "path", None),
1270-
getattr(sys, "meta_path", None),
1271-
getattr(sys, "path_hooks", None)
1272-
)
12731207
parent = name.rpartition('.')[0]
12741208
parent_spec = None
12751209
if parent:
1210+
# _resolve_parents() has normally imported the parent already. It can
1211+
# be missing again by now if its import failed in another thread or
1212+
# something removed it from sys.modules; it is then imported afresh,
1213+
# like any other module that is not in sys.modules. That import runs
1214+
# while holding name's lock, the lock order _resolve_parents() exists
1215+
# to avoid; it is tolerated here because the case is so rare.
12761216
if parent not in sys.modules:
12771217
_call_with_frames_removed(import_, parent)
12781218
# Crazy side-effects!
@@ -1330,31 +1270,90 @@ def _find_and_load_unlocked(name, import_, *, lazy_submodule=False):
13301270
_NEEDS_LOADING = object()
13311271

13321272

1273+
# Return whether module (a sys.modules value) is still executing.
1274+
def _is_initializing(module):
1275+
return getattr(getattr(module, '__spec__', None), '_initializing', False)
1276+
1277+
1278+
# Import, or wait for, the parent packages of name.
1279+
#
1280+
# _find_and_load() calls this before taking name's module lock, so that no
1281+
# thread holds a submodule's lock while waiting for its parent. This avoids
1282+
# the deadlocks and import errors of gh-83065 and gh-159963.
1283+
def _resolve_parents(name, import_):
1284+
# Wait for each ancestor package that another thread is still
1285+
# initializing, outermost first. Importing beneath a package while its
1286+
# __init__ is part way through its own imports could give one of the two
1287+
# threads a partially initialized module. Missing ancestors are not
1288+
# imported: only the immediate parent has to exist.
1289+
parts = name.split('.')
1290+
for i in range(1, len(parts) - 1):
1291+
ancestor = '.'.join(parts[:i])
1292+
if _is_initializing(sys.modules.get(ancestor)):
1293+
_lock_unlock_module(ancestor)
1294+
# Import the immediate parent, or wait for it likewise.
1295+
parent = name.rpartition('.')[0]
1296+
if parent:
1297+
module = sys.modules.get(parent, _NEEDS_LOADING)
1298+
if module is _NEEDS_LOADING or _is_initializing(module):
1299+
_call_with_frames_removed(import_, parent)
1300+
1301+
13331302
def _find_and_load(name, import_, *, lazy_submodule=False):
13341303
"""Find and load the module."""
13351304

13361305
# Optimization: we avoid unneeded module locking if the module
13371306
# already exists in sys.modules and is fully initialized.
13381307
module = sys.modules.get(name, _NEEDS_LOADING)
1339-
if (module is _NEEDS_LOADING or
1340-
getattr(getattr(module, "__spec__", None), "_initializing", False)):
1341-
1342-
if '.' in name:
1343-
lock_manager = _HierarchicalLockManager(name)
1344-
else:
1345-
lock_manager = _ModuleLockManager(name)
1346-
1347-
with lock_manager:
1348-
module = sys.modules.get(name, _NEEDS_LOADING)
1349-
if module is _NEEDS_LOADING:
1350-
return _find_and_load_unlocked(
1351-
name, import_, lazy_submodule=lazy_submodule)
1352-
1353-
# Optimization: only call _bootstrap._lock_unlock_module() if
1354-
# module.__spec__._initializing is True.
1355-
# NOTE: because of this, initializing must be set *before*
1356-
# putting the new module in sys.modules.
1357-
_lock_unlock_module(name)
1308+
if module is _NEEDS_LOADING or _is_initializing(module):
1309+
if module is not _NEEDS_LOADING:
1310+
# Wait for the thread executing the module.
1311+
# NOTE: this relies on __spec__._initializing being set *before*
1312+
# the module is put in sys.modules.
1313+
lock = _get_module_lock(name)
1314+
try:
1315+
lock.acquire()
1316+
except _DeadlockError:
1317+
# A circular import split across threads: accept the
1318+
# partially initialized module.
1319+
pass
1320+
else:
1321+
# The module is gone if the thread loading it failed. Look
1322+
# while still holding the lock, so that another thread cannot
1323+
# have begun loading it again.
1324+
try:
1325+
module = sys.modules.get(name, _NEEDS_LOADING)
1326+
finally:
1327+
lock.release()
1328+
1329+
if module is _NEEDS_LOADING:
1330+
sys.audit(
1331+
"import",
1332+
name,
1333+
None,
1334+
getattr(sys, "path", None),
1335+
getattr(sys, "meta_path", None),
1336+
getattr(sys, "path_hooks", None)
1337+
)
1338+
_resolve_parents(name, import_)
1339+
lock = _get_module_lock(name)
1340+
try:
1341+
lock.acquire()
1342+
except _DeadlockError:
1343+
# A circular import split across threads. As in
1344+
# _lock_unlock_module(), accept a partially initialized
1345+
# module if there is one.
1346+
module = sys.modules.get(name, _NEEDS_LOADING)
1347+
if module is _NEEDS_LOADING:
1348+
raise
1349+
else:
1350+
try:
1351+
module = sys.modules.get(name, _NEEDS_LOADING)
1352+
if module is _NEEDS_LOADING:
1353+
return _find_and_load_unlocked(
1354+
name, import_, lazy_submodule=lazy_submodule)
1355+
finally:
1356+
lock.release()
13581357
else:
13591358
# Verify the module is still in sys.modules. Another thread may have
13601359
# removed it (due to import failure) between our sys.modules.get()

‎Lib/test/test_importlib/import_/test_packages.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from test.test_importlib import util
22
import sys
3+
import types
34
import unittest
45
from test.support import import_helper
56

@@ -100,6 +101,21 @@ def module_injection():
100101
finally:
101102
import_helper.unload(subname)
102103

104+
def test_missing_grandparent_not_imported(self):
105+
# Only the immediate parent has to be present. With a hand-made
106+
# 'pkg.sub' package in sys.modules and no 'pkg' at all, importing
107+
# pkg.sub.mod must neither fail nor try to import 'pkg'.
108+
parent = types.ModuleType('pkg.sub')
109+
parent.__path__ = []
110+
with util.mock_spec('pkg.sub.mod') as mock:
111+
with util.import_state(meta_path=[mock]):
112+
with util.uncache('pkg', 'pkg.sub', 'pkg.sub.mod'):
113+
sys.modules['pkg.sub'] = parent
114+
module = self.__import__('pkg.sub.mod', fromlist=['*'])
115+
self.assertEqual(module.__name__, 'pkg.sub.mod')
116+
self.assertIs(parent.mod, module)
117+
self.assertNotIn('pkg', sys.modules)
118+
103119

104120
(Frozen_ParentTests,
105121
Source_ParentTests

0 commit comments

Comments
 (0)