Skip to content

Commit fa7e941

Browse files
committed
Make SSHKit::Runner::Parallel fail slow
Using a different SSHKit runner doesn't work well, because the group runner uses the Parallel runner internally. So instead we'll patch its behaviour to fail slow. We'll also get it to return all the errors so we can report on all the hosts that failed.
1 parent 78c0a0b commit fa7e941

File tree

3 files changed

+45
-24
lines changed

3 files changed

+45
-24
lines changed

bin/kamal

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ require "kamal"
77

88
begin
99
Kamal::Cli::Main.start(ARGV)
10+
rescue SSHKit::Runner::MultipleExecuteError => e
11+
e.execute_errors.each do |execute_error|
12+
puts " \e[31mERROR (#{execute_error.cause.class}): #{execute_error.message}\e[0m"
13+
end
14+
if ENV["VERBOSE"]
15+
puts "Backtrace for the first error:"
16+
puts e.execute_errors.first.cause.backtrace
17+
end
18+
exit 1
1019
rescue SSHKit::Runner::ExecuteError => e
1120
puts " \e[31mERROR (#{e.cause.class}): #{e.message}\e[0m"
1221
puts e.cause.backtrace if ENV["VERBOSE"]

lib/kamal/commander.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ def configure_sshkit_with(config)
150150
sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
151151
sshkit.ssh_options = config.ssh.options
152152
end
153-
SSHKit.config.default_runner = SSHKit::Runner::ParallelCompleteAll
154153
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
155154
SSHKit.config.output_verbosity = verbosity
156155
end

lib/kamal/sshkit_with_ext.rb

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -104,33 +104,46 @@ def start_with_concurrency_limit(*args)
104104
prepend LimitConcurrentStartsInstance
105105
end
106106

107-
require "thread"
108-
109-
module SSHKit
110-
module Runner
111-
class ParallelCompleteAll < Abstract
112-
def execute
113-
threads = hosts.map do |host|
114-
Thread.new(host) do |h|
115-
begin
116-
backend(h, &block).run
117-
rescue ::StandardError => e
118-
e2 = SSHKit::Runner::ExecuteError.new e
119-
raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}"
120-
end
121-
end
107+
class SSHKit::Runner::MultipleExecuteError < SSHKit::StandardError
108+
attr_reader :execute_errors
109+
110+
def initialize(execute_errors)
111+
@execute_errors = execute_errors
112+
end
113+
end
114+
115+
class SSHKit::Runner::Parallel
116+
# SSHKit joins the threads in sequence and fails on the first error it encounters, which means that we wait threads
117+
# before the first failure to complete but not for ones after.
118+
#
119+
# We'll patch it to wait for them all to complete, and to record all the threads that errored so we can see when a
120+
# problem occurs on multiple hosts.
121+
module CompleteAll
122+
def execute
123+
threads = hosts.map do |host|
124+
Thread.new(host) do |h|
125+
backend(h, &block).run
126+
rescue ::StandardError => e
127+
e2 = SSHKit::Runner::ExecuteError.new e
128+
raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}"
122129
end
130+
end
123131

124-
exception = nil
125-
threads.each do |t|
126-
begin
127-
t.join
128-
rescue SSHKit::Runner::ExecuteError => e
129-
exception ||= e
130-
end
132+
exceptions = []
133+
threads.each do |t|
134+
begin
135+
t.join
136+
rescue SSHKit::Runner::ExecuteError => e
137+
exceptions << e
131138
end
132-
raise exception if exception
139+
end
140+
if exceptions.one?
141+
raise exceptions.first
142+
elsif exceptions.many?
143+
raise SSHKit::Runner::MultipleExecuteError.new(exceptions)
133144
end
134145
end
135146
end
147+
148+
prepend CompleteAll
136149
end

0 commit comments

Comments
 (0)