How to implement resonable process timeout

def do_work
  process = Process.new ("D:/tool.bat", ["-version"])

  p process.pid 

  process.wait 
end

spawn do
  loop do 
    do_work
  end
end

spawn do
  loop do 
    do_work
  end
end

sleep

I want to use the process class to run an external tool. What would be a reasonable solution to implement a timeout which terminates the process after the specified time ?

Use channel and select with timeout. Something like

def do_work
 channel = Channel(Process::Status).new(capacity: 1)
 process = nil
 spawn do
   process = Process.new ("D:/tool.bat", ["-version"])
   status = process.wait
   channel.send(status) unless channel.closed?
 end 

 select
 when status = channel.receive
    # process terminates before timeout
    # .....
 when timeout(MY_TIMEOUT) # MY_TIMEOUT is your own custom timeout value you want to wait
   # this will be reached when `timeout` is met and process still running
   channel.close
   process.try &.terminate rescue nil
   # process.terminate may raise, so better to catch and handle. above example just ignoring the exception
 end
end   

HIH

1 Like

The proposed solution works. However on windows I get periodically a block of warnings:

GC Warning: Finalization cycle involving 0000019E29E6E360
GC Warning: Finalization cycle involving 0000019E29E67300
GC Warning: Finalization cycle involving 0000019E29E4A300
GC Warning: Finalization cycle involving 0000019E29E4A660
GC Warning: Finalization cycle involving 0000019E29E4A780
GC Warning: Finalization cycle involving 0000019E29E4AAE0
GC Warning: Finalization cycle involving 0000019E29E4AE40
GC Warning: Finalization cycle involving 0000019E29E4AF00

The task manager also shows an increase of the process memory consumption

This does not happen on linux !

Is this related to Memory Leak on Windows? ?

Windows support is still in preview and it might be related to that. For status of windows port refer to the issue-5430

While digging in the crystal sources I found in ‘spec/std/thread_spec.cr’ a hint:

  it "returns current thread object" do
    current = nil
    thread = Thread.new { current = Thread.current }
    thread.join
    current.should be(thread)
    current.should_not be(Thread.current)
  ensure
    # avoids a "GC Warning: Finalization cycle" caused by *current*
    # referencing the thread itself, preventing the finalizer to run:
    current = nil
  end

The warnings vanish when I set the process = nil as the last statement in the do_work method !

Can somebody explain me why in this case the finalizer is NOT called, thus preventing the GC warning.