python / python/cpython

_test_multiprocessing._kill_process() uses a fixed 10 s alarm, failing on slow builds

Đang mở
#157,184 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

tests topic-multiprocessing type-bug
Ngôn ngữ chính
Python
Star
77.2k
Fork
35.9k
Chỉ số merge pull request
Chỉ số pull request đang chờ

Mô tả

Bug report

Bug description:

_test_multiprocessing._kill_process() guards the join() of the killed child with a SIGALRM fixed at 10 seconds:

        if hasattr(signal, 'alarm'):
            # On the Gentoo buildbot waitpid() often seems to block forever.
            # We use alarm() to interrupt it if it blocks for too long.
            def handler(*args):
                raise RuntimeError('join took too long: %s' % p)
            old_handler = signal.signal(signal.SIGALRM, handler)
            try:
                signal.alarm(10)
                self.assertEqual(join(), None)

The alarm is meant to turn a blocked waitpid() into a readable error instead of a hang. But the value has been a literal 10 since it was added in 2013 (cc5c728513a), so on a build slow enough that reaping the child legitimately takes longer than ten seconds, it fires on a healthy run and fails the test.

That happened on the "Sanitizers / UBSan" job of 57594aae5e: https://github.com/python/cpython/actions/runs/34016012199/job/101439811595

ERROR: test_interrupt (test.test_multiprocessing_fork.test_processes.WithProcessesTestProcess.test_interrupt)
  File ".../Lib/test/_test_multiprocessing.py", line 651, in test_interrupt
    exitcode = self._kill_process(multiprocessing.Process.interrupt)
  File ".../Lib/test/_test_multiprocessing.py", line 632, in _kill_process
    self.assertEqual(join(), None)
  File ".../Lib/multiprocessing/popen_fork.py", line 28, in poll
    pid, sts = os.waitpid(self.pid, flag)
  File ".../Lib/test/_test_multiprocessing.py", line 628, in handler
    raise RuntimeError('join took too long: %s' % p)
RuntimeError: join took too long: <Process name='Process-162' pid=18960 parent=17748 started daemon>

The traceback shows the alarm interrupting os.waitpid(), which is the normal path, not a hang.

test.support already provides timeouts for this, and regrtest scales them for slow builds (Lib/test/libregrtest/setup.py raises SHORT_TIMEOUT and LONG_TIMEOUT from --timeout). LONG_TIMEOUT is documented for exactly this use:

# Timeout in seconds to detect when a test hangs.
#
# It is long enough to reduce the risk of test failure on the slowest Python
# buildbots. It should not be used to mark a test as failed if the test takes
# "too long".

and the comment on SHORT_TIMEOUT says "If a test using SHORT_TIMEOUT starts to fail randomly on slow buildbots, use LONG_TIMEOUT instead". The same _kill_process() already uses support.SHORT_TIMEOUT a few lines above, for the event wait.

Reproducer

The alarm fires whenever the child takes longer than ten seconds to be reaped. Replicating the alarm block with a child that is slow to exit:

import multiprocessing, signal, sys, time

def child(event, delay):
    def slow_exit(*args):
        time.sleep(delay)      # a loaded machine: the child is slow to die
        sys.exit(0)
    signal.signal(signal.SIGINT, slow_exit)
    event.set()
    time.sleep(100)

if __name__ == '__main__':
    alarm_secs, delay = int(sys.argv[1]), float(sys.argv[2])
    event = multiprocessing.Event()
    p = multiprocessing.Process(target=child, args=(event, delay))
    p.daemon = True
    p.start()
    event.wait(30)
    p.interrupt()

    def handler(*args):
        raise RuntimeError('join took too long: %s' % p)
    signal.signal(signal.SIGALRM, handler)
    try:
        signal.alarm(alarm_secs)
        p.join()
        print("join returned, exitcode", p.exitcode)
    except RuntimeError as exc:
        print("RuntimeError:", exc)
    finally:
        signal.alarm(0)
$ ./python mp_alarm.py 10 12
RuntimeError: join took too long: <Process name='Process-1' pid=13882 parent=13880 started daemon>
$ ./python mp_alarm.py 300 12
join returned, exitcode 0

The four tests that go through _kill_process() (test_interrupt, test_interrupt_no_handler, test_terminate, test_kill) are affected, in every start-method variant.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux (CI), macOS (reproducer)

Linked PRs
  • gh-157185

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu trong Lib/test/_test_multiprocessing.py tại _kill_process(), sau đó so sánh việc xử lý alarm của nó với các định nghĩa và việc scaling timeout của support trong Lib/test/libregrtest/setup.py. Kiểm tra bốn test bị ảnh hưởng bằng từng start method; hoàn thành nghĩa là việc reaping một child chậm nhưng khỏe mạnh không còn kích hoạt timeout, trong khi các join thực sự bị block vẫn thất bại một cách rõ ràng.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
testing-qa
Loại issue
Lỗi
Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức độ hoạt động
Đình trệ
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
35/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.