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.
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.
Posted by Tobias Kraze to makandra dev (2010-08-25 13:39)