Rsync synchronization for remote workers
This commit is contained in:
parent
5b53276af6
commit
eee69c0096
77
README.rdoc
77
README.rdoc
|
@ -25,32 +25,93 @@ In your rakefile:
|
||||||
t.add_files 'test/integration/**/*_test.rb'
|
t.add_files 'test/integration/**/*_test.rb'
|
||||||
end
|
end
|
||||||
|
|
||||||
Then you can run 'rake hydra'.
|
Run:
|
||||||
|
|
||||||
|
$ rake hydra
|
||||||
|
|
||||||
|
Hydra defaults to Single Core mode, so you may want to configure it
|
||||||
|
to use two (or more) of your cores if you have a multi-processing machine.
|
||||||
|
|
||||||
== Configuration
|
== Configuration
|
||||||
|
|
||||||
Place the config file in the main project directory as
|
Place the config file in the main project directory as
|
||||||
'hydra.yml' or 'config/hydra.yml'.
|
'hydra.yml' or 'config/hydra.yml'.
|
||||||
|
|
||||||
|
=== Examples
|
||||||
|
|
||||||
|
==== Dual Core
|
||||||
|
|
||||||
|
workers:
|
||||||
|
- type: local
|
||||||
|
runners: 2
|
||||||
|
|
||||||
|
==== Dual Core, with a remote Quad Core server
|
||||||
|
|
||||||
|
The -p3022 tells it to connect on a different port
|
||||||
|
|
||||||
workers:
|
workers:
|
||||||
- type: local
|
- type: local
|
||||||
runners: 2
|
runners: 2
|
||||||
- type: ssh
|
- type: ssh
|
||||||
connect: user@example.com -p3022
|
connect: user@example.com
|
||||||
|
ssh_opts: -p3022
|
||||||
directory: /absolute/path/to/project
|
directory: /absolute/path/to/project
|
||||||
runners: 4
|
runners: 4
|
||||||
|
|
||||||
The "connect" option is passed to SSH. So if you've setup an
|
==== Two Remote Quad Cores with Synchronization
|
||||||
ssh config alias to a server, you can use that.
|
|
||||||
|
|
||||||
The "directory" option is the path for the project directory
|
You can use the 'sync' configuration to allow rsync to synchronize
|
||||||
where the tests should be run.
|
the local and remote directories every time you run hydra.
|
||||||
|
|
||||||
The "runners" option is how many processes will be running
|
workers:
|
||||||
on the remote machine. It's best to pick the same number
|
- type: ssh
|
||||||
|
connect: user@alpha.example.com
|
||||||
|
directory: /path/to/project/on/alpha/
|
||||||
|
runners: 4
|
||||||
|
- type: ssh
|
||||||
|
connect: user@beta.example.com
|
||||||
|
directory: /path/to/project/on/beta/
|
||||||
|
runners: 4
|
||||||
|
|
||||||
|
sync:
|
||||||
|
directory: /my/local/project/directory
|
||||||
|
exclude:
|
||||||
|
- tmp
|
||||||
|
- log
|
||||||
|
- doc
|
||||||
|
|
||||||
|
=== Workers Options
|
||||||
|
|
||||||
|
==== type
|
||||||
|
|
||||||
|
Either "local" or "ssh".
|
||||||
|
|
||||||
|
==== runners
|
||||||
|
|
||||||
|
The *runners* option is how many processes will be running
|
||||||
|
on the machine. It's best to pick the same number
|
||||||
as the number of cores on that machine (as well as your
|
as the number of cores on that machine (as well as your
|
||||||
own).
|
own).
|
||||||
|
|
||||||
|
=== SSH Options
|
||||||
|
|
||||||
|
==== connect
|
||||||
|
|
||||||
|
The *connect* option is passed to SSH. So if you've setup an
|
||||||
|
ssh config alias to a server, you can use that. It is also
|
||||||
|
used in rsync, so you cannot use options.
|
||||||
|
|
||||||
|
==== ssh_opts
|
||||||
|
|
||||||
|
The *ssh_opts* option is passed to SSH and to Rsync's RSH so
|
||||||
|
that you can use the same ssh options for connecting and rsync.
|
||||||
|
Use ssh_opts to set the port or compression options.
|
||||||
|
|
||||||
|
==== directory
|
||||||
|
|
||||||
|
The *directory* option is the path for the project directory
|
||||||
|
where the tests should be run.
|
||||||
|
|
||||||
== Copyright
|
== Copyright
|
||||||
|
|
||||||
Copyright (c) 2010 Nick Gauthier. See LICENSE for details.
|
Copyright (c) 2010 Nick Gauthier. See LICENSE for details.
|
||||||
|
|
19
TODO
19
TODO
|
@ -1,24 +1,5 @@
|
||||||
= Hydra TODO
|
= Hydra TODO
|
||||||
|
|
||||||
== Rsync
|
|
||||||
Split SSH into:
|
|
||||||
|
|
||||||
connect: user@site.com
|
|
||||||
ssh_options: -p 3022
|
|
||||||
|
|
||||||
Then make an rsync section:
|
|
||||||
|
|
||||||
rsync:
|
|
||||||
source: /path/to/code/locally
|
|
||||||
exclude:
|
|
||||||
- tmp
|
|
||||||
- log
|
|
||||||
- config/test/database.yml
|
|
||||||
- db/*.sqlite3
|
|
||||||
etc...
|
|
||||||
|
|
||||||
options: -az
|
|
||||||
|
|
||||||
== Setup
|
== Setup
|
||||||
|
|
||||||
Provide a hydra:setup task to override to be run remotely before testing
|
Provide a hydra:setup task to override to be run remotely before testing
|
||||||
|
|
|
@ -47,6 +47,7 @@ Gem::Specification.new do |s|
|
||||||
"test/fixtures/config.yml",
|
"test/fixtures/config.yml",
|
||||||
"test/fixtures/hello_world.rb",
|
"test/fixtures/hello_world.rb",
|
||||||
"test/fixtures/slow.rb",
|
"test/fixtures/slow.rb",
|
||||||
|
"test/fixtures/sync_test.rb",
|
||||||
"test/fixtures/write_file.rb",
|
"test/fixtures/write_file.rb",
|
||||||
"test/master_test.rb",
|
"test/master_test.rb",
|
||||||
"test/message_test.rb",
|
"test/message_test.rb",
|
||||||
|
@ -67,6 +68,7 @@ Gem::Specification.new do |s|
|
||||||
"test/ssh_test.rb",
|
"test/ssh_test.rb",
|
||||||
"test/fixtures/write_file.rb",
|
"test/fixtures/write_file.rb",
|
||||||
"test/fixtures/slow.rb",
|
"test/fixtures/slow.rb",
|
||||||
|
"test/fixtures/sync_test.rb",
|
||||||
"test/fixtures/assert_true.rb",
|
"test/fixtures/assert_true.rb",
|
||||||
"test/fixtures/hello_world.rb",
|
"test/fixtures/hello_world.rb",
|
||||||
"test/master_test.rb",
|
"test/master_test.rb",
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
require 'hydra/hash'
|
require 'hydra/hash'
|
||||||
|
require 'open3'
|
||||||
module Hydra #:nodoc:
|
module Hydra #:nodoc:
|
||||||
# Hydra class responsible for delegate work down to workers.
|
# Hydra class responsible for delegate work down to workers.
|
||||||
#
|
#
|
||||||
# The Master is run once for any given testing session.
|
# The Master is run once for any given testing session.
|
||||||
class Master
|
class Master
|
||||||
include Hydra::Messages::Master
|
include Hydra::Messages::Master
|
||||||
|
include Open3
|
||||||
traceable('MASTER')
|
traceable('MASTER')
|
||||||
# Create a new Master
|
# Create a new Master
|
||||||
#
|
#
|
||||||
|
@ -22,12 +24,15 @@ module Hydra #:nodoc:
|
||||||
if config_file
|
if config_file
|
||||||
opts.merge!(YAML.load_file(config_file).stringify_keys!)
|
opts.merge!(YAML.load_file(config_file).stringify_keys!)
|
||||||
end
|
end
|
||||||
@files = opts.fetch('files') { [] }
|
@files = Array(opts.fetch('files') { nil })
|
||||||
|
raise "No files, nothing to do" if @files.empty?
|
||||||
@files.sort!{|a,b| File.size(b) <=> File.size(a)} # dumb heuristic
|
@files.sort!{|a,b| File.size(b) <=> File.size(a)} # dumb heuristic
|
||||||
@incomplete_files = @files.dup
|
@incomplete_files = @files.dup
|
||||||
@workers = []
|
@workers = []
|
||||||
@listeners = []
|
@listeners = []
|
||||||
@verbose = opts.fetch('verbose') { false }
|
@verbose = opts.fetch('verbose') { false }
|
||||||
|
@sync = opts.fetch('sync') { nil }
|
||||||
|
|
||||||
# default is one worker that is configured to use a pipe with one runner
|
# default is one worker that is configured to use a pipe with one runner
|
||||||
worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
|
worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
|
||||||
|
|
||||||
|
@ -42,10 +47,10 @@ module Hydra #:nodoc:
|
||||||
|
|
||||||
# Message handling
|
# Message handling
|
||||||
|
|
||||||
# Send a file down to a worker. If there are no more files, this will shut the
|
# Send a file down to a worker.
|
||||||
# worker down.
|
|
||||||
def send_file(worker)
|
def send_file(worker)
|
||||||
f = @files.pop
|
f = @files.pop
|
||||||
|
trace "Sending #{f.inspect}"
|
||||||
worker[:io].write(RunFile.new(:file => f)) if f
|
worker[:io].write(RunFile.new(:file => f)) if f
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -101,7 +106,27 @@ module Hydra #:nodoc:
|
||||||
"ruby -e \"require 'rubygems'; require 'hydra'; Hydra::Worker.new(:io => Hydra::Stdio.new, :runners => #{runners}, :verbose => #{@verbose});\""
|
"ruby -e \"require 'rubygems'; require 'hydra'; Hydra::Worker.new(:io => Hydra::Stdio.new, :runners => #{runners}, :verbose => #{@verbose});\""
|
||||||
}
|
}
|
||||||
|
|
||||||
trace "Synchronizing with #{connect} [NOT REALLY]"
|
if @sync
|
||||||
|
@sync.stringify_keys!
|
||||||
|
trace "Synchronizing with #{connect}\n\t#{@sync.inspect}"
|
||||||
|
local_dir = @sync.fetch('directory') {
|
||||||
|
raise "You must specify a synchronization directory"
|
||||||
|
}
|
||||||
|
exclude_paths = @sync.fetch('exclude') { [] }
|
||||||
|
exclude_opts = exclude_paths.inject(''){|memo, path| memo += "--exclude=#{path} "}
|
||||||
|
|
||||||
|
rsync_command = [
|
||||||
|
'rsync',
|
||||||
|
'-avz',
|
||||||
|
'--delete',
|
||||||
|
exclude_opts,
|
||||||
|
File.expand_path(local_dir)+'/',
|
||||||
|
"-e \"ssh #{ssh_opts}\"",
|
||||||
|
"#{connect}:#{directory}"
|
||||||
|
].join(" ")
|
||||||
|
trace rsync_command
|
||||||
|
trace `#{rsync_command}`
|
||||||
|
end
|
||||||
|
|
||||||
trace "Booting SSH worker"
|
trace "Booting SSH worker"
|
||||||
ssh = Hydra::SSH.new("#{ssh_opts} #{connect}", directory, command)
|
ssh = Hydra::SSH.new("#{ssh_opts} #{connect}", directory, command)
|
||||||
|
|
|
@ -13,6 +13,7 @@ module Hydra #:nodoc:
|
||||||
return nil unless message
|
return nil unless message
|
||||||
return Message.build(eval(message.chomp))
|
return Message.build(eval(message.chomp))
|
||||||
rescue SyntaxError, NameError
|
rescue SyntaxError, NameError
|
||||||
|
# uncomment to help catch remote errors by seeing all traffic
|
||||||
#$stderr.write "Not a message: [#{message.inspect}]\n"
|
#$stderr.write "Not a message: [#{message.inspect}]\n"
|
||||||
return gets
|
return gets
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,7 +33,13 @@ module Hydra #:nodoc:
|
||||||
# Run a test file and report the results
|
# Run a test file and report the results
|
||||||
def run_file(file)
|
def run_file(file)
|
||||||
trace "Running file: #{file}"
|
trace "Running file: #{file}"
|
||||||
require file
|
begin
|
||||||
|
require file
|
||||||
|
rescue LoadError => ex
|
||||||
|
trace "#{file} does not exist [#{ex.to_s}]"
|
||||||
|
@io.write Results.new(:output => ex.to_s, :file => file)
|
||||||
|
return
|
||||||
|
end
|
||||||
output = []
|
output = []
|
||||||
@result = Test::Unit::TestResult.new
|
@result = Test::Unit::TestResult.new
|
||||||
@result.add_listener(Test::Unit::TestResult::FAULT) do |value|
|
@result.add_listener(Test::Unit::TestResult::FAULT) do |value|
|
||||||
|
|
|
@ -91,7 +91,6 @@ module Hydra #:nodoc:
|
||||||
@listeners.each{|l| l.join }
|
@listeners.each{|l| l.join }
|
||||||
@io.close
|
@io.close
|
||||||
trace "Done processing messages"
|
trace "Done processing messages"
|
||||||
exit(0) # avoids test summaries
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_messages_from_master
|
def process_messages_from_master
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
require 'test/unit'
|
||||||
|
|
||||||
|
class SyncTest < Test::Unit::TestCase
|
||||||
|
def test_truth
|
||||||
|
assert true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -57,7 +57,6 @@ class MasterTest < Test::Unit::TestCase
|
||||||
assert (finish-start) < 15, "took #{finish-start} seconds"
|
assert (finish-start) < 15, "took #{finish-start} seconds"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
should "run a test via ssh" do
|
should "run a test via ssh" do
|
||||||
Hydra::Master.new(
|
Hydra::Master.new(
|
||||||
:files => [test_file],
|
:files => [test_file],
|
||||||
|
@ -80,5 +79,57 @@ class MasterTest < Test::Unit::TestCase
|
||||||
assert File.exists?(target_file)
|
assert File.exists?(target_file)
|
||||||
assert_equal "HYDRA", File.read(target_file)
|
assert_equal "HYDRA", File.read(target_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
should "synchronize a test file over ssh with rsync" do
|
||||||
|
local = File.join(Dir.tmpdir, 'hydra', 'local')
|
||||||
|
remote = File.join(Dir.tmpdir, 'hydra', 'remote')
|
||||||
|
sync_test = File.join(File.dirname(__FILE__), 'fixtures', 'sync_test.rb')
|
||||||
|
[local, remote].each{|f| FileUtils.rm_rf f; FileUtils.mkdir_p f}
|
||||||
|
|
||||||
|
# setup the folders:
|
||||||
|
# local:
|
||||||
|
# - test_a
|
||||||
|
# - test_c
|
||||||
|
# remote:
|
||||||
|
# - test_b
|
||||||
|
#
|
||||||
|
# add test_c to exludes
|
||||||
|
FileUtils.cp(sync_test, File.join(local, 'test_a.rb'))
|
||||||
|
FileUtils.cp(sync_test, File.join(local, 'test_c.rb'))
|
||||||
|
FileUtils.cp(sync_test, File.join(remote, 'test_b.rb'))
|
||||||
|
|
||||||
|
# ensure a is not on remote
|
||||||
|
assert !File.exists?(File.join(remote, 'test_a.rb')), "A should not be on remote"
|
||||||
|
# ensure c is not on remote
|
||||||
|
assert !File.exists?(File.join(remote, 'test_c.rb')), "C should not be on remote"
|
||||||
|
# ensure b is on remote
|
||||||
|
assert File.exists?(File.join(remote, 'test_b.rb')), "B should be on remote"
|
||||||
|
|
||||||
|
# fake as if the test got run, so only the sync code is really being tested
|
||||||
|
fake_result = Hydra::Messages::Worker::Results.new(
|
||||||
|
:file => 'test_a.rb', :output => '.'
|
||||||
|
).serialize.inspect
|
||||||
|
|
||||||
|
Hydra::Master.new(
|
||||||
|
:files => ['test_a.rb'],
|
||||||
|
:workers => [{
|
||||||
|
:type => :ssh,
|
||||||
|
:connect => 'localhost',
|
||||||
|
:directory => remote,
|
||||||
|
:runners => 1,
|
||||||
|
:command => "ruby -e 'puts #{fake_result}' && exit"
|
||||||
|
}],
|
||||||
|
:sync => {
|
||||||
|
:directory => local,
|
||||||
|
:exclude => ['test_c.rb']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# ensure a is copied
|
||||||
|
assert File.exists?(File.join(remote, 'test_a.rb')), "A was not copied"
|
||||||
|
# ensure c is not copied
|
||||||
|
assert !File.exists?(File.join(remote, 'test_c.rb')), "C was copied, should be excluded"
|
||||||
|
# ensure b is deleted
|
||||||
|
assert !File.exists?(File.join(remote, 'test_b.rb')), "B was not deleted"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue