Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

Commit

Permalink
Initial version.
Browse files Browse the repository at this point in the history
  • Loading branch information
wisq committed Aug 5, 2011
0 parents commit 8d4459a
Show file tree
Hide file tree
Showing 18 changed files with 869 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
*.gem
.bundle
Gemfile.lock
pkg/*
4 changes: 4 additions & 0 deletions Gemfile
@@ -0,0 +1,4 @@
source "http://rubygems.org"

# Specify your gem's dependencies in rails_parallel.gemspec
gemspec
33 changes: 33 additions & 0 deletions README.markdown
@@ -0,0 +1,33 @@
rails_parallel
==============

rails_parallel makes your Rails tests scale with the number of CPU cores available.

It also speeds up the testing process in general, by making heavy use of forking to only have to load the Rails environment once.

Installation
------------

To load rails_parallel, require "rails_parallel/rake" early in your Rakefile. One possibility is to load it conditionally based on an environment variable:

require 'rails_parallel/rake' if ENV['PARALLEL']

You'll want to add a lib/tasks/rails_parallel.rake with at least the following:

# RailsParallel handles the DB schema.
Rake::Task['test:prepare'].clear_prerequisites if Object.const_get(:RailsParallel)

namespace :parallel do
# Run this task if you have non-test tasks to run first and you want the
# RailsParallel worker to start loading your environment earlier.
task :launch do
RailsParallel::Rake.launch
end

# RailsParallel runs this if it needs to reload the DB.
namespace :db do
task :setup => ['db:drop', 'db:create', 'db:schema:load']
end
end

This gem was designed as an internal project and currently makes certain assumptions about your project setup, such as the use of MySQL and a separate versioned schema (rather than db/schema.rb). These will become more generic in future versions.
2 changes: 2 additions & 0 deletions Rakefile
@@ -0,0 +1,2 @@
require 'bundler'
Bundler::GemHelper.install_tasks
23 changes: 23 additions & 0 deletions bin/rails_parallel_worker
@@ -0,0 +1,23 @@
#!/usr/bin/env ruby

ENV['RAILS_ENV'] = 'test'

begin
puts 'RP: Loading RailsParallel.'
$LOAD_PATH << 'lib'
require 'rails_parallel/runner'
require 'rails_parallel/object_socket'

socket = ObjectSocket.new(IO.for_fd(ARGV.first.to_i))
socket << :started

puts 'RP: Loading Rails.'
require "#{ENV['RAILS_PARALLEL_ROOT']}/config/environment"

puts 'RP: Ready for testing.'
RailsParallel::Runner.launch(socket)
puts 'RP: Shutting down.'
Kernel.exit!(0)
rescue Interrupt, SignalException
Kernel.exit!(1)
end
3 changes: 3 additions & 0 deletions lib/rails_parallel.rb
@@ -0,0 +1,3 @@
module RailsParallel
# Nothing here. Require 'rails_parallel/rake' in your Rakefile if you want RP.
end
32 changes: 32 additions & 0 deletions lib/rails_parallel/collector.rb
@@ -0,0 +1,32 @@
require 'test/unit/collector'

module RailsParallel
class Collector
include Test::Unit::Collector

NAME = 'collected from the ObjectSpace'

def prepare(timings, test_name)
@suites = {}
::ObjectSpace.each_object(Class) do |klass|
@suites[klass.name] = klass.suite if Test::Unit::TestCase > klass
end

@pending = @suites.keys.sort_by do |name|
[
0 - timings.fetch(test_name, name), # runtime, descending
0 - @suites[name].size, # no. of tests, descending
name
]
end
end

def next_suite
@pending.shift
end

def suite_for(name)
@suites[name]
end
end
end
39 changes: 39 additions & 0 deletions lib/rails_parallel/forks.rb
@@ -0,0 +1,39 @@
module RailsParallel
module Forks
def fork_and_run
ActiveRecord::Base.connection.disconnect! if ActiveRecord::Base.connected?

fork do
begin
yield
Kernel.exit!(0)
rescue Interrupt, SignalException
Kernel.exit!(1)
rescue Exception => e
puts "Error: #{e}"
puts(*e.backtrace.map {|t| "\t#{t}"})
before_exit
Kernel.exit!(1)
end
end
end

def wait_for(pid, nonblock = false)
pid = Process.waitpid(pid, nonblock ? Process::WNOHANG : 0)
check_status($?) if pid
pid
end

def wait_any(nonblock = false)
wait_for(-1, nonblock)
end

def check_status(stat)
raise "error: #{stat.inspect}" unless stat.success?
end

def before_exit
# cleanup here (in children)
end
end
end
76 changes: 76 additions & 0 deletions lib/rails_parallel/object_socket.rb
@@ -0,0 +1,76 @@
require 'rubygems'
require 'socket'

class ObjectSocket
BLOCK_SIZE = 4096

attr_reader :socket

def self.pair
Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0).map { |s| new(s) }
end

def initialize(socket)
@socket = socket
@buffer = ''
end

def nonblock=(val)
@nonblock = val
end

def close
@socket.close
end

def nonblocking(&block)
with_nonblock(true, &block)
end
def blocking(&block)
with_nonblock(false, &block)
end

def each_object(&block)
first = true
loop do
process_buffer(&block) if first
first = false

@buffer += @nonblock ? @socket.read_nonblock(BLOCK_SIZE) : @socket.readpartial(BLOCK_SIZE)
process_buffer(&block)
end
rescue Errno::EAGAIN
# end of nonblocking data
end

def next_object
each_object { |o| return o }
nil # no pending data in nonblock mode
end

def <<(obj)
data = Marshal.dump(obj)
@socket.syswrite [data.size, data].pack('Na*')
self # chainable
end

private

def process_buffer
while @buffer.size >= 4
size = 4 + @buffer.unpack('N').first
break unless @buffer.size >= size

packet = @buffer.slice!(0, size)
yield Marshal.load(packet[4..-1])
end
end

def with_nonblock(value)
old_value = @nonblock
@nonblock = value
return yield
ensure
@nonblock = old_value
end
end

0 comments on commit 8d4459a

Please sign in to comment.