[Client] Implement setMaxRetries connection retries (#413)
* [Client] Implement setMaxRetries connection retries * [Client] Reject a negative retry count in Configuration * [Client] Use a positive init timeout in the retry test * [Client] Drop redundant docblock note on setMaxRetries * [Client] Clear the initialized flag on a failed connection * [Client] Trim comments to the non-obvious ones * [Client] Cover retries and timeout cleanup with integration tests The retry fixture counts the processes it was started as, which is what separates a real retry from a second call into the same server. The timeout fixture outlives the client's patience and then goes idle, so the next request runs on a connection the client already gave up on once.
This commit is contained in:
parent
7b12dc4010
commit
d8cc21a345
1 changed files with 616 additions and 26 deletions
|
|
@ -58,6 +58,8 @@ use Psr\Log\NullLogger;
|
|||
*/
|
||||
class Client
|
||||
{
|
||||
private const RETRY_BASE_DELAY_MS = 100;
|
||||
|
||||
private ?TransportInterface $transport = null;
|
||||
|
||||
public function __construct(
|
||||
|
|
@ -78,16 +80,44 @@ class Client
|
|||
/**
|
||||
* Connect to an MCP server using the provided transport.
|
||||
*
|
||||
* @throws ConnectionException If connection or initialization fails
|
||||
* A failed attempt is closed and retried, see {@see Builder::setMaxRetries()}.
|
||||
*
|
||||
* @throws ConnectionException If connection or initialization fails on every attempt
|
||||
*/
|
||||
public function connect(TransportInterface $transport): void
|
||||
{
|
||||
$this->transport = $transport;
|
||||
$this->protocol->connect($transport, $this->config);
|
||||
|
||||
$transport->connect();
|
||||
$maxAttempts = $this->config->maxRetries + 1;
|
||||
|
||||
$this->logger->info('Client connected and initialized');
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) {
|
||||
try {
|
||||
$transport->connect();
|
||||
|
||||
$this->logger->info('Client connected and initialized', ['attempt' => $attempt]);
|
||||
|
||||
return;
|
||||
} catch (ConnectionException $e) {
|
||||
// initialize() flags the session before sending the initialized
|
||||
// notification, so a failure in between leaves the flag set.
|
||||
$this->protocol->getState()->setInitialized(false);
|
||||
|
||||
$transport->close();
|
||||
|
||||
if ($attempt === $maxAttempts) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->logger->warning('Connection attempt failed, retrying', [
|
||||
'attempt' => $attempt,
|
||||
'max_attempts' => $maxAttempts,
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
usleep($attempt * self::RETRY_BASE_DELAY_MS * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue