nulogy-hydra 0.23.2.1

Sign up to get free protection for your applications and to get access to all the features.
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