9

In some web dev I do, I have multiple operations beginning, like GET requests to external APIs, and I want them to both start at the same time because one doesn't rely on the result of the other. I want things to be able to run in the background. I found the concurrent-ruby library which seems to work well. By mixing it into a class you create, the class's methods have asynchronous versions which run on a background thread. This lead me to write code like the following, where FirstAsyncWorker and SecondAsyncWorker are classes I've coded, into which I've mixed the Concurrent::Async module, and coded a method named "work" which sends an HTTP request:

def index
  op1_result = FirstAsyncWorker.new.async.work
  op2_result = SecondAsyncWorker.new.async.work
  
  render text: results(op1_result, op2_result)
end

However, the controller will implicitly render a response at the end of the action method's execution. So the response gets sent before op1_result and op2_result get values and the only thing sent to the browser is "#".

My solution to this so far is to use Ruby threads. I write code like:

def index
  op1_result = nil
  op2_result = nil

  op1 = Thread.new do
    op1_result = get_request_without_concurrent
  end

  op2 = Thread.new do
    op2_result = get_request_without_concurrent
  end

  # Wait for the two operations to finish
  op1.join
  op2.join

  render text: results(op1_result, op2_result)
end

I don't use a mutex because the two threads don't access the same memory. But I wonder if this is the best approach. Is there a better way to use the concurrent-ruby library, or other libraries better suited to this situation?

Matt Welke
  • 1,441
  • 1
  • 15
  • 40

1 Answers1

12

I ended up answering my own question after some more research into the concurrent-ruby library. Futures ended up being what I was after! Simply put, they execute a block of code in a background thread and attempting to access the Future's calculated value blocks the main thread until that background thread has completed its work. My Rails controller actions end up looking like:

def index
  op1 = Concurrent::Future.execute { get_request }
  op2 = Concurrent::Future.execute { another_request }

  render text: "The result is #{result(op1.value, op2.value)}."
end

The line with render blocks until both async tasks have finished, at which point result can begin running.

Matt Welke
  • 1,441
  • 1
  • 15
  • 40
  • I would also suggest to use value! instead as if value so if the block raises an exception, value! will raise again instead of nil. – Simon1901 Feb 25 '21 at 08:54
  • Yeah you'd have to think about what error handling strategy you want to use. I didn't end up studying this library in detail, I just used it to figure out how to block until all needed data was ready. At the time, I was familiar with Node.js and was curious how one would pull this pattern off in Ruby when every Ruby library out there seems to block instead of use callbacks. – Matt Welke Feb 25 '21 at 16:16