Skip to content

Commit d290be7

Browse files
committed
gh-154175: Remove time.sleep in test_io daemon test
Deterministically get into the deadlock timeout by using mocked Raw I/O that blocks on threading events. The test still takes quite a bit of walltime as `_enter_buffered_busy` has a 1 second timeout on a lock acquisition which this hits.
1 parent 7a91841 commit d290be7

1 file changed

Lines changed: 46 additions & 19 deletions

File tree

Lib/test/test_io/test_general.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,43 +1354,70 @@ def read(self, n=-1):
13541354
def check_daemon_threads_shutdown_deadlock(self, stream_name):
13551355
# Issue #23309: deadlocks at shutdown should be avoided when a
13561356
# daemon thread and the main thread both write to a file.
1357+
#
1358+
# Focus on I/O and interaction in locks, test_threading checks daemon
1359+
# threads exit generally. Get into the specific I/O states we care
1360+
# about by using mock I/O which block in the right locks.
13571361
code = """if 1:
1362+
import atexit
1363+
import io
13581364
import sys
1359-
import time
1360-
import threading
1365+
from threading import current_thread, Event, main_thread, Thread
1366+
1367+
go = Event()
1368+
entered = Event()
1369+
1370+
# Register with atexit before importing test.support which imports
1371+
# logging so that it runs last.
1372+
@atexit.register
1373+
def release_daemon_thread():
1374+
# Get daemon thread to hold BufferedWriter lock.
1375+
go.set()
1376+
entered.wait()
1377+
13611378
from test.support import SuppressCrashReport
13621379
1363-
file = sys.{stream_name}
1380+
# Mock I/O that blocks daemon thread in Raw I/O write.
1381+
class GatedRaw(io.RawIOBase):
1382+
name = '<{stream_name}>'
1383+
1384+
def writable(self):
1385+
return True
1386+
1387+
def write(self, b):
1388+
if current_thread() is not main_thread():
1389+
# In daemon, have buffered lock. Hold it.
1390+
entered.set()
1391+
Event().wait()
1392+
return len(b)
1393+
1394+
file = io.TextIOWrapper(io.BufferedWriter(GatedRaw()),
1395+
encoding='utf-8')
1396+
sys.{stream_name} = file
13641397
13651398
def run():
1366-
while True:
1367-
file.write('.')
1368-
file.flush()
1399+
go.wait()
1400+
file.write('.')
1401+
file.flush()
13691402
13701403
crash = SuppressCrashReport()
13711404
crash.__enter__()
13721405
# don't call __exit__(): the crash occurs at Python shutdown
13731406
1374-
thread = threading.Thread(target=run)
1407+
thread = Thread(target=run)
13751408
thread.daemon = True
13761409
thread.start()
13771410
1378-
time.sleep(0.5)
13791411
file.write('!')
13801412
file.flush()
1381-
""".format_map(locals())
1413+
""".format(stream_name=stream_name)
13821414
res, _ = run_python_until_end("-c", code)
13831415
err = res.err.decode()
1384-
if res.rc != 0:
1385-
# Failure: should be a fatal error
1386-
pattern = (r"Fatal Python error: _enter_buffered_busy: "
1387-
r"could not acquire lock "
1388-
r"for <(_io\.)?BufferedWriter name='<{stream_name}>'> "
1389-
r"at interpreter shutdown, possibly due to "
1390-
r"daemon threads".format_map(locals()))
1391-
self.assertRegex(err, pattern)
1392-
else:
1393-
self.assertFalse(err.strip('.!'))
1416+
# Should exit non-zero with fatal error.
1417+
self.assertNotEqual(res.rc, 0)
1418+
self.assertIn("Fatal Python error: _enter_buffered_busy", err)
1419+
self.assertIn(f"<_io.BufferedWriter name='<{stream_name}>'>", err)
1420+
self.assertIn("possibly due to daemon threads", err)
13941421

13951422
@threading_helper.requires_working_threading()
13961423
@support.requires_resource('walltime')

0 commit comments

Comments
 (0)