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 online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
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)