当从子进程调用时,Python程序永远挂起
当从子进程调用时,Python程序永远挂起
pip测试套件使用子进程调用来运行集成测试。最近有一个PR,删除了一些旧的兼容性代码。具体来说,它用b""
字面值明确地替换了一个使用b()
函数的地方。然而,这似乎破坏了某些东西,导致一个特定的子进程调用会永远挂起。更糟糕的是,它只在Python 3.3上永远挂起(也许只有Python 3.3.5),而且不能在Travis之外轻松重现。
相关的Pull Requests:
- https://github.com/pypa/pip/pull/1901
- https://github.com/pypa/pip/pull/1900
- https://github.com/pypa/pip/pull/1878
其他Pull Requests也出现了类似的问题,但它们在不同版本的Python和不同的测试案例上失败。这些Pull Requests是:
另一个用户今天在IRC上向我报告了类似的问题,他们说他们可以在Ubuntu 14.04上本地复现这个问题,使用的是deadsnakes上的Python 3.3(但在OSX上不行),而且不仅仅在Travis上像我迄今为止大多数情况下能够复现。他们给我发了一些复现的步骤,如下所示:
$ git clone [email protected]:xavfernandez/pip.git $ cd pip $ git checkout debug_stuck $ pip install pytest==2.5.2 scripttest==1.3 virtualenv==1.11.6 mock==1.0.1 pretend==1.0.8 setuptools==4.0 $ # The below should pass just fine $ py.test -k test_env_vars_override_config_file -v -s $ # Now edit pip/req/req_set.py and remove method remove_me_to_block or change its content to print('KO') or pass $ # The below should hang forever $ py.test -k test_env_vars_override_config_file -v -s
在上面的示例中,remove_me_to_block
方法在任何地方都没有被调用,但它的存在就足以使测试不阻塞,而没有它(或者改变它的内容)将使测试永远阻塞。
大部分的调试工作都集中在这个PR的更改上(https://github.com/pypa/pip/pull/1901)。逐个提交时,测试通过,直到应用了这个特定提交-https://github.com/dstufft/pip/commit/d296df620916b4cd2379d9fab988cbc088e28fe0。具体来说,使用b'\r\n'
或(entry + endline).encode("utf-8")
的更改将触发问题,然而这两个东西都不在pip install -vvv INITools
的执行路径中,这是它无法执行的命令。
在尝试追踪问题时,我注意到如果我用(lambda: "something")().encode("utf8")
替换至少一个对"something".encode("utf8")
的调用,它就能正常工作。
在尝试调试期间,遇到的另一个问题是,我尝试的各种方法(添加打印语句,空操作的atexit
函数,使用trollious进行异步子进程)只是将问题从特定的测试用例/Python版本转移到了不同的测试用例/Python版本上。
我知道subprocess
模块如果直接从subprocess.Popen().stdout/stderr/stdin
读取/写入,可能会产生死锁。然而,这段代码使用的是communicate()
方法,它应该解决这些问题。在communicate()
的wait()
调用中,进程永远挂起等待pip进程退出。
其他信息:
- 这是一个非常难以捉摸的问题,我设法通过各种本不应该对其产生任何影响的事情使其消失或改变。
- 我追踪了pip本身的执行,一直到代码路径的末尾,直到调用了
sys.exit()
。 - 将
sys.exit()
替换为os._exit()
可以修复所有的挂起问题,但我不想这样做,因为这样我们就跳过了Python解释器的清理工作。 - 没有其他线程在运行(使用
threading.enumerate
进行验证)。 - 我尝试过一些组合,有的让它即使不使用
subprocess.PIPE
作为stdout/stderr/stdin也会挂起,然而其他组合如果不使用它们就不会挂起(或者会在不同的测试案例/Python版本上移动)。 - 它似乎与时间无关,任何特定的提交要么在受影响的测试案例/Python上100%失败,要么0%失败。
- 往往被更改的代码在pip子进程中的特定代码路径根本没有被执行,然而这个更改的存在似乎会破坏它。
- 我尝试过使用
PYTHONDONTWRITEBYTECODE=1
禁用字节码生成,这在某些组合中有影响,但在其他情况下没有影响。 - 子进程调用的命令并非在每次调用时都挂起(类似的命令通过测试套件发出),但它总是在特定提交的相同位置挂起。
- 到目前为止,我完全无法在测试套件之外复现这个问题,但我不能确定它是否与此相关。
我完全无法理解是什么原因导致了这个问题。
更新 #1
使用faulthandler.dump_traceback_later()
得到了这个结果:
超时(0:00:05)! 当前线程 0x00007f417bd92740: 文件 "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,[apcode]__getattr__函数
[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数超时(0:00:05)!
当前线程 0x00007f417bd92740:
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[重复的行已剪切]
文件 "/tmp-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py",行 24,
__getattr__
函数[/apcode]
这对我来说是否有用?我以前从未使用过这个标志,所以不知道这是否正常。