From aa827ec9c494181f3a463f73340f4eb8c73f4bcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:45:33 +0000 Subject: [PATCH 1/7] Initial plan From fe412916e221bae7a9679fe59f6ad78a64a86bf9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:57:19 +0000 Subject: [PATCH 2/7] Add SSH-first availability check for reused Branchers - Extract SSH reachability check into reusable method checkSshReachability() - Add waitForReusedAvailability() method that checks SSH first - Refactor waitForAvailability() to use internal method - Update DeployRunner to use waitForReusedAvailability() for reused Branchers - When reusing old Branchers, SSH is checked first to avoid 404 errors on missing logbook flows Co-authored-by: tdgroot <1165302+tdgroot@users.noreply.github.com> --- src/Brancher/BrancherHypernodeManager.php | 177 +++++++++++++++++----- src/DeployRunner.php | 23 ++- 2 files changed, 154 insertions(+), 46 deletions(-) diff --git a/src/Brancher/BrancherHypernodeManager.php b/src/Brancher/BrancherHypernodeManager.php index 9f96175..5c9f3c3 100644 --- a/src/Brancher/BrancherHypernodeManager.php +++ b/src/Brancher/BrancherHypernodeManager.php @@ -102,6 +102,66 @@ public function createForHypernode(string $hypernode, array $data = []): string return $this->hypernodeClient->brancherApp->create($hypernode, $data); } + /** + * Check if brancher Hypernode is reachable via SSH. + * + * @param string $brancherHypernode Name of the brancher Hypernode + * @param int $timeout Maximum time to wait for reachability + * @param int $reachabilityCheckCount Number of consecutive successful checks required + * @param int $reachabilityCheckInterval Seconds between reachability checks + * @return bool True if reachable, false if timed out + */ + private function checkSshReachability( + string $brancherHypernode, + int $timeout, + int $reachabilityCheckCount, + int $reachabilityCheckInterval + ): bool { + $latest = microtime(true); + $timeElapsed = 0; + $consecutiveSuccesses = 0; + + while ($timeElapsed < $timeout) { + $now = microtime(true); + $timeElapsed += $now - $latest; + $latest = $now; + + $connection = @fsockopen(sprintf("%s.hypernode.io", $brancherHypernode), 22); + if ($connection) { + fclose($connection); + $consecutiveSuccesses++; + $this->log->info( + sprintf( + 'Brancher Hypernode %s reachability check %d/%d succeeded.', + $brancherHypernode, + $consecutiveSuccesses, + $reachabilityCheckCount + ) + ); + + if ($consecutiveSuccesses >= $reachabilityCheckCount) { + return true; + } + sleep($reachabilityCheckInterval); + } else { + if ($consecutiveSuccesses > 0) { + $this->log->info( + sprintf( + 'Brancher Hypernode %s reachability check failed, resetting counter (was at %d/%d).', + $brancherHypernode, + $consecutiveSuccesses, + $reachabilityCheckCount + ) + ); + } + $consecutiveSuccesses = 0; + sleep($reachabilityCheckInterval); + } + } + + return false; + } + /** * Wait for brancher Hypernode to become available. * @@ -121,6 +181,81 @@ public function waitForAvailability( int $reachabilityCheckCount = 6, int $reachabilityCheckInterval = 10 ): void { + $this->waitForAvailabilityInternal($brancherHypernode, $timeout, $reachabilityCheckCount, $reachabilityCheckInterval, false); + } + + /** + * Wait for reused brancher Hypernode to become available. + * For reused Branchers, first checks SSH connectivity before checking logbook flows, + * as older Branchers may not have recent logbook entries. + * + * @param string $brancherHypernode Name of the brancher Hypernode + * @param int $timeout Maximum time to wait for availability + * @param int $reachabilityCheckCount Number of consecutive successful checks required + * @param int $reachabilityCheckInterval Seconds between reachability checks + * @return void + * @throws CreateBrancherHypernodeFailedException + * @throws HypernodeApiClientException + * @throws HypernodeApiServerException + * @throws TimeoutException + */ + public function waitForReusedAvailability( + string $brancherHypernode, + int $timeout = 1500, + int $reachabilityCheckCount = 6, + int $reachabilityCheckInterval = 10 + ): void { + $this->waitForAvailabilityInternal($brancherHypernode, $timeout, $reachabilityCheckCount, $reachabilityCheckInterval, true); + } + + /** + * Internal method to wait for brancher Hypernode to become available. + * + * @param string $brancherHypernode Name of the brancher Hypernode + * @param int $timeout Maximum time to wait for availability + * @param int $reachabilityCheckCount Number of consecutive successful checks required + * @param int $reachabilityCheckInterval Seconds between reachability checks + * @param bool $checkSshFirst For reused Branchers, check SSH first before logbook + * @return void + * @throws CreateBrancherHypernodeFailedException + * @throws HypernodeApiClientException + * @throws HypernodeApiServerException + * @throws TimeoutException + */ + private function waitForAvailabilityInternal( + string $brancherHypernode, + int $timeout, + int $reachabilityCheckCount, + int $reachabilityCheckInterval, + bool $checkSshFirst + ): void { + // For reused Branchers, first check if SSH is already reachable + if ($checkSshFirst) { + $this->log->info( + sprintf( + 'Checking if reused brancher Hypernode %s is already reachable via SSH...', + $brancherHypernode + ) + ); + + if ($this->checkSshReachability($brancherHypernode, $timeout, $reachabilityCheckCount, $reachabilityCheckInterval)) { + $this->log->info( + sprintf( + 'Reused brancher Hypernode %s is already reachable!', + $brancherHypernode + ) + ); + return; + } + + $this->log->info( + sprintf( + 'Reused brancher Hypernode %s is not yet reachable, proceeding with full availability wait...', + $brancherHypernode + ) + ); + } + $latest = microtime(true); $timeElapsed = 0; $resolved = false; @@ -181,46 +316,8 @@ public function waitForAvailability( ); } - $consecutiveSuccesses = 0; - while ($timeElapsed < $timeout) { - $now = microtime(true); - $timeElapsed += $now - $latest; - $latest = $now; - - $connection = @fsockopen(sprintf("%s.hypernode.io", $brancherHypernode), 22); - if ($connection) { - fclose($connection); - $consecutiveSuccesses++; - $this->log->info( - sprintf( - 'Brancher Hypernode %s reachability check %d/%d succeeded.', - $brancherHypernode, - $consecutiveSuccesses, - $reachabilityCheckCount - ) - ); - - if ($consecutiveSuccesses >= $reachabilityCheckCount) { - break; - } - sleep($reachabilityCheckInterval); - } else { - if ($consecutiveSuccesses > 0) { - $this->log->info( - sprintf( - 'Brancher Hypernode %s reachability check failed, resetting counter (was at %d/%d).', - $brancherHypernode, - $consecutiveSuccesses, - $reachabilityCheckCount - ) - ); - } - $consecutiveSuccesses = 0; - sleep($reachabilityCheckInterval); - } - } - - if ($consecutiveSuccesses < $reachabilityCheckCount) { + $remainingTimeout = $timeout - $timeElapsed; + if (!$this->checkSshReachability($brancherHypernode, $remainingTimeout, $reachabilityCheckCount, $reachabilityCheckInterval)) { throw new TimeoutException( sprintf('Timed out waiting for brancher Hypernode %s to become reachable', $brancherHypernode) ); diff --git a/src/DeployRunner.php b/src/DeployRunner.php index 4bb2b15..eed23e3 100644 --- a/src/DeployRunner.php +++ b/src/DeployRunner.php @@ -296,8 +296,10 @@ private function maybeConfigureBrancherServer(Server $server, bool $reuseBranche $data = $settings; $data['labels'] = $labels; + $isReused = false; if ($reuseBrancher && $brancherApp = $this->brancherHypernodeManager->reuseExistingBrancherHypernode($parentApp, $labels)) { $this->log->info(sprintf('Found existing brancher Hypernode, name is %s.', $brancherApp)); + $isReused = true; } else { $brancherApp = $this->brancherHypernodeManager->createForHypernode($parentApp, $data); $this->log->info(sprintf('Successfully requested brancher Hypernode, name is %s.', $brancherApp)); @@ -306,12 +308,21 @@ private function maybeConfigureBrancherServer(Server $server, bool $reuseBranche try { $this->log->info('Waiting for brancher Hypernode to become available...'); - $this->brancherHypernodeManager->waitForAvailability( - $brancherApp, - $timeout, - $reachabilityCheckCount, - $reachabilityCheckInterval - ); + if ($isReused) { + $this->brancherHypernodeManager->waitForReusedAvailability( + $brancherApp, + $timeout, + $reachabilityCheckCount, + $reachabilityCheckInterval + ); + } else { + $this->brancherHypernodeManager->waitForAvailability( + $brancherApp, + $timeout, + $reachabilityCheckCount, + $reachabilityCheckInterval + ); + } $this->log->info('Brancher Hypernode has become available!'); } catch (CreateBrancherHypernodeFailedException | TimeoutException $e) { if (in_array($brancherApp, $this->brancherHypernodesRegistered)) { From 447b1d991840b6bf29afa3653d790bd515b8eb7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:59:32 +0000 Subject: [PATCH 3/7] Fix timeout allocation and time tracking issues - Allocate 50% of timeout for initial SSH check to leave time for fallback - Adjust remaining timeout based on time spent on SSH check - Fix time elapsed calculation to use start time instead of incremental approach Co-authored-by: tdgroot <1165302+tdgroot@users.noreply.github.com> --- src/Brancher/BrancherHypernodeManager.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Brancher/BrancherHypernodeManager.php b/src/Brancher/BrancherHypernodeManager.php index 5c9f3c3..ead8379 100644 --- a/src/Brancher/BrancherHypernodeManager.php +++ b/src/Brancher/BrancherHypernodeManager.php @@ -117,14 +117,10 @@ private function checkSshReachability( int $reachabilityCheckCount, int $reachabilityCheckInterval ): bool { - $latest = microtime(true); - $timeElapsed = 0; + $startTime = microtime(true); $consecutiveSuccesses = 0; - while ($timeElapsed < $timeout) { - $now = microtime(true); - $timeElapsed += $now - $latest; - $latest = $now; + while ((microtime(true) - $startTime) < $timeout) { $connection = @fsockopen(sprintf("%s.hypernode.io", $brancherHypernode), 22); if ($connection) { @@ -238,7 +234,11 @@ private function waitForAvailabilityInternal( ) ); - if ($this->checkSshReachability($brancherHypernode, $timeout, $reachabilityCheckCount, $reachabilityCheckInterval)) { + // Allocate 50% of timeout for initial SSH check to leave time for fallback + $sshCheckTimeout = (int) ($timeout * 0.5); + $sshCheckStartTime = microtime(true); + + if ($this->checkSshReachability($brancherHypernode, $sshCheckTimeout, $reachabilityCheckCount, $reachabilityCheckInterval)) { $this->log->info( sprintf( 'Reused brancher Hypernode %s is already reachable!', @@ -248,6 +248,10 @@ private function waitForAvailabilityInternal( return; } + // Adjust timeout for remaining checks based on time already spent + $timeSpentOnSshCheck = microtime(true) - $sshCheckStartTime; + $timeout = max(1, (int) ($timeout - $timeSpentOnSshCheck)); + $this->log->info( sprintf( 'Reused brancher Hypernode %s is not yet reachable, proceeding with full availability wait...', From c3e76e2c0cc079d075d8f333683ee8f2e862f0ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:00:59 +0000 Subject: [PATCH 4/7] Address code review nitpicks - Remove unnecessary empty line in checkSshReachability loop - Add REUSED_SSH_CHECK_TIMEOUT_RATIO constant for timeout allocation Co-authored-by: tdgroot <1165302+tdgroot@users.noreply.github.com> --- src/Brancher/BrancherHypernodeManager.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Brancher/BrancherHypernodeManager.php b/src/Brancher/BrancherHypernodeManager.php index ead8379..cf6f78c 100644 --- a/src/Brancher/BrancherHypernodeManager.php +++ b/src/Brancher/BrancherHypernodeManager.php @@ -14,6 +14,12 @@ class BrancherHypernodeManager { + /** + * Ratio of timeout to allocate for initial SSH check when reusing a Brancher. + * The remaining time is reserved for logbook flow checks if SSH fails. + */ + private const REUSED_SSH_CHECK_TIMEOUT_RATIO = 0.5; + private LoggerInterface $log; private HypernodeClient $hypernodeClient; @@ -121,7 +127,6 @@ private function checkSshReachability( $consecutiveSuccesses = 0; while ((microtime(true) - $startTime) < $timeout) { - $connection = @fsockopen(sprintf("%s.hypernode.io", $brancherHypernode), 22); if ($connection) { fclose($connection); @@ -234,8 +239,8 @@ private function waitForAvailabilityInternal( ) ); - // Allocate 50% of timeout for initial SSH check to leave time for fallback - $sshCheckTimeout = (int) ($timeout * 0.5); + // Allocate a portion of timeout for initial SSH check to leave time for fallback + $sshCheckTimeout = (int) ($timeout * self::REUSED_SSH_CHECK_TIMEOUT_RATIO); $sshCheckStartTime = microtime(true); if ($this->checkSshReachability($brancherHypernode, $sshCheckTimeout, $reachabilityCheckCount, $reachabilityCheckInterval)) { From dbefe7ac04986fc298361f0636da99e189849d3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:02:22 +0000 Subject: [PATCH 5/7] Optimize sleep calls in SSH reachability check - Only sleep if there's enough time remaining for another check - Avoid wasting timeout on unnecessary sleeps near the timeout boundary Co-authored-by: tdgroot <1165302+tdgroot@users.noreply.github.com> --- src/Brancher/BrancherHypernodeManager.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Brancher/BrancherHypernodeManager.php b/src/Brancher/BrancherHypernodeManager.php index cf6f78c..2692ea0 100644 --- a/src/Brancher/BrancherHypernodeManager.php +++ b/src/Brancher/BrancherHypernodeManager.php @@ -143,7 +143,11 @@ private function checkSshReachability( if ($consecutiveSuccesses >= $reachabilityCheckCount) { return true; } - sleep($reachabilityCheckInterval); + + // Only sleep if there's enough time remaining for another check + if ((microtime(true) - $startTime + $reachabilityCheckInterval) < $timeout) { + sleep($reachabilityCheckInterval); + } } else { if ($consecutiveSuccesses > 0) { $this->log->info( @@ -156,7 +160,11 @@ private function checkSshReachability( ); } $consecutiveSuccesses = 0; - sleep($reachabilityCheckInterval); + + // Only sleep if there's enough time remaining for another check + if ((microtime(true) - $startTime + $reachabilityCheckInterval) < $timeout) { + sleep($reachabilityCheckInterval); + } } } From 2c3448e6731b1ad71e96d10b0e109a49d5ce63e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:03:17 +0000 Subject: [PATCH 6/7] Remove trailing whitespace Co-authored-by: tdgroot <1165302+tdgroot@users.noreply.github.com> --- src/Brancher/BrancherHypernodeManager.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Brancher/BrancherHypernodeManager.php b/src/Brancher/BrancherHypernodeManager.php index 2692ea0..be14318 100644 --- a/src/Brancher/BrancherHypernodeManager.php +++ b/src/Brancher/BrancherHypernodeManager.php @@ -143,7 +143,7 @@ private function checkSshReachability( if ($consecutiveSuccesses >= $reachabilityCheckCount) { return true; } - + // Only sleep if there's enough time remaining for another check if ((microtime(true) - $startTime + $reachabilityCheckInterval) < $timeout) { sleep($reachabilityCheckInterval); @@ -160,7 +160,7 @@ private function checkSshReachability( ); } $consecutiveSuccesses = 0; - + // Only sleep if there's enough time remaining for another check if ((microtime(true) - $startTime + $reachabilityCheckInterval) < $timeout) { sleep($reachabilityCheckInterval); @@ -246,11 +246,11 @@ private function waitForAvailabilityInternal( $brancherHypernode ) ); - + // Allocate a portion of timeout for initial SSH check to leave time for fallback $sshCheckTimeout = (int) ($timeout * self::REUSED_SSH_CHECK_TIMEOUT_RATIO); $sshCheckStartTime = microtime(true); - + if ($this->checkSshReachability($brancherHypernode, $sshCheckTimeout, $reachabilityCheckCount, $reachabilityCheckInterval)) { $this->log->info( sprintf( @@ -260,11 +260,11 @@ private function waitForAvailabilityInternal( ); return; } - + // Adjust timeout for remaining checks based on time already spent $timeSpentOnSshCheck = microtime(true) - $sshCheckStartTime; $timeout = max(1, (int) ($timeout - $timeSpentOnSshCheck)); - + $this->log->info( sprintf( 'Reused brancher Hypernode %s is not yet reachable, proceeding with full availability wait...', From 7e30c4bda3d034ad39491243566466554e21a76b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:09:43 +0000 Subject: [PATCH 7/7] Simplify SSH check to use fixed 10-second timeout Replace percentage-based timeout calculation with fixed 10-second timeout for initial SSH check on reused Branchers, as suggested by @tdgroot Co-authored-by: tdgroot <1165302+tdgroot@users.noreply.github.com> --- src/Brancher/BrancherHypernodeManager.php | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Brancher/BrancherHypernodeManager.php b/src/Brancher/BrancherHypernodeManager.php index be14318..cf75a66 100644 --- a/src/Brancher/BrancherHypernodeManager.php +++ b/src/Brancher/BrancherHypernodeManager.php @@ -15,10 +15,9 @@ class BrancherHypernodeManager { /** - * Ratio of timeout to allocate for initial SSH check when reusing a Brancher. - * The remaining time is reserved for logbook flow checks if SSH fails. + * Timeout in seconds for initial SSH check when reusing a Brancher. */ - private const REUSED_SSH_CHECK_TIMEOUT_RATIO = 0.5; + private const REUSED_SSH_CHECK_TIMEOUT_SECONDS = 10; private LoggerInterface $log; private HypernodeClient $hypernodeClient; @@ -247,11 +246,7 @@ private function waitForAvailabilityInternal( ) ); - // Allocate a portion of timeout for initial SSH check to leave time for fallback - $sshCheckTimeout = (int) ($timeout * self::REUSED_SSH_CHECK_TIMEOUT_RATIO); - $sshCheckStartTime = microtime(true); - - if ($this->checkSshReachability($brancherHypernode, $sshCheckTimeout, $reachabilityCheckCount, $reachabilityCheckInterval)) { + if ($this->checkSshReachability($brancherHypernode, self::REUSED_SSH_CHECK_TIMEOUT_SECONDS, $reachabilityCheckCount, $reachabilityCheckInterval)) { $this->log->info( sprintf( 'Reused brancher Hypernode %s is already reachable!', @@ -261,10 +256,6 @@ private function waitForAvailabilityInternal( return; } - // Adjust timeout for remaining checks based on time already spent - $timeSpentOnSshCheck = microtime(true) - $sshCheckStartTime; - $timeout = max(1, (int) ($timeout - $timeSpentOnSshCheck)); - $this->log->info( sprintf( 'Reused brancher Hypernode %s is not yet reachable, proceeding with full availability wait...',