@@ -263,6 +263,72 @@ def test_multiprocessing_pool_circular_import(self, size):
263263 'partial' , 'pool_in_threads.py' )
264264 script_helper .assert_python_ok (fn )
265265
266+ @unittest .expectedFailure # TODO: RUSTPYTHON
267+ def test_import_failure_race_condition (self ):
268+ # Regression test for race condition where a thread could receive
269+ # a partially-initialized module when another thread's import fails.
270+ # The race occurs when:
271+ # 1. Thread 1 starts importing, adds module to sys.modules
272+ # 2. Thread 2 sees the module in sys.modules
273+ # 3. Thread 1's import fails, removes module from sys.modules
274+ # 4. Thread 2 should NOT return the stale module reference
275+ os .mkdir (TESTFN )
276+ self .addCleanup (shutil .rmtree , TESTFN )
277+ sys .path .insert (0 , TESTFN )
278+ self .addCleanup (sys .path .remove , TESTFN )
279+
280+ # Create a module that partially initializes then fails
281+ modname = 'failing_import_module'
282+ with open (os .path .join (TESTFN , modname + '.py' ), 'w' ) as f :
283+ f .write ('''
284+ import time
285+ PARTIAL_ATTR = 'initialized'
286+ time.sleep(0.05) # Widen race window
287+ raise RuntimeError("Intentional import failure")
288+ ''' )
289+ self .addCleanup (forget , modname )
290+ importlib .invalidate_caches ()
291+
292+ errors = []
293+ results = []
294+
295+ def do_import (delay = 0 ):
296+ time .sleep (delay )
297+ try :
298+ mod = __import__ (modname )
299+ # If we got a module, verify it's in sys.modules
300+ if modname not in sys .modules :
301+ errors .append (
302+ f"Got module { mod !r} but { modname !r} not in sys.modules"
303+ )
304+ elif sys .modules [modname ] is not mod :
305+ errors .append (
306+ f"Got different module than sys.modules[{ modname !r} ]"
307+ )
308+ else :
309+ results .append (('success' , mod ))
310+ except RuntimeError :
311+ results .append (('RuntimeError' ,))
312+ except Exception as e :
313+ errors .append (f"Unexpected exception: { e } " )
314+
315+ # Run multiple iterations to increase chance of hitting the race
316+ for _ in range (10 ):
317+ errors .clear ()
318+ results .clear ()
319+ if modname in sys .modules :
320+ del sys .modules [modname ]
321+
322+ t1 = threading .Thread (target = do_import , args = (0 ,))
323+ t2 = threading .Thread (target = do_import , args = (0.01 ,))
324+ t1 .start ()
325+ t2 .start ()
326+ t1 .join ()
327+ t2 .join ()
328+
329+ # Neither thread should have errors about stale modules
330+ self .assertEqual (errors , [], f"Race condition detected: { errors } " )
331+
266332
267333def setUpModule ():
268334 thread_info = threading_helper .threading_setup ()
0 commit comments