diff --git a/composer.json b/composer.json index 9c5febb..526d507 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "require": { "php": "^7.4|^8.0", "antidot-fw/framework": "^0.1.3", + "beberlei/assert": "^3.3", "psr/container": "^1.0.0", "ramsey/uuid": "^4.1", "react/http": "^1.2" diff --git a/src/Child.php b/src/Child.php new file mode 100644 index 0000000..eded1e8 --- /dev/null +++ b/src/Child.php @@ -0,0 +1,45 @@ + SocketFactory::class, ], ], - 'server' => [] + 'server' => [ + 'workers' => 1, + 'host' => self::DEFAULT_HOST, + 'port' => self::DEFAULT_PORT, + 'max_concurrency' => self::DEFAULT_CONCURRENCY, + 'buffer_size' => self::DEFAULT_BUFFER_SIZE, + ] ]; } } diff --git a/src/PromiseResponse.php b/src/PromiseResponse.php index 00bfb16..f6471e8 100644 --- a/src/PromiseResponse.php +++ b/src/PromiseResponse.php @@ -4,10 +4,8 @@ namespace Antidot\React; -use Psr\Http\Message\ResponseInterface; use React\Promise\PromiseInterface; use RingCentral\Psr7\Response; -use Throwable; class PromiseResponse extends Response implements PromiseInterface { diff --git a/src/ServerFactory.php b/src/ServerFactory.php index a65c044..3b04fe0 100644 --- a/src/ServerFactory.php +++ b/src/ServerFactory.php @@ -5,8 +5,8 @@ namespace Antidot\React; use Antidot\Application\Http\Application; +use Assert\Assertion; use Psr\Container\ContainerInterface; -use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Server; @@ -15,6 +15,8 @@ use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; use React\Promise\PromiseInterface; +use function PHPUnit\Framework\assertArrayHasKey; +use function PHPUnit\Framework\assertIsInt; use function React\Promise\resolve; class ServerFactory @@ -22,19 +24,23 @@ class ServerFactory public function __invoke(ContainerInterface $container): Server { $application = $container->get(Application::class); - assert($application instanceof ReactApplication); + Assertion::isInstanceOf($application, ReactApplication::class); /** @var LoopInterface $loop */ $loop = $container->get(LoopInterface::class); /** @var array $globalConfig */ $globalConfig = $container->get('config'); /** @var array $config */ $config = $globalConfig['server']; + Assertion::keyExists($config, 'max_concurrency'); + Assertion::keyExists($config, 'buffer_size'); + Assertion::integer($config['max_concurrency']); + Assertion::integer($config['buffer_size']); $server = new Server( $loop, new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(($config['max_concurrency']) ?? 100), - new RequestBodyBufferMiddleware($config['buffer_size'] ?? 4 * 1024 * 1024), // 4 MiB + new LimitConcurrentRequestsMiddleware($config['max_concurrency']), + new RequestBodyBufferMiddleware($config['buffer_size']), new RequestBodyParserMiddleware(), static fn (ServerRequestInterface $request): PromiseInterface => resolve($application->handle($request)) ); diff --git a/src/SocketFactory.php b/src/SocketFactory.php index 4924a02..9ab3911 100644 --- a/src/SocketFactory.php +++ b/src/SocketFactory.php @@ -4,12 +4,17 @@ namespace Antidot\React; +use Assert\Assertion; use Psr\Container\ContainerInterface; use React\EventLoop\LoopInterface; use React\Socket\Server as Socket; class SocketFactory { + private const DEFAULT_TCP_CONFIG = ['tcp' => ['so_reuseport' => false]]; + private const REUSE_PORT = true; + private const DEFAULT_WORKERS_NUMBER = 1; + public function __invoke(ContainerInterface $container): Socket { /** @var LoopInterface $loop */ @@ -18,11 +23,28 @@ public function __invoke(ContainerInterface $container): Socket $globalConfig = $container->get('config'); /** @var array $config */ $config = $globalConfig['server']; + Assertion::notEmptyKey($config, 'host'); + Assertion::notEmptyKey($config, 'port'); + Assertion::keyExists($config, 'workers'); + /** @var string $host */ + $host = $config['host']; + Assertion::ipv4($host); + /** @var int $port */ + $port = $config['port']; + Assertion::integer($port); + /** @var int $workersNumber */ + $workersNumber = $config['workers']; + Assertion::integer($workersNumber); + $tcpConfig = self::DEFAULT_TCP_CONFIG; + if ($this->needMoreThanOne($workersNumber)) { + $tcpConfig['tcp']['so_reuseport'] = self::REUSE_PORT; + } + + return new Socket(sprintf('%s:%s', $host, $port), $loop, $tcpConfig); + } - return new Socket(sprintf( - '%s:%s', - $config['host'] ?? '0.0.0.0', - $config['port'] ?? '8080' - ), $loop); + private function needMoreThanOne(int $workersNumber): bool + { + return self::DEFAULT_WORKERS_NUMBER < $workersNumber; } } diff --git a/test/ChildTest.php b/test/ChildTest.php new file mode 100644 index 0000000..162e941 --- /dev/null +++ b/test/ChildTest.php @@ -0,0 +1,23 @@ +start(); + $process->wait(); + $this->assertSame('test passed', $process->getOutput()); + } +} diff --git a/test/Container/Config/ConfigProviderTest.php b/test/Container/Config/ConfigProviderTest.php index 98b2aff..c81815e 100644 --- a/test/Container/Config/ConfigProviderTest.php +++ b/test/Container/Config/ConfigProviderTest.php @@ -29,7 +29,13 @@ public function testItShouldReturnTheConfigArray(): void Socket::class => SocketFactory::class, ] ], - 'server' => [] + 'server' => [ + 'workers' => 1, + 'host' => '0.0.0.0', + 'port' => 8080, + 'max_concurrency' => 100, + 'buffer_size' => 4194304, + ] ], $configProvider(), ); diff --git a/test/ServerFactoryTest.php b/test/ServerFactoryTest.php index 0c7c28d..1b62a43 100644 --- a/test/ServerFactoryTest.php +++ b/test/ServerFactoryTest.php @@ -24,12 +24,59 @@ public function testItShouldCreateReactServerInstances(): void ->willReturnOnConsecutiveCalls( $this->createMock(ReactApplication::class), $this->createMock(LoopInterface::class), - ['server' => [ - - ]] + ['server' => ['max_concurrency' => 100, 'buffer_size' => 43242]] ); $factory = new ServerFactory(); $server = $factory($container); $this->assertInstanceOf(Server::class, $server); } + + public function testItShouldThrowExceptionWithNonReactApplicationInstance(): void + { + $this->expectException(\InvalidArgumentException::class); + $container = $this->createMock(ContainerInterface::class); + $container->expects($this->once()) + ->method('get') + ->with(Application::class) + ->willReturn( + $this->createMock(Application::class) + ); + $factory = new ServerFactory(); + $factory($container); + } + + /** @dataProvider getInvalidConfig */ + public function testItShouldThrowExceptionWithInvalidConfig(array $config): void + { + $this->expectException(\InvalidArgumentException::class); + $container = $this->createMock(ContainerInterface::class); + $container->expects($this->exactly(3)) + ->method('get') + ->withConsecutive([Application::class], [LoopInterface::class], ['config']) + ->willReturnOnConsecutiveCalls( + $this->createMock(ReactApplication::class), + $this->createMock(LoopInterface::class), + $config + ); + $factory = new ServerFactory(); + $factory($container); + } + + public function getInvalidConfig() + { + return [ + [ + ['server' => ['max_concurrency' => 100]] + ], + [ + ['server' => ['buffer_size' => 43525]] + ], + [ + ['server' => ['max_concurrency' => 'hello', 'buffer_size' => 43525]] + ], + [ + ['server' => ['max_concurrency' => 100, 'buffer_size' => []]] + ], + ]; + } } diff --git a/test/SocketFactoryTest.php b/test/SocketFactoryTest.php index 1fadca6..7161b66 100644 --- a/test/SocketFactoryTest.php +++ b/test/SocketFactoryTest.php @@ -5,6 +5,7 @@ namespace AntidotTest\React; use Antidot\React\SocketFactory; +use Assert\AssertionFailedException; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use React\EventLoop\LoopInterface; @@ -20,11 +21,52 @@ public function testItShouldcreateReactSocketInstances(): void ->withConsecutive([LoopInterface::class], ['config']) ->willReturnOnConsecutiveCalls( $this->createMock(LoopInterface::class), - ['server' => []] + ['server' => ['workers' => 1, 'host' => '0.0.0.0', 'port' => 8080]] ); $factory = new SocketFactory(); $socket = $factory($container); $this->assertInstanceOf(Socket::class, $socket); } + + /** @dataProvider getInvalidConfig */ + public function testItShouldThrowExceptionWithInvalidConfig(array $config): void + { + $this->expectException(AssertionFailedException::class); + $container = $this->createMock(ContainerInterface::class); + $container->expects($this->exactly(2)) + ->method('get') + ->withConsecutive([LoopInterface::class], ['config']) + ->willReturnOnConsecutiveCalls( + $this->createMock(LoopInterface::class), + $config + ); + + $factory = new SocketFactory(); + $factory($container); + } + + public function getInvalidConfig() + { + return [ + 'Bad Host' => [ + ['server' => ['port' => 8080, 'workers' => 3, 'host' => '756875.67867.7668.787']] + ], + [ + ['server' => ['host' => '0.0.0.0', 'port' => ['test'], 'workers' => 3]] + ], + [ + ['server' => ['host' => '0.0.0.0', 'port' => 8888, 'workers' => 'some']] + ], + [ + ['server' => ['port' => 43525, 'workers' => 3]] + ], + [ + ['server' => ['port' => 43525, 'host' => '0.0.0.0']] + ], + [ + ['server' => ['host' => '0.0.0.0', 'workers' => 3]] + ], + ]; + } }