当从子进程调用时,Python程序永远挂起

13 浏览
0 Comments

当从子进程调用时,Python程序永远挂起

pip测试套件使用子进程调用来运行集成测试。最近有一个PR,删除了一些旧的兼容性代码。具体来说,它用b""字面值明确地替换了一个使用b()函数的地方。然而,这似乎破坏了某些东西,导致一个特定的子进程调用会永远挂起。更糟糕的是,它只在Python 3.3上永远挂起(也许只有Python 3.3.5),而且不能在Travis之外轻松重现。

相关的Pull Requests:

其他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]

这对我来说是否有用?我以前从未使用过这个标志,所以不知道这是否正常。

0