8000 [FrameworkBundle][RateLimiter] compound rate limiter config by kbond · Pull Request #60155 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[FrameworkBundle][RateLimiter] compound rate limiter config #60155

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ CHANGELOG
* Deprecate setting the `framework.profiler.collect_serializer_data` config option to `false`
* Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default
* Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead
* Allow configuring compound rate limiters

7.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2518,7 +2518,12 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $
->enumNode('policy')
->info('The algorithm to be used by this limiter.')
->isRequired()
->values(['fixed_window', 'token_bucket', 'sliding_window', 'no_limit'])
->values(['fixed_window', 'token_bucket', 'sliding_window', 'compound', 'no_limit'])
->end()
->arrayNode('limiters')
->info('The limiter names to use when using the "compound" policy.')
->beforeNormalization()->castToArray()->end()
->scalarPrototype()->end()
->end()
->integerNode('limit')
->info('The maximum allowed hits in a fixed interval or burst.')
Expand All @@ -2537,8 +2542,8 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $
->end()
->end()
->validate()
->ifTrue(fn ($v) => 'no_limit' !== $v['policy'] && !isset($v['limit']))
->thenInvalid('A limit must be provided when using a policy different than "no_limit".')
->ifTrue(static fn ($v) => !\in_array($v['policy'], ['no_limit', 'compound']) && !isset($v['limit']))
->thenInvalid('A limit must be provided when using a policy different than "compound" or "no_limit".')
->end()
->end()
->end()
Expand Down
10000
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
use Symfony\Component\Console\Debug\CliRequest;
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
Expand Down Expand Up @@ -158,6 +159,7 @@
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\RateLimiter\CompoundRateLimiterFactory;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
Expand Down Expand Up @@ -3232,7 +3234,18 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde
{
$loader->load('rate_limiter.php');

$limiters = [];
$compoundLimiters = [];

foreach ($config['limiters'] as $name => $limiterConfig) {
if ('compound' === $limiterConfig['policy']) {
$compoundLimiters[$name] = $limiterConfig;

continue;
}

$limiters[] = $name;

// default configuration (when used by other DI extensions)
$limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.rate_limiter'];

Expand Down Expand Up @@ -3273,6 +3286,30 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde
$factoryAlias->setDeprecated('symfony/dependency-injection', '7.3', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.');
}
}

if ($compoundLimiters && !class_exists(CompoundRateLimiterFactory::class)) {
throw new LogicException('Configuring compound rate limiters is only available in symfony/rate-limiter 7.3+.');
}

foreach ($compoundLimiters as $name => $limiterConfig) {
if (!$limiterConfig['limiters']) {
throw new LogicException(\sprintf('Compound rate limiter "%s" requires at least one sub-limiter.', $name));
}

if (\array_diff($limiterConfig['limiters'], $limiters)) {
throw new LogicException(\sprintf('Compound rate limiter "%s" requires at least one sub-limiter to be configured.', $name));
}

$container->register($limiterId = 'limiter.'.$name, CompoundRateLimiterFactory::class)
->addTag('rate_limiter', ['name' => $name])
->addArgument(new IteratorArgument(\array_map(
static fn (string $name) => new Reference('limiter.'.$name),
$limiterConfig['limiters']
)))
;

$container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter');
}
}

private function registerUidConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\RateLimiter\CompoundRateLimiterFactory;
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;

class PhpFrameworkExtensionTest extends FrameworkExtensionTestCase
Expand Down Expand Up @@ -290,4 +292,77 @@ public function testRateLimiterIsTagged()
$this->assertSame('first', $container->getDefinition('limiter.first')->getTag('rate_limiter')[0]['name']);
$this->assertSame('second', $container->getDefinition('limiter.second')->getTag('rate_limiter')[0]['name']);
}

public function testRateLimiterCompoundPolicy()
{
if (!class_exists(CompoundRateLimiterFactory::class)) {
$this->markTestSkipped('CompoundRateLimiterFactory is not available.');
}

$container = $this->createContainerFromClosure(function (ContainerBuilder $container) {
$container->loadFromExtension('framework', [
'annotations' => false,
'http_method_override' => false,
'handle_all_throwables' => true,
'php_errors' => ['log' => true],
'lock' => true,
'rate_limiter' => [
'first' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'],
'second' => ['policy' => 'sliding_window', 'limit' => 10, 'interval' => '1 hour'],
'compound' => ['policy' => 'compound', 'limiters' => ['first', 'second']],
],
]);
});

$definition = $container->getDefinition('limiter.compound');
$this->assertSame(CompoundRateLimiterFactory::class, $definition->getClass());
$this->assertEquals(
[
'limiter.first',
'limiter.second',
],
$definition->getArgument(0)->getValues()
);
$this->assertSame('limiter.compound', (string) $container->getAlias(RateLimiterFactoryInterface::class.' $compoundLimiter'));
}

public function testRateLimiterCompoundPolicyNoLimiters()
{
if (!class_exists(CompoundRateLimiterFactory::class)) {
$this->markTestSkipped('CompoundRateLimiterFactory is not available.');
}

$this->expectException(\LogicException::class);
$this->createContainerFromClosure(function ($container) {
$container->loadFromExtension('framework', [
'annotations' => false,
'http_method_override' => false,
'handle_all_throwables' => true,
'php_errors' => ['log' => true],
'rate_limiter' => [
'compound' => ['policy' => 'compound'],
],
]);
});
}

public function testRateLimiterCompoundPolicyInvalidLimiters()
{
if (!class_exists(CompoundRateLimiterFactory::class)) {
$this->markTestSkipped('CompoundRateLimiterFactory is not available.');
}

$this->expectException(\LogicException::class);
$this->createContainerFromClosure(function ($container) {
$container->loadFromExtension('framework', [
'annotations' => false,
'http_method_override' => false,
'handle_all_throwables' => true,
'php_errors' => ['log' => true],
'rate_limiter' => [
'compound' => ['policy' => 'compound', 'limiters' => ['invalid1', 'invalid2']],
],
]);
});
}
}
Loading
0