academy

Testing Asynchronous Threads in Ruby

Peter Ohler

Peter Ohler on

Testing Asynchronous Threads in Ruby

Threads and asynchronous environments are initially a bit tricky. Without a good mental model to organize interaction, it is easy to get into trouble and end up with unexpected results. On top of that, testing asynchronous code can be difficult without the right tools or test patterns.

Thinking about threads as people and shared objects as ‘things’ that can be owned helps to organize the working of a multithreaded system. In this episode we will go through an example to learn all about testing asynchronous Ruby code.

If you are using Rails or Rack or really any application that as a web browser front end you are in an asynchronous environment. The Rack #call is always called asynchronously. So whether you know it or not, there is a good chance you are already using multithreaded components.

Testing: Trigger, collect and check

Testing an asynchronous callback API can be made synchronous by following a pattern of three steps; trigger, collect, and check. Think if each thread as a separate individual and objects as, well, things that can be owned by just one individual at a time.

We’ll use the example of Batman and his 7 different suits. Because that is a practical example and we can understand the importance of knowing whether all suits are with Alfred to be washed when you are about to run out and save the city.

Example: Laundry day at the batcave

The example is Alfred washing Batman's suits. The SuitWashScheduler is a scheduler that invokes a callback for each washing event. The scheduler makes seven callbacks at one-second intervals starting one second after starting. The trigger is the creation of the SuitWashScheduler.

1class SuitWashScheduler
2  def initialize(cnt)
3    Thread.new {
4      cnt.times {
5        sleep(1.0)
6        yield
7      }
8    }
9  end
10end

Collecting

Collecting results must be thread safe to avoid race conditions. Any object shared across more than one thread has to be protected. Protection is a way of keeping track of the owner of an object. Only the owner can make changes or view the object. A suit can only be with Batman to use in fights, or with Alfred to be washed.

To stay friendly a thread (in the metaphor Batman or Alfred) only takes ownership for short times and then gives up the ownership. A Mutex is usually used to keep track of the owner. The SuitwashScheduler callback will own the result counter when the counter is incremented. The callback that is run in the SuitWashScheduler thread signals that all results have been received when the counter hits the target.

Writing the example starts with setting up some globals. In a real application the globals would be replaced by class or object attributes.

1$main_thread = Thread.current
2$mu = Mutex.new
3$count = 0
4$target = 7

Management and owners

The $main_thread and $mu are used to manage the threads and waiting for completion of the test while the $target and $count track the test results. Remember this is a trivial test so collecting and checking results has to be simple.

The test is started by creating a new instance of the SuitWashScheduler, giving the initializer the $target number of iterations. In this case, the 7 suits that need washing. The block provided will be run in the SuitWashScheduler thread. For each iteration the $count is incremented and printed.

Looking ahead we realize that the main, test thread is going to be checking the$count also which means it will need ownership of the $count as well so a means of taking ownership of $count is needed. The $mu Mutex instance is the ownership token. In the block passed to the SuitWashScheduler.new call a $mu.synchronize block takes ownership long enough to set the $count and check the results. More on the results check in a moment.

1SuitWashScheduler.new($target) {
2  $mu.synchronize {
3    $count += 1
4    puts $count
5    $main_thread.wakeup if $target <= $count
6  }
7}

Check: are all suits done?

Back at the main thread we need to wait for the test to finish. Batman needs to wait before all 7 suits are done. There are two conditions to check for; the tests updates the $count as expected or Batman gets bored waiting for the test to finish and times out. Before checking the $count to see if it has reached the $target, the ownership of $count is needed. Just like in the block for the SuitWashScheduler a call to $mu.synchronize is used.

But that can't be right, if we lock up the main thread how can the SuitWashScheduler thread ever change the $count? Luckily for us there is a neat trick that takes care of this. The Mutex class has a #sleep method that gives up ownership and waits until either it times out or is woken. Once woken either through the timeout or a #wakeup call to the main thread $mu attempts to take ownership again before continuing. Once ownership has been achieved the results can be checked and a determination of the pass or fail state of the test can be made.

1$mu.synchronize {
2  $mu.sleep($target + 1)
3  if $target != $count
4    puts 'FAILED'
5  else
6    puts 'Passed! All suits are washed and clean'
7  end
8}

If you want to get deeper into this, you can make the example a bit more interesting by trying to create multiple schedulers and see how the Mutex keep the $count changes from colliding. As if Batman sends some suits to Alfred to be washed, and some others to the dry cleaners. Make sure to change the logic to make sure the $target check is a total of all the expected yields.

Roundup

Working with threads and asynchronous environment gets easier with the right mental model. In this post we used people as a metaphor for threads and physical objects (suits) as metaphor for shared objects, that can only be owned by one thread or person at a time. We think this way to abstract makes it easier to understand and remember.

We hope the examples with make you remember the mechanisms of async, but we hope the image of Batman running out while all his suits are being washed won’t stick with you for too long.

P.S. If you are done with all the batman metaphors on the blog, let us know.

Share this article

RSS
Peter Ohler

Peter Ohler

Guest author Peter Ohler creates quite a bit of high-performance code, and writes about it too, every now and then. He made the Agoo gem, which is a pretty cool high-performance HTTP server.

All articles by Peter Ohler

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps