Why does the presence of a forked process in a thread change the behavior of sleep within a trap block?

Viewed 82

This works as expected:

signals = %w[INT TERM]
signals.each do |signal|
  Signal.trap(signal) do
    puts "trapping the signal and sleeping for 5 seconds"
    sleep 5
    puts "done, exiting"
    exit
  end
end

sleep 100

so does this:

# ...signal trap code from above...

t = Thread.new{ sleep 10 }
t.join

so does this:

# ...signal trap code from above...

`sleep 10`

however, this does not:

# ...signal trap code from above...

t = Thread.new{ `sleep 10` }
t.join

For the first three, starting the code and then immediately sending control-c results in ruby waiting 5 seconds before exiting.

For the fourth, starting the code and then immediately sending control-c results in ruby immediately exiting. What's amazing is that the two puts messages, "trapping the..." and "done...", are both printed, but the sleep 5 in between them is seemingly skipped over.

using terrapin instead of backticks produces another clue - terrapin complains that the child processes exited with non-zero state. when attempting to print this status, it prints nothing, perhaps suggesting that the process was unceremoniously hard killed.

So it seems that ruby has some sort of default behavior regarding child processes created within threads that are not the main thread. I suspected this might be by design but I wasn't able to find any documentation or discussion about it.

I also tried

t = Thread.new{ `ls -R /` }

instead of sleep, just in case there was some sort of interaction with competing sleep implementations, same behavior.

Why is this happening?

More Experiments

This also behaves as expected. it waits 10 seconds for the subprocess within the thread to finish. so, the strange behavior only happens in the context of a signal trap.

thread = Thread.new { `sleep 10` }
thread.join

To test if something is happening involving a subthread trapping the signal at the same time, either by design or from a bug. But this behaves as expected:

@main_thread = Thread.current.object_id
puts "main thread: #{@main_thread}"

signals = %w[INT TERM]
signals.each do |signal|
  Signal.trap(signal) do
    puts Thread.current.object_id
    next unless Thread.current.object_id == @main_thread
    puts "thread from trap: #{Thread.current.object_id}"
    puts "trapping the signal and sleeping for 5 seconds"
    sleep 5
    puts "done, exiting"
    exit
  end
end
0 Answers
Related