Merge pull request #1523 from acelaya-forks/feature/roadrunner-support

Feature/roadrunner support
This commit is contained in:
Alejandro Celaya 2022-08-27 19:48:48 +02:00 committed by GitHub
commit e2eed8a728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 398 additions and 56 deletions

View File

@ -1,3 +1,4 @@
bin/rr
config/autoload/*local* config/autoload/*local*
data/infra data/infra
data/cache/* data/cache/*
@ -22,4 +23,4 @@ infection*
**/test* **/test*
build* build*
**/.* **/.*
bin/helper !config/roadrunner/.rr.yml

View File

@ -11,7 +11,7 @@ inputs:
required: true required: true
php-extensions: php-extensions:
description: 'The PHP extensions to install' description: 'The PHP extensions to install'
required: true required: false
default: '' default: ''
extensions-cache-key: extensions-cache-key:
description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled' description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled'

View File

@ -29,16 +29,32 @@ jobs:
with: with:
test-group: unit test-group: unit
api-tests:
uses: './.github/workflows/ci-tests.yml'
with:
test-group: api
cli-tests: cli-tests:
uses: './.github/workflows/ci-tests.yml' uses: './.github/workflows/ci-tests.yml'
with: with:
test-group: cli test-group: cli
openswoole-api-tests:
uses: './.github/workflows/ci-tests.yml'
with:
test-group: api
roadrunner-api-tests:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: [ '8.1' ]
steps:
- uses: actions/checkout@v3
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
- run: composer install --no-interaction --prefer-dist
- run: ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr
- run: composer test:api:rr
sqlite-db-tests: sqlite-db-tests:
uses: './.github/workflows/ci-db-tests.yml' uses: './.github/workflows/ci-db-tests.yml'
with: with:
@ -80,7 +96,7 @@ jobs:
api-mutation-tests: api-mutation-tests:
needs: needs:
- api-tests - openswoole-api-tests
uses: './.github/workflows/ci-mutation-tests.yml' uses: './.github/workflows/ci-mutation-tests.yml'
with: with:
test-group: api test-group: api
@ -95,7 +111,7 @@ jobs:
upload-coverage: upload-coverage:
needs: needs:
- unit-tests - unit-tests
- api-tests - openswoole-api-tests
- cli-tests - cli-tests
- sqlite-db-tests - sqlite-db-tests
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04

View File

@ -8,9 +8,19 @@ on:
- 'v*' - 'v*'
jobs: jobs:
build: build-openswool:
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
secrets: inherit secrets: inherit
with: with:
image-name: shlinkio/shlink image-name: shlinkio/shlink
version-arg-name: SHLINK_VERSION version-arg-name: SHLINK_VERSION
build-roadrunner:
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
secrets: inherit
with:
image-name: shlinkio/shlink
version-arg-name: SHLINK_VERSION
tags-suffix: roadrunner
extra-build-args: |
SHLINK_RUNTIME=rr

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
.idea .idea
bin/.rr.*
bin/rr
config/roadrunner/.pid
build build
!docker/build !docker/build
composer.lock composer.lock

View File

@ -2,6 +2,8 @@ FROM php:8.1.9-alpine3.16 as base
ARG SHLINK_VERSION=latest ARG SHLINK_VERSION=latest
ENV SHLINK_VERSION ${SHLINK_VERSION} ENV SHLINK_VERSION ${SHLINK_VERSION}
ARG SHLINK_RUNTIME=openswoole
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
ENV OPENSWOOLE_VERSION 4.11.1 ENV OPENSWOOLE_VERSION 4.11.1
ENV PDO_SQLSRV_VERSION 5.10.1 ENV PDO_SQLSRV_VERSION 5.10.1
ENV MS_ODBC_SQL_VERSION 17.5.2.2 ENV MS_ODBC_SQL_VERSION 17.5.2.2
@ -22,8 +24,10 @@ RUN \
# Install openswoole and sqlsrv driver for x86_64 builds # Install openswoole and sqlsrv driver for x86_64 builds
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \ RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
pecl install openswoole-${OPENSWOOLE_VERSION} && \ if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
docker-php-ext-enable openswoole && \ pecl install openswoole-${OPENSWOOLE_VERSION} && \
docker-php-ext-enable openswoole ; \
fi; \
if [ $(uname -m) == "x86_64" ]; then \ if [ $(uname -m) == "x86_64" ]; then \
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --no-cache --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ apk add --no-cache --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
@ -38,7 +42,12 @@ FROM base as builder
COPY . . COPY . .
COPY --from=composer:2 /usr/bin/composer ./composer.phar COPY --from=composer:2 /usr/bin/composer ./composer.phar
RUN apk add --no-cache git && \ RUN apk add --no-cache git && \
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \ php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interactionc ; \
elif [ $SHLINK_RUNTIME == 'rr' ]; then \
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
fi; \
php composer.phar clear-cache && \ php composer.phar clear-cache && \
rm -r docker composer.* && \ rm -r docker composer.* && \
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
@ -49,9 +58,12 @@ FROM base
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>" LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
COPY --from=builder /etc/shlink . COPY --from=builder /etc/shlink .
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
php ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; \
fi;
# Expose default openswoole port # Expose default port
EXPOSE 8080 EXPOSE 8080
# Copy config specific for the image # Copy config specific for the image

View File

@ -15,7 +15,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u
- [Full documentation](#full-documentation) - [Full documentation](#full-documentation)
- [Docker image](#docker-image) - [Docker image](#docker-image)
- [Self hosted](#self-hosted) - [Self-hosted](#self-hosted)
- [Download](#download) - [Download](#download)
- [Configure](#configure) - [Configure](#configure)
- [Using shlink](#using-shlink) - [Using shlink](#using-shlink)

32
bin/roadrunner-worker.php Normal file
View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
use Mezzio\Application;
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\EventDispatcher\RoadRunner\RoadRunnerTaskConsumerToListener;
use Spiral\RoadRunner\Http\PSR7Worker;
use function Shlinkio\Shlink\Config\env;
(static function (): void {
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
$rrMode = env('RR_MODE');
if ($rrMode === 'http') {
// This was spin-up as a web worker
$app = $container->get(Application::class);
$worker = $container->get(PSR7Worker::class);
while ($req = $worker->waitRequest()) {
try {
$worker->respond($app->handle($req));
} catch (Throwable $e) {
$worker->getWorker()->error((string) $e);
}
}
} else {
$container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks();
}
})();

View File

@ -1,25 +1,38 @@
#!/usr/bin/env sh #!/usr/bin/env sh
export APP_ENV=test export APP_ENV=test
export DB_DRIVER=postgres
export TEST_ENV=api export TEST_ENV=api
export GENERATE_COVERAGE=${GENERATE_COVERAGE:-"no"} export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}"
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
# Reset logs # Reset logs
OUTPUT_LOGS=data/log/api-tests/output.log
rm -rf data/log/api-tests rm -rf data/log/api-tests
mkdir data/log/api-tests mkdir data/log/api-tests
touch data/log/api-tests/output.log touch $OUTPUT_LOGS
# Try to stop server just in case it hanged in last execution # Try to stop server just in case it hanged in last execution
vendor/bin/laminas mezzio:swoole:stop [ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f
echo 'Starting server...' echo 'Starting server...'
vendor/bin/laminas mezzio:swoole:start -d [ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d
sleep 2 [ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \
-o=http.address=0.0.0.0:9999 \
-o=logs.encoding=json \
-o=logs.channels.http.encoding=json \
-o=logs.channels.server.encoding=json \
-o=logs.output="${PWD}/${OUTPUT_LOGS}" \
-o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \
-o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" &
sleep 2 # Let's give the server a couple of seconds to start
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $* vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $*
testsExitCode=$? testsExitCode=$?
vendor/bin/laminas mezzio:swoole:stop [ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999
# Exit this script with the same code as the tests. If tests failed, this script has to fail # Exit this script with the same code as the tests. If tests failed, this script has to fail
exit $testsExitCode exit $testsExitCode

View File

@ -24,6 +24,7 @@ rsync -av * "${builtContent}" \
--exclude=*docker* \ --exclude=*docker* \
--exclude=Dockerfile \ --exclude=Dockerfile \
--include=.htaccess \ --include=.htaccess \
--include=config/roadrunner/.rr.yml \
--exclude-from=./.dockerignore --exclude-from=./.dockerignore
cd "${builtContent}" cd "${builtContent}"
@ -36,6 +37,9 @@ ${composerBin} install --no-dev --prefer-dist $composerFlags
if [[ $noSwoole ]]; then if [[ $noSwoole ]]; then
# If generating a dist not for openswoole, uninstall mezzio-swoole # If generating a dist not for openswoole, uninstall mezzio-swoole
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags ${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
else
# If generating a dist for openswoole, uninstall RoadRunner
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev $composerFlags
fi fi
# Delete development files # Delete development files

View File

@ -44,11 +44,13 @@
"pugx/shortid-php": "^1.0", "pugx/shortid-php": "^1.0",
"ramsey/uuid": "^4.3", "ramsey/uuid": "^4.3",
"shlinkio/shlink-common": "^5.0", "shlinkio/shlink-common": "^5.0",
"shlinkio/shlink-config": "^2.0", "shlinkio/shlink-config": "dev-main#33004e6 as 2.1",
"shlinkio/shlink-event-dispatcher": "^2.5", "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6",
"shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-importer": "^4.0",
"shlinkio/shlink-installer": "^8.1", "shlinkio/shlink-installer": "^8.1",
"shlinkio/shlink-ip-geolocation": "^3.0", "shlinkio/shlink-ip-geolocation": "^3.0",
"spiral/roadrunner": "^2.11",
"spiral/roadrunner-jobs": "^2.3",
"symfony/console": "^6.1", "symfony/console": "^6.1",
"symfony/filesystem": "^6.1", "symfony/filesystem": "^6.1",
"symfony/lock": "^6.1", "symfony/lock": "^6.1",
@ -120,6 +122,7 @@
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite", "test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite", "test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
"test:api": "bin/test/run-api-tests.sh", "test:api": "bin/test/run-api-tests.sh",
"test:api:rr": "TEST_RUNTIME=rr bin/test/run-api-tests.sh",
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api", "test:api:ci": "GENERATE_COVERAGE=yes composer test:api",
"test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api", "test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api",
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml", "test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml",

View File

@ -3,12 +3,22 @@
declare(strict_types=1); declare(strict_types=1);
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Mezzio\Container; use Mezzio\Container;
use Psr\Http\Client\ClientInterface; use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\WorkerInterface;
return [ return [
'dependencies' => [ 'dependencies' => [
'factories' => [
PSR7Worker::class => ConfigAbstractFactory::class,
],
'delegators' => [ 'delegators' => [
Mezzio\Application::class => [ Mezzio\Application::class => [
Container\ApplicationConfigInjectionDelegator::class, Container\ApplicationConfigInjectionDelegator::class,
@ -26,4 +36,13 @@ return [
], ],
], ],
ConfigAbstractFactory::class => [
PSR7Worker::class => [
WorkerInterface::class,
ServerRequestFactoryInterface::class,
StreamFactoryInterface::class,
UploadedFileFactoryInterface::class,
],
],
]; ];

View File

@ -7,7 +7,7 @@ return [
'mercure' => [ 'mercure' => [
'public_hub_url' => 'http://localhost:8001', 'public_hub_url' => 'http://localhost:8001',
'internal_hub_url' => 'http://shlink_mercure_proxy', 'internal_hub_url' => 'http://shlink_mercure_proxy',
'jwt_secret' => 'mercure_jwt_key', 'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
], ],
]; ];

View File

@ -2,14 +2,19 @@
declare(strict_types=1); declare(strict_types=1);
$isSwoole = extension_loaded('openswoole'); use function Shlinkio\Shlink\Config\runningInOpenswoole;
use function Shlinkio\Shlink\Config\runningInRoadRunner;
return [ return [
'url_shortener' => [ 'url_shortener' => [
'domain' => [ 'domain' => [
'schema' => 'http', 'schema' => 'http',
'hostname' => sprintf('localhost:%s', $isSwoole ? '8080' : '8000'), 'hostname' => sprintf('localhost:%s', match (true) {
runningInRoadRunner() => '8800',
runningInOpenswoole() => '8080',
default => '8000',
}),
], ],
'auto_resolve_titles' => true, 'auto_resolve_titles' => true,
// 'multi_segment_slugs_enabled' => true, // 'multi_segment_slugs_enabled' => true,

View File

@ -13,11 +13,13 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
use function class_exists; use function class_exists;
use function Shlinkio\Shlink\Config\env; use function Shlinkio\Shlink\Config\env;
use function Shlinkio\Shlink\Config\openswooleIsInstalled;
use function Shlinkio\Shlink\Config\runningInRoadRunner;
use const PHP_SAPI; use const PHP_SAPI;
$isCli = PHP_SAPI === 'cli';
$isTestEnv = env('APP_ENV') === 'test'; $isTestEnv = env('APP_ENV') === 'test';
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
return (new ConfigAggregator\ConfigAggregator([ return (new ConfigAggregator\ConfigAggregator([
! $isTestEnv ! $isTestEnv
@ -26,7 +28,7 @@ return (new ConfigAggregator\ConfigAggregator([
Mezzio\ConfigProvider::class, Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class, Mezzio\Router\ConfigProvider::class,
Mezzio\Router\FastRouteRouter\ConfigProvider::class, Mezzio\Router\FastRouteRouter\ConfigProvider::class,
$isCli && class_exists(Swoole\ConfigProvider::class) $enableSwoole && class_exists(Swoole\ConfigProvider::class)
? Swoole\ConfigProvider::class ? Swoole\ConfigProvider::class
: new ConfigAggregator\ArrayProvider([]), : new ConfigAggregator\ArrayProvider([]),
ProblemDetails\ConfigProvider::class, ProblemDetails\ConfigProvider::class,

View File

@ -0,0 +1,49 @@
version: '2.7'
rpc:
listen: tcp://127.0.0.1:6001
server:
command: 'php ../../bin/roadrunner-worker.php'
http:
address: '0.0.0.0:8080'
middleware: ['static']
static:
dir: '../../public'
forbid: ['.php', '.htaccess']
pool:
num_workers: 16
jobs:
pool:
num_workers: 16
timeout: 300
consume: ['shlink']
pipelines:
shlink:
driver: memory
config:
priority: 10
prefetch: 10
logs:
mode: development
channels:
http:
level: debug
server:
level: debug
metrics:
level: debug
reload:
interval: 1s
patterns: ['.php']
services:
http:
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
recursive: true
jobs:
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
recursive: true

36
config/roadrunner/.rr.yml Normal file
View File

@ -0,0 +1,36 @@
version: '2.7'
rpc:
listen: tcp://127.0.0.1:6001
server:
command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php'
http:
address: '0.0.0.0:${PORT}'
middleware: ['static']
static:
dir: '../../public'
forbid: ['.php', '.htaccess']
pool:
num_workers: ${WEB_WORKER_NUM}
jobs:
timeout: 300 # 5 minutes
pool:
num_workers: ${TASK_WORKER_NUM}
consume: ['shlink']
pipelines:
shlink:
driver: memory
config:
priority: 10
prefetch: 10
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

View File

@ -10,8 +10,8 @@ use Psr\Container\ContainerInterface;
use function register_shutdown_function; use function register_shutdown_function;
use function sprintf; use function sprintf;
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST; use const ShlinkioTest\Shlink\API_TESTS_HOST;
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; use const ShlinkioTest\Shlink\API_TESTS_PORT;
/** @var ContainerInterface $container */ /** @var ContainerInterface $container */
$container = require __DIR__ . '/../container.php'; $container = require __DIR__ . '/../container.php';
@ -24,7 +24,7 @@ $httpClient = $container->get('shlink_test_api_client');
register_shutdown_function(function () use ($httpClient): void { register_shutdown_function(function () use ($httpClient): void {
$httpClient->request( $httpClient->request(
'GET', 'GET',
sprintf('http://%s:%s/api-tests/stop-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT), sprintf('http://%s:%s/api-tests/stop-coverage', API_TESTS_HOST, API_TESTS_PORT),
); );
}); });

View File

@ -4,5 +4,5 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink; namespace ShlinkioTest\Shlink;
const SWOOLE_TESTING_HOST = '127.0.0.1'; const API_TESTS_HOST = '127.0.0.1';
const SWOOLE_TESTING_PORT = 9999; const API_TESTS_PORT = 9999;

View File

@ -34,8 +34,8 @@ use function Shlinkio\Shlink\Config\env;
use function sprintf; use function sprintf;
use function sys_get_temp_dir; use function sys_get_temp_dir;
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST; use const ShlinkioTest\Shlink\API_TESTS_HOST;
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; use const ShlinkioTest\Shlink\API_TESTS_PORT;
$isApiTest = env('TEST_ENV') === 'api'; $isApiTest = env('TEST_ENV') === 'api';
$isCliTest = env('TEST_ENV') === 'cli'; $isCliTest = env('TEST_ENV') === 'cli';
@ -136,8 +136,8 @@ return [
'mezzio-swoole' => [ 'mezzio-swoole' => [
'enable_coroutine' => false, 'enable_coroutine' => false,
'swoole-http-server' => [ 'swoole-http-server' => [
'host' => SWOOLE_TESTING_HOST, 'host' => API_TESTS_HOST,
'port' => SWOOLE_TESTING_PORT, 'port' => API_TESTS_PORT,
'process-name' => 'shlink_test', 'process-name' => 'shlink_test',
'options' => [ 'options' => [
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid', 'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
@ -188,7 +188,7 @@ return [
'dependencies' => [ 'dependencies' => [
'services' => [ 'services' => [
'shlink_test_api_client' => new Client([ 'shlink_test_api_client' => new Client([
'base_uri' => sprintf('http://%s:%s/', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT), 'base_uri' => sprintf('http://%s:%s/', API_TESTS_HOST, API_TESTS_PORT),
'http_errors' => false, 'http_errors' => false,
]), ]),
], ],

View File

@ -0,0 +1,73 @@
FROM php:8.1.9-alpine3.16
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
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 --no-interaction --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

View File

@ -13,6 +13,12 @@ services:
- /etc/passwd:/etc/passwd:ro - /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group: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: shlink_db_mysql:
user: 1000:1000 user: 1000:1000
volumes: volumes:

View File

@ -73,6 +73,30 @@ services:
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - '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: shlink_db_mysql:
container_name: shlink_db_mysql container_name: shlink_db_mysql
image: mysql:5.7 image: mysql:5.7
@ -144,8 +168,8 @@ services:
- "3080:80" - "3080:80"
environment: environment:
SERVER_NAME: ":80" SERVER_NAME: ":80"
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key 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" MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000"
shlink_rabbitmq: shlink_rabbitmq:

View File

@ -6,11 +6,14 @@ namespace Shlinkio\Shlink;
use Shlinkio\Shlink\Common\Logger\LoggerType; use Shlinkio\Shlink\Common\Logger\LoggerType;
use function Shlinkio\Shlink\Config\runningInRoadRunner;
return [ return [
'logger' => [ 'logger' => [
'Shlink' => [ 'Shlink' => [
'type' => LoggerType::STREAM->value, 'type' => LoggerType::STREAM->value,
'destination' => runningInRoadRunner() ? 'php://stderr' : 'php://stdout',
], ],
], ],

View File

@ -31,6 +31,15 @@ if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then
/usr/sbin/crond & /usr/sbin/crond &
fi fi
# When restarting the container, openswoole might think it is already in execution # RoadRunner config needs these to have been set, so falling back to default values if not set yet
# This forces the app to be started every second until the exit code is 0 export PORT="${PORT:-"8765"}"
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"16"}"
export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"16"}"
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then
# When restarting the container, openswoole 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/laminas mezzio:swoole:start; do sleep 1 ; done
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then
./bin/rr serve -c config/roadrunner/.rr.yml
fi

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Run docker containers if they are not up yet # Run docker containers if they are not up yet
if ! [[ $(docker ps | grep shlink_swoole) ]]; then if ! [[ $(docker ps | grep shlink) ]]; then
docker-compose up -d docker-compose up -d
fi fi

View File

@ -5,10 +5,11 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\EventDispatcher\Event; namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
use JsonSerializable; use JsonSerializable;
use Shlinkio\Shlink\EventDispatcher\Util\JsonUnserializable;
abstract class AbstractVisitEvent implements JsonSerializable abstract class AbstractVisitEvent implements JsonSerializable, JsonUnserializable
{ {
public function __construct(public readonly string $visitId) final public function __construct(public readonly string $visitId)
{ {
} }
@ -16,4 +17,9 @@ abstract class AbstractVisitEvent implements JsonSerializable
{ {
return ['visitId' => $this->visitId]; return ['visitId' => $this->visitId];
} }
public static function fromPayload(array $payload): self
{
return new static($payload['visitId'] ?? '');
}
} }

View File

@ -5,8 +5,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\EventDispatcher\Event; namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
use JsonSerializable; use JsonSerializable;
use Shlinkio\Shlink\EventDispatcher\Util\JsonUnserializable;
final class ShortUrlCreated implements JsonSerializable final class ShortUrlCreated implements JsonSerializable, JsonUnserializable
{ {
public function __construct(public readonly string $shortUrlId) public function __construct(public readonly string $shortUrlId)
{ {
@ -18,4 +19,9 @@ final class ShortUrlCreated implements JsonSerializable
'shortUrlId' => $this->shortUrlId, 'shortUrlId' => $this->shortUrlId,
]; ];
} }
public static function fromPayload(array $payload): self
{
return new self($payload['shortUrlId'] ?? '');
}
} }

View File

@ -6,8 +6,18 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
final class UrlVisited extends AbstractVisitEvent final class UrlVisited extends AbstractVisitEvent
{ {
public function __construct(string $visitId, public readonly ?string $originalIpAddress = null) private ?string $originalIpAddress = null;
public static function withOriginalIpAddress(string $visitId, ?string $originalIpAddress): self
{ {
parent::__construct($visitId); $instance = new self($visitId);
$instance->originalIpAddress = $originalIpAddress;
return $instance;
}
public function originalIpAddress(): ?string
{
return $this->originalIpAddress;
} }
} }

View File

@ -41,7 +41,7 @@ class LocateVisit
return; return;
} }
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress, $visit); $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
$this->eventDispatcher->dispatch(new VisitLocated($visitId)); $this->eventDispatcher->dispatch(new VisitLocated($visitId));
} }

View File

@ -72,6 +72,6 @@ class VisitsTracker implements VisitsTrackerInterface
$this->em->persist($visit); $this->em->persist($visit);
$this->em->flush(); $this->em->flush();
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->remoteAddress)); $this->eventDispatcher->dispatch(UrlVisited::withOriginalIpAddress($visit->getId(), $visitor->remoteAddress));
} }
} }

View File

@ -193,7 +193,7 @@ class LocateVisitTest extends TestCase
{ {
$ipAddr = $originalIpAddress ?? $visit->getRemoteAddr(); $ipAddr = $originalIpAddress ?? $visit->getRemoteAddr();
$location = new Location('', '', '', '', 0.0, 0.0, ''); $location = new Location('', '', '', '', 0.0, 0.0, '');
$event = new UrlVisited('123', $originalIpAddress); $event = UrlVisited::withOriginalIpAddress('123', $originalIpAddress);
$findVisit = $this->em->find(Visit::class, '123')->willReturn($visit); $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
$flush = $this->em->flush()->will(function (): void { $flush = $this->em->flush()->will(function (): void {

View File

@ -11,14 +11,14 @@ use Psr\Http\Server\RequestHandlerInterface;
class DropDefaultDomainFromRequestMiddleware implements MiddlewareInterface class DropDefaultDomainFromRequestMiddleware implements MiddlewareInterface
{ {
public function __construct(private string $defaultDomain) public function __construct(private readonly string $defaultDomain)
{ {
} }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
/** @var array $body */ /** @var array $body */
$body = $request->getParsedBody(); $body = $request->getParsedBody() ?? [];
$request = $request->withQueryParams($this->sanitizeDomainFromPayload($request->getQueryParams())) $request = $request->withQueryParams($this->sanitizeDomainFromPayload($request->getQueryParams()))
->withParsedBody($this->sanitizeDomainFromPayload($body)); ->withParsedBody($this->sanitizeDomainFromPayload($body));