[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
93
tests/Integration/RetryTest.php
Normal file
93
tests/Integration/RetryTest.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Tests\Integration;
|
||||
|
||||
use Mcp\Exception\ConnectionException;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
|
||||
/**
|
||||
* Connection retries, counted in processes rather than calls.
|
||||
*
|
||||
* A retry is only worth anything if it starts a fresh server, which is what the
|
||||
* spawn counter pins down: the fixture bumps it on every start, so the test can
|
||||
* tell a genuine second process from a second call into the same one.
|
||||
*
|
||||
* A failed attempt costs the init timeout, so these are the slowest tests here.
|
||||
*
|
||||
* @see Fixture/retry.php for the server under test
|
||||
*/
|
||||
final class RetryTest extends IntegrationTestCase
|
||||
{
|
||||
private string $counter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$counter = tempnam(sys_get_temp_dir(), 'mcp-spawns');
|
||||
|
||||
if (false === $counter) {
|
||||
$this->fail('Could not create the spawn counter file.');
|
||||
}
|
||||
|
||||
$this->counter = $counter;
|
||||
file_put_contents($this->counter, '0');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
@unlink($this->counter);
|
||||
}
|
||||
|
||||
#[TestDox('a failed attempt is retried against a newly spawned server')]
|
||||
public function testRetrySpawnsAnotherServer(): void
|
||||
{
|
||||
$client = $this->connect(
|
||||
'retry',
|
||||
$this->clientBuilder()->setInitTimeout(2)->setMaxRetries(1),
|
||||
$this->environment(failing: 1),
|
||||
);
|
||||
|
||||
$this->assertSame(2, $this->spawns());
|
||||
$this->assertTrue($client->isConnected());
|
||||
$this->assertSame('quick', $client->callTool('fast')->content[0]->text ?? null);
|
||||
}
|
||||
|
||||
#[TestDox('no retries means the first failure is the last word')]
|
||||
public function testRetriesCanBeDisabled(): void
|
||||
{
|
||||
$client = $this->clientBuilder()->setInitTimeout(2)->setMaxRetries(0)->build();
|
||||
|
||||
try {
|
||||
$client->connect($this->transport('retry', $this->environment(failing: 1)));
|
||||
$this->fail('Connecting should not have succeeded.');
|
||||
} catch (ConnectionException) {
|
||||
$this->assertSame(1, $this->spawns());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function environment(int $failing): array
|
||||
{
|
||||
return [
|
||||
'MCP_SPAWN_COUNTER' => $this->counter,
|
||||
'MCP_FAIL_SPAWNS' => (string) $failing,
|
||||
];
|
||||
}
|
||||
|
||||
private function spawns(): int
|
||||
{
|
||||
return (int) file_get_contents($this->counter);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue