Skip to content

Commit b27adba

Browse files
committed
ref
1 parent e0d92ee commit b27adba

File tree

6 files changed

+81
-34
lines changed

6 files changed

+81
-34
lines changed

docs/components/platform.rst

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ Thanks to Symfony's Cache component, platform calls can be cached to reduce call
461461

462462
echo $secondResult->getContent().\PHP_EOL;
463463

464-
High availability
464+
High Availability
465465
-----------------
466466

467467
As most platform exposes a REST API, errors can occurs during generation phase due to network issues, timeout and more.
@@ -474,14 +474,21 @@ the ``Symfony\AI\Platform\FailoverPlatform`` can be used to automatically call a
474474
use Symfony\AI\Platform\FailoverPlatform;
475475
use Symfony\AI\Platform\Message\Message;
476476
use Symfony\AI\Platform\Message\MessageBag;
477+
use Symfony\Component\RateLimiter\RateLimiterFactory;
478+
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
477479

478480
$ollamaPlatform = OllamaPlatformFactory::create('http://127.0.0.1:11434', http_client());
479481
$openAiPlatform = OpenAiPlatformFactory::create('sk-foo', http_client());
480482

481483
$failoverPlatform = new FailoverPlatform([
482484
$ollamaPlatform, // # Ollama will fail as 'gpt-4o' is not available in the catalog
483485
$openAiPlatform,
484-
]);
486+
], new RateLimiterFactory([
487+
'policy' => 'sliding_window',
488+
'id' => 'failover',
489+
'interval' => '60 seconds',
490+
'limit' => 1,
491+
], new InMemoryStorage()));
485492

486493
$result = $failoverPlatform->invoke('gpt-4o', new MessageBag(
487494
Message::forSystem('You are a helpful assistant.'),
@@ -503,10 +510,7 @@ This platform can also be configured when using the bundle::
503510
platforms:
504511
- 'ai.platform.ollama'
505512
- 'ai.platform.openai'
506-
retry_period: 120
507-
508-
A ``retry_period`` can be configured to determine after how many seconds a failed platform must be tried again,
509-
default value is ``60``.
513+
rate_limiter: ''
510514

511515
.. note::
512516

examples/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
"symfony/finder": "^7.4|^8.0",
188188
"symfony/http-foundation": "^7.4|^8.0",
189189
"symfony/process": "^7.4|^8.0",
190+
"symfony/rate-limiter": "^7.4|^8.0",
190191
"symfony/var-dumper": "^7.4|^8.0"
191192
},
192193
"require-dev": {
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Symfony\AI\Platform\FailoverPlatform;
1515
use Symfony\AI\Platform\Message\Message;
1616
use Symfony\AI\Platform\Message\MessageBag;
17+
use Symfony\Component\RateLimiter\RateLimiterFactory;
18+
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
1719

1820
require_once dirname(__DIR__).'/bootstrap.php';
1921

@@ -23,7 +25,12 @@
2325
$platform = new FailoverPlatform([
2426
$ollamaPlatform, // # Ollama will fail as 'gpt-4o' is not available in the catalog
2527
$openAiPlatform,
26-
]);
28+
], new RateLimiterFactory([
29+
'policy' => 'sliding_window',
30+
'id' => 'failover',
31+
'interval' => '3 seconds',
32+
'limit' => 1,
33+
], new InMemoryStorage()));
2734

2835
$result = $platform->invoke('gpt-4o', new MessageBag(
2936
Message::forSystem('You are a helpful assistant.'),

src/platform/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"phpstan/phpstan": "^2.1.17",
6565
"phpstan/phpstan-strict-rules": "^2.0",
6666
"phpunit/phpunit": "^11.5.46",
67+
"symfony/ai-agent": "@dev",
6768
"symfony/cache": "^7.3|^8.0",
6869
"symfony/console": "^7.3|^8.0",
6970
"symfony/dotenv": "^7.3|^8.0",
@@ -72,6 +73,7 @@
7273
"symfony/http-client": "^7.3|^8.0",
7374
"symfony/http-client-contracts": "^3.5",
7475
"symfony/process": "^7.3|^8.0",
76+
"symfony/rate-limiter": "^7.3|^8.0",
7577
"symfony/var-dumper": "^7.3|^8.0"
7678
},
7779
"minimum-stability": "dev",

src/platform/src/FailoverPlatform.php

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,28 @@
1717
use Symfony\AI\Platform\Exception\RuntimeException;
1818
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
1919
use Symfony\AI\Platform\Result\DeferredResult;
20-
use Symfony\Component\Clock\ClockInterface;
21-
use Symfony\Component\Clock\MonotonicClock;
20+
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
2221

2322
/**
2423
* @author Guillaume Loulier <[email protected]>
2524
*/
2625
final class FailoverPlatform implements PlatformInterface
2726
{
28-
/**
29-
* @var \SplObjectStorage<PlatformInterface, int>
30-
*/
31-
private \SplObjectStorage $failedPlatforms;
32-
3327
/**
3428
* @param PlatformInterface[] $platforms
3529
*/
3630
public function __construct(
3731
private readonly iterable $platforms,
38-
private readonly ClockInterface $clock = new MonotonicClock(),
39-
private readonly int $retryPeriod = 60,
32+
private readonly RateLimiterFactoryInterface $rateLimiterFactory,
4033
private readonly LoggerInterface $logger = new NullLogger(),
4134
) {
4235
if ([] === $platforms) {
4336
throw new LogicException(\sprintf('"%s" must have at least one platform configured.', self::class));
4437
}
4538

46-
$this->failedPlatforms = new \SplObjectStorage();
39+
if (!interface_exists(RateLimiterFactoryInterface::class)) {
40+
throw new RuntimeException('For using the FailoverPlatform, a symfony/rate-limiter implementation is required. Try running "composer require symfony/rate-limiter".');
41+
}
4742
}
4843

4944
public function invoke(string $model, object|array|string $input, array $options = []): DeferredResult
@@ -59,18 +54,16 @@ public function getModelCatalog(): ModelCatalogInterface
5954
private function do(\Closure $func): DeferredResult|ModelCatalogInterface
6055
{
6156
foreach ($this->platforms as $platform) {
62-
if ($this->failedPlatforms->offsetExists($platform) && ($this->clock->now()->getTimestamp() - $this->failedPlatforms->offsetGet($platform)) > $this->retryPeriod) {
63-
$this->failedPlatforms->offsetUnset($platform);
64-
}
57+
$limiter = $this->rateLimiterFactory->create($platform::class);
6558

66-
if ($this->failedPlatforms->offsetExists($platform)) {
59+
if (!$limiter->consume()->isAccepted()) {
6760
continue;
6861
}
6962

7063
try {
7164
return $func($platform);
7265
} catch (\Throwable $throwable) {
73-
$this->failedPlatforms->offsetSet($platform, $this->clock->now()->getTimestamp());
66+
$limiter->consume();
7467

7568
$this->logger->error('The {platform} platform due to an error/exception: {message}', [
7669
'platform' => $platform::class,

src/platform/tests/FailoverPlatformTest.php

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\AI\Platform\Tests;
1313

14-
use PHPUnit\Framework\Attributes\Group;
1514
use PHPUnit\Framework\TestCase;
1615
use Psr\Log\LoggerInterface;
1716
use Symfony\AI\Platform\Exception\RuntimeException;
@@ -25,9 +24,10 @@
2524
use Symfony\AI\Platform\ResultConverterInterface;
2625
use Symfony\AI\Platform\Test\InMemoryPlatform;
2726
use Symfony\Component\Clock\MonotonicClock;
27+
use Symfony\Component\RateLimiter\RateLimiterFactory;
28+
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
2829
use Symfony\Contracts\HttpClient\ResponseInterface;
2930

30-
#[Group('time-sensitive')]
3131
final class FailoverPlatformTest extends TestCase
3232
{
3333
public function testPlatformCannotPerformInvokeWithoutRemainingPlatform()
@@ -46,7 +46,12 @@ public function testPlatformCannotPerformInvokeWithoutRemainingPlatform()
4646
$failoverPlatform = new FailoverPlatform([
4747
$failedPlatform,
4848
$mainPlatform,
49-
], logger: $logger);
49+
], new RateLimiterFactory([
50+
'policy' => 'sliding_window',
51+
'id' => 'failover',
52+
'interval' => '60 seconds',
53+
'limit' => 3,
54+
], new InMemoryStorage()), logger: $logger);
5055

5156
$this->expectException(RuntimeException::class);
5257
$this->expectExceptionMessage('All platforms failed.');
@@ -70,7 +75,12 @@ public function testPlatformCannotRetrieveModelCatalogWithoutRemainingPlatform()
7075
$failoverPlatform = new FailoverPlatform([
7176
$failedPlatform,
7277
$mainPlatform,
73-
], logger: $logger);
78+
], new RateLimiterFactory([
79+
'policy' => 'sliding_window',
80+
'id' => 'failover',
81+
'interval' => '60 seconds',
82+
'limit' => 3,
83+
], new InMemoryStorage()), logger: $logger);
7484

7585
$this->expectException(RuntimeException::class);
7686
$this->expectExceptionMessage('All platforms failed.');
@@ -90,7 +100,12 @@ public function testPlatformCanPerformInvokeWithRemainingPlatform()
90100
$failoverPlatform = new FailoverPlatform([
91101
$failedPlatform,
92102
new InMemoryPlatform(static fn (): string => 'foo'),
93-
], logger: $logger);
103+
], new RateLimiterFactory([
104+
'policy' => 'sliding_window',
105+
'id' => 'failover',
106+
'interval' => '60 seconds',
107+
'limit' => 3,
108+
], new InMemoryStorage()), logger: $logger);
94109

95110
$result = $failoverPlatform->invoke('foo', 'foo');
96111

@@ -109,7 +124,12 @@ public function testPlatformCanRetrieveModelCatalogWithRemainingPlatform()
109124
$failoverPlatform = new FailoverPlatform([
110125
$failedPlatform,
111126
new InMemoryPlatform(static fn (): string => 'foo'),
112-
], logger: $logger);
127+
], new RateLimiterFactory([
128+
'policy' => 'sliding_window',
129+
'id' => 'failover',
130+
'interval' => '60 seconds',
131+
'limit' => 3,
132+
], new InMemoryStorage()), logger: $logger);
113133

114134
$this->assertInstanceOf(FallbackModelCatalog::class, $failoverPlatform->getModelCatalog());
115135
}
@@ -142,7 +162,12 @@ public function testPlatformCanPerformInvokeWhileRemovingPlatformAfterRetryPerio
142162
$failoverPlatform = new FailoverPlatform([
143163
$failedPlatform,
144164
new InMemoryPlatform(static fn (): string => 'bar'),
145-
], clock: $clock, retryPeriod: 1, logger: $logger);
165+
], new RateLimiterFactory([
166+
'policy' => 'sliding_window',
167+
'id' => 'failover',
168+
'interval' => '2 seconds',
169+
'limit' => 1,
170+
], new InMemoryStorage()), logger: $logger);
146171

147172
$firstResult = $failoverPlatform->invoke('foo', 'foo');
148173

@@ -177,7 +202,12 @@ public function testPlatformCanRetrieveModelCatalogWhileRemovingPlatformAfterRet
177202
$failoverPlatform = new FailoverPlatform([
178203
$failedPlatform,
179204
new InMemoryPlatform(static fn (): string => 'bar'),
180-
], clock: $clock, retryPeriod: 1, logger: $logger);
205+
], new RateLimiterFactory([
206+
'policy' => 'sliding_window',
207+
'id' => 'failover',
208+
'interval' => '60 seconds',
209+
'limit' => 1,
210+
], new InMemoryStorage()), logger: $logger);
181211

182212
$this->assertInstanceOf(FallbackModelCatalog::class, $failoverPlatform->getModelCatalog());
183213

@@ -221,12 +251,17 @@ public function testPlatformCannotPerformInvokeWhileAllPlatformFailedDuringRetry
221251
});
222252

223253
$logger = $this->createMock(LoggerInterface::class);
224-
$logger->expects($this->exactly(2))->method('error');
254+
$logger->expects($this->exactly(1))->method('error');
225255

226256
$failoverPlatform = new FailoverPlatform([
227257
$failedPlatform,
228258
$firstPlatform,
229-
], clock: $clock, retryPeriod: 6, logger: $logger);
259+
], new RateLimiterFactory([
260+
'policy' => 'sliding_window',
261+
'id' => 'failover',
262+
'interval' => '60 seconds',
263+
'limit' => 3,
264+
], new InMemoryStorage()), logger: $logger);
230265

231266
$firstResult = $failoverPlatform->invoke('foo', 'foo');
232267

@@ -269,12 +304,17 @@ public function testPlatformCannotRetrieveModelCatalogWhileAllPlatformFailedDuri
269304
});
270305

271306
$logger = $this->createMock(LoggerInterface::class);
272-
$logger->expects($this->exactly(2))->method('error');
307+
$logger->expects($this->exactly(1))->method('error');
273308

274309
$failoverPlatform = new FailoverPlatform([
275310
$failedPlatform,
276311
$firstPlatform,
277-
], clock: $clock, retryPeriod: 6, logger: $logger);
312+
], new RateLimiterFactory([
313+
'policy' => 'sliding_window',
314+
'id' => 'failover',
315+
'interval' => '60 seconds',
316+
'limit' => 3,
317+
], new InMemoryStorage()), logger: $logger);
278318

279319
$this->assertInstanceOf(FallbackModelCatalog::class, $failoverPlatform->getModelCatalog());
280320

0 commit comments

Comments
 (0)