Project

General

Profile

Actions

Bug #21614

open

thread_sched_wait_events race with timer_thread_wakeup

Added by luke-gru (Luke Gruber) 2 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:123318]

Description

thread_sched_wait_events checks th->sched.waiting_reason.flags under thread_sched_lock but not under
timer_thread.waiting_lock. The timer thread sets it under the timer_thread.waiting_lock only. This race
results in the assertion failure:

../ruby/thread_pthread.c:909: Assertion Failed: thread_sched_to_running_common:sched->running != th

Reproduction script (run it many times):

require "timeout"

def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false,
                encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error,
                stdout_filter: nil, stderr_filter: nil, ios: nil,
                signal: :TERM,
                rubybin: "ruby", precommand: nil,
                **opt)
  timeout = 60

  in_c, in_p = IO.pipe
  out_p, out_c = IO.pipe if capture_stdout
  err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout
  opt[:in] = in_c
  opt[:out] = out_c if capture_stdout
  opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr
  if encoding
    out_p.set_encoding(encoding) if out_p
    err_p.set_encoding(encoding) if err_p
  end
  ios.each {|i, o = i|opt[i] = o} if ios

  c = "C"
  child_env = {}
  if Array === args and Hash === args.first
    child_env.update(args.shift)
  end

  args = [args] if args.kind_of?(String)
  # use the same parser as current ruby
  if args.none? { |arg| arg.start_with?("--parser=") }
    args = ["--parser=prism"] + args
  end
  pid = spawn(child_env, *precommand, rubybin, *args, opt)
  in_c.close
  out_c&.close
  out_c = nil
  err_c&.close
  err_c = nil
  if block_given?
    return yield in_p, out_p, err_p, pid
  else
    th_stdout = Thread.new { out_p.read } if capture_stdout
    th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout
    in_p.write stdin_data.to_str unless stdin_data.empty?
    in_p.close
    if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout))
      timeout_error = nil
    else
      $stderr.puts "Error: timed out"
      status = terminate(pid, signal, opt[:pgroup])
      terminated = Time.now
    end
    stdout = th_stdout.value if capture_stdout
    stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout
    out_p.close if capture_stdout
    err_p.close if capture_stderr && capture_stderr != :merge_to_stdout
    status ||= Process.wait2(pid)[1]
    stdout = stdout_filter.call(stdout) if stdout_filter
    stderr = stderr_filter.call(stderr) if stderr_filter
    if timeout_error
      bt = caller_locations
      msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)"
      raise timeout_error, msg, bt.map(&:to_s)
    end
    return stdout, stderr, status
  end
ensure
  [th_stdout, th_stderr].each do |th|
    th.kill if th
  end
  [in_c, in_p, out_c, out_p, err_c, err_p].each do |io|
    io&.close
  end
  [th_stdout, th_stderr].each do |th|
    th.join if th
  end
end

def terminate(pid, signal = :TERM, pgroup = nil)
  case pgroup
  when 0, true
    pgroup = -pid
  when nil, false
    pgroup = pid
  end

  begin
    Process.kill signal, pgroup
  rescue Errno::EINVAL
  rescue Errno::ESRCH
  end
  $?
end

rs = []
100.times do
  rs << Ractor.new do
    script = '$stdout.sync=true; def rec; print "."; rec; end; Fiber.new{rec}.resume'
    invoke_ruby([{}, '-e', script], "", true, true)
  end
end

rs.each(&:join)

This script is just taken directly from a failed test in the test suite that was run under multiple ractors at once (test_vm_stack_size).

No data to display

Actions

Also available in: Atom PDF

Like0