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,27 @@
1
+ require 'yaml'
2
+ require 'net/http'
3
+
4
+ module Hydra
5
+ class UnknownProxyType < RuntimeError
6
+
7
+ end
8
+
9
+ class ProxyConfig
10
+ def self.load(config_yml)
11
+ config = YAML::load(config_yml)
12
+ if config.has_key?('proxy')
13
+ proxy_info = config['proxy']
14
+ #only file supported so far
15
+ if proxy_info['type'] == "file"
16
+ YAML::load_file(proxy_info['path'])
17
+ elsif proxy_info['type'] == "http"
18
+ YAML::load(Net::HTTP.get(URI.parse(proxy_info['path'])))
19
+ else
20
+ raise UnknownProxyType.new("Proxy config file does not know what to do with type '#{proxy_info["type"]}'.")
21
+ end
22
+ else
23
+ config
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,306 @@
1
+ require 'test/unit'
2
+ require 'test/unit/testresult'
3
+ Test::Unit.run = true
4
+
5
+ module Hydra #:nodoc:
6
+ # Hydra class responsible for running test files.
7
+ #
8
+ # The Runner is never run directly by a user. Runners are created by a
9
+ # Worker to run test files.
10
+ #
11
+ # The general convention is to have one Runner for each logical processor
12
+ # of a machine.
13
+ class Runner
14
+ include Hydra::Messages::Runner
15
+ traceable('RUNNER')
16
+
17
+ DEFAULT_LOG_FILE = 'hydra-runner.log'
18
+
19
+ # Boot up a runner. It takes an IO object (generally a pipe from its
20
+ # parent) to send it messages on which files to execute.
21
+ def initialize(opts = {})
22
+ redirect_output( opts.fetch( :runner_log_file ) { DEFAULT_LOG_FILE } )
23
+ reg_trap_sighup
24
+
25
+ @io = opts.fetch(:io) { raise "No IO Object" }
26
+ @verbose = opts.fetch(:verbose) { false }
27
+ @event_listeners = Array( opts.fetch( :runner_listeners ) { nil } )
28
+
29
+ $stdout.sync = true
30
+ runner_begin
31
+
32
+ trace 'Booted. Sending Request for file'
33
+ @io.write RequestFile.new
34
+ begin
35
+ process_messages
36
+ rescue => ex
37
+ trace ex.to_s
38
+ raise ex
39
+ end
40
+ end
41
+
42
+ def reg_trap_sighup
43
+ for sign in [:SIGHUP, :INT]
44
+ trap sign do
45
+ stop
46
+ end
47
+ end
48
+ @runner_began = true
49
+ end
50
+
51
+ def runner_begin
52
+ trace "Firing runner_begin event"
53
+ @event_listeners.each {|l| l.runner_begin( self ) }
54
+ end
55
+
56
+ # Run a test file and report the results
57
+ def run_file(file)
58
+ trace "Running file: #{file}"
59
+
60
+ output = ""
61
+ if file =~ /_spec.rb$/i
62
+ output = run_rspec_file(file)
63
+ elsif file =~ /.feature$/i
64
+ output = run_cucumber_file(file)
65
+ elsif file =~ /.js$/i or file =~ /.json$/i
66
+ output = run_javascript_file(file)
67
+ else
68
+ output = run_test_unit_file(file)
69
+ end
70
+
71
+ output = "." if output == ""
72
+
73
+ @io.write Results.new(:output => output, :file => file)
74
+ return output
75
+ end
76
+
77
+ # Stop running
78
+ def stop
79
+ runner_end if @runner_began
80
+ @runner_began = @running = false
81
+ end
82
+
83
+ def runner_end
84
+ trace "Ending runner #{self.inspect}"
85
+ @event_listeners.each {|l| l.runner_end( self ) }
86
+ end
87
+
88
+ def format_exception(ex)
89
+ "#{ex.class.name}: #{ex.message}\n #{ex.backtrace.join("\n ")}"
90
+ end
91
+
92
+ private
93
+
94
+ # The runner will continually read messages and handle them.
95
+ def process_messages
96
+ trace "Processing Messages"
97
+ @running = true
98
+ while @running
99
+ begin
100
+ message = @io.gets
101
+ if message and !message.class.to_s.index("Worker").nil?
102
+ trace "Received message from worker"
103
+ trace "\t#{message.inspect}"
104
+ message.handle(self)
105
+ else
106
+ @io.write Ping.new
107
+ end
108
+ rescue IOError => ex
109
+ trace "Runner lost Worker"
110
+ stop
111
+ end
112
+ end
113
+ end
114
+
115
+ def format_ex_in_file(file, ex)
116
+ "Error in #{file}:\n #{format_exception(ex)}"
117
+ end
118
+
119
+ # Run all the Test::Unit Suites in a ruby file
120
+ def run_test_unit_file(file)
121
+ begin
122
+ require file
123
+ rescue LoadError => ex
124
+ trace "#{file} does not exist [#{ex.to_s}]"
125
+ return ex.to_s
126
+ rescue Exception => ex
127
+ trace "Error requiring #{file} [#{ex.to_s}]"
128
+ return format_ex_in_file(file, ex)
129
+ end
130
+ output = []
131
+ @result = Test::Unit::TestResult.new
132
+ @result.add_listener(Test::Unit::TestResult::FAULT) do |value|
133
+ output << value
134
+ end
135
+
136
+ klasses = Runner.find_classes_in_file(file)
137
+ begin
138
+ klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
139
+ rescue => ex
140
+ output << format_ex_in_file(file, ex)
141
+ end
142
+
143
+ return output.join("\n")
144
+ end
145
+
146
+ # run all the Specs in an RSpec file (NOT IMPLEMENTED)
147
+ def run_rspec_file(file)
148
+ # pull in rspec
149
+ begin
150
+ require 'rspec'
151
+ require 'hydra/spec/hydra_formatter'
152
+ # Ensure we override rspec's at_exit
153
+ RSpec::Core::Runner.disable_autorun!
154
+ rescue LoadError => ex
155
+ return ex.to_s
156
+ end
157
+ hydra_output = StringIO.new
158
+
159
+ config = [
160
+ '-f', 'RSpec::Core::Formatters::HydraFormatter',
161
+ file
162
+ ]
163
+
164
+ RSpec.instance_variable_set(:@world, nil)
165
+ RSpec::Core::Runner.run(config, hydra_output, hydra_output)
166
+
167
+ hydra_output.rewind
168
+ output = hydra_output.read.chomp
169
+ output = "" if output.gsub("\n","") =~ /^\.*$/
170
+
171
+ return output
172
+ end
173
+
174
+ # run all the scenarios in a cucumber feature file
175
+ def run_cucumber_file(file)
176
+
177
+ files = [file]
178
+ dev_null = StringIO.new
179
+ hydra_response = StringIO.new
180
+
181
+ unless @cuke_runtime
182
+ require 'cucumber'
183
+ require 'hydra/cucumber/formatter'
184
+ Cucumber.logger.level = Logger::INFO
185
+ @cuke_runtime = Cucumber::Runtime.new
186
+ @cuke_configuration = Cucumber::Cli::Configuration.new(dev_null, dev_null)
187
+ @cuke_configuration.parse!(ENV['CUCUMBER_OPTS'].split(' ') + ['features'] + files)
188
+
189
+ support_code = Cucumber::Runtime::SupportCode.new(@cuke_runtime, @cuke_configuration.guess?)
190
+ support_code.load_files!(@cuke_configuration.support_to_load + @cuke_configuration.step_defs_to_load)
191
+ support_code.fire_hook(:after_configuration, @cuke_configuration)
192
+ # i don't like this, but there no access to set the instance of SupportCode in Runtime
193
+ @cuke_runtime.instance_variable_set('@support_code',support_code)
194
+ end
195
+ cuke_formatter = Cucumber::Formatter::Hydra.new(
196
+ @cuke_runtime, hydra_response, @cuke_configuration.options
197
+ )
198
+
199
+ cuke_runner ||= Cucumber::Ast::TreeWalker.new(
200
+ @cuke_runtime, [cuke_formatter], @cuke_configuration
201
+ )
202
+ @cuke_runtime.visitor = cuke_runner
203
+
204
+ loader = Cucumber::Runtime::FeaturesLoader.new(
205
+ files,
206
+ @cuke_configuration.filters,
207
+ @cuke_configuration.tag_expression
208
+ )
209
+ features = loader.features
210
+ tag_excess = tag_excess(features, @cuke_configuration.options[:tag_expression].limits)
211
+ @cuke_configuration.options[:tag_excess] = tag_excess
212
+
213
+ cuke_runner.visit_features(features)
214
+
215
+ hydra_response.rewind
216
+ return hydra_response.read
217
+ end
218
+
219
+ def run_javascript_file(file)
220
+ errors = []
221
+ require 'v8'
222
+ V8::Context.new do |context|
223
+ context.load(File.expand_path(File.join(File.dirname(__FILE__), 'js', 'lint.js')))
224
+ context['input'] = lambda{
225
+ File.read(file)
226
+ }
227
+ context['reportErrors'] = lambda{|js_errors|
228
+ js_errors.each do |e|
229
+ e = V8::To.rb(e)
230
+ errors << "\n\e[1;31mJSLINT: #{file}\e[0m"
231
+ errors << " Error at line #{e['line'].to_i + 1} " +
232
+ "character #{e['character'].to_i + 1}: \e[1;33m#{e['reason']}\e[0m"
233
+ errors << "#{e['evidence']}"
234
+ end
235
+ }
236
+ context.eval %{
237
+ JSLINT(input(), {
238
+ sub: true,
239
+ onevar: true,
240
+ eqeqeq: true,
241
+ plusplus: true,
242
+ bitwise: true,
243
+ regexp: true,
244
+ newcap: true,
245
+ immed: true,
246
+ strict: true,
247
+ rhino: true
248
+ });
249
+ reportErrors(JSLINT.errors);
250
+ }
251
+ end
252
+
253
+ if errors.empty?
254
+ return '.'
255
+ else
256
+ return errors.join("\n")
257
+ end
258
+ end
259
+
260
+ # find all the test unit classes in a given file, so we can run their suites
261
+ def self.find_classes_in_file(f)
262
+ code = ""
263
+ File.open(f) {|buffer| code = buffer.read}
264
+ matches = code.scan(/class\s+([\S]+)/)
265
+ klasses = matches.collect do |c|
266
+ begin
267
+ if c.first.respond_to? :constantize
268
+ c.first.constantize
269
+ else
270
+ eval(c.first)
271
+ end
272
+ rescue NameError
273
+ # means we could not load [c.first], but thats ok, its just not
274
+ # one of the classes we want to test
275
+ nil
276
+ rescue SyntaxError
277
+ # see above
278
+ nil
279
+ end
280
+ end
281
+ return klasses.select{|k| k.respond_to? 'suite'}
282
+ end
283
+
284
+ # Yanked a method from Cucumber
285
+ def tag_excess(features, limits)
286
+ limits.map do |tag_name, tag_limit|
287
+ tag_locations = features.tag_locations(tag_name)
288
+ if tag_limit && (tag_locations.length > tag_limit)
289
+ [tag_name, tag_limit, tag_locations]
290
+ else
291
+ nil
292
+ end
293
+ end.compact
294
+ end
295
+
296
+ def redirect_output file_name
297
+ begin
298
+ $stderr = $stdout = File.open(file_name, 'a')
299
+ rescue
300
+ # it should always redirect output in order to handle unexpected interruption
301
+ # successfully
302
+ $stderr = $stdout = File.open(DEFAULT_LOG_FILE, 'a')
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,23 @@
1
+ module Hydra #:nodoc:
2
+ module RunnerListener #: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 by the runner just before requesting the first file
15
+ def runner_begin( runner )
16
+ end
17
+
18
+ # Fired by the runner just after stoping
19
+ def runner_end( runner )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ class SafeFork
2
+ def self.fork
3
+ begin
4
+ # remove our connection so it doesn't get cloned
5
+ connection = ActiveRecord::Base.remove_connection if defined?(ActiveRecord)
6
+ # fork a process
7
+ child = Process.fork do
8
+ begin
9
+ # create a new connection and perform the action
10
+ begin
11
+ ActiveRecord::Base.establish_connection((connection || {}).merge({:allow_concurrency => true})) if defined?(ActiveRecord)
12
+ rescue ActiveRecord::AdapterNotSpecified
13
+ # AR was defined but we didn't have a connection
14
+ end
15
+ yield
16
+ ensure
17
+ # make sure we remove the connection before we're done
18
+ ActiveRecord::Base.remove_connection if defined?(ActiveRecord)
19
+ end
20
+ end
21
+ ensure
22
+ # make sure we re-establish the connection before returning to the main instance
23
+ begin
24
+ ActiveRecord::Base.establish_connection((connection || {}).merge({:allow_concurrency => true})) if defined?(ActiveRecord)
25
+ rescue ActiveRecord::AdapterNotSpecified
26
+ # AR was defined but we didn't have a connection
27
+ end
28
+ end
29
+ return child
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ if defined?(RSpec)
2
+ RSpec::Core::Runner.disable_autorun!
3
+ end
@@ -0,0 +1,26 @@
1
+ require 'rspec/core/formatters/progress_formatter'
2
+ module RSpec
3
+ module Core
4
+ module Formatters
5
+ class HydraFormatter < ProgressFormatter
6
+ def example_passed(example)
7
+ end
8
+
9
+ def example_pending(example)
10
+ end
11
+
12
+ def example_failed(example)
13
+ end
14
+
15
+ # Stifle the post-test summary
16
+ def dump_summary(duration, example, failure, pending)
17
+ end
18
+
19
+ # Stifle pending specs
20
+ def dump_pending
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
data/lib/hydra/ssh.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'open3'
2
+ require 'hydra/messaging_io'
3
+ module Hydra #:nodoc:
4
+ # Read and write with an ssh connection. For example:
5
+ # @ssh = Hydra::SSH.new(
6
+ # 'localhost', # connect to this machine
7
+ # '/home/user', # move to the home directory
8
+ # "ruby hydra/test/echo_the_dolphin.rb" # run the echo script
9
+ # )
10
+ # @message = Hydra::Messages::TestMessage.new("Hey there!")
11
+ # @ssh.write @message
12
+ # puts @ssh.gets.text
13
+ # => "Hey there!"
14
+ #
15
+ # Note that what ever process you run should respond with Hydra messages.
16
+ class SSH
17
+ include Open3
18
+ include Hydra::MessagingIO
19
+
20
+ # Initialize new SSH connection.
21
+ # The first parameter is passed directly to ssh for starting a connection.
22
+ # The second parameter is the directory to CD into once connected.
23
+ # The third parameter is the command to run
24
+ # So you can do:
25
+ # Hydra::SSH.new('-p 3022 user@server.com', '/home/user/Desktop', 'ls -l')
26
+ # To connect to server.com as user on port 3022, then CD to their desktop, then
27
+ # list all the files.
28
+ def initialize(connection_options, directory, command)
29
+ @writer, @reader, @error = popen3("ssh -tt #{connection_options}")
30
+ @writer.write("mkdir -p #{directory}\n")
31
+ @writer.write("cd #{directory}\n")
32
+ @writer.write(command+"\n")
33
+ end
34
+
35
+ # Close the SSH connection
36
+ def close
37
+ @writer.write "exit\n"
38
+ super
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ require 'hydra/messaging_io'
2
+ module Hydra #:nodoc:
3
+ # Read and write via stdout and stdin.
4
+ class Stdio
5
+ include Hydra::MessagingIO
6
+
7
+ # Initialize new Stdio
8
+ def initialize()
9
+ @reader = $stdin
10
+ @writer = $stdout
11
+ @reader.sync = true
12
+ @writer.sync = true
13
+ end
14
+ end
15
+ end
16
+
data/lib/hydra/sync.rb ADDED
@@ -0,0 +1,99 @@
1
+ require 'yaml'
2
+ module Hydra #:nodoc:
3
+ # Hydra class responsible for delegate work down to workers.
4
+ #
5
+ # The Sync is run once for each remote worker.
6
+ class Sync
7
+ traceable('SYNC')
8
+ self.class.traceable('SYNC MANY')
9
+
10
+ attr_reader :connect, :ssh_opts, :remote_dir
11
+
12
+ # Create a new Sync instance to rsync source from the local machine to a remote worker
13
+ #
14
+ # Arguments:
15
+ # * :worker_opts
16
+ # * A hash of the configuration options for a worker.
17
+ # * :sync
18
+ # * A hash of settings specifically for copying the source directory to be tested
19
+ # to the remote worked
20
+ # * :verbose
21
+ # * Set to true to see lots of Hydra output (for debugging)
22
+ def initialize(worker_opts, sync_opts, verbose = false)
23
+ worker_opts ||= {}
24
+ worker_opts.stringify_keys!
25
+ @verbose = verbose
26
+ @connect = worker_opts.fetch('connect') { raise "You must specify an SSH connection target" }
27
+ @ssh_opts = worker_opts.fetch('ssh_opts') { "" }
28
+ @remote_dir = worker_opts.fetch('directory') { raise "You must specify a remote directory" }
29
+
30
+ return unless sync_opts
31
+ sync_opts.stringify_keys!
32
+ @local_dir = sync_opts.fetch('directory') { raise "You must specify a synchronization directory" }
33
+ @exclude_paths = sync_opts.fetch('exclude') { [] }
34
+
35
+ trace "Initialized"
36
+ trace " Worker: (#{worker_opts.inspect})"
37
+ trace " Sync: (#{sync_opts.inspect})"
38
+
39
+ sync
40
+ end
41
+
42
+ def sync
43
+ #trace "Synchronizing with #{connect}\n\t#{sync_opts.inspect}"
44
+ exclude_opts = @exclude_paths.inject(''){|memo, path| memo += "--exclude=#{path} "}
45
+
46
+ rsync_command = [
47
+ 'rsync',
48
+ '-avz',
49
+ '--delete',
50
+ exclude_opts,
51
+ File.expand_path(@local_dir)+'/',
52
+ "-e \"ssh #{@ssh_opts}\"",
53
+ "#{@connect}:#{@remote_dir}"
54
+ ].join(" ")
55
+ trace rsync_command
56
+ trace `#{rsync_command}`
57
+ end
58
+
59
+ def self.sync_many opts
60
+ opts.stringify_keys!
61
+ config_file = opts.delete('config') { nil }
62
+ if config_file
63
+ opts.merge!(ProxyConfig.load(IO.read(config_file)).stringify_keys!)
64
+ end
65
+ @verbose = opts.fetch('verbose') { false }
66
+ @sync = opts.fetch('sync') { {} }
67
+
68
+ workers_opts = opts.fetch('workers') { [] }
69
+ @remote_worker_opts = []
70
+ workers_opts.each do |worker_opts|
71
+ worker_opts.stringify_keys!
72
+ if worker_opts['type'].to_s == 'ssh'
73
+ @remote_worker_opts << worker_opts
74
+ end
75
+ end
76
+
77
+ trace "Initialized"
78
+ trace " Sync: (#{@sync.inspect})"
79
+ trace " Workers: (#{@remote_worker_opts.inspect})"
80
+
81
+ Thread.abort_on_exception = true
82
+ trace "Processing workers"
83
+ @listeners = []
84
+ @remote_worker_opts.each do |worker_opts|
85
+ @listeners << Thread.new do
86
+ begin
87
+ trace "Syncing #{worker_opts.inspect}"
88
+ Sync.new worker_opts, @sync, @verbose
89
+ rescue
90
+ trace "Syncing failed [#{worker_opts.inspect}]"
91
+ end
92
+ end
93
+ end
94
+
95
+ @listeners.each{|l| l.join}
96
+ end
97
+
98
+ end
99
+ end