Skip to content
This repository has been archived by the owner on Jan 17, 2022. It is now read-only.

Commit

Permalink
feat(apiserver): Create API Server component (#32)
Browse files Browse the repository at this point in the history
API Server component will allow to manage Swoole HTTP Server trough HTTP interface.

Command `swoole:server:status` is added to get informations about currently running HTTP server.

Closes #2
  • Loading branch information
k911 committed Apr 6, 2019
1 parent 7271cf8 commit a8d0ec2
Show file tree
Hide file tree
Showing 31 changed files with 1,297 additions and 108 deletions.
64 changes: 56 additions & 8 deletions docs/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,75 @@ swoole:
http_server:
port: 9501
host: localhost
running_mode: 'process'
running_mode: process
socket_type: tcp
ssl_enabled: false
trusted_hosts: localhost,127.0.0.1
trusted_proxies:
- '*'
- 127.0.0.1/8
- 192.168.2./16
static:
strategy: 'advanced'
public_dir: '%kernel.project_dir%/public'
hmr: 'auto'

# enables static file serving
static: advanced
# equals to:
# ---
# static:
# public_dir: '%kernel.project_dir%/public'
# strategy: advanced
# ---
# strategy can be one of: (default) auto, off, advanced, default
# - off: turn off feature
# - auto: use 'advanced' when debug enabled or not production environment
# - advanced: use request handler class \K911\Swoole\Server\RequestHandler\AdvancedStaticFilesServer
# - default: use default swoole static serving (faster than advanced, but supports less content types)

# enables hot module reload using inotify
hmr: auto
# hmr can be one of: off, (default) auto, inotify
# - off: turn off feature
# - auto: use inotify if installed in the system
# - inotify: use inotify

# enables api server on specific port
# by default it is disabled (can be also enabled using --api flag via cli)
api: true
# equals to:
# ---
# api:
# enabled: true
# host: 0.0.0.0
# port: 9200

# additional swoole symfony bundle services
services:
# see: \K911\Swoole\Bridge\Symfony\HttpKernel\DebugHttpKernelRequestHandler
debug_handler: true

# see: \K911\Swoole\Bridge\Symfony\HttpFoundation\TrustAllProxiesRequestHandler
trust_all_proxies_handler: true

# see: \K911\Swoole\Bridge\Symfony\HttpFoundation\CloudFrontRequestFactory
cloudfront_proto_header_handler: true

# see: \K911\Swoole\Bridge\Doctrine\ORM\EntityManagerHandler
entity_manager_handler: true

# swoole http server settings
# see https://www.swoole.co.uk/docs/modules/swoole-server/configuration
settings:
worker_count: 4
reactor_count: 2
log_file: '%kernel.logs_dir%/swoole_%kernel.environment%.log'
worker_count: 4
# when not set, swoole sets these are automatically set based on count of host CPU cores

log_level: auto
pid_file: '/var/run/swoole_http_server.pid'
# can be one of: (default) auto, debug, trace, info, notice, warning, error
# - auto: when debug set to debug, when not set to notice
# - {debug,trace,info,notice,warning,error}: see swoole configuration

log_file: '%kernel.logs_dir%/swoole_%kernel.environment%.log'
pid_file: /var/run/swoole_http_server.pid

buffer_output_size: 2097152
# in bytes, 2097152b = 2 MiB
```
61 changes: 45 additions & 16 deletions src/Bridge/Symfony/Bundle/Command/AbstractServerStartCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
use function K911\Swoole\decode_string_as_set;
use function K911\Swoole\format_bytes;
use function K911\Swoole\get_max_memory;
use K911\Swoole\Server\Config\Socket;
use K911\Swoole\Server\Configurator\ConfiguratorInterface;
use K911\Swoole\Server\HttpServer;
use K911\Swoole\Server\HttpServerConfiguration;
use K911\Swoole\Server\HttpServerFactory;
use K911\Swoole\Server\Runtime\BootableInterface;
use Swoole\Http\Server;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand Down Expand Up @@ -72,13 +74,16 @@ private function getDefaultPublicDir(): string
*/
protected function configure(): void
{
$defaultSocket = $this->serverConfiguration->getDefaultSocket();
$this->addOption('host', null, InputOption::VALUE_REQUIRED, 'Host name to listen to.', $defaultSocket->host())
->addOption('port', null, InputOption::VALUE_REQUIRED, 'Range 0-65535. When 0 random available port is chosen.', $defaultSocket->port())
->addOption('serve-static', 's', InputOption::VALUE_NONE, 'Enables serving static content from public directory.')
$sockets = $this->serverConfiguration->getSockets();
$serverSocket = $sockets->getServerSocket();
$this->addOption('host', null, InputOption::VALUE_REQUIRED, 'Host name to bind to. To bind to any host, use: 0.0.0.0', $serverSocket->host())
->addOption('port', null, InputOption::VALUE_REQUIRED, 'Listen for Swoole HTTP Server on this port, when 0 random available port is chosen', $serverSocket->port())
->addOption('serve-static', 's', InputOption::VALUE_NONE, 'Enables serving static content from public directory')
->addOption('public-dir', null, InputOption::VALUE_REQUIRED, 'Public directory', $this->getDefaultPublicDir())
->addOption('trusted-hosts', null, InputOption::VALUE_REQUIRED, 'Trusted hosts', $this->parameterBag->get('swoole.http_server.trusted_hosts'))
->addOption('trusted-proxies', null, InputOption::VALUE_REQUIRED, 'Trusted proxies', $this->parameterBag->get('swoole.http_server.trusted_proxies'));
->addOption('trusted-proxies', null, InputOption::VALUE_REQUIRED, 'Trusted proxies', $this->parameterBag->get('swoole.http_server.trusted_proxies'))
->addOption('api', null, InputOption::VALUE_NONE, 'Enable API Server')
->addOption('api-port', null, InputOption::VALUE_REQUIRED, 'Listen for API Server on this port', $this->parameterBag->get('swoole.http_server.api.port'));
}

private function ensureXdebugDisabled(SymfonyStyle $io): void
Expand Down Expand Up @@ -138,20 +143,22 @@ final protected function execute(InputInterface $input, OutputInterface $output)
exit(1);
}

$server = HttpServerFactory::make(
$this->serverConfiguration->getDefaultSocket(),
$this->serverConfiguration->getRunningMode()
);
$this->serverConfigurator->configure($server);
$this->server->attach($server);
$swooleServer = $this->makeSwooleHttpServer();
$this->serverConfigurator->configure($swooleServer);
$this->server->attach($swooleServer);

// TODO: Lock server configuration here
// $this->serverConfiguration->lock();

$runtimeConfiguration = ['symfonyStyle' => $io] + $this->prepareRuntimeConfiguration($this->serverConfiguration, $input);
$this->bootManager->boot($runtimeConfiguration);

$io->success(\sprintf('Swoole HTTP Server started on http://%s', $this->serverConfiguration->getDefaultSocket()->addressPort()));
$sockets = $this->serverConfiguration->getSockets();
$serverSocket = $sockets->getServerSocket();
$io->success(\sprintf('Swoole HTTP Server started on http://%s', $serverSocket->addressPort()));
if ($sockets->hasApiSocket()) {
$io->success(\sprintf('API Server started on http://%s', $sockets->getApiSocket()->addressPort()));
}
$io->table(['Configuration', 'Values'], $this->prepareConfigurationRowsToPrint($this->serverConfiguration, $runtimeConfiguration));

if ($this->testing) {
Expand All @@ -163,6 +170,18 @@ final protected function execute(InputInterface $input, OutputInterface $output)
return 0;
}

private function makeSwooleHttpServer(): Server
{
$sockets = $this->serverConfiguration->getSockets();
$serverSocket = $sockets->getServerSocket();

return HttpServerFactory::make(
$serverSocket,
$this->serverConfiguration->getRunningMode(),
...($sockets->hasApiSocket() ? [$sockets->getApiSocket()] : [])
);
}

/**
* @param HttpServerConfiguration $serverConfiguration
* @param InputInterface $input
Expand All @@ -171,16 +190,26 @@ final protected function execute(InputInterface $input, OutputInterface $output)
*/
protected function prepareServerConfiguration(HttpServerConfiguration $serverConfiguration, InputInterface $input): void
{
$sockets = $serverConfiguration->getSockets();

$port = $input->getOption('port');
$host = $input->getOption('host');
Assertion::numeric($port, 'Port must be numeric');
Assertion::string($host, 'Host must be string');

$socket = $serverConfiguration->getDefaultSocket()
Assertion::numeric($port, 'Port must be a number.');
Assertion::string($host, 'Host must be a string.');

$newServerSocket = $sockets->getServerSocket()
->withPort((int) $port)
->withHost($host);

$serverConfiguration->changeDefaultSocket($socket);
$sockets->changeServerSocket($newServerSocket);

if ((bool) $input->getOption('api') || $sockets->hasApiSocket()) {
$apiPort = $input->getOption('api-port');
Assertion::numeric($apiPort, 'Port must be a number.');

$sockets->changeApiSocket(new Socket('0.0.0.0', (int) $apiPort));
}

if (\filter_var($input->getOption('serve-static'), FILTER_VALIDATE_BOOLEAN)) {
$publicDir = $input->getOption('public-dir');
Expand Down
155 changes: 155 additions & 0 deletions src/Bridge/Symfony/Bundle/Command/ServerStatusCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php

declare(strict_types=1);

namespace K911\Swoole\Bridge\Symfony\Bundle\Command;

use Assert\Assertion;
use K911\Swoole\Server\Api\ApiServerInterface;
use K911\Swoole\Server\Config\Socket;
use K911\Swoole\Server\Config\Sockets;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

final class ServerStatusCommand extends Command
{
private $apiServer;
private $sockets;
private $parameterBag;
private $testing = false;

public function __construct(
Sockets $sockets,
ApiServerInterface $apiServer,
ParameterBagInterface $parameterBag
) {
$this->apiServer = $apiServer;
$this->sockets = $sockets;
$this->parameterBag = $parameterBag;

parent::__construct();
}

/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this->setDescription('Get current status of the Swoole HTTP Server by querying running API Server.')
->addOption('api-host', null, InputOption::VALUE_REQUIRED, 'API Server listens on this host.', $this->parameterBag->get('swoole.http_server.api.host'))
->addOption('api-port', null, InputOption::VALUE_REQUIRED, 'API Server listens on this port.', $this->parameterBag->get('swoole.http_server.api.port'));
}

/**
* {@inheritdoc}
*
* @throws \Assert\AssertionFailedException
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$this->prepareClientConfiguration($input);

$this->goAndWait(function () use ($io): void {
try {
$status = $this->apiServer->status();
$metrics = $this->apiServer->metrics();
} catch (\RuntimeException $runtimeException) {
$io->error('Could not connect to Swoole API Server');

return;
}
$io->success('Fetched status and metrics');
$this->showStatus($io, $status);
$this->showMetrics($io, $metrics);
});

return 0;
}

public function goAndWait(callable $callback): void
{
if ($this->testing) {
$callback();

return;
}

\go($callback);
\swoole_event_wait();
}

private function showStatus(SymfonyStyle $io, array $status): void
{
$server = $status['server'];
$processes = $server['processes'];

$rows = [
['Host', $server['host']],
['Port', $server['port']],
['Running mode', $server['runningMode']],
['Master PID', $processes['master']['pid']],
['Manager PID', $processes['manager']['pid']],
[\sprintf('Worker[%d] PID', $processes['worker']['id']), $processes['worker']['pid']],
];

foreach ($server['listeners'] as $id => ['host' => $host, 'port' => $port]) {
$rows[] = [\sprintf('Listener[%d] Host', $id), $host];
$rows[] = [\sprintf('Listener[%d] Port', $id), $port];
}

$io->table([
'Configuration', 'Value',
], $rows);
}

private function showMetrics(SymfonyStyle $io, array $metrics): void
{
$date = \DateTimeImmutable::createFromFormat(DATE_ATOM, $metrics['date']);
Assertion::isInstanceOf($date, \DateTimeImmutable::class);
$server = $metrics['server'];
$runningSeconds = $date->getTimestamp() - $server['start_time'];

$idleWorkers = $server['idle_worker_num'];
$workers = $server['worker_num'];
$activeWorkers = $workers - $idleWorkers;

$io->table([
'Metric', 'Quantity', 'Unit',
], [
['Requests', $server['request_count'], '1'],
['Up time', $runningSeconds, 'Seconds'],
['Active connections', $server['connection_num'], '1'],
['Accepted connections', $server['accept_count'], '1'],
['Closed connections', $server['close_count'], '1'],
['Active workers', $activeWorkers, '1'],
['Idle workers', $idleWorkers, '1'],
]);
}

/**
* @param InputInterface $input
*
* @throws \Assert\AssertionFailedException
*/
protected function prepareClientConfiguration(InputInterface $input): void
{
$host = $input->getOption('api-host');
$port = $input->getOption('api-port');

Assertion::numeric($port, 'Port must be a number.');
Assertion::string($host, 'Host must be a string.');

$this->sockets->changeApiSocket(new Socket($host, (int) $port));
}

public function enableTestMode(): void
{
$this->testing = true;
}
}
28 changes: 28 additions & 0 deletions src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,34 @@ public function getConfigTreeBuilder(): TreeBuilder
->treatFalseLike('off')
->values(['off', 'auto', 'inotify'])
->end()
->arrayNode('api')
->addDefaultsIfNotSet()
->beforeNormalization()
->ifTrue(function ($v): bool {
return \is_string($v) || \is_bool($v) || \is_numeric($v) || null === $v;
})
->then(function ($v): array {
return [
'enabled' => (bool) $v,
'host' => '0.0.0.0',
'port' => 9200,
];
})
->end()
->children()
->booleanNode('enabled')
->defaultFalse()
->end()
->scalarNode('host')
->cannotBeEmpty()
->defaultValue('0.0.0.0')
->end()
->scalarNode('port')
->cannotBeEmpty()
->defaultValue(9200)
->end()
->end()
->end()
->arrayNode('static')
->addDefaultsIfNotSet()
->beforeNormalization()
Expand Down

0 comments on commit a8d0ec2

Please sign in to comment.