Read more

Test concurrent Ruby code

Tobias Kraze
August 25, 2010Software engineer at makandra GmbH

To test concurrent code, you will need to run multiple threads. Unfortunately, when you use blocking system calls (e.g. locks on the database), Ruby 1.8 threads won't work because system calls will block the whole interpreter.

Illustration UI/UX Design

UI/UX Design by makandra brand

We make sure that your target audience has the best possible experience with your digital product. You get:

  • Design tailored to your audience
  • Proven processes customized to your needs
  • An expert team of experienced designers
Read more Show archive.org snapshot

Luckily you can use processes instead. fork spins off a new process, IO.pipe sends messages between processes, Process.exit! kills the current process. You will need to take care of ActiveRecord database connections.

Here is a full-fledged example:

describe Lock, '.acquire' do

  before :each do
    @reader, @writer = IO.pipe
  end

  def fork_with_new_connection
    config = ActiveRecord::Base.remove_connection
    fork do
      begin
        ActiveRecord::Base.establish_connection(config)
        yield
      ensure
        ActiveRecord::Base.remove_connection
        Process.exit!
      end
    end
    ActiveRecord::Base.establish_connection(config)
  end

  it 'should synchronize processes on the same lock' do
    (1..20).each do |i|
      fork_with_new_connection do
        @reader.close
        ActiveRecord::Base.connection.reconnect!
        Lock.acquire('lock') do
          @writer.puts "Started: #{i}"
          sleep 0.01
          @writer.puts "Finished: #{i}"
        end
        @writer.close
      end
    end
    @writer.close

    # test whether we always get alternating "Started" / "Finished" lines
    lines = @reader.lines.to_a
    lines.should be_present # it is empty if the processes all crashed due to a typo or similar
    lines.each_slice(2) do |start, finish|
      start.should =~ /Started: (.*)/
      start_thread = $1
      finish.should =~ /Finished: (.*)/
      finish_thread = $1
      finish_thread.should == start_thread
    end

    @reader.close
  end
end

Note how Process.waitall waits for all child processes to finish.

IO.pipe caveat: Closing the pipe is important. In particular the reading process has to close the output pipe before it can begin to read.

Tobias Kraze
August 25, 2010Software engineer at makandra GmbH
Posted by Tobias Kraze to makandra dev (2010-08-25 15:39)