Merge pull request #284 from acelaya/feature/swoole

Feature/swoole
This commit is contained in:
Alejandro Celaya 2018-11-25 22:12:50 +01:00 committed by GitHub
commit afa2a5b0f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 510 additions and 33 deletions

View file

@ -18,6 +18,7 @@ matrix:
before_install:
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- yes | pecl install swoole
- phpenv config-rm xdebug.ini || return 0
install:

View file

@ -8,7 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
#### Added
* *Nothing*
* [#208](https://github.com/shlinkio/shlink/issues/208) Added initial support to run shlink using [swoole](https://www.swoole.co.uk/), a non-blocking IO server which improves the performance of shlink from 4 to 10 times.
Run shlink with `./vendor/bin/zend-expressive-swoole start` to start-up the service, which will be exposed in port `8080`.
Adding the `-d` flag, it will be started as a background service. Then you can use the `./vendor/bin/zend-expressive-swoole stop` command in order to stop it.
#### Changed

View file

@ -3,8 +3,11 @@
declare(strict_types=1);
use Interop\Container\ContainerInterface;
use Shlinkio\Shlink\Common\Exec\ExecutionContext;
use Symfony\Component\Console\Application as CliApp;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
putenv(sprintf('CURRENT_SHLINK_CONTEXT=%s', ExecutionContext::CLI));
$container->get(CliApp::class)->run();

View file

@ -42,6 +42,7 @@
"zendframework/zend-expressive-fastroute": "^3.0",
"zendframework/zend-expressive-helpers": "^5.0",
"zendframework/zend-expressive-platesrenderer": "^2.0",
"zendframework/zend-expressive-swoole": "^2.0",
"zendframework/zend-i18n": "^2.7",
"zendframework/zend-inputfilter": "^2.8",
"zendframework/zend-paginator": "^2.6",

View file

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common;
use function Shlinkio\Shlink\Common\env;
return [
@ -10,9 +10,9 @@ return [
'proxies_dir' => 'data/proxies',
],
'connection' => [
'user' => Common\env('DB_USER'),
'password' => Common\env('DB_PASSWORD'),
'dbname' => Common\env('DB_NAME', 'shlink'),
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'dbname' => env('DB_NAME', 'shlink'),
'charset' => 'utf8',
],
],

View file

@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor;
use Zend\Expressive\Swoole\Log\AccessLogInterface;
use const PHP_EOL;
return [
@ -19,13 +21,19 @@ return [
],
'handlers' => [
'rotating_file_handler' => [
'shlink_rotating_handler' => [
'class' => RotatingFileHandler::class,
'level' => Logger::INFO,
'filename' => 'data/log/shlink_log.log',
'max_files' => 30,
'formatter' => 'dashed',
],
'swoole_access_handler' => [
'class' => StreamHandler::class,
'level' => Logger::INFO,
'stream' => 'php://stdout',
'formatter' => 'dashed',
],
],
'processors' => [
@ -39,9 +47,30 @@ return [
'loggers' => [
'Shlink' => [
'handlers' => ['rotating_file_handler'],
'handlers' => ['shlink_rotating_handler'],
'processors' => ['exception_with_new_line', 'psr3'],
],
'Swoole' => [
'handlers' => ['swoole_access_handler'],
'processors' => ['psr3'],
],
],
],
'dependencies' => [
'factories' => [
'Logger_Shlink' => Common\Factory\LoggerFactory::class,
'Logger_Swoole' => Common\Factory\LoggerFactory::class,
AccessLogInterface::class => Common\Logger\Swoole\AccessLogFactory::class,
],
],
'zend-expressive-swoole' => [
'swoole-http-server' => [
'logger' => [
'logger_name' => 'Logger_Swoole',
],
],
],

View file

@ -1,11 +1,13 @@
<?php
declare(strict_types=1);
use Monolog\Logger;
return [
'logger' => [
'handlers' => [
'rotating_file_handler' => [
'shlink_rotating_handler' => [
'level' => Logger::DEBUG,
],
],

View file

@ -10,10 +10,18 @@ return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
],
'middleware' => (function () {
$middleware = [
ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
];
if (Common\Exec\ExecutionContext::currentContextIsSwoole()) {
$middleware[] = Common\Middleware\CloseDbConnectionMiddleware::class;
}
return $middleware;
})(),
'priority' => 12,
],
'pre-routing-rest' => [

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
return [
'zend-expressive-swoole' => [
'enable_coroutine' => true,
'swoole-http-server' => [
'host' => '0.0.0.0',
],
],
];

View file

@ -6,7 +6,6 @@ namespace Shlinkio\Shlink;
use Acelaya\ExpressiveErrorHandler;
use Zend\ConfigAggregator;
use Zend\Expressive;
use function class_exists;
return (new ConfigAggregator\ConfigAggregator([
Expressive\ConfigProvider::class,
@ -14,9 +13,7 @@ return (new ConfigAggregator\ConfigAggregator([
Expressive\Router\FastRouteRouter\ConfigProvider::class,
Expressive\Plates\ConfigProvider::class,
Expressive\Helper\ConfigProvider::class,
class_exists(Expressive\Swoole\ConfigProvider::class)
? Expressive\Swoole\ConfigProvider::class
: new ConfigAggregator\ArrayProvider([]),
Expressive\Swoole\ConfigProvider::class,
ExpressiveErrorHandler\ConfigProvider::class,
Common\ConfigProvider::class,
Core\ConfigProvider::class,

6
config/pipeline.php Normal file
View file

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
// FIXME Dummy file just to prevent expressive-swoole fail while loading
return function () {
};

6
config/routes.php Normal file
View file

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
// FIXME Dummy file just to prevent expressive-swoole fail while loading
return function () {
};

View file

@ -0,0 +1,98 @@
FROM php:7.1.22-cli-alpine3.7
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
RUN apk update
# Install common php extensions
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install iconv
RUN docker-php-ext-install mbstring
RUN docker-php-ext-install calendar
RUN apk add --no-cache --virtual sqlite-libs
RUN apk add --no-cache --virtual sqlite-dev
RUN docker-php-ext-install pdo_sqlite
RUN apk add --no-cache --virtual icu-dev
RUN docker-php-ext-install intl
RUN apk add --no-cache --virtual zlib-dev
RUN docker-php-ext-install zip
RUN apk add --no-cache --virtual libmcrypt-dev
RUN docker-php-ext-install mcrypt
RUN apk add --no-cache --virtual libpng-dev
RUN docker-php-ext-install gd
# Install redis extension
ADD https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz /tmp/phpredis.tar.gz
RUN mkdir -p /usr/src/php/ext/redis\
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
# configure and install
RUN docker-php-ext-configure redis\
&& docker-php-ext-install redis
# cleanup
RUN rm /tmp/phpredis.tar.gz
# Install memcached extension
RUN apk add --no-cache --virtual cyrus-sasl-dev
RUN apk add --no-cache --virtual libmemcached-dev
ADD https://github.com/php-memcached-dev/php-memcached/archive/php7.tar.gz /tmp/memcached.tar.gz
RUN mkdir -p /usr/src/php/ext/memcached\
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
# configure and install
RUN docker-php-ext-configure memcached\
&& docker-php-ext-install memcached
# cleanup
RUN rm /tmp/memcached.tar.gz
# Install APCu extension
ADD https://pecl.php.net/get/apcu-5.1.3.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu\
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
# configure and install
RUN docker-php-ext-configure apcu\
&& docker-php-ext-install apcu
# cleanup
RUN rm /tmp/apcu.tar.gz
# Install APCu-BC extension
ADD https://pecl.php.net/get/apcu_bc-1.0.3.tgz /tmp/apcu_bc.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu-bc\
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
# configure and install
RUN docker-php-ext-configure apcu-bc\
&& docker-php-ext-install apcu-bc
# cleanup
RUN rm /tmp/apcu_bc.tar.gz
# Load APCU.ini before APC.ini
RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
# Install swoole
# First line fixes an error when installing pecl extensions. Found in https://github.com/docker-library/php/issues/233
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
pecl install swoole && \
docker-php-ext-enable swoole && \
apk del .phpize-deps
# Install composer
RUN php -r "readfile('https://getcomposer.org/installer');" | php
RUN chmod +x composer.phar
RUN mv composer.phar /usr/local/bin/composer
# Make home directory writable by anyone
RUN chmod 777 /home
VOLUME /home/shlink
WORKDIR /home/shlink
# Expose swoole port
EXPOSE 8080
CMD /usr/local/bin/composer update && \
# When restarting the container, swoole might think it is already in execution
# This forces the app to be started every second until the exit code is 0
until php ./vendor/bin/zend-expressive-swoole start; do sleep 1 ; done

View file

@ -6,3 +6,9 @@ services:
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
shlink_swoole:
user: 1000:1000
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro

View file

@ -26,6 +26,18 @@ services:
links:
- shlink_db
shlink_swoole:
container_name: shlink_swoole
build:
context: .
dockerfile: ./data/infra/swoole.Dockerfile
ports:
- "8080:8080"
volumes:
- ./:/home/shlink
links:
- shlink_db
shlink_db:
container_name: shlink_db
build:

View file

@ -1,2 +1,2 @@
#!/usr/bin/env bash
docker exec -it shlink_php /bin/sh -c "cd /home/shlink/www && $*"
docker exec -it shlink_swoole /bin/sh -c "$*"

View file

@ -23,7 +23,6 @@ return [
EntityManager::class => Factory\EntityManagerFactory::class,
GuzzleClient::class => InvokableFactory::class,
Cache::class => Factory\CacheFactory::class,
'Logger_Shlink' => Factory\LoggerFactory::class,
Filesystem::class => InvokableFactory::class,
Reader::class => ConfigAbstractFactory::class,
@ -31,6 +30,7 @@ return [
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
Middleware\CloseDbConnectionMiddleware::class => ConfigAbstractFactory::class,
IpAddress::class => Middleware\IpAddressMiddlewareFactory::class,
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
@ -78,6 +78,7 @@ return [
Template\Extension\TranslatorExtension::class => ['translator'],
Middleware\LocaleMiddleware::class => ['translator'],
Middleware\CloseDbConnectionMiddleware::class => ['em'],
IpGeolocation\IpApiLocationResolver::class => ['httpClient'],
IpGeolocation\GeoLite2LocationResolver::class => [Reader::class],

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exec;
use const PHP_SAPI;
use function Shlinkio\Shlink\Common\env;
abstract class ExecutionContext
{
public const WEB = 'shlink_web';
public const CLI = 'shlink_cli';
public static function currentContextIsSwoole(): bool
{
return PHP_SAPI === 'cli' && env('CURRENT_SHLINK_CONTEXT', self::WEB) === self::WEB;
}
}

View file

@ -27,6 +27,6 @@ class TranslatorFactory implements FactoryInterface
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config');
return Translator::factory(isset($config['translator']) ? $config['translator'] : []);
return Translator::factory($config['translator'] ?? []);
}
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Logger\Swoole;
use Interop\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Zend\Expressive\Swoole\Log;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class AccessLogFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->has('config') ? $container->get('config') : [];
$config = $config['zend-expressive-swoole']['swoole-http-server']['logger'] ?? [];
return new Log\Psr3AccessLogDecorator(
$this->getLogger($container, $config),
$this->getFormatter($container, $config),
$config['use-hostname-lookups'] ?? false
);
}
private function getLogger(ContainerInterface $container, array $config): LoggerInterface
{
$loggerName = $config['logger_name'] ?? LoggerInterface::class;
return $container->has($loggerName) ? $container->get($loggerName) : new Log\StdoutLogger();
}
private function getFormatter(ContainerInterface $container, array $config): Log\AccessLogFormatterInterface
{
if ($container->has(Log\AccessLogFormatterInterface::class)) {
return $container->get(Log\AccessLogFormatterInterface::class);
}
return new Log\AccessLogFormatter($config['format'] ?? Log\AccessLogFormatter::FORMAT_COMMON);
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class CloseDbConnectionMiddleware implements MiddlewareInterface
{
/** @var EntityManagerInterface */
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Process an incoming server request and return a response, optionally delegating
* response creation to a handler.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$handledRequest = $handler->handle($request);
$this->em->getConnection()->close();
$this->em->clear();
return $handledRequest;
}
}

View file

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Logger\Swoole;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use ReflectionObject;
use Shlinkio\Shlink\Common\Logger\Swoole\AccessLogFactory;
use Zend\Expressive\Swoole\Log\AccessLogFormatter;
use Zend\Expressive\Swoole\Log\AccessLogFormatterInterface;
use Zend\Expressive\Swoole\Log\Psr3AccessLogDecorator;
use Zend\Expressive\Swoole\Log\StdoutLogger;
use Zend\ServiceManager\ServiceManager;
use function is_string;
class AccessLogFactoryTest extends TestCase
{
/** @var AccessLogFactory */
private $factory;
public function setUp()
{
$this->factory = new AccessLogFactory();
}
/**
* @test
*/
public function createsService()
{
$service = ($this->factory)(new ServiceManager(), '');
$this->assertInstanceOf(Psr3AccessLogDecorator::class, $service);
}
/**
* @test
* @dataProvider provideLoggers
* @param array $config
* @param string|LoggerInterface $expectedLogger
*/
public function wrapsProperLogger(array $config, $expectedLogger)
{
$service = ($this->factory)(new ServiceManager(['services' => $config]), '');
$ref = new ReflectionObject($service);
$loggerProp = $ref->getProperty('logger');
$loggerProp->setAccessible(true);
$logger = $loggerProp->getValue($service);
if (is_string($expectedLogger)) {
$this->assertInstanceOf($expectedLogger, $logger);
} else {
$this->assertSame($expectedLogger, $logger);
}
}
public function provideLoggers(): iterable
{
yield 'without-any-logger' => [[], StdoutLogger::class];
yield 'with-standard-logger' => (function () {
$logger = new NullLogger();
return [[LoggerInterface::class => $logger], $logger];
})();
yield 'with-custom-logger' => (function () {
$logger = new NullLogger();
return [[
'config' => [
'zend-expressive-swoole' => [
'swoole-http-server' => [
'logger' => [
'logger_name' => 'my-logger',
],
],
],
],
'my-logger' => $logger,
], $logger];
})();
}
/**
* @test
* @dataProvider provideFormatters
* @param array $config
* @param string|AccessLogFormatterInterface $expectedFormatter
*/
public function wrappsProperFormatter(array $config, $expectedFormatter, string $expectedFormat)
{
$service = ($this->factory)(new ServiceManager(['services' => $config]), '');
$ref = new ReflectionObject($service);
$formatterProp = $ref->getProperty('formatter');
$formatterProp->setAccessible(true);
$formatter = $formatterProp->getValue($service);
$ref = new ReflectionObject($formatter);
$formatProp = $ref->getProperty('format');
$formatProp->setAccessible(true);
$format = $formatProp->getValue($formatter);
if (is_string($expectedFormatter)) {
$this->assertInstanceOf($expectedFormatter, $formatter);
} else {
$this->assertSame($expectedFormatter, $formatter);
}
$this->assertSame($expectedFormat, $format);
}
public function provideFormatters(): iterable
{
yield 'with-registered-formatter-and-default-format' => (function () {
$formatter = new AccessLogFormatter();
return [[AccessLogFormatterInterface::class => $formatter], $formatter, AccessLogFormatter::FORMAT_COMMON];
})();
yield 'with-registered-formatter-and-custom-format' => (function () {
$formatter = new AccessLogFormatter(AccessLogFormatter::FORMAT_AGENT);
return [[AccessLogFormatterInterface::class => $formatter], $formatter, AccessLogFormatter::FORMAT_AGENT];
})();
yield 'with-no-formatter-and-not-configured-format' => [
[],
AccessLogFormatter::class,
AccessLogFormatter::FORMAT_COMMON,
];
yield 'with-no-formatter-and-configured-format' => [[
'config' => [
'zend-expressive-swoole' => [
'swoole-http-server' => [
'logger' => [
'format' => AccessLogFormatter::FORMAT_COMBINED_DEBIAN,
],
],
],
],
], AccessLogFormatter::class, AccessLogFormatter::FORMAT_COMBINED_DEBIAN];
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Middleware;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Middleware\CloseDbConnectionMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
class CloseDbConnectionMiddlewareTest extends TestCase
{
/** @var CloseDbConnectionMiddleware */
private $middleware;
/** @var ObjectProphecy */
private $handler;
/** @var ObjectProphecy */
private $em;
public function setUp()
{
$this->handler = $this->prophesize(RequestHandlerInterface::class);
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->middleware = new CloseDbConnectionMiddleware($this->em->reveal());
}
/**
* @test
*/
public function connectionIsClosedWhenMiddlewareIsProcessed()
{
$req = ServerRequestFactory::fromGlobals();
$resp = new Response();
$conn = $this->prophesize(Connection::class);
$closeConn = $conn->close()->will(function () {
});
$getConn = $this->em->getConnection()->willReturn($conn->reveal());
$clear = $this->em->clear()->will(function () {
});
$handle = $this->handler->handle($req)->willReturn($resp);
$result = $this->middleware->process($req, $this->handler->reveal());
$this->assertSame($result, $resp);
$getConn->shouldHaveBeenCalledOnce();
$closeConn->shouldHaveBeenCalledOnce();
$clear->shouldHaveBeenCalledOnce();
$handle->shouldHaveBeenCalledOnce();
}
}

View file

@ -107,13 +107,7 @@ class UrlShortener implements UrlShortenerInterface
}
}
/**
* Tries to perform a GET request to provided url, returning true on success and false on failure
*
* @param UriInterface $url
* @return void
*/
private function checkUrlExists(UriInterface $url)
private function checkUrlExists(UriInterface $url): void
{
try {
$this->httpClient->request('GET', $url, ['allow_redirects' => [
@ -124,12 +118,6 @@ class UrlShortener implements UrlShortenerInterface
}
}
/**
* Generates the unique shortcode for an autoincrement ID
*
* @param float $id
* @return string
*/
private function convertAutoincrementIdToShortCode(float $id): string
{
$id += self::ID_INCREMENT; // Increment the Id so that the generated shortcode is not too short
@ -145,7 +133,7 @@ class UrlShortener implements UrlShortenerInterface
return $this->chars[(int) $id] . $code;
}
private function processCustomSlug($customSlug)
private function processCustomSlug(?string $customSlug): ?string
{
if ($customSlug === null) {
return null;

View file

@ -56,4 +56,5 @@
<file>config</file>
<file>public/index.php</file>
<exclude-pattern>config/params/*</exclude-pattern>
<exclude-pattern>public/index.php</exclude-pattern>
</ruleset>

View file

@ -2,8 +2,11 @@
declare(strict_types=1);
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\Common\Exec\ExecutionContext;
use Zend\Expressive\Application;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
putenv(sprintf('CURRENT_SHLINK_CONTEXT=%s', ExecutionContext::WEB));
$container->get(Application::class)->run();