【问题标题】:Timeout within a popen works, but popen inside a timeout doesn't?popen 内的超时有效,但超时内的 popen 无效?
【发布时间】:2013-06-18 17:34:57
【问题描述】:

用代码最容易解释:

require 'timeout'

puts "this block will properly kill the sleep after a second"

IO.popen("sleep 60") do |io|
  begin
    Timeout.timeout(1) do
      while (line=io.gets) do
        output += line
      end
    end
  rescue Timeout::Error => ex
    Process.kill 9, io.pid
    puts "timed out: this block worked correctly"
  end
end

puts "but this one blocks for >1 minute"

begin
  pid = 0
  Timeout.timeout(1) do
    IO.popen("sleep 60") do |io|
      pid = io.pid
      while (line=io.gets) do
        output += line
      end
    end
  end
rescue Timeout::Error => ex
  puts "timed out: the exception gets thrown, but much too late"
end

我对这两个模块的心智模型是相同的:

那么,我错过了什么?

编辑:drmaciver 在推特上建议,在第一种情况下,由于某种原因,管道套接字进入非阻塞模式,但在第二种情况下却没有。我想不出为什么会发生这种情况,也无法弄清楚如何获取描述符的标志,但这至少是一个合理的答案?正在研究这种可能性。

【问题讨论】:

  • 你在运行哪个 ruby​​?
  • 这种行为至少出现在 1.8.7 和 1.9.3 上。两个块上所有 60 个的 jruby 块,这是我事先猜到的行为。
  • 请注意,我在两个块之间的puts("but this one...")等到第一个 sleep 完成,因为第一个 IO#popen 块尽职尽责地调用waitpid()。如果你不想这样,那么你的救援逻辑需要杀死子进程。
  • @pilcrow 真正的代码确实会杀死 proc,但我为这段代码删除了它......我不应该这样做。将重新添加它,谢谢
  • @llimllib Timeout 接受任意类作为第二个参数来覆盖默认的 Timeout::Error。如果您插入自己的类来记录初始化时的系统时间,您应该能够确定您的问题是由于 Timeout 尝试引发的时间,还是由于 Ruby VM 在子进程上下文中传播引发的异常的方式。不是答案,但也许是通往答案的道路。

标签: ruby multithreading io timeout subprocess


【解决方案1】:

啊哈,微妙的。

在第二种情况下,在 IO#popen 块的末尾有一个隐藏的阻塞 ensure 子句。 Timeout::Error is 及时提出,但在执行从隐式 ensure 子句返回之前,您不能 rescue 它。

Under the hood, IO.popen(cmd) { |io| ... } 做这样的事情:

def my_illustrative_io_popen(cmd, &block)
  begin
    pio = IO.popen(cmd)
    block.call(pio)      # This *is* interrupted...
  ensure
    pio.close            # ...but then control goes here, which blocks on cmd's termination
  end

而 IO#close 调用实际上或多或少是一个 pclose(3),它会阻止你在 waitpid(2) 中,直到睡着的孩子退出。

你可以这样验证:

#!/usr/bin/env ruby

require 'timeout'

BEGIN { $BASETIME = Time.now.to_i }

def xputs(msg)
  puts "%4.2f: %s" % [(Time.now.to_f - $BASETIME), msg]
end

begin
  Timeout.timeout(3) do
    begin
      xputs "popen(sleep 10)"
      pio = IO.popen("sleep 10")
      sleep 100                     # or loop over pio.gets or whatever
    ensure
      xputs "Entering ensure block"
      #Process.kill 9, pio.pid      # <--- This would solve your problem!
      pio.close
      xputs "Leaving ensure block"
    end
  end
rescue Timeout::Error => ex
  xputs "rescuing: #{ex}"
end

那么,你能做什么?

您必须以显式方式执行此操作,因为解释器不会公开覆盖 IO#popen ensure 逻辑的方法。例如,您可以使用上面的代码作为起始模板并取消注释 kill() 行。

【讨论】:

  • 我盯着 io.c 看了这么久,只看了几行 上面 确保,根本没有看到或考虑它。很好的答案,非常感谢。
  • 是否可以通过此解决方案获得退出状态?
【解决方案2】:

在第一个块中,在子节点中引发超时,将其杀死并将控制权返回给父节点。在第二个块中,超时在父级中提出。孩子永远不会收到信号。

io.chttps://github.com/ruby/ruby/blob/trunk/io.c#L6021timeout.rbhttps://github.com/ruby/ruby/blob/trunk/lib/timeout.rb#L51

【讨论】:

  • 我知道这不是一个详细的答案比尔,但这就是我阅读积木的方式。
  • 传递给 IO#popen 的块在父进程的上下文中执行。当您说子进程可以或不可以“[获取]信号”时,我不确定您的意思。
  • @JonathanJulian 我已经在分屏中打开了这两个文件,试图弄清楚。据我所知,在这两个示例中,超时都是从主线程产生的。这是 popen 运行它传递的块的地方:github.com/ruby/ruby/blob/trunk/io.c#L6075
  • @JonathanJulian 我对其进行了检测以表明超时都是从同一个线程产生的:gist.github.com/llimllib/48da4b10d9b6bd6538d5
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-07-05
  • 1970-01-01
  • 1970-01-01
  • 2017-12-19
  • 1970-01-01
  • 1970-01-01
  • 2022-11-16
相关资源
最近更新 更多