|
14 | 14 | from test.support.script_helper import assert_python_ok, assert_python_failure
|
15 | 15 | from test.support import threading_helper
|
16 | 16 | from test.support import import_helper
|
| 17 | +from test.support import skip_if_sanitizer |
17 | 18 | import textwrap
|
18 | 19 | import unittest
|
19 | 20 | import warnings
|
@@ -471,6 +472,79 @@ def g456():
|
471 | 472 | leave_g.set()
|
472 | 473 | t.join()
|
473 | 474 |
|
| 475 | + @skip_if_sanitizer(memory=True, address=True, reason= "Test too slow " |
| 476 | + "when the address sanitizer is enabled.") |
| 477 | + @threading_helper.reap_threads |
| 478 | + @threading_helper.requires_working_threading() |
| 479 | + @support.requires_fork() |
| 480 | + def test_current_frames_exceptions_deadlock(self): |
| 481 | + """ |
| 482 | + Reproduce the bug raised in GH-106883 and GH-116969. |
| 483 | + """ |
| 484 | + import threading |
| 485 | + import time |
| 486 | + import signal |
| 487 | + |
| 488 | + class MockObject: |
| 489 | + def __init__(self): |
| 490 | + # Create some garbage |
| 491 | + self._list = list(range(10000)) |
| 492 | + # Call the functions under test |
| 493 | + self._trace = sys._current_frames() |
| 494 | + self._exceptions = sys._current_exceptions() |
| 495 | + |
| 496 | + def __del__(self): |
| 497 | + # The presence of the __del__ method causes the deadlock when |
| 498 | + # there is one thread executing the _current_frames or |
| 499 | + # _current_exceptions functions and the other thread is |
| 500 | + # running the GC: |
| 501 | + # thread 1 has the interpreter lock and it is trying to |
| 502 | + # acquire the GIL; thread 2 holds the GIL but is trying to |
| 503 | + # acquire the interpreter lock. |
| 504 | + # When the GC is running and it finds that an |
| 505 | + # object has the __del__ method, it needs to execute the |
| 506 | + # Python code in it and it requires the GIL to execute it |
| 507 | + # (which will never happen because it is held by another thread |
| 508 | + # blocked on the acquisition of the interpreter lock) |
| 509 | + pass |
| 510 | + |
| 511 | + def thread_function(num_objects): |
| 512 | + obj = None |
| 513 | + for _ in range(num_objects): |
| 514 | + obj = MockObject() |
| 515 | + |
| 516 | + # The number of objects should be big enough to increase the |
| 517 | + # chances to call the GC. |
| 518 | + NUM_OBJECTS = 1000 |
| 519 | + NUM_THREADS = 10 |
| 520 | + |
| 521 | + # 40 seconds should be enough for the test to be executed: if it |
| 522 | + # is more than 40 seconds it means that the process is in deadlock |
| 523 | + # hence the test fails |
| 524 | + TIMEOUT = 40 |
| 525 | + |
| 526 | + # Test the sys._current_frames and sys._current_exceptions calls |
| 527 | + pid = os.fork() |
| 528 | + if pid: # parent process |
| 529 | + try: |
| 530 | + support.wait_process(pid, exitcode=0, timeout=TIMEOUT) |
| 531 | + except KeyboardInterrupt: |
| 532 | + # When pressing CTRL-C kill the deadlocked process |
| 533 | + os.kill(pid, signal.SIGTERM) |
| 534 | + raise |
| 535 | + else: # child process |
| 536 | + # Run the actual test in the forked process. |
| 537 | + threads = [] |
| 538 | + for i in range(NUM_THREADS): |
| 539 | + thread = threading.Thread( |
| 540 | + target=thread_function, args=(NUM_OBJECTS,) |
| 541 | + ) |
| 542 | + threads.append(thread) |
| 543 | + thread.start() |
| 544 | + for t in threads: |
| 545 | + t.join() |
| 546 | + os._exit(0) |
| 547 | + |
474 | 548 | @threading_helper.reap_threads
|
475 | 549 | @threading_helper.requires_working_threading()
|
476 | 550 | def test_current_exceptions(self):
|
|
0 commit comments