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

时间:2014-07-02 03:35:07

标签: python subprocess

pip测试套件使用子进程调用来运行集成测试。最近放置了PR,删除了一些旧的兼容性代码。特别是它替换了b()函数,明确使用b""文字。然而,这似乎打破了特定子进程调用永远挂起的地方。更糟糕的是,它只会永远挂在Python 3.3(可能只有Python 3.3.5)上,并且不能轻易地在Travis之外复制。

相关的拉动请求:

其他Pull请求也会出现类似的问题,但是在不同版本的Python和不同的测试用例中它们会失败。这些Pull请求是:

另一位用户今天在IRC上向我报告了一个类似的问题,他们说他们可以在Ubuntu 14.04上使用Python 3.3从死神(但不是在OSX上)本地复制它,而不仅仅是像我一样在Travis上复制它。迄今为止能够做到的。他们已经给我发送了重现的步骤:

$ git clone git@github.com: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的执行路径中,这是它无法执行的命令。< / p>

在尝试追查问题时,我注意到如果我用"something".encode("utf8")替换至少一次(lambda: "something")().encode("utf8")来电,那就可以了。

尝试调试时的另一个问题是,我尝试过的各种事情(添加打印语句,无操作atexit函数,使用异常的异步子进程)只会将问题从特定Python版本的特定测试用例,以及不同版本的Python上的不同测试用例。

我知道如果你直接从subprocess读/写,subprocess.Popen().stdout/stderr/stdin模块就会死锁。但是,此代码使用的communicate()方法可以解决这些问题。它位于wait()调用内部,communicate()执行该进程永远挂起,等待pip进程退出。

其他信息:

  • 非常heisenbug-ey,我设法让它消失或根据不应对其产生任何影响的各种事物进行转变。
  • 我已经跟踪了pip内部的执行,一直到代码路径的末尾,直到sys.exit()被调用。
  • sys.exit()替换os._exit()修复了所有悬而未决的问题,但是我不愿意这样做,因为我们正在跳过Python解释器的清理工作。
  • 没有其他线程在运行(通过threading.enumerate验证)。
  • 即使没有将subprocess.PIPE用于stdout / stderr / stdin,我也有一些更改的组合,但是其他组合会让它挂起那些没有被使用(或者它将转移到不同的测试用例/ python版本)。
  • 它似乎与时间无关,任何特定的提交都会在影响测试用例/ Pythons上100%失败或在0%的时间内失败。
  • 通常情况下,改变的代码甚至不会被pip子流程中的特定代码路径执行,但是仅仅存在改变似乎会破坏它。
  • 我尝试使用PYTHONDONTWRITEBYTECODE=1禁用字节码生成并且在一个组合中产生效果,但在其他组合中它没有效果。
  • 子进程调用的命令在每次调用时都会挂起(类似的命令是通过测试套件发出的)但是它总是挂在特定提交的完全相同的位置。
  • 到目前为止,我已经完全无法在测试套件中通过subproccess调用此内容来重现这一点,但是,如果它不是或者不是,我就不知道这个事实。与此有关。

我完全不知道是什么造成了这种情况。

更新#1

使用faulthandler.dump_traceback_later()我得到了这个结果:

Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/requests/packages/urllib3/response.py", line 287 in closed
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/requests/packages/urllib3/response.py", line 287 in closed
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/requests/packages/urllib3/response.py", line 285 in closed
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
  [ Duplicate Lines Snipped ]
  File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__

这告诉我,问题可能与垃圾收集和urllib3有关吗? Filewrapper中的pip._vendor.cachecontrol.filewrapper用作urllib3响应对象(子类io.IOBase)的包装器,以便我们可以通过read()方法存储每个读取的结果调用缓冲区并返回它,然后一旦文件被完全消耗,就用该缓冲区的内容运行回调,以便我们可以将项存储在缓存中。这可能会以某种方式与GC进行交互吗?

更新#2

如果我在Filewrapper类中添加def __del__(self): pass方法,那么在我尝试的情况下一切正常。我进行了测试以确保这不是因为我恰好定义了一个方法(通常将其修改为#{1}},然后它又开始失败了。我不确定为什么这完全有效,而且无操作def __del2__(self): pass方法似乎不太理想。

更新#3

在执行挂起的pip命令期间两次向stderr添加__del__打印的东西,它们是:

import gc; gc.set_debug(gc.DEBUG_UNCOLLECTABLE)

然后

gc: uncollectable <CallbackFileWrapper 0x7f66385c1cd0>
gc: uncollectable <dict 0x7f663821d5a8>
gc: uncollectable <functools.partial 0x7f663831de10>
gc: uncollectable <_io.BytesIO 0x7f663804dd50>
gc: uncollectable <method 0x7f6638219170>
gc: uncollectable <tuple 0x7f663852bd40>
gc: uncollectable <HTTPResponse 0x7f663831c7d0>
gc: uncollectable <PreparedRequest 0x7f66385c1a90>
gc: uncollectable <dict 0x7f663852cb48>
gc: uncollectable <dict 0x7f6637fdcab8>
gc: uncollectable <HTTPHeaderDict 0x7f663831cb90>
gc: uncollectable <CaseInsensitiveDict 0x7f66385c1ad0>
gc: uncollectable <dict 0x7f6638218ab8>
gc: uncollectable <RequestsCookieJar 0x7f663805d7d0>
gc: uncollectable <dict 0x7f66382140e0>
gc: uncollectable <dict 0x7f6638218680>
gc: uncollectable <list 0x7f6638218e18>
gc: uncollectable <dict 0x7f6637f14878>
gc: uncollectable <dict 0x7f663852c5a8>
gc: uncollectable <dict 0x7f663852cb00>
gc: uncollectable <method 0x7f6638219d88>
gc: uncollectable <DefaultCookiePolicy 0x7f663805d590>
gc: uncollectable <list 0x7f6637f14518>
gc: uncollectable <list 0x7f6637f285a8>
gc: uncollectable <list 0x7f6637f144d0>
gc: uncollectable <list 0x7f6637f14ab8>
gc: uncollectable <list 0x7f6637f28098>
gc: uncollectable <list 0x7f6637f14c20>
gc: uncollectable <list 0x7f6637f145a8>
gc: uncollectable <list 0x7f6637f14440>
gc: uncollectable <list 0x7f663852c560>
gc: uncollectable <list 0x7f6637f26170>
gc: uncollectable <list 0x7f663821e4d0>
gc: uncollectable <list 0x7f6637f2d050>
gc: uncollectable <list 0x7f6637f14fc8>
gc: uncollectable <list 0x7f6637f142d8>
gc: uncollectable <list 0x7f663821d050>
gc: uncollectable <list 0x7f6637f14128>
gc: uncollectable <tuple 0x7f6637fa8d40>
gc: uncollectable <tuple 0x7f66382189e0>
gc: uncollectable <tuple 0x7f66382183f8>
gc: uncollectable <tuple 0x7f663866cc68>
gc: uncollectable <tuple 0x7f6637f1e710>
gc: uncollectable <tuple 0x7f6637fc77a0>
gc: uncollectable <tuple 0x7f6637f289e0>
gc: uncollectable <tuple 0x7f6637f19f80>
gc: uncollectable <tuple 0x7f6638534d40>
gc: uncollectable <tuple 0x7f6637f259e0>
gc: uncollectable <tuple 0x7f6637f1c7a0>
gc: uncollectable <tuple 0x7f6637fc8c20>
gc: uncollectable <tuple 0x7f6638603878>
gc: uncollectable <tuple 0x7f6637f23440>
gc: uncollectable <tuple 0x7f663852c248>
gc: uncollectable <tuple 0x7f6637f2a0e0>
gc: uncollectable <tuple 0x7f66386a6ea8>
gc: uncollectable <tuple 0x7f663852f9e0>
gc: uncollectable <tuple 0x7f6637f28560>

这是有用的信息吗?我之前从未使用过那面旗帜所以我不知道这是不寻常的。

1 个答案:

答案 0 :(得分:19)

在Python 2中,如果一组对象在链(引用循环)中链接在一起,并且至少一个对象具有__del__方法,则垃圾收集器将不会删除这些对象。如果您有一个参考周期,添加__del__()方法可能只是隐藏错误(解决方法错误)。

根据您的更新#3,您似乎遇到了这样的问题。