rubycelluloid

Non blocking subprocess with Celluloid


What is the best way to spawn a subprocess without blocking the actor ?

My goal is to run multiple commands, and when they are done, get the output and the exit code. I tried this, but obviously, the popen call is blocking:

#!/usr/bin/env ruby

require 'celluloid/current'
require 'celluloid/io'

class MyProcessLauncher
    include Celluloid::IO

    def run
        every(1) { puts "tick" }

        every(5) {
            puts "Starting subprocess"
            ::IO.popen("sleep 10 && echo 'subprocess done'", :err=>[:child, :out]) { |io|
                puts io.read
            }
            puts $?.exitstatus
        }
    end
end

MyProcessLauncher.new.run
sleep

The output is :

tick
tick
tick
tick
Starting subprocess
subprocess done
0
tick
Starting subprocess

but I expect five 'tick' between each 'Starting subprocess'...

Thanks !


Solution

  • You need to defer or use a future if you want the return value.

    defer ( easy if you need no return )

    In your every(5) block, to use defer, wrap your popen call (including the exit status display call) with defer {}

    future ( a bit more complicated )

    There are several ways to implement this approach. My suggestion would be to move the contents of every(5) to its own method, and initialize an array @futures or similar when starting MyProcessLauncher ... then use this inside the every(5) call:

    @futures = future.new_method
    

    Also, add a new loop to process your futures. After every(5) add a new handler ( either another every loop, or an async call to a processor method which is recursive ) to process your values, essentially doing this:

    value = @futures.pop.value if @futures.any?
    

    I'd ask a follow up question about futures if you have trouble and can't find examples of how to process futures. Otherwise, there you have two ways to handle popen calls without blocking.