hydra 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION +1 -1
- data/caliper.yml +6 -0
- data/hydra.gemspec +6 -2
- data/lib/hydra.rb +1 -0
- data/lib/hydra/hash.rb +16 -0
- data/lib/hydra/master.rb +66 -34
- data/lib/hydra/message/worker_messages.rb +1 -2
- data/lib/hydra/runner.rb +10 -11
- data/lib/hydra/trace.rb +24 -0
- data/lib/hydra/worker.rb +15 -13
- data/test/fixtures/config.yml +4 -0
- data/test/fixtures/slow.rb +1 -1
- data/test/master_test.rb +14 -7
- metadata +6 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.7.0
|
data/caliper.yml
ADDED
data/hydra.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{hydra}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.7.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Nick Gauthier"]
|
12
|
-
s.date = %q{2010-02-
|
12
|
+
s.date = %q{2010-02-04}
|
13
13
|
s.description = %q{Spread your tests over multiple machines to test your code faster.}
|
14
14
|
s.email = %q{nick@smartlogicsolutions.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -25,8 +25,10 @@ Gem::Specification.new do |s|
|
|
25
25
|
"Rakefile",
|
26
26
|
"TODO",
|
27
27
|
"VERSION",
|
28
|
+
"caliper.yml",
|
28
29
|
"hydra.gemspec",
|
29
30
|
"lib/hydra.rb",
|
31
|
+
"lib/hydra/hash.rb",
|
30
32
|
"lib/hydra/master.rb",
|
31
33
|
"lib/hydra/message.rb",
|
32
34
|
"lib/hydra/message/master_messages.rb",
|
@@ -37,8 +39,10 @@ Gem::Specification.new do |s|
|
|
37
39
|
"lib/hydra/runner.rb",
|
38
40
|
"lib/hydra/ssh.rb",
|
39
41
|
"lib/hydra/stdio.rb",
|
42
|
+
"lib/hydra/trace.rb",
|
40
43
|
"lib/hydra/worker.rb",
|
41
44
|
"test/fixtures/assert_true.rb",
|
45
|
+
"test/fixtures/config.yml",
|
42
46
|
"test/fixtures/echo_the_dolphin.rb",
|
43
47
|
"test/fixtures/slow.rb",
|
44
48
|
"test/fixtures/write_file.rb",
|
data/lib/hydra.rb
CHANGED
data/lib/hydra/hash.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Hash
|
2
|
+
# Stringify the keys in the hash. Returns a new hash.
|
3
|
+
def stringify_keys
|
4
|
+
inject({}) do |options, (key, value)|
|
5
|
+
options[key.to_s] = value
|
6
|
+
options
|
7
|
+
end
|
8
|
+
end
|
9
|
+
# Stringify the keys in the hash in place.
|
10
|
+
def stringify_keys!
|
11
|
+
keys.each do |key|
|
12
|
+
self[key.to_s] = delete(key)
|
13
|
+
end
|
14
|
+
self
|
15
|
+
end
|
16
|
+
end
|
data/lib/hydra/master.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
require 'hydra/hash'
|
1
2
|
module Hydra #:nodoc:
|
2
3
|
# Hydra class responsible for delegate work down to workers.
|
3
4
|
#
|
4
5
|
# The Master is run once for any given testing session.
|
5
6
|
class Master
|
6
7
|
include Hydra::Messages::Master
|
8
|
+
traceable('MASTER')
|
7
9
|
# Create a new Master
|
8
10
|
#
|
9
11
|
# Options:
|
@@ -15,16 +17,23 @@ module Hydra #:nodoc:
|
|
15
17
|
# * An array of hashes. Each hash should be the configuration options
|
16
18
|
# for a worker.
|
17
19
|
def initialize(opts = { })
|
18
|
-
|
20
|
+
opts.stringify_keys!
|
21
|
+
config_file = opts.delete('config') { nil }
|
22
|
+
if config_file
|
23
|
+
opts.merge!(YAML.load_file(config_file).stringify_keys!)
|
24
|
+
end
|
25
|
+
@files = opts.fetch('files') { [] }
|
26
|
+
@incomplete_files = @files.dup
|
19
27
|
@workers = []
|
20
28
|
@listeners = []
|
21
|
-
@verbose = opts.fetch(
|
29
|
+
@verbose = opts.fetch('verbose') { false }
|
22
30
|
# default is one worker that is configured to use a pipe with one runner
|
23
|
-
worker_cfg = opts.fetch(
|
24
|
-
[ { :type => :local, :runners => 1} ]
|
25
|
-
}
|
31
|
+
worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
|
26
32
|
|
27
|
-
|
33
|
+
trace "Initialized"
|
34
|
+
trace " Files: (#{@files.inspect})"
|
35
|
+
trace " Workers: (#{worker_cfg.inspect})"
|
36
|
+
trace " Verbose: (#{@verbose.inspect})"
|
28
37
|
|
29
38
|
boot_workers worker_cfg
|
30
39
|
process_messages
|
@@ -36,70 +45,93 @@ module Hydra #:nodoc:
|
|
36
45
|
# worker down.
|
37
46
|
def send_file(worker)
|
38
47
|
f = @files.pop
|
39
|
-
if f
|
40
|
-
|
48
|
+
worker[:io].write(RunFile.new(:file => f)) if f
|
49
|
+
end
|
50
|
+
|
51
|
+
# Process the results coming back from the worker.
|
52
|
+
def process_results(worker, message)
|
53
|
+
$stdout.write message.output
|
54
|
+
# only delete one
|
55
|
+
@incomplete_files.delete_at(@incomplete_files.index(message.file))
|
56
|
+
trace "#{@incomplete_files.size} Files Remaining"
|
57
|
+
if @incomplete_files.empty?
|
58
|
+
shutdown_all_workers
|
41
59
|
else
|
42
|
-
worker
|
43
|
-
Thread.exit
|
60
|
+
send_file(worker)
|
44
61
|
end
|
45
62
|
end
|
46
63
|
|
47
64
|
private
|
48
65
|
|
49
66
|
def boot_workers(workers)
|
50
|
-
|
51
|
-
workers.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
67
|
+
trace "Booting #{workers.size} workers"
|
68
|
+
workers.each do |worker|
|
69
|
+
worker.stringify_keys!
|
70
|
+
trace "worker opts #{worker.inspect}"
|
71
|
+
type = worker.fetch('type') { 'local' }
|
72
|
+
if type.to_s == 'local'
|
73
|
+
boot_local_worker(worker)
|
74
|
+
elsif type.to_s == 'ssh'
|
75
|
+
@workers << worker # will boot later, during the listening phase
|
76
|
+
else
|
77
|
+
raise "Worker type not recognized: (#{type.to_s})"
|
78
|
+
end
|
56
79
|
end
|
57
80
|
end
|
58
81
|
|
59
82
|
def boot_local_worker(worker)
|
60
|
-
runners = worker.fetch(
|
61
|
-
|
83
|
+
runners = worker.fetch('runners') { raise "You must specify the number of runners" }
|
84
|
+
trace "Booting local worker"
|
62
85
|
pipe = Hydra::Pipe.new
|
63
86
|
child = Process.fork do
|
64
87
|
pipe.identify_as_child
|
65
|
-
Hydra::Worker.new(:io => pipe, :runners => runners)
|
88
|
+
Hydra::Worker.new(:io => pipe, :runners => runners, :verbose => @verbose)
|
66
89
|
end
|
67
90
|
pipe.identify_as_parent
|
68
91
|
@workers << { :pid => child, :io => pipe, :idle => false, :type => :local }
|
69
92
|
end
|
70
93
|
|
71
94
|
def boot_ssh_worker(worker)
|
72
|
-
runners = worker.fetch(
|
73
|
-
connect = worker.fetch(
|
74
|
-
directory = worker.fetch(
|
75
|
-
command = worker.fetch(
|
95
|
+
runners = worker.fetch('runners') { raise "You must specify the number of runners" }
|
96
|
+
connect = worker.fetch('connect') { raise "You must specify SSH connection options" }
|
97
|
+
directory = worker.fetch('directory') { raise "You must specify a remote directory" }
|
98
|
+
command = worker.fetch('command') {
|
76
99
|
"ruby -e \"require 'rubygems'; require 'hydra'; Hydra::Worker.new(:io => Hydra::Stdio.new, :runners => #{runners}, :verbose => #{@verbose});\""
|
77
100
|
}
|
78
101
|
|
79
|
-
|
102
|
+
trace "Booting SSH worker"
|
80
103
|
ssh = Hydra::SSH.new(connect, directory, command)
|
81
104
|
return { :io => ssh, :idle => false, :type => :ssh }
|
82
105
|
end
|
83
106
|
|
107
|
+
def shutdown_all_workers
|
108
|
+
trace "Shutting down all workers"
|
109
|
+
@workers.each do |worker|
|
110
|
+
worker[:io].write(Shutdown.new) if worker[:io]
|
111
|
+
worker[:io].close if worker[:io]
|
112
|
+
end
|
113
|
+
@listeners.each{|t| t.exit}
|
114
|
+
end
|
115
|
+
|
84
116
|
def process_messages
|
85
117
|
Thread.abort_on_exception = true
|
86
118
|
|
87
|
-
|
88
|
-
|
119
|
+
trace "Processing Messages"
|
120
|
+
trace "Workers: #{@workers.inspect}"
|
89
121
|
@workers.each do |worker|
|
90
122
|
@listeners << Thread.new do
|
91
|
-
|
92
|
-
|
123
|
+
trace "Listening to #{worker.inspect}"
|
124
|
+
if worker.fetch('type') { 'local' }.to_s == 'ssh'
|
125
|
+
worker = boot_ssh_worker(worker)
|
126
|
+
@workers << worker
|
127
|
+
end
|
93
128
|
while true
|
94
129
|
begin
|
95
|
-
$stdout.write "MASTER| listen....\n" if @verbose
|
96
130
|
message = worker[:io].gets
|
97
|
-
|
131
|
+
trace "got message: #{message}"
|
98
132
|
message.handle(self, worker) if message
|
99
|
-
rescue IOError
|
100
|
-
|
101
|
-
worker[:io].close
|
102
|
-
@workers.delete(worker)
|
133
|
+
rescue IOError
|
134
|
+
trace "lost Worker [#{worker.inspect}]"
|
103
135
|
Thread.exit
|
104
136
|
end
|
105
137
|
end
|
@@ -30,8 +30,7 @@ module Hydra #:nodoc:
|
|
30
30
|
# Message relaying the results of a worker up to the master
|
31
31
|
class Results < Hydra::Messages::Runner::Results
|
32
32
|
def handle(master, worker) #:nodoc:
|
33
|
-
|
34
|
-
master.send_file(worker)
|
33
|
+
master.process_results(worker, self)
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
data/lib/hydra/runner.rb
CHANGED
@@ -10,6 +10,7 @@ module Hydra #:nodoc:
|
|
10
10
|
# of a machine.
|
11
11
|
class Runner
|
12
12
|
include Hydra::Messages::Runner
|
13
|
+
traceable('RUNNER')
|
13
14
|
# Boot up a runner. It takes an IO object (generally a pipe from its
|
14
15
|
# parent) to send it messages on which files to execute.
|
15
16
|
def initialize(opts = {})
|
@@ -18,20 +19,20 @@ module Hydra #:nodoc:
|
|
18
19
|
|
19
20
|
Test::Unit.run = true
|
20
21
|
$stdout.sync = true
|
21
|
-
|
22
|
+
trace 'Booted. Sending Request for file'
|
22
23
|
|
23
24
|
@io.write RequestFile.new
|
24
25
|
begin
|
25
26
|
process_messages
|
26
27
|
rescue => ex
|
27
|
-
|
28
|
+
trace ex.to_s
|
28
29
|
raise ex
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
33
|
# Run a test file and report the results
|
33
34
|
def run_file(file)
|
34
|
-
|
35
|
+
trace "Running file: #{file}"
|
35
36
|
require file
|
36
37
|
output = []
|
37
38
|
@result = Test::Unit::TestResult.new
|
@@ -60,20 +61,20 @@ module Hydra #:nodoc:
|
|
60
61
|
|
61
62
|
# The runner will continually read messages and handle them.
|
62
63
|
def process_messages
|
63
|
-
|
64
|
+
trace "Processing Messages"
|
64
65
|
@running = true
|
65
66
|
while @running
|
66
67
|
begin
|
67
68
|
message = @io.gets
|
68
69
|
if message
|
69
|
-
|
70
|
-
|
70
|
+
trace "Received message from worker"
|
71
|
+
trace "\t#{message.inspect}"
|
71
72
|
message.handle(self)
|
72
73
|
else
|
73
74
|
@io.write Ping.new
|
74
75
|
end
|
75
76
|
rescue IOError => ex
|
76
|
-
|
77
|
+
trace "Runner lost Worker"
|
77
78
|
@running = false
|
78
79
|
end
|
79
80
|
end
|
@@ -91,11 +92,9 @@ module Hydra #:nodoc:
|
|
91
92
|
eval(c.first)
|
92
93
|
end
|
93
94
|
rescue NameError
|
94
|
-
|
95
|
-
nil
|
95
|
+
trace "Could not load [#{c.first}] from [#{f}]"
|
96
96
|
rescue SyntaxError
|
97
|
-
|
98
|
-
nil
|
97
|
+
trace "Could not load [#{c.first}] from [#{f}]"
|
99
98
|
end
|
100
99
|
end
|
101
100
|
return klasses.select{|k| k.respond_to? 'suite'}
|
data/lib/hydra/trace.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hydra #:nodoc:
|
2
|
+
# Trace output when in verbose mode.
|
3
|
+
module Trace
|
4
|
+
module ClassMethods
|
5
|
+
# Make a class traceable. Takes one parameter,
|
6
|
+
# which is the prefix for the trace to identify this class
|
7
|
+
def traceable(prefix = self.class.to_s)
|
8
|
+
include Hydra::Trace::InstanceMethods
|
9
|
+
class << self; attr_accessor :_traceable_prefix; end
|
10
|
+
self._traceable_prefix = prefix
|
11
|
+
$stdout.sync = true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
# Trace some output with the class's prefix and a newline.
|
17
|
+
# Checks to ensure we're running verbosely.
|
18
|
+
def trace(str)
|
19
|
+
$stdout.write "#{self.class._traceable_prefix}| #{str}\n" if @verbose
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
Object.extend(Hydra::Trace::ClassMethods)
|
data/lib/hydra/worker.rb
CHANGED
@@ -8,6 +8,7 @@ module Hydra #:nodoc:
|
|
8
8
|
# network.
|
9
9
|
class Worker
|
10
10
|
include Hydra::Messages::Worker
|
11
|
+
traceable('WORKER')
|
11
12
|
# Create a new worker.
|
12
13
|
# * io: The IO object to use to communicate with the master
|
13
14
|
# * num_runners: The number of runners to launch
|
@@ -52,10 +53,10 @@ module Hydra #:nodoc:
|
|
52
53
|
# the worker to send shutdown messages to its runners.
|
53
54
|
def shutdown
|
54
55
|
@running = false
|
55
|
-
|
56
|
+
trace "Notifying #{@runners.size} Runners of Shutdown"
|
56
57
|
@runners.each do |r|
|
57
|
-
|
58
|
-
|
58
|
+
trace "Sending Shutdown to Runner"
|
59
|
+
trace "\t#{r.inspect}"
|
59
60
|
r[:io].write(Shutdown.new)
|
60
61
|
end
|
61
62
|
Thread.exit
|
@@ -64,7 +65,7 @@ module Hydra #:nodoc:
|
|
64
65
|
private
|
65
66
|
|
66
67
|
def boot_runners(num_runners) #:nodoc:
|
67
|
-
|
68
|
+
trace "Booting #{num_runners} Runners"
|
68
69
|
num_runners.times do
|
69
70
|
pipe = Hydra::Pipe.new
|
70
71
|
child = Process.fork do
|
@@ -74,12 +75,12 @@ module Hydra #:nodoc:
|
|
74
75
|
pipe.identify_as_parent
|
75
76
|
@runners << { :pid => child, :io => pipe, :idle => false }
|
76
77
|
end
|
77
|
-
|
78
|
+
trace "#{@runners.size} Runners booted"
|
78
79
|
end
|
79
80
|
|
80
81
|
# Continuously process messages
|
81
82
|
def process_messages #:nodoc:
|
82
|
-
|
83
|
+
trace "Processing Messages"
|
83
84
|
@running = true
|
84
85
|
|
85
86
|
Thread.abort_on_exception = true
|
@@ -89,7 +90,7 @@ module Hydra #:nodoc:
|
|
89
90
|
|
90
91
|
@listeners.each{|l| l.join }
|
91
92
|
@io.close
|
92
|
-
|
93
|
+
trace "Done processing messages"
|
93
94
|
end
|
94
95
|
|
95
96
|
def process_messages_from_master
|
@@ -98,14 +99,15 @@ module Hydra #:nodoc:
|
|
98
99
|
begin
|
99
100
|
message = @io.gets
|
100
101
|
if message
|
101
|
-
|
102
|
-
|
102
|
+
trace "Received Message from Master"
|
103
|
+
trace "\t#{message.inspect}"
|
103
104
|
message.handle(self)
|
104
105
|
else
|
106
|
+
trace "Nothing from Master, Pinging"
|
105
107
|
@io.write Ping.new
|
106
108
|
end
|
107
109
|
rescue IOError => ex
|
108
|
-
|
110
|
+
trace "Worker lost Master"
|
109
111
|
Thread.exit
|
110
112
|
end
|
111
113
|
end
|
@@ -119,12 +121,12 @@ module Hydra #:nodoc:
|
|
119
121
|
begin
|
120
122
|
message = r[:io].gets
|
121
123
|
if message
|
122
|
-
|
123
|
-
|
124
|
+
trace "Received Message from Runner"
|
125
|
+
trace "\t#{message.inspect}"
|
124
126
|
message.handle(self, r)
|
125
127
|
end
|
126
128
|
rescue IOError => ex
|
127
|
-
|
129
|
+
trace "Worker lost Runner [#{r.inspect}]"
|
128
130
|
Thread.exit
|
129
131
|
end
|
130
132
|
end
|
data/test/fixtures/slow.rb
CHANGED
data/test/master_test.rb
CHANGED
@@ -21,18 +21,16 @@ class MasterTest < Test::Unit::TestCase
|
|
21
21
|
should "run a test 6 times on 1 worker with 2 runners" do
|
22
22
|
Hydra::Master.new(
|
23
23
|
:files => [test_file]*6,
|
24
|
-
:local =>
|
25
|
-
:runners => 2
|
26
|
-
}
|
24
|
+
:workers => [ { :type => :local, :runners => 2 } ]
|
27
25
|
)
|
28
26
|
assert File.exists?(target_file)
|
29
27
|
assert_equal "HYDRA"*6, File.read(target_file)
|
30
28
|
end
|
31
29
|
|
32
|
-
# The test being run sleeps for
|
33
|
-
# series, it would take at least
|
30
|
+
# The test being run sleeps for 5 seconds. So, if this was run in
|
31
|
+
# series, it would take at least 50 seconds. This test ensures that
|
34
32
|
# in runs in less than that amount of time. Since there are 10
|
35
|
-
# runners to run the file 10 times, it should only take
|
33
|
+
# runners to run the file 10 times, it should only take 5-10 seconds
|
36
34
|
# based on overhead.
|
37
35
|
should "run a slow test 10 times on 1 worker with 10 runners quickly" do
|
38
36
|
start = Time.now
|
@@ -43,7 +41,7 @@ class MasterTest < Test::Unit::TestCase
|
|
43
41
|
]
|
44
42
|
)
|
45
43
|
finish = Time.now
|
46
|
-
assert (finish-start) <
|
44
|
+
assert (finish-start) < 30, "took #{finish-start} seconds"
|
47
45
|
end
|
48
46
|
|
49
47
|
should "run a slow test 10 times on 2 workers with 5 runners each quickly" do
|
@@ -73,5 +71,14 @@ class MasterTest < Test::Unit::TestCase
|
|
73
71
|
assert File.exists?(target_file)
|
74
72
|
assert_equal "HYDRA", File.read(target_file)
|
75
73
|
end
|
74
|
+
|
75
|
+
should "run a test with config from a yaml file" do
|
76
|
+
Hydra::Master.new(
|
77
|
+
:files => [test_file],
|
78
|
+
:config => File.join(File.dirname(__FILE__), 'fixtures', 'config.yml')
|
79
|
+
)
|
80
|
+
assert File.exists?(target_file)
|
81
|
+
assert_equal "HYDRA", File.read(target_file)
|
82
|
+
end
|
76
83
|
end
|
77
84
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hydra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Gauthier
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-02-
|
12
|
+
date: 2010-02-04 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -50,8 +50,10 @@ files:
|
|
50
50
|
- Rakefile
|
51
51
|
- TODO
|
52
52
|
- VERSION
|
53
|
+
- caliper.yml
|
53
54
|
- hydra.gemspec
|
54
55
|
- lib/hydra.rb
|
56
|
+
- lib/hydra/hash.rb
|
55
57
|
- lib/hydra/master.rb
|
56
58
|
- lib/hydra/message.rb
|
57
59
|
- lib/hydra/message/master_messages.rb
|
@@ -62,8 +64,10 @@ files:
|
|
62
64
|
- lib/hydra/runner.rb
|
63
65
|
- lib/hydra/ssh.rb
|
64
66
|
- lib/hydra/stdio.rb
|
67
|
+
- lib/hydra/trace.rb
|
65
68
|
- lib/hydra/worker.rb
|
66
69
|
- test/fixtures/assert_true.rb
|
70
|
+
- test/fixtures/config.yml
|
67
71
|
- test/fixtures/echo_the_dolphin.rb
|
68
72
|
- test/fixtures/slow.rb
|
69
73
|
- test/fixtures/write_file.rb
|