Skip to content

Commit 472a4f2

Browse files
committed
refactor: break down large methods in SSH Connection module
- Extract _is_connection_healthy into 7 focused methods - Split _execute_command_with_retry into 8 smaller methods - Break down _read_channel_output into 6 specialized methods - Improve _ensure_authenticated with 3 cleaner methods - Each method now has single responsibility and clear naming - Maintains backward compatibility and all tests pass - Improves readability, testability, and maintainability
1 parent abc734c commit 472a4f2

File tree

1 file changed

+214
-102
lines changed

1 file changed

+214
-102
lines changed

lib/TorrustDeploy/Infrastructure/SSH/Connection.pm

Lines changed: 214 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -109,51 +109,91 @@ sub _is_connection_expired {
109109
sub _is_connection_healthy {
110110
my ($self) = @_;
111111

112+
return 0 unless $self->_basic_connection_checks_pass();
113+
return 1 unless $self->_should_perform_health_check();
114+
115+
return $self->_perform_and_cache_health_check();
116+
}
117+
118+
sub _basic_connection_checks_pass {
119+
my ($self) = @_;
120+
112121
return 0 unless $self->_has_connection && $self->_authenticated;
113122
return 0 if $self->_is_connection_expired;
114123

115-
# Skip health check if disabled or recently checked
116-
return 1 unless $self->health_check_enabled;
124+
return 1;
125+
}
126+
127+
sub _should_perform_health_check {
128+
my ($self) = @_;
117129

118-
if ($self->_last_health_check) {
119-
my $time_since_check = time() - $self->_last_health_check;
120-
return 1 if $time_since_check < 30; # Skip check if done within 30 seconds
121-
}
130+
return 0 unless $self->health_check_enabled;
131+
return 0 if $self->_is_recent_health_check_cached();
132+
133+
return 1;
134+
}
135+
136+
sub _is_recent_health_check_cached {
137+
my ($self) = @_;
138+
139+
return 0 unless $self->_last_health_check;
140+
141+
my $time_since_check = time() - $self->_last_health_check;
142+
return $time_since_check < 30; # Cache for 30 seconds
143+
}
144+
145+
sub _perform_and_cache_health_check {
146+
my ($self) = @_;
147+
148+
my $health_result = $self->_execute_health_check_command();
149+
$self->_last_health_check(time());
150+
151+
return $health_result;
152+
}
153+
154+
sub _execute_health_check_command {
155+
my ($self) = @_;
122156

123-
# Perform actual health check with a simple command
124-
my $health_result = eval {
157+
my $result = eval {
125158
my $ssh2 = $self->_ssh2;
126159
my $channel = $ssh2->channel();
127160
return 0 unless $channel;
128161

129-
# Use a simple echo command as health check
130-
my $test_command = 'echo "health_check"';
131-
return 0 unless $channel->exec($test_command);
132-
133-
# Try to read response
134-
my $output = '';
135-
my $timeout = time() + 5; # 5 second timeout for health check
136-
137-
while (time() < $timeout) {
138-
my $buffer;
139-
my $bytes = $channel->read($buffer, 1024);
140-
last if $bytes <= 0;
141-
$output .= $buffer;
142-
last if $output =~ /health_check/;
143-
}
144-
145-
$channel->close();
146-
return $output =~ /health_check/;
162+
return $self->_run_echo_health_check($channel);
147163
};
148164

149-
$self->_last_health_check(time());
165+
return 0 if $@ || !$result;
166+
return 1;
167+
}
168+
169+
sub _run_echo_health_check {
170+
my ($self, $channel) = @_;
150171

151-
if ($@ || !$health_result) {
152-
# Health check failed, connection is unhealthy
153-
return 0;
172+
# Use a simple echo command as health check
173+
my $test_command = 'echo "health_check"';
174+
return 0 unless $channel->exec($test_command);
175+
176+
my $output = $self->_read_health_check_output($channel);
177+
$channel->close();
178+
179+
return $output =~ /health_check/;
180+
}
181+
182+
sub _read_health_check_output {
183+
my ($self, $channel) = @_;
184+
185+
my $output = '';
186+
my $timeout = time() + 5; # 5 second timeout for health check
187+
188+
while (time() < $timeout) {
189+
my $buffer;
190+
my $bytes = $channel->read($buffer, 1024);
191+
last if $bytes <= 0;
192+
$output .= $buffer;
193+
last if $output =~ /health_check/;
154194
}
155195

156-
return 1;
196+
return $output;
157197
}
158198

159199
sub test_password_connection {
@@ -216,64 +256,98 @@ sub _execute_command_with_retry {
216256
my ($self, $command, $max_retries) = @_;
217257

218258
for my $attempt (1..$max_retries) {
219-
# Ensure we have an authenticated connection
220-
unless ($self->_ensure_authenticated()) {
221-
return {
222-
output => "Authentication failed",
223-
success => 0,
224-
exit_code => 255,
225-
};
226-
}
259+
my $result = $self->_attempt_command_execution($command);
227260

228-
my $result = eval {
229-
my $ssh2 = $self->_ssh2;
230-
my $channel = $ssh2->channel();
231-
232-
unless ($channel) {
233-
croak "Failed to create channel: " . ($ssh2->error || 'Unknown error');
234-
}
235-
236-
# Execute command
237-
unless ($channel->exec($command)) {
238-
croak "Failed to execute command '$command': " . ($ssh2->error || 'Unknown error');
239-
}
240-
241-
# Read output with timeout
242-
my $output = $self->_read_channel_output($channel);
243-
244-
# Wait for command completion and get exit status
245-
$channel->wait_closed();
246-
my $exit_code = $channel->exit_status();
247-
$exit_code = 0 unless defined $exit_code;
248-
249-
return {
250-
output => $output,
251-
success => $exit_code == 0,
252-
exit_code => $exit_code,
253-
};
254-
};
255-
256-
# If command succeeded, return result
257-
if (!$@ && $result) {
258-
return $result;
259-
}
261+
# If command succeeded (result exists and is defined), return it
262+
return $result if $result;
260263

261264
# Command failed - check if we should retry
262-
if ($attempt < $max_retries && $self->auto_reconnect) {
263-
# Clear connection and try again
264-
$self->_clear_connection();
265-
$self->_clear_connection_timestamp();
266-
$self->_authenticated(0);
267-
next;
268-
}
269-
270-
# No more retries or auto_reconnect disabled
271-
return {
272-
output => "SSH command execution failed: $@",
273-
success => 0,
274-
exit_code => 255,
275-
};
265+
last unless $self->_should_retry_command($attempt, $max_retries);
266+
$self->_prepare_for_retry();
276267
}
268+
269+
# No more retries or auto_reconnect disabled
270+
return $self->_create_failure_result("SSH command execution failed: " . ($@ || "Unknown error"));
271+
}
272+
273+
sub _attempt_command_execution {
274+
my ($self, $command) = @_;
275+
276+
# Ensure we have an authenticated connection
277+
return $self->_create_failure_result("Authentication failed")
278+
unless $self->_ensure_authenticated();
279+
280+
my $result = eval { $self->_execute_single_command($command) };
281+
282+
# Return result if successful (even if command failed with non-zero exit)
283+
return $result if !$@ && $result;
284+
285+
# eval failed, return undef to trigger retry logic
286+
return undef;
287+
}
288+
289+
sub _execute_single_command {
290+
my ($self, $command) = @_;
291+
292+
my $ssh2 = $self->_ssh2;
293+
my $channel = $self->_create_command_channel($ssh2, $command);
294+
295+
my $output = $self->_read_channel_output($channel);
296+
my $exit_code = $self->_get_command_exit_code($channel);
297+
298+
return {
299+
output => $output,
300+
success => $exit_code == 0,
301+
exit_code => $exit_code,
302+
};
303+
}
304+
305+
sub _create_command_channel {
306+
my ($self, $ssh2, $command) = @_;
307+
308+
my $channel = $ssh2->channel();
309+
croak "Failed to create channel: " . ($ssh2->error || 'Unknown error')
310+
unless $channel;
311+
312+
croak "Failed to execute command '$command': " . ($ssh2->error || 'Unknown error')
313+
unless $channel->exec($command);
314+
315+
return $channel;
316+
}
317+
318+
sub _get_command_exit_code {
319+
my ($self, $channel) = @_;
320+
321+
$channel->wait_closed();
322+
my $exit_code = $channel->exit_status();
323+
return defined $exit_code ? $exit_code : 0;
324+
}
325+
326+
sub _should_retry_command {
327+
my ($self, $attempt, $max_retries) = @_;
328+
329+
return 0 if $attempt >= $max_retries;
330+
return 0 unless $self->auto_reconnect;
331+
332+
return 1;
333+
}
334+
335+
sub _prepare_for_retry {
336+
my ($self) = @_;
337+
338+
$self->_clear_connection();
339+
$self->_clear_connection_timestamp();
340+
$self->_authenticated(0);
341+
}
342+
343+
sub _create_failure_result {
344+
my ($self, $error_message) = @_;
345+
346+
return {
347+
output => $error_message,
348+
success => 0,
349+
exit_code => 255,
350+
};
277351
}
278352

279353
sub execute_command_with_sudo {
@@ -316,17 +390,24 @@ sub force_reconnect {
316390
sub _ensure_authenticated {
317391
my ($self) = @_;
318392

319-
# Check if existing connection is healthy
320-
if ($self->_is_connection_healthy()) {
321-
return 1;
322-
}
393+
return 1 if $self->_is_connection_healthy();
394+
395+
$self->_reset_unhealthy_connection();
396+
return $self->_attempt_authentication();
397+
}
398+
399+
sub _reset_unhealthy_connection {
400+
my ($self) = @_;
323401

324-
# Connection is unhealthy or expired, clear it and reconnect
325402
if ($self->_has_connection) {
326403
$self->_clear_connection();
327404
$self->_clear_connection_timestamp();
328405
$self->_authenticated(0);
329406
}
407+
}
408+
409+
sub _attempt_authentication {
410+
my ($self) = @_;
330411

331412
# Try password authentication first
332413
return 1 if $self->test_password_connection();
@@ -356,38 +437,69 @@ sub _find_public_key_path {
356437
sub _read_channel_output {
357438
my ($self, $channel) = @_;
358439

359-
my $output = '';
360-
my $buffer;
440+
$self->_setup_non_blocking_read($channel);
441+
my $output = $self->_read_with_timeout($channel);
442+
$output .= $self->_read_remaining_data($channel);
443+
444+
return $output;
445+
}
446+
447+
sub _setup_non_blocking_read {
448+
my ($self, $channel) = @_;
361449

362-
# Set up non-blocking read with timeout
363450
$channel->blocking(0);
451+
}
452+
453+
sub _read_with_timeout {
454+
my ($self, $channel) = @_;
364455

456+
my $output = '';
365457
my $start_time = time();
366458
my $timeout = $self->command_timeout;
367459

368460
while (time() - $start_time < $timeout) {
369-
my $bytes_read = $channel->read($buffer, 4096);
461+
my $buffer = $self->_try_read_chunk($channel);
370462

371-
if (defined $bytes_read && $bytes_read > 0) {
463+
if (defined $buffer && length($buffer) > 0) {
372464
$output .= $buffer;
373465
next;
374466
}
375467

376-
# Check if channel is closed
377468
last if $channel->eof();
378-
379-
# Small sleep to prevent busy waiting
380-
select(undef, undef, undef, 0.1);
469+
$self->_small_delay_to_prevent_busy_waiting();
381470
}
382471

472+
return $output;
473+
}
474+
475+
sub _try_read_chunk {
476+
my ($self, $channel) = @_;
477+
478+
my $buffer;
479+
my $bytes_read = $channel->read($buffer, 4096);
480+
481+
return (defined $bytes_read && $bytes_read > 0) ? $buffer : undef;
482+
}
483+
484+
sub _small_delay_to_prevent_busy_waiting {
485+
my ($self) = @_;
486+
487+
select(undef, undef, undef, 0.1);
488+
}
489+
490+
sub _read_remaining_data {
491+
my ($self, $channel) = @_;
492+
493+
my $remaining_output = '';
494+
383495
# Final blocking read to get any remaining data
384496
$channel->blocking(1);
385-
while (my $bytes_read = $channel->read($buffer, 4096)) {
497+
while (my $bytes_read = $channel->read(my $buffer, 4096)) {
386498
last unless defined $bytes_read && $bytes_read > 0;
387-
$output .= $buffer;
499+
$remaining_output .= $buffer;
388500
}
389501

390-
return $output;
502+
return $remaining_output;
391503
}
392504

393505
# Cleanup on destruction

0 commit comments

Comments
 (0)