Skip to content

Commit

Permalink
Allow to use multiple reboot strategies (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
Baldinof committed Feb 13, 2023
1 parent 8cd0e97 commit f0c0389
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 23 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ baldinof_road_runner:
```

If you are building long-running application and need to reboot it every XXX request to prevent memory leaks you can use `max_jobs` reboot strategy:

```yaml
# config/packages/baldinof_road_runner.yaml
baldinof_road_runner:
Expand All @@ -94,6 +95,22 @@ baldinof_road_runner:
max_jobs_dispersion: 0.2 # dispersion 20% used to prevent simultaneous reboot of all active workers (kernel will rebooted between 800 and 1000 requests)
```

You can combine reboot strategies:


```yaml
# config/packages/baldinof_road_runner.yaml
baldinof_road_runner:
kernel_reboot:
strategy: [on_exception, max_jobs]
allowed_exceptions:
- Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
- Symfony\Component\Serializer\Exception\ExceptionInterface
- App\Exception\YourDomainException
max_jobs: 1000
max_jobs_dispersion: 0.2
```


## Events

Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ parameters:
ignoreErrors:
- message: '#Access to protected property Symfony\\Component\\HttpKernel\\Kernel::\$startTime.#'
path: 'src/Http/KernelHandler.php'
- message: '#Cannot call method arrayNode#'
- message: '#Call to an undefined method#'
path: 'src/DependencyInjection'
- message: '#Cannot cast mixed to string.#'
path: 'src/Integration/PHP/NativeSessionMiddleware.php'
Expand Down
54 changes: 36 additions & 18 deletions src/DependencyInjection/BaldinofRoadRunnerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Baldinof\RoadRunnerBundle\Integration\Sentry\SentryMiddleware;
use Baldinof\RoadRunnerBundle\Integration\Symfony\ConfigureVarDumperListener;
use Baldinof\RoadRunnerBundle\Reboot\AlwaysRebootStrategy;
use Baldinof\RoadRunnerBundle\Reboot\ChainRebootStrategy;
use Baldinof\RoadRunnerBundle\Reboot\KernelRebootStrategyInterface;
use Baldinof\RoadRunnerBundle\Reboot\MaxJobsRebootStrategy;
use Baldinof\RoadRunnerBundle\Reboot\OnExceptionRebootStrategy;
Expand Down Expand Up @@ -52,25 +53,42 @@ public function load(array $configs, ContainerBuilder $container): void
$this->loadDebug($container);
}

if ($config['kernel_reboot']['strategy'] === Configuration::KERNEL_REBOOT_STRATEGY_ALWAYS) {
$container
->register(KernelRebootStrategyInterface::class, AlwaysRebootStrategy::class)
->setAutoconfigured(true);
} elseif ($config['kernel_reboot']['strategy'] === Configuration::KERNEL_REBOOT_STRATEGY_ON_EXCEPTION) {
$container
->register(KernelRebootStrategyInterface::class, OnExceptionRebootStrategy::class)
->addArgument($config['kernel_reboot']['allowed_exceptions'])
->addArgument(new Reference(LoggerInterface::class))
->setAutoconfigured(true)
->addTag('monolog.logger', ['channel' => self::MONOLOG_CHANNEL]);
} elseif ($config['kernel_reboot']['strategy'] === Configuration::KERNEL_REBOOT_STRATEGY_MAX_JOBS) {
$container
->register(KernelRebootStrategyInterface::class, MaxJobsRebootStrategy::class)
->addArgument($config['kernel_reboot']['max_jobs'])
->addArgument($config['kernel_reboot']['max_jobs_dispersion'])
->setAutoconfigured(true);
$strategies = $config['kernel_reboot']['strategy'];
$strategyServices = [];

foreach ($strategies as $strategy) {
if ($strategy === Configuration::KERNEL_REBOOT_STRATEGY_ALWAYS) {
$strategyService = (new Definition(AlwaysRebootStrategy::class))
->setAutoconfigured(true);
} elseif ($strategy === Configuration::KERNEL_REBOOT_STRATEGY_ON_EXCEPTION) {
$strategyService = (new Definition(OnExceptionRebootStrategy::class))
->addArgument($config['kernel_reboot']['allowed_exceptions'])
->addArgument(new Reference(LoggerInterface::class))
->setAutoconfigured(true)
->addTag('monolog.logger', ['channel' => self::MONOLOG_CHANNEL]);
} elseif ($strategy === Configuration::KERNEL_REBOOT_STRATEGY_MAX_JOBS) {
$strategyService = (new Definition(MaxJobsRebootStrategy::class))
->addArgument($config['kernel_reboot']['max_jobs'])
->addArgument($config['kernel_reboot']['max_jobs_dispersion'])
->setAutoconfigured(true);
} else {
$strategyService = new Reference($strategy);
}

$strategyServices[] = $strategyService;
}

if (\count($strategyServices) > 1) {
$container->register(KernelRebootStrategyInterface::class, ChainRebootStrategy::class)
->setArguments([$strategyServices]);
} else {
$container->setAlias(KernelRebootStrategyInterface::class, $config['kernel_reboot']['strategy']);
$strategy = $strategyServices[0];

if ($strategy instanceof Reference) {
$container->setAlias(KernelRebootStrategyInterface::class, (string) $strategy);
} else {
$container->setDefinition(KernelRebootStrategyInterface::class, $strategy);
}
}

$container->setParameter('baldinof_road_runner.middlewares', $config['middlewares']);
Expand Down
6 changes: 4 additions & 2 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ public function getConfigTreeBuilder(): TreeBuilder
->arrayNode('kernel_reboot')
->addDefaultsIfNotSet()
->children()
->scalarNode('strategy')
->arrayNode('strategy')
->info(sprintf(
'Possible values are "%s", "%s", "%s" or any service that implements "%s"/',
self::KERNEL_REBOOT_STRATEGY_ALWAYS,
self::KERNEL_REBOOT_STRATEGY_ON_EXCEPTION,
self::KERNEL_REBOOT_STRATEGY_MAX_JOBS,
KernelRebootStrategyInterface::class
))
->defaultValue(self::KERNEL_REBOOT_STRATEGY_ON_EXCEPTION)
->defaultValue([self::KERNEL_REBOOT_STRATEGY_ON_EXCEPTION])
->beforeNormalization()->castToArray()->end()
->scalarPrototype()->end()
->end()
->arrayNode('allowed_exceptions')
->info('Only used when `reboot_kernel.strategy: on_exception`. Exceptions defined here will not cause kernel reboots.')
Expand Down
39 changes: 39 additions & 0 deletions src/Reboot/ChainRebootStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Baldinof\RoadRunnerBundle\Reboot;

class ChainRebootStrategy implements KernelRebootStrategyInterface
{
/**
* @var iterable<KernelRebootStrategyInterface>
*/
private iterable $strategies;

/**
* @param iterable<KernelRebootStrategyInterface> $strategies
*/
public function __construct(iterable $strategies)
{
$this->strategies = $strategies;
}

public function shouldReboot(): bool
{
foreach ($this->strategies as $strategy) {
if ($strategy->shouldReboot()) {
return true;
}
}

return false;
}

public function clear(): void
{
foreach ($this->strategies as $strategy) {
$strategy->clear();
}
}
}
5 changes: 3 additions & 2 deletions src/Reboot/MaxJobsRebootStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ class MaxJobsRebootStrategy implements KernelRebootStrategyInterface
public function __construct(int $maxJobs, float $dispersion)
{
$minJobs = $maxJobs - (int) round($maxJobs * $dispersion);
$this->maxJobs = \random_int($minJobs, $maxJobs);
$this->maxJobs = random_int($minJobs, $maxJobs);
}

public function shouldReboot(): bool
{
if ($this->jobsCount < $this->maxJobs) {
$this->jobsCount++;
++$this->jobsCount;

return false;
}

Expand Down
49 changes: 49 additions & 0 deletions tests/BaldinofRoadRunnerBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
use Baldinof\RoadRunnerBundle\Integration\PHP\NativeSessionMiddleware;
use Baldinof\RoadRunnerBundle\Integration\Sentry\SentryMiddleware;
use Baldinof\RoadRunnerBundle\Integration\Symfony\StreamedResponseListener;
use Baldinof\RoadRunnerBundle\Reboot\AlwaysRebootStrategy;
use Baldinof\RoadRunnerBundle\Reboot\ChainRebootStrategy;
use Baldinof\RoadRunnerBundle\Reboot\KernelRebootStrategyInterface;
use Baldinof\RoadRunnerBundle\Reboot\MaxJobsRebootStrategy;
use Baldinof\RoadRunnerBundle\Reboot\OnExceptionRebootStrategy;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use PHPUnit\Framework\TestCase;
use Sentry\SentryBundle\SentryBundle;
Expand Down Expand Up @@ -184,6 +189,50 @@ public function test_it_removes_session_middleware()
$this->assertFalse($c->has(NativeSessionMiddleware::class));
}

public function test_it_supports_single_strategy()
{
$k = $this->getKernel([
'baldinof_road_runner' => [
'kernel_reboot' => [
'strategy' => 'always',
],
],
]);

$k->boot();

$c = $k->getContainer()->get('test.service_container');

$this->assertInstanceOf(AlwaysRebootStrategy::class, $c->get(KernelRebootStrategyInterface::class));
}

public function test_it_supports_multiple_strategies()
{
$k = $this->getKernel([
'baldinof_road_runner' => [
'kernel_reboot' => [
'strategy' => ['on_exception', 'max_jobs'],
],
],
]);

$k->boot();

$c = $k->getContainer()->get('test.service_container');

$strategy = $c->get(KernelRebootStrategyInterface::class);

$this->assertInstanceOf(ChainRebootStrategy::class, $strategy);

$strategies = (function () {
return $this->strategies;
})->bindTo($strategy, ChainRebootStrategy::class)();

$this->assertCount(2, $strategies);
$this->assertInstanceOf(OnExceptionRebootStrategy::class, $strategies[0]);
$this->assertInstanceOf(MaxJobsRebootStrategy::class, $strategies[1]);
}

/**
* @param BundleInterface[] $extraBundles
*/
Expand Down
63 changes: 63 additions & 0 deletions tests/Reboot/ChainRebootStrategyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Tests\Baldinof\RoadRunnerBundle\Reboot;

use Baldinof\RoadRunnerBundle\Reboot\ChainRebootStrategy;
use Baldinof\RoadRunnerBundle\Reboot\KernelRebootStrategyInterface;
use PHPUnit\Framework\TestCase;

class ChainRebootStrategyTest extends TestCase
{
public function test_it_does_not_reboot_by_default()
{
$strategy = new ChainRebootStrategy([]);
$this->assertFalse($strategy->shouldReboot());
}

public function test_it_reboot_if_one_strategy_reboot()
{
$strategy = new ChainRebootStrategy([
$this->createStrategy(true),
$this->createStrategy(false),
]);

$this->assertTrue($strategy->shouldReboot());

$strategy = new ChainRebootStrategy([
$this->createStrategy(false),
$this->createStrategy(true),
]);
}

public function test_it_does_not_reboot_if_no_strategy_reboot()
{
$strategy = new ChainRebootStrategy([
$this->createStrategy(false),
$this->createStrategy(false),
]);
$this->assertFalse($strategy->shouldReboot());
}

private function createStrategy(bool $shouldReboot): KernelRebootStrategyInterface
{
return new class($shouldReboot) implements KernelRebootStrategyInterface {
private bool $shouldReboot;

public function __construct(bool $shouldReboot)
{
$this->shouldReboot = $shouldReboot;
}

public function shouldReboot(): bool
{
return $this->shouldReboot;
}

public function clear(): void
{
}
};
}
}
2 changes: 2 additions & 0 deletions tests/Worker/WorkerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Tests\Baldinof\RoadRunnerBundle\Worker;

use AllowDynamicProperties;
use Baldinof\RoadRunnerBundle\Event\WorkerExceptionEvent;
use Baldinof\RoadRunnerBundle\Event\WorkerKernelRebootedEvent;
use Baldinof\RoadRunnerBundle\Event\WorkerStopEvent;
Expand All @@ -27,6 +28,7 @@
use Symfony\Component\HttpKernel\RebootableInterface;
use Symfony\Component\HttpKernel\TerminableInterface;

#[\AllowDynamicProperties]
class WorkerTest extends TestCase
{
use ProphecyTrait;
Expand Down

0 comments on commit f0c0389

Please sign in to comment.