You may have heard before that Ruby isn't a concurrent language. In other words, it's not capable of doing two things at once. For the most part, you're right - the canonical version of Ruby that we use at Lumos (called MRI) locks the interpreter to a single thread of execution. This means you can never truly run a task "in the background" within the same process. MRI relaxes the lock however for certain IO operations, including file operations and network activity. The Ruby interpreter will "unlock" itself whenever it detects an IO operation, meaning other threads get to run. Pretty cool :)
Since Ruby does support at least some level of concurrency, it would be cool if we could use it in our apps. There are a couple of great Ruby concurrency libraries out there - namely Celluloid and concurrent-ruby. I'm going to talk about concurrent-ruby in this gist because it's the one I know best.
Lots of other languages have concurrency primitives built-in, and one of the most popular is the "promise". Promises encapsulate a unit of deferred execution - in other words, they execute a ruby block in a separate thread. Promises can be in a number of different states, but when the block finishes executing, they get marked as either "fulfilled" (succeeded) or "rejected" (failed).
Let's use concurrent-ruby's Promise class to execute a web request in the background:
require 'net/http'
require 'concurrent'
promise = Concurrent::Promise.execute do
Net::HTTP.get('http://google.com')
end
promise.state # => :unscheduled
sleep 0.1
promise.state # => :pending
sleep 2
promise.state # => :fulfilledCalling promise.value will block (halt your program) until the future is done executing.
Instead of waiting for the future to finish, which could take some time, you can chain another operation:
promise.then do |response|
# the `response` block argument is the response object from our Net::HTTP call above
puts response.body
endWhat happens if an error gets raised inside our promise?
promise = Concurrent::Promise.execute do
raise 'jelly beans!'
end
promise.then { puts 'next!' }
promise.state # => :rejectedIn this scenario, "next" never gets written to the console. When chaining promises, it's important to remember that a then block wont'get executed if its parent gets rejected.
Concurrent-ruby contains a bunch more awesome tools to make concurrency easier in Ruby. Go check it out!
💭 Sweet !!!