nulogy-hydra 0.23.2.1

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.
Files changed (75) hide show
  1. data/.document +5 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +39 -0
  4. data/Rakefile +56 -0
  5. data/TODO +18 -0
  6. data/VERSION +1 -0
  7. data/caliper.yml +6 -0
  8. data/hydra-icon-64x64.png +0 -0
  9. data/hydra.gemspec +131 -0
  10. data/hydra_gray.png +0 -0
  11. data/lib/hydra/cucumber/formatter.rb +29 -0
  12. data/lib/hydra/hash.rb +16 -0
  13. data/lib/hydra/js/lint.js +5150 -0
  14. data/lib/hydra/listener/abstract.rb +39 -0
  15. data/lib/hydra/listener/minimal_output.rb +24 -0
  16. data/lib/hydra/listener/notifier.rb +17 -0
  17. data/lib/hydra/listener/progress_bar.rb +48 -0
  18. data/lib/hydra/listener/report_generator.rb +31 -0
  19. data/lib/hydra/master.rb +252 -0
  20. data/lib/hydra/message/master_messages.rb +19 -0
  21. data/lib/hydra/message/runner_messages.rb +46 -0
  22. data/lib/hydra/message/worker_messages.rb +52 -0
  23. data/lib/hydra/message.rb +47 -0
  24. data/lib/hydra/messaging_io.rb +49 -0
  25. data/lib/hydra/pipe.rb +61 -0
  26. data/lib/hydra/proxy_config.rb +27 -0
  27. data/lib/hydra/runner.rb +306 -0
  28. data/lib/hydra/runner_listener/abstract.rb +23 -0
  29. data/lib/hydra/safe_fork.rb +31 -0
  30. data/lib/hydra/spec/autorun_override.rb +3 -0
  31. data/lib/hydra/spec/hydra_formatter.rb +26 -0
  32. data/lib/hydra/ssh.rb +41 -0
  33. data/lib/hydra/stdio.rb +16 -0
  34. data/lib/hydra/sync.rb +99 -0
  35. data/lib/hydra/tasks.rb +366 -0
  36. data/lib/hydra/threadsafe_io.rb +18 -0
  37. data/lib/hydra/tmpdir.rb +11 -0
  38. data/lib/hydra/trace.rb +29 -0
  39. data/lib/hydra/worker.rb +168 -0
  40. data/lib/hydra.rb +16 -0
  41. data/nulogy-hydra.gemspec +122 -0
  42. data/test/fixtures/assert_true.rb +7 -0
  43. data/test/fixtures/bad_proxy_config.yml +4 -0
  44. data/test/fixtures/config.yml +4 -0
  45. data/test/fixtures/conflicting.rb +10 -0
  46. data/test/fixtures/features/step_definitions.rb +21 -0
  47. data/test/fixtures/features/write_alternate_file.feature +7 -0
  48. data/test/fixtures/features/write_file.feature +7 -0
  49. data/test/fixtures/hello_world.rb +3 -0
  50. data/test/fixtures/hydra_worker_init.rb +2 -0
  51. data/test/fixtures/js_file.js +4 -0
  52. data/test/fixtures/json_data.json +4 -0
  53. data/test/fixtures/many_outputs_to_console.rb +9 -0
  54. data/test/fixtures/master_listeners.rb +10 -0
  55. data/test/fixtures/proxy_config.yml +4 -0
  56. data/test/fixtures/proxy_config_http.yml +4 -0
  57. data/test/fixtures/runner_listeners.rb +23 -0
  58. data/test/fixtures/slow.rb +9 -0
  59. data/test/fixtures/sync_test.rb +8 -0
  60. data/test/fixtures/task_test_config.yml +6 -0
  61. data/test/fixtures/write_file.rb +10 -0
  62. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  63. data/test/fixtures/write_file_spec.rb +9 -0
  64. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  65. data/test/master_test.rb +383 -0
  66. data/test/message_test.rb +31 -0
  67. data/test/pipe_test.rb +38 -0
  68. data/test/proxy_config_test.rb +31 -0
  69. data/test/runner_test.rb +196 -0
  70. data/test/ssh_test.rb +25 -0
  71. data/test/sync_test.rb +113 -0
  72. data/test/task_test.rb +21 -0
  73. data/test/test_helper.rb +107 -0
  74. data/test/worker_test.rb +60 -0
  75. metadata +208 -0
@@ -0,0 +1,39 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Abstract listener that implements all the events
4
+ # but does nothing.
5
+ class Abstract
6
+ # Create a new listener.
7
+ #
8
+ # Output: The IO object for outputting any information.
9
+ # Defaults to STDOUT, but you could pass a file in, or STDERR
10
+ def initialize(output = $stdout)
11
+ @output = output
12
+ end
13
+
14
+ # Fired when testing has started
15
+ def testing_begin(files)
16
+ end
17
+
18
+ # Fired when testing finishes, after the workers shutdown
19
+ def testing_end
20
+ end
21
+
22
+ # Fired after runner processes have been started
23
+ def worker_begin(worker)
24
+ end
25
+
26
+ # Fired before shutting down the worker
27
+ def worker_end(worker)
28
+ end
29
+
30
+ # Fired when a file is started
31
+ def file_begin(file)
32
+ end
33
+
34
+ # Fired when a file is finished
35
+ def file_end(file, output)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Minimal output listener. Outputs all the files at the start
4
+ # of testing and outputs a ./F/E per file. As well as
5
+ # full error output, if any.
6
+ class MinimalOutput < Hydra::Listener::Abstract
7
+ # output a starting message
8
+ def testing_begin(files)
9
+ @output.write "Hydra Testing:\n#{files.inspect}\n"
10
+ end
11
+
12
+ # output a finished message
13
+ def testing_end
14
+ @output.write "\nHydra Completed\n"
15
+ end
16
+
17
+ # For each file, just output a . for a successful file, or the
18
+ # Failure/Error output from the tests
19
+ def file_end(file, output)
20
+ @output.write output
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Sends a command to Notifier when the testing has finished
4
+ # http://manpages.ubuntu.com/manpages/gutsy/man1/notify-send.1.html
5
+ class Notifier < Hydra::Listener::Abstract
6
+ # output a finished notification
7
+ def testing_end
8
+ icon_path = File.join(
9
+ File.dirname(__FILE__), '..', '..', '..',
10
+ 'hydra-icon-64x64.png'
11
+ )
12
+ `notify-send -i #{icon_path} "Hydra" "Testing Completed"`
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,48 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Output a progress bar as files are completed
4
+ class ProgressBar < Hydra::Listener::Abstract
5
+ # Store the total number of files
6
+ def testing_begin(files)
7
+ @total_files = files.size
8
+ @files_completed = 0
9
+ @test_output = ""
10
+ @errors = false
11
+ render_progress_bar
12
+ end
13
+
14
+ # Increment completed files count and update bar
15
+ def file_end(file, output)
16
+ unless output == '.'
17
+ @output.write "\r#{' '*60}\r#{output}\n"
18
+ @errors = true
19
+ end
20
+ @files_completed += 1
21
+ render_progress_bar
22
+ end
23
+
24
+ # Break the line
25
+ def testing_end
26
+ render_progress_bar
27
+ @output.write "\n"
28
+ end
29
+
30
+ private
31
+
32
+ def render_progress_bar
33
+ width = 30
34
+ complete = ((@files_completed.to_f / @total_files.to_f) * width).to_i
35
+ @output.write "\r" # move to beginning
36
+ @output.write 'Hydra Testing ['
37
+ @output.write @errors ? "\033[0;31m" : "\033[0;32m"
38
+ complete.times{@output.write '#'}
39
+ @output.write '>'
40
+ (width-complete).times{@output.write ' '}
41
+ @output.write "\033[0m"
42
+ @output.write "] #{@files_completed}/#{@total_files}"
43
+ @output.flush
44
+ end
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,31 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Output a textual report at the end of testing
4
+ class ReportGenerator < Hydra::Listener::Abstract
5
+ # Initialize a new report
6
+ def testing_begin(files)
7
+ @report = { }
8
+ end
9
+
10
+ # Log the start time of a file
11
+ def file_begin(file)
12
+ @report[file] ||= { }
13
+ @report[file]['start'] = Time.now.to_f
14
+ end
15
+
16
+ # Log the end time of a file and compute the file's testing
17
+ # duration
18
+ def file_end(file, output)
19
+ @report[file]['end'] = Time.now.to_f
20
+ @report[file]['duration'] = @report[file]['end'] - @report[file]['start']
21
+ @report[file]['all_tests_passed_last_run'] = (output == '.')
22
+ end
23
+
24
+ # output the report
25
+ def testing_end
26
+ YAML.dump(@report, @output)
27
+ @output.close
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,252 @@
1
+ require 'hydra/hash'
2
+ require 'open3'
3
+ require 'hydra/tmpdir'
4
+ require 'hydra/proxy_config'
5
+ require 'erb'
6
+ require 'yaml'
7
+ require 'hydra/threadsafe_io'
8
+
9
+ module Hydra #:nodoc:
10
+ # Hydra class responsible for delegate work down to workers.
11
+ #
12
+ # The Master is run once for any given testing session.
13
+ class YmlLoadError < StandardError; end
14
+
15
+ class Master
16
+ include Hydra::Messages::Master
17
+ include Open3
18
+ traceable('MASTER')
19
+ attr_reader :failed_files
20
+
21
+ # Create a new Master
22
+ #
23
+ # Options:
24
+ # * :files
25
+ # * An array of test files to be run. These should be relative paths from
26
+ # the root of the project, since they may be run on different machines
27
+ # which may have different paths.
28
+ # * :workers
29
+ # * An array of hashes. Each hash should be the configuration options
30
+ # for a worker.
31
+ # * :listeners
32
+ # * An array of Hydra::Listener objects. See Hydra::Listener::MinimalOutput for an
33
+ # example listener
34
+ # * :verbose
35
+ # * Set to true to see lots of Hydra output (for debugging)
36
+ # * :autosort
37
+ # * Set to false to disable automatic sorting by historical run-time per file
38
+ def initialize(opts = { })
39
+
40
+ $stdout = ThreadsafeIO.new($stdout)
41
+
42
+ opts.stringify_keys!
43
+ config_file = opts.delete('config') { nil }
44
+ if config_file
45
+
46
+ begin
47
+ config_erb = ERB.new(IO.read(config_file)).result(binding)
48
+ rescue Exception => e
49
+ raise(YmlLoadError,"config file was found, but could not be parsed with ERB.\n#{$!.inspect}")
50
+ end
51
+
52
+ begin
53
+ config_yml = ProxyConfig.load(config_erb)
54
+ rescue StandardError => e
55
+ raise(YmlLoadError,"config file was found, but could not be parsed.\n#{$!.inspect}")
56
+ end
57
+
58
+ opts.merge!(config_yml.stringify_keys!)
59
+ end
60
+ @files = Array(opts.fetch('files') { nil })
61
+ raise "No files, nothing to do" if @files.empty?
62
+ @incomplete_files = @files.dup
63
+ @failed_files = []
64
+ @workers = []
65
+ @listeners = []
66
+ @event_listeners = Array(opts.fetch('listeners') { nil } )
67
+ @event_listeners.select{|l| l.is_a? String}.each do |l|
68
+ @event_listeners.delete_at(@event_listeners.index(l))
69
+ listener = eval(l)
70
+ @event_listeners << listener if listener.is_a?(Hydra::Listener::Abstract)
71
+ end
72
+
73
+ @string_runner_event_listeners = Array( opts.fetch( 'runner_listeners' ) { nil } )
74
+
75
+ @runner_log_file = opts.fetch('runner_log_file') { nil }
76
+ @verbose = opts.fetch('verbose') { false }
77
+ @autosort = opts.fetch('autosort') { true }
78
+ @sync = opts.fetch('sync') { nil }
79
+ @environment = opts.fetch('environment') { 'test' }
80
+
81
+ if @autosort
82
+ sort_files_from_report
83
+ @event_listeners << Hydra::Listener::ReportGenerator.new(File.new(heuristic_file, 'w'))
84
+ end
85
+
86
+ # default is one worker that is configured to use a pipe with one runner
87
+ worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
88
+
89
+ trace "Initialized"
90
+ trace " Files: (#{@files.inspect})"
91
+ trace " Workers: (#{worker_cfg.inspect})"
92
+ trace " Verbose: (#{@verbose.inspect})"
93
+
94
+ @event_listeners.each{|l| l.testing_begin(@files) }
95
+
96
+ boot_workers worker_cfg
97
+ process_messages
98
+ end
99
+
100
+ # Message handling
101
+ def worker_begin(worker)
102
+ @event_listeners.each {|l| l.worker_begin(worker) }
103
+ end
104
+
105
+ # Send a file down to a worker.
106
+ def send_file(worker)
107
+ f = @files.shift
108
+ if f
109
+ trace "Sending #{f.inspect}"
110
+ @event_listeners.each{|l| l.file_begin(f) }
111
+ worker[:io].write(RunFile.new(:file => f))
112
+ else
113
+ trace "No more files to send"
114
+ end
115
+ end
116
+
117
+ # Process the results coming back from the worker.
118
+ def process_results(worker, message)
119
+ if message.output =~ /ActiveRecord::StatementInvalid(.*)[Dd]eadlock/ or
120
+ message.output =~ /PGError: ERROR(.*)[Dd]eadlock/ or
121
+ message.output =~ /Mysql::Error: SAVEPOINT(.*)does not exist: ROLLBACK/ or
122
+ message.output =~ /Mysql::Error: Deadlock found/
123
+ trace "Deadlock detected running [#{message.file}]. Will retry at the end"
124
+ @files.push(message.file)
125
+ send_file(worker)
126
+ else
127
+ @incomplete_files.delete_at(@incomplete_files.index(message.file))
128
+ trace "#{@incomplete_files.size} Files Remaining"
129
+ @event_listeners.each{|l| l.file_end(message.file, message.output) }
130
+ unless message.output == '.'
131
+ @failed_files << message.file
132
+ end
133
+ if @incomplete_files.empty?
134
+ @workers.each do |worker|
135
+ @event_listeners.each{|l| l.worker_end(worker) }
136
+ end
137
+
138
+ shutdown_all_workers
139
+ else
140
+ send_file(worker)
141
+ end
142
+ end
143
+ end
144
+
145
+ # A text report of the time it took to run each file
146
+ attr_reader :report_text
147
+
148
+ private
149
+
150
+ def boot_workers(workers)
151
+ trace "Booting #{workers.size} workers"
152
+ workers.each do |worker|
153
+ worker.stringify_keys!
154
+ trace "worker opts #{worker.inspect}"
155
+ type = worker.fetch('type') { 'local' }
156
+ if type.to_s == 'local'
157
+ boot_local_worker(worker)
158
+ elsif type.to_s == 'ssh'
159
+ @workers << worker # will boot later, during the listening phase
160
+ else
161
+ raise "Worker type not recognized: (#{type.to_s})"
162
+ end
163
+ end
164
+ end
165
+
166
+ def boot_local_worker(worker)
167
+ runners = worker.fetch('runners') { raise "You must specify the number of runners" }
168
+ trace "Booting local worker"
169
+ pipe = Hydra::Pipe.new
170
+ child = SafeFork.fork do
171
+ pipe.identify_as_child
172
+ Hydra::Worker.new(:io => pipe, :runners => runners, :verbose => @verbose, :runner_listeners => @string_runner_event_listeners, :runner_log_file => @runner_log_file )
173
+ end
174
+
175
+ pipe.identify_as_parent
176
+ @workers << { :pid => child, :io => pipe, :idle => false, :type => :local }
177
+ end
178
+
179
+ def boot_ssh_worker(worker)
180
+ sync = Sync.new(worker, @sync, @verbose)
181
+
182
+ runners = worker.fetch('runners') { raise "You must specify the number of runners" }
183
+ command = worker.fetch('command') {
184
+ "RAILS_ENV=#{@environment} ruby -e \"require 'rubygems'; require 'hydra'; Hydra::Worker.new(:io => Hydra::Stdio.new, :runners => #{runners}, :verbose => #{@verbose}, :runner_listeners => \'#{@string_runner_event_listeners}\', :runner_log_file => \'#{@runner_log_file}\' );\""
185
+ }
186
+
187
+ trace "Booting SSH worker"
188
+ ssh = Hydra::SSH.new("#{sync.ssh_opts} #{sync.connect}", sync.remote_dir, command)
189
+ return { :io => ssh, :idle => false, :type => :ssh, :connect => sync.connect }
190
+ end
191
+
192
+ def shutdown_all_workers
193
+ trace "Shutting down all workers"
194
+ @workers.each do |worker|
195
+ worker[:io].write(Shutdown.new) if worker[:io]
196
+ worker[:io].close if worker[:io]
197
+ end
198
+ @listeners.each{|t| t.exit}
199
+ end
200
+
201
+ def process_messages
202
+ Thread.abort_on_exception = true
203
+
204
+ trace "Processing Messages"
205
+ trace "Workers: #{@workers.inspect}"
206
+ @workers.each do |worker|
207
+ @listeners << Thread.new do
208
+ trace "Listening to #{worker.inspect}"
209
+ if worker.fetch('type') { 'local' }.to_s == 'ssh'
210
+ worker = boot_ssh_worker(worker)
211
+ @workers << worker
212
+ end
213
+ while true
214
+ begin
215
+ message = worker[:io].gets
216
+ trace "got message: #{message}"
217
+ # if it exists and its for me.
218
+ # SSH gives us back echoes, so we need to ignore our own messages
219
+ if message and !message.class.to_s.index("Worker").nil?
220
+ message.handle(self, worker)
221
+ end
222
+ rescue IOError
223
+ trace "lost Worker [#{worker.inspect}]"
224
+ Thread.exit
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ @listeners.each{|l| l.join}
231
+ @event_listeners.each{|l| l.testing_end}
232
+ end
233
+
234
+ def sort_files_from_report
235
+ if File.exists? heuristic_file
236
+ report = YAML.load_file(heuristic_file)
237
+ return unless report
238
+ sorted_files = report.sort{ |a,b|
239
+ b[1]['duration'] <=> a[1]['duration']
240
+ }.collect{|tuple| tuple[0]}
241
+
242
+ sorted_files.each do |f|
243
+ @files.push(@files.delete_at(@files.index(f))) if @files.index(f)
244
+ end
245
+ end
246
+ end
247
+
248
+ def heuristic_file
249
+ @heuristic_file ||= File.join(Dir.consistent_tmpdir, 'hydra_heuristics.yml')
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,19 @@
1
+ module Hydra #:nodoc:
2
+ module Messages #:nodoc:
3
+ module Master #:nodoc:
4
+ # Message telling a worker to delegate a file to a runner
5
+ class RunFile < Hydra::Messages::Worker::RunFile
6
+ def handle(worker) #:nodoc:
7
+ worker.delegate_file(self)
8
+ end
9
+ end
10
+
11
+ # Message telling the worker to shut down.
12
+ class Shutdown < Hydra::Messages::Worker::Shutdown
13
+ def handle(worker) #:nodoc:
14
+ worker.shutdown
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,46 @@
1
+ module Hydra #:nodoc:
2
+ module Messages #:nodoc:
3
+ module Runner #:nodoc:
4
+ # Message indicating that a Runner needs a file to run
5
+ class RequestFile < Hydra::Message
6
+ def handle(worker, runner) #:nodoc:
7
+ worker.request_file(self, runner)
8
+ end
9
+ end
10
+
11
+ # Message for the Runner to respond with its results
12
+ class Results < Hydra::Message
13
+ # The output from running the test
14
+ attr_accessor :output
15
+ # The file that was run
16
+ attr_accessor :file
17
+ def serialize #:nodoc:
18
+ super(:output => @output, :file => @file)
19
+ end
20
+ def handle(worker, runner) #:nodoc:
21
+ worker.relay_results(self, runner)
22
+ end
23
+ end
24
+
25
+ # Message a runner sends to a worker to verify the connection
26
+ class Ping < Hydra::Message
27
+ def handle(worker, runner) #:nodoc:
28
+ # We don't do anything to handle a ping. It's just to test
29
+ # the connectivity of the IO
30
+ end
31
+ end
32
+
33
+ # The runner forks to run rspec messages
34
+ # so that specs don't get rerun. It uses
35
+ # this message to report the results. See
36
+ # Runner::run_rspec_file.
37
+ class RSpecResult < Hydra::Message
38
+ # the output of the spec
39
+ attr_accessor :output
40
+ def serialize #:nodoc:
41
+ super(:output => @output)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ module Hydra #:nodoc:
2
+ module Messages #:nodoc:
3
+ module Worker #:nodoc:
4
+ # Message indicating that a worker needs a file to delegate to a runner
5
+ class RequestFile < Hydra::Message
6
+ def handle(master, worker) #:nodoc:
7
+ master.send_file(worker)
8
+ end
9
+ end
10
+
11
+ class WorkerBegin < Hydra::Message
12
+ def handle(master, worker)
13
+ master.worker_begin(worker)
14
+ end
15
+ end
16
+
17
+ # Message telling the Runner to run a file
18
+ class RunFile < Hydra::Message
19
+ # The file that should be run
20
+ attr_accessor :file
21
+ def serialize #:nodoc:
22
+ super(:file => @file)
23
+ end
24
+ def handle(runner) #:nodoc:
25
+ runner.run_file(@file)
26
+ end
27
+ end
28
+
29
+ # Message to tell the Runner to shut down
30
+ class Shutdown < Hydra::Message
31
+ def handle(runner) #:nodoc:
32
+ runner.stop
33
+ end
34
+ end
35
+
36
+ # Message relaying the results of a worker up to the master
37
+ class Results < Hydra::Messages::Runner::Results
38
+ def handle(master, worker) #:nodoc:
39
+ master.process_results(worker, self)
40
+ end
41
+ end
42
+
43
+ # Message a worker sends to a master to verify the connection
44
+ class Ping < Hydra::Message
45
+ def handle(master, worker) #:nodoc:
46
+ # We don't do anything to handle a ping. It's just to test
47
+ # the connectivity of the IO
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,47 @@
1
+ module Hydra #:nodoc:
2
+ # Base message object. Used to pass messages with parameters around
3
+ # via IO objects.
4
+ # class MyMessage < Hydra::Message
5
+ # attr_accessor :my_var
6
+ # def serialize
7
+ # super(:my_var => @my_var)
8
+ # end
9
+ # end
10
+ # m = MyMessage.new(:my_var => 'my value')
11
+ # m.my_var
12
+ # => "my value"
13
+ # m.serialize
14
+ # => "{:class=>TestMessage::MyMessage, :my_var=>\"my value\"}"
15
+ # Hydra::Message.build(eval(@m.serialize)).my_var
16
+ # => "my value"
17
+ class Message
18
+ # Create a new message. Opts is a hash where the keys
19
+ # are attributes of the message and the values are
20
+ # set to the attribute.
21
+ def initialize(opts = {})
22
+ opts.delete :class
23
+ opts.each do |variable,value|
24
+ self.send("#{variable}=",value)
25
+ end
26
+ end
27
+
28
+ # Build a message from a hash. The hash must contain
29
+ # the :class symbol, which is the class of the message
30
+ # that it will build to.
31
+ def self.build(hash)
32
+ hash.delete(:class).new(hash)
33
+ end
34
+
35
+ # Serialize the message for output on an IO channel.
36
+ # This is really just a string representation of a hash
37
+ # with no newlines. It adds in the class automatically
38
+ def serialize(opts = {})
39
+ opts.merge({:class => self.class}).inspect
40
+ end
41
+ end
42
+ end
43
+
44
+ require 'hydra/message/runner_messages'
45
+ require 'hydra/message/worker_messages'
46
+ require 'hydra/message/master_messages'
47
+
@@ -0,0 +1,49 @@
1
+ module Hydra #:nodoc:
2
+ # Module that implemets methods that auto-serialize and deserialize messaging
3
+ # objects.
4
+ module MessagingIO
5
+ # Read a Message from the input IO object. Automatically build
6
+ # a message from the response and return it.
7
+ #
8
+ # IO.gets
9
+ # => Hydra::Message # or subclass
10
+ def gets
11
+ while true
12
+ begin
13
+ raise IOError unless @reader
14
+ message = @reader.gets
15
+ return nil unless message
16
+ return Message.build(eval(message.chomp))
17
+ rescue SyntaxError, NameError
18
+ # uncomment to help catch remote errors by seeing all traffic
19
+ #$stderr.write "Not a message: [#{message.inspect}]\n"
20
+ end
21
+ end
22
+ end
23
+
24
+ # Write a Message to the output IO object. It will automatically
25
+ # serialize a Message object.
26
+ # IO.write Hydra::Message.new
27
+ def write(message)
28
+ raise IOError unless @writer
29
+ raise UnprocessableMessage unless message.is_a?(Hydra::Message)
30
+ @writer.write(message.serialize+"\n")
31
+ rescue Errno::EPIPE
32
+ raise IOError
33
+ end
34
+
35
+ # Closes the IO object.
36
+ def close
37
+ @reader.close if @reader
38
+ @writer.close if @writer
39
+ end
40
+
41
+ # IO will return this error if it cannot process a message.
42
+ # For example, if you tried to write a string, it would fail,
43
+ # because the string is not a message.
44
+ class UnprocessableMessage < RuntimeError
45
+ # Custom error message
46
+ attr_accessor :message
47
+ end
48
+ end
49
+ end
data/lib/hydra/pipe.rb ADDED
@@ -0,0 +1,61 @@
1
+ require 'hydra/messaging_io'
2
+ module Hydra #:nodoc:
3
+ # Read and write between two processes via pipes. For example:
4
+ # @pipe = Hydra::Pipe.new
5
+ # @child = Process.fork do
6
+ # @pipe.identify_as_child
7
+ # puts "A message from my parent:\n#{@pipe.gets.text}"
8
+ # @pipe.close
9
+ # end
10
+ # @pipe.identify_as_parent
11
+ # @pipe.write Hydra::Messages::TestMessage.new(:text => "Hello!")
12
+ # @pipe.close
13
+ #
14
+ # Note that the TestMessage class is only available in tests, and
15
+ # not in Hydra by default.
16
+ #
17
+ #
18
+ # When the process forks, the pipe is copied. When a pipe is
19
+ # identified as a parent or child, it is choosing which ends
20
+ # of the pipe to use.
21
+ #
22
+ # A pipe is actually two pipes:
23
+ #
24
+ # Parent == Pipe 1 ==> Child
25
+ # Parent <== Pipe 2 == Child
26
+ #
27
+ # It's like if you had two cardboard tubes and you were using
28
+ # them to drop balls with messages in them between processes.
29
+ # One tube is for sending from parent to child, and the other
30
+ # tube is for sending from child to parent.
31
+ class Pipe
32
+ include Hydra::MessagingIO
33
+ # Creates a new uninitialized pipe pair.
34
+ def initialize
35
+ @child_read, @parent_write = IO.pipe
36
+ @parent_read, @child_write = IO.pipe
37
+ end
38
+
39
+ # Identify this side of the pipe as the child.
40
+ def identify_as_child
41
+ @parent_write.close
42
+ @parent_read.close
43
+ @reader = @child_read
44
+ @writer = @child_write
45
+ end
46
+
47
+ # Identify this side of the pipe as the parent
48
+ def identify_as_parent
49
+ @child_write.close
50
+ @child_read.close
51
+ @reader = @parent_read
52
+ @writer = @parent_write
53
+ end
54
+
55
+ # Output pipe nicely
56
+ def inspect
57
+ "#<#{self.class} @reader=#{@reader.to_s}, @writer=#{@writer.to_s}>"
58
+ end
59
+
60
+ end
61
+ end