ruby/test/ruby/test_sleep.rb
Takashi Kokubun ac596948d4 merge revision(s) 7e81bf5c0c8f43602e6d901f4253dca2f3d71745: [Backport #21812]
[PATCH] Fix sleep spurious wakeup from sigchld (#15802)

	When sleeping with `sleep`, currently the main thread can get woken up from sigchld
	from any thread (subprocess exited). The timer thread wakes up the main thread when this
	happens, as it checks for signals. The main thread then executes the ruby sigchld handler
	if one is registered and is supposed to go back to sleep immediately. This is not ideal but
	it's the way it's worked for a while. In commit 8d8159e7d8 I added writes to `th->status`
	before and after `wait_running_turn` in `thread_sched_to_waiting_until_wakeup`, which is
	called from `sleep`. This is usually the right way to set the thread's status, but `sleep`
	is an exception because the writes to `th->status` are done in `sleep_forever`. There's a
	loop that checks `th->status` in `sleep_forever`. When the main thread got woken up from
	sigchld it saw the changed `th->status` and continued to run the main thread instead of
	going back to sleep.

	The following script shows the error. It was returning instead of sleeping forever.

	```ruby
	t = Thread.new do
	  sleep 0.3
	  `echo hello`  # Spawns subprocess
	  puts "Subprocess exited"
	end

	puts "Main thread sleeping..."
	result = sleep  # Should block forever
	puts "sleep returned: #{result.inspect}"
	```

	Fixes [Bug #21812]
2026-01-12 17:14:52 -08:00

35 lines
774 B
Ruby

# frozen_string_literal: false
require 'test/unit'
require 'etc'
require 'timeout'
class TestSleep < Test::Unit::TestCase
def test_sleep_5sec
EnvUtil.without_gc do
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
sleep 5
slept = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
bottom = 5.0
assert_operator(slept, :>=, bottom)
assert_operator(slept, :<=, 6.0, "[ruby-core:18015]: longer than expected")
end
end
def test_sleep_forever_not_woken_by_sigchld
begin
t = Thread.new do
sleep 0.5
`echo hello`
end
assert_raise Timeout::Error do
Timeout.timeout 2 do
sleep # Should block forever
end
end
ensure
t.join
end
end
end