diff --git a/.gitignore b/.gitignore index 933c25ee..4154e11b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .idea +bin/.rr.* +bin/rr build !docker/build composer.lock diff --git a/bin/roadrunner-worker.php b/bin/roadrunner-worker.php new file mode 100644 index 00000000..a8d3506a --- /dev/null +++ b/bin/roadrunner-worker.php @@ -0,0 +1,31 @@ +get(Application::class); + $worker = new PSR7Worker( + Worker::create(), + $container->get(ServerRequestFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(UploadedFileFactoryInterface::class), + ); + + while ($req = $worker->waitRequest()) { + try { + $worker->respond($app->handle($req)); + } catch (Throwable $throwable) { + $worker->getWorker()->error((string) $throwable); + } + } +})(); diff --git a/composer.json b/composer.json index 168418a5..f9d3e27a 100644 --- a/composer.json +++ b/composer.json @@ -44,11 +44,13 @@ "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.3", "shlinkio/shlink-common": "^5.0", - "shlinkio/shlink-config": "^2.0", + "shlinkio/shlink-config": "dev-main#24ccd64 as 2.1", "shlinkio/shlink-event-dispatcher": "^2.5", "shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-installer": "^8.1", "shlinkio/shlink-ip-geolocation": "^3.0", + "spiral/roadrunner": "^2.11", + "spiral/roadrunner-jobs": "^2.3", "symfony/console": "^6.1", "symfony/filesystem": "^6.1", "symfony/lock": "^6.1", diff --git a/config/autoload/mercure.local.php.dist b/config/autoload/mercure.local.php.dist index b10ad86e..e818404b 100644 --- a/config/autoload/mercure.local.php.dist +++ b/config/autoload/mercure.local.php.dist @@ -7,7 +7,7 @@ return [ 'mercure' => [ 'public_hub_url' => 'http://localhost:8001', 'internal_hub_url' => 'http://shlink_mercure_proxy', - 'jwt_secret' => 'mercure_jwt_key', + 'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error', ], ]; diff --git a/config/autoload/url-shortener.local.php.dist b/config/autoload/url-shortener.local.php.dist index 0069ffa9..1fffcd0d 100644 --- a/config/autoload/url-shortener.local.php.dist +++ b/config/autoload/url-shortener.local.php.dist @@ -2,14 +2,18 @@ declare(strict_types=1); -$isSwoole = extension_loaded('openswoole'); +use function Shlinkio\Shlink\Config\swooleIsInstalled; return [ 'url_shortener' => [ 'domain' => [ 'schema' => 'http', - 'hostname' => sprintf('localhost:%s', $isSwoole ? '8080' : '8000'), + 'hostname' => sprintf('localhost:%s', match (true) { + swooleIsInstalled() => '8080', // Swoole + PHP_SAPI === 'cli' => '8800', // Roadrunner + default => '8000', // FPM + }), ], 'auto_resolve_titles' => true, // 'multi_segment_slugs_enabled' => true, diff --git a/config/config.php b/config/config.php index 6c38707d..2763d23d 100644 --- a/config/config.php +++ b/config/config.php @@ -13,10 +13,11 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider; use function class_exists; use function Shlinkio\Shlink\Config\env; +use function Shlinkio\Shlink\Config\swooleIsInstalled; use const PHP_SAPI; -$isCli = PHP_SAPI === 'cli'; +$enableSwoole = PHP_SAPI === 'cli' && swooleIsInstalled(); $isTestEnv = env('APP_ENV') === 'test'; return (new ConfigAggregator\ConfigAggregator([ @@ -26,7 +27,7 @@ return (new ConfigAggregator\ConfigAggregator([ Mezzio\ConfigProvider::class, Mezzio\Router\ConfigProvider::class, Mezzio\Router\FastRouteRouter\ConfigProvider::class, - $isCli && class_exists(Swoole\ConfigProvider::class) + $enableSwoole && class_exists(Swoole\ConfigProvider::class) ? Swoole\ConfigProvider::class : new ConfigAggregator\ArrayProvider([]), ProblemDetails\ConfigProvider::class, diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml new file mode 100644 index 00000000..af667e0a --- /dev/null +++ b/config/roadrunner/.rr.dev.yml @@ -0,0 +1,41 @@ +version: '2.7' + +server: + command: 'php ../../bin/roadrunner-worker.php' + +http: + address: '0.0.0.0:8080' + static: + dir: ../../public + forbid: + - .php + - .htaccess + pool: + num_workers: 1 + supervisor: + max_worker_memory: 100 + +jobs: + pool: + num_workers: 2 + max_worker_memory: 100 + consume: { } + +logs: + mode: development + channels: + http: + level: debug # Log all http requests, set to info to disable + server: + level: debug # Everything written to worker stderr is logged + metrics: + level: debug + +reload: + enabled: true + interval: 1s + patterns: ['.php', '.yml', '.yaml'] + services: + http: + dirs: ['.'] + recursive: true diff --git a/config/roadrunner/.rr.yml b/config/roadrunner/.rr.yml new file mode 100644 index 00000000..ebfe9673 --- /dev/null +++ b/config/roadrunner/.rr.yml @@ -0,0 +1,32 @@ +version: '2.7' + +server: + command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php' + +http: + address: '0.0.0.0:8080' + static: + dir: ../../public + forbid: + - .php + - .htaccess + pool: + num_workers: 16 # TODO Make configurable + supervisor: + max_worker_memory: 100 + +jobs: + pool: + num_workers: 16 # TODO Make configurable + max_worker_memory: 100 + consume: { } + +logs: + mode: production + channels: + http: + level: info # Log all http requests, set to info to disable + server: + level: debug # Everything written to worker stderr is logged + metrics: + level: error diff --git a/data/infra/roadrunner.Dockerfile b/data/infra/roadrunner.Dockerfile new file mode 100644 index 00000000..7a68a7ae --- /dev/null +++ b/data/infra/roadrunner.Dockerfile @@ -0,0 +1,73 @@ +FROM php:8.1.9-alpine3.16 +MAINTAINER Alejandro Celaya + +ENV APCU_VERSION 5.1.21 +ENV PDO_SQLSRV_VERSION 5.10.1 +ENV MS_ODBC_SQL_VERSION 17.5.2.2 + +RUN apk update + +# Install common php extensions +RUN docker-php-ext-install pdo_mysql +RUN docker-php-ext-install calendar + +RUN apk add --no-cache oniguruma-dev +RUN docker-php-ext-install mbstring + +RUN apk add --no-cache sqlite-libs +RUN apk add --no-cache sqlite-dev +RUN docker-php-ext-install pdo_sqlite + +RUN apk add --no-cache icu-dev +RUN docker-php-ext-install intl + +RUN apk add --no-cache libzip-dev zlib-dev +RUN docker-php-ext-install zip + +RUN apk add --no-cache libpng-dev +RUN docker-php-ext-install gd + +RUN apk add --no-cache postgresql-dev +RUN docker-php-ext-install pdo_pgsql + +RUN docker-php-ext-install sockets +RUN docker-php-ext-install bcmath + +# Install APCu extension +ADD https://pecl.php.net/get/apcu-$APCU_VERSION.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 \ + && docker-php-ext-configure apcu \ + && docker-php-ext-install apcu \ + && rm /tmp/apcu.tar.gz \ + && rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \ + && echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini + +# Install pcov and sqlsrv driver +RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ + apk add --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ + apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \ + pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \ + docker-php-ext-enable pdo_sqlsrv pcov && \ + apk del .phpize-deps && \ + rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk + +# Install composer +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +# Make home directory writable by anyone +RUN chmod 777 /home + +VOLUME /home/shlink +WORKDIR /home/shlink + +# Expose roadrunner port +EXPOSE 8080 + +CMD \ + # Install dependencies if the vendor dir does not exist + if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \ + # Download roadrunner binary + if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --location bin/ && chmod +x bin/rr ; fi && \ + # This forces the app to be started every second until the exit code is 0 + until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done diff --git a/docker-compose.override.yml.dist b/docker-compose.override.yml.dist index 990d1b5d..1c5409c6 100644 --- a/docker-compose.override.yml.dist +++ b/docker-compose.override.yml.dist @@ -13,6 +13,12 @@ services: - /etc/passwd:/etc/passwd:ro - /etc/group:/etc/group:ro + shlink_roadrunner: + user: 1000:1000 + volumes: + - /etc/passwd:/etc/passwd:ro + - /etc/group:/etc/group:ro + shlink_db_mysql: user: 1000:1000 volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 739c0079..8293ab03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -73,6 +73,30 @@ services: extra_hosts: - 'host.docker.internal:host-gateway' + shlink_roadrunner: + container_name: shlink_roadrunner + build: + context: . + dockerfile: ./data/infra/roadrunner.Dockerfile + ports: + - "8800:8080" + volumes: + - ./:/home/shlink + - ./data/infra/php.ini:/usr/local/etc/php/php.ini + links: + - shlink_db_mysql + - shlink_db_postgres + - shlink_db_maria + - shlink_db_ms + - shlink_redis + - shlink_mercure + - shlink_mercure_proxy + - shlink_rabbitmq + environment: + LC_ALL: C + extra_hosts: + - 'host.docker.internal:host-gateway' + shlink_db_mysql: container_name: shlink_db_mysql image: mysql:5.7 @@ -144,8 +168,8 @@ services: - "3080:80" environment: SERVER_NAME: ":80" - MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key - MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key + MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error + MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000" shlink_rabbitmq: