Skip to content

Commit e0982d9

Browse files
committed
Don't return a module another thread has begun loading again
After waiting for a module that another thread was executing, _find_and_load() looked in sys.modules again only after releasing the module's lock. If that import had failed and a thread had already begun retrying it, the waiter returned the new, still executing module. Look while still holding the lock. Reported by Emma Smith in review of GH-159965.
1 parent 8d87716 commit e0982d9

2 files changed

Lines changed: 82 additions & 3 deletions

File tree

‎Lib/importlib/_bootstrap.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,11 +1301,24 @@ def _find_and_load(name, import_, *, lazy_submodule=False):
13011301
module = sys.modules.get(name, _NEEDS_LOADING)
13021302
if module is _NEEDS_LOADING or _is_initializing(module):
13031303
if module is not _NEEDS_LOADING:
1304+
# Wait for the thread executing the module.
13041305
# NOTE: this relies on __spec__._initializing being set *before*
13051306
# the module is put in sys.modules.
1306-
_lock_unlock_module(name)
1307-
# The module is gone if the thread loading it failed.
1308-
module = sys.modules.get(name, _NEEDS_LOADING)
1307+
lock = _get_module_lock(name)
1308+
try:
1309+
lock.acquire()
1310+
except _DeadlockError:
1311+
# A circular import split across threads: accept the
1312+
# partially initialized module.
1313+
pass
1314+
else:
1315+
# The module is gone if the thread loading it failed. Look
1316+
# while still holding the lock, so that another thread cannot
1317+
# have begun loading it again.
1318+
try:
1319+
module = sys.modules.get(name, _NEEDS_LOADING)
1320+
finally:
1321+
lock.release()
13091322

13101323
if module is _NEEDS_LOADING:
13111324
sys.audit(

‎Lib/test/test_importlib/test_threaded_import.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,72 @@ def test_waiter_retries_after_failed_submodule_import(self):
961961
])
962962
self.assertEqual(sys.modules[f"{pkg}.mod"].X, 1)
963963

964+
def test_waiter_not_given_module_being_retried(self):
965+
# Thread B waits for thread A's import of a module, which fails.
966+
# A retries, so sys.modules may hold A's new, still executing
967+
# module by the time B looks there again. B must wait for that
968+
# attempt too rather than return a half initialized module.
969+
self._setup_package_tree({
970+
"again_mod.py": "import again_sync; again_sync.body()\nX = 1\n",
971+
})
972+
sync = types.ModuleType("again_sync")
973+
sys.modules["again_sync"] = sync
974+
self.addCleanup(sys.modules.pop, "again_sync", None)
975+
released = threading.Event() # B stopped waiting for the 1st attempt
976+
retrying = threading.Event() # the 2nd attempt is in sys.modules
977+
errors = []
978+
results = []
979+
980+
def a():
981+
try:
982+
try:
983+
import again_mod
984+
except RuntimeError:
985+
pass
986+
released.wait(support.SHORT_TIMEOUT)
987+
import again_mod
988+
except Exception as e:
989+
errors.append(f"A: {type(e).__name__}: {e}")
990+
991+
def b():
992+
try:
993+
module = importlib.import_module("again_mod")
994+
results.append(hasattr(module, "X"))
995+
except Exception as e:
996+
errors.append(f"B: {type(e).__name__}: {e}")
997+
998+
ta = threading.Thread(target=a, daemon=True)
999+
tb = threading.Thread(target=b, daemon=True)
1000+
1001+
def body():
1002+
if tb.ident is None:
1003+
tb.start()
1004+
self._wait_until_blocked_on(tb, "again_mod")
1005+
raise RuntimeError("first attempt fails")
1006+
retrying.set()
1007+
if threading.current_thread() is not tb:
1008+
# Finish only once B is waiting for this attempt.
1009+
self._wait_until_blocked_on(tb, "again_mod")
1010+
sync.body = body
1011+
1012+
# Hold B back, once it has stopped waiting for the first attempt,
1013+
# until the second attempt is under way.
1014+
release = importlib._bootstrap._ModuleLock.release
1015+
1016+
def stalling_release(lock):
1017+
release(lock)
1018+
if (lock.name == "again_mod" and not released.is_set()
1019+
and threading.current_thread() is tb):
1020+
released.set()
1021+
retrying.wait(support.SHORT_TIMEOUT)
1022+
1023+
with support.swap_attr(importlib._bootstrap._ModuleLock, "release",
1024+
stalling_release):
1025+
ta.start()
1026+
self._join_import_threads([ta, tb])
1027+
self.assertEqual(results, [True])
1028+
self.assertEqual(errors, [])
1029+
9641030

9651031
def setUpModule():
9661032
thread_info = threading_helper.threading_setup()

0 commit comments

Comments
 (0)