diff --git a/.dockerignore b/.dockerignore index 870f3610..beca6373 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +bin/rr config/autoload/*local* data/infra data/cache/* @@ -22,4 +23,4 @@ infection* **/test* build* **/.* -bin/helper +!config/roadrunner/.rr.yml diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index eb7c8979..78cbdf1c 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -11,7 +11,7 @@ inputs: required: true php-extensions: description: 'The PHP extensions to install' - required: true + required: false default: '' extensions-cache-key: description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57985e8a..f9d3660e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,16 +29,32 @@ jobs: with: test-group: unit - api-tests: - uses: './.github/workflows/ci-tests.yml' - with: - test-group: api - cli-tests: uses: './.github/workflows/ci-tests.yml' with: 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: uses: './.github/workflows/ci-db-tests.yml' with: @@ -80,7 +96,7 @@ jobs: api-mutation-tests: needs: - - api-tests + - openswoole-api-tests uses: './.github/workflows/ci-mutation-tests.yml' with: test-group: api @@ -95,7 +111,7 @@ jobs: upload-coverage: needs: - unit-tests - - api-tests + - openswoole-api-tests - cli-tests - sqlite-db-tests runs-on: ubuntu-22.04 diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/docker-image-build.yml index cbd8b213..11396f3f 100644 --- a/.github/workflows/docker-image-build.yml +++ b/.github/workflows/docker-image-build.yml @@ -8,9 +8,19 @@ on: - 'v*' jobs: - build: + build-openswool: uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main secrets: inherit with: image-name: shlinkio/shlink 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 diff --git a/.gitignore b/.gitignore index 933c25ee..daea5f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ .idea +bin/.rr.* +bin/rr +config/roadrunner/.pid build !docker/build composer.lock diff --git a/Dockerfile b/Dockerfile index 2944db45..2835d75f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,8 @@ FROM php:8.1.9-alpine3.16 as base ARG SHLINK_VERSION=latest ENV SHLINK_VERSION ${SHLINK_VERSION} +ARG SHLINK_RUNTIME=openswoole +ENV SHLINK_RUNTIME ${SHLINK_RUNTIME} ENV OPENSWOOLE_VERSION 4.11.1 ENV PDO_SQLSRV_VERSION 5.10.1 ENV MS_ODBC_SQL_VERSION 17.5.2.2 @@ -22,8 +24,10 @@ RUN \ # Install openswoole and sqlsrv driver for x86_64 builds RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \ - pecl install openswoole-${OPENSWOOLE_VERSION} && \ - docker-php-ext-enable openswoole && \ + if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \ + pecl install openswoole-${OPENSWOOLE_VERSION} && \ + docker-php-ext-enable openswoole ; \ + fi; \ 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 && \ apk add --no-cache --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ @@ -38,7 +42,12 @@ FROM base as builder COPY . . COPY --from=composer:2 /usr/bin/composer ./composer.phar 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 && \ rm -r docker composer.* && \ sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php @@ -49,9 +58,12 @@ FROM base LABEL maintainer="Alejandro Celaya " 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 # Copy config specific for the image diff --git a/README.md b/README.md index 1fe3b89c..bb99634e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u - [Full documentation](#full-documentation) - [Docker image](#docker-image) -- [Self hosted](#self-hosted) +- [Self-hosted](#self-hosted) - [Download](#download) - [Configure](#configure) - [Using shlink](#using-shlink) diff --git a/bin/roadrunner-worker.php b/bin/roadrunner-worker.php new file mode 100644 index 00000000..c4a89a85 --- /dev/null +++ b/bin/roadrunner-worker.php @@ -0,0 +1,32 @@ +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(); + } +})(); diff --git a/bin/test/run-api-tests.sh b/bin/test/run-api-tests.sh index 3f6e27e6..1cbf948a 100755 --- a/bin/test/run-api-tests.sh +++ b/bin/test/run-api-tests.sh @@ -1,25 +1,38 @@ #!/usr/bin/env sh + export APP_ENV=test -export DB_DRIVER=postgres 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 +OUTPUT_LOGS=data/log/api-tests/output.log rm -rf 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 -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...' -vendor/bin/laminas mezzio:swoole:start -d -sleep 2 +[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d +[ "$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 $* 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 $testsExitCode diff --git a/build.sh b/build.sh index e274210a..d9cda64d 100755 --- a/build.sh +++ b/build.sh @@ -24,6 +24,7 @@ rsync -av * "${builtContent}" \ --exclude=*docker* \ --exclude=Dockerfile \ --include=.htaccess \ + --include=config/roadrunner/.rr.yml \ --exclude-from=./.dockerignore cd "${builtContent}" @@ -36,6 +37,9 @@ ${composerBin} install --no-dev --prefer-dist $composerFlags if [[ $noSwoole ]]; then # If generating a dist not for openswoole, uninstall mezzio-swoole ${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 # Delete development files diff --git a/composer.json b/composer.json index 168418a5..d8b2874f 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-event-dispatcher": "^2.5", + "shlinkio/shlink-config": "dev-main#33004e6 as 2.1", + "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", "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", @@ -120,6 +122,7 @@ "test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite", "test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite", "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: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", diff --git a/config/autoload/dependencies.global.php b/config/autoload/dependencies.global.php index dbc553f1..657caffb 100644 --- a/config/autoload/dependencies.global.php +++ b/config/autoload/dependencies.global.php @@ -3,12 +3,22 @@ declare(strict_types=1); use GuzzleHttp\Client; +use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Mezzio\Container; 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 [ 'dependencies' => [ + 'factories' => [ + PSR7Worker::class => ConfigAbstractFactory::class, + ], + 'delegators' => [ Mezzio\Application::class => [ Container\ApplicationConfigInjectionDelegator::class, @@ -26,4 +36,13 @@ return [ ], ], + ConfigAbstractFactory::class => [ + PSR7Worker::class => [ + WorkerInterface::class, + ServerRequestFactoryInterface::class, + StreamFactoryInterface::class, + UploadedFileFactoryInterface::class, + ], + ], + ]; 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..f49570e1 100644 --- a/config/autoload/url-shortener.local.php.dist +++ b/config/autoload/url-shortener.local.php.dist @@ -2,14 +2,19 @@ declare(strict_types=1); -$isSwoole = extension_loaded('openswoole'); +use function Shlinkio\Shlink\Config\runningInOpenswoole; +use function Shlinkio\Shlink\Config\runningInRoadRunner; return [ 'url_shortener' => [ 'domain' => [ '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, // 'multi_segment_slugs_enabled' => true, diff --git a/config/config.php b/config/config.php index 6c38707d..15a45348 100644 --- a/config/config.php +++ b/config/config.php @@ -13,11 +13,13 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider; use function class_exists; use function Shlinkio\Shlink\Config\env; +use function Shlinkio\Shlink\Config\openswooleIsInstalled; +use function Shlinkio\Shlink\Config\runningInRoadRunner; use const PHP_SAPI; -$isCli = PHP_SAPI === 'cli'; $isTestEnv = env('APP_ENV') === 'test'; +$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner(); return (new ConfigAggregator\ConfigAggregator([ ! $isTestEnv @@ -26,7 +28,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..7adf4520 --- /dev/null +++ b/config/roadrunner/.rr.dev.yml @@ -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 diff --git a/config/roadrunner/.rr.yml b/config/roadrunner/.rr.yml new file mode 100644 index 00000000..d44801ee --- /dev/null +++ b/config/roadrunner/.rr.yml @@ -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 diff --git a/config/test/bootstrap_api_tests.php b/config/test/bootstrap_api_tests.php index 52c9d4fb..bc119284 100644 --- a/config/test/bootstrap_api_tests.php +++ b/config/test/bootstrap_api_tests.php @@ -10,8 +10,8 @@ use Psr\Container\ContainerInterface; use function register_shutdown_function; use function sprintf; -use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST; -use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; +use const ShlinkioTest\Shlink\API_TESTS_HOST; +use const ShlinkioTest\Shlink\API_TESTS_PORT; /** @var ContainerInterface $container */ $container = require __DIR__ . '/../container.php'; @@ -24,7 +24,7 @@ $httpClient = $container->get('shlink_test_api_client'); register_shutdown_function(function () use ($httpClient): void { $httpClient->request( '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), ); }); diff --git a/config/test/constants.php b/config/test/constants.php index a2c880fc..c767abc9 100644 --- a/config/test/constants.php +++ b/config/test/constants.php @@ -4,5 +4,5 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink; -const SWOOLE_TESTING_HOST = '127.0.0.1'; -const SWOOLE_TESTING_PORT = 9999; +const API_TESTS_HOST = '127.0.0.1'; +const API_TESTS_PORT = 9999; diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 8998dd22..9b338d7a 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -34,8 +34,8 @@ use function Shlinkio\Shlink\Config\env; use function sprintf; use function sys_get_temp_dir; -use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST; -use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; +use const ShlinkioTest\Shlink\API_TESTS_HOST; +use const ShlinkioTest\Shlink\API_TESTS_PORT; $isApiTest = env('TEST_ENV') === 'api'; $isCliTest = env('TEST_ENV') === 'cli'; @@ -136,8 +136,8 @@ return [ 'mezzio-swoole' => [ 'enable_coroutine' => false, 'swoole-http-server' => [ - 'host' => SWOOLE_TESTING_HOST, - 'port' => SWOOLE_TESTING_PORT, + 'host' => API_TESTS_HOST, + 'port' => API_TESTS_PORT, 'process-name' => 'shlink_test', 'options' => [ 'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid', @@ -188,7 +188,7 @@ return [ 'dependencies' => [ 'services' => [ '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, ]), ], diff --git a/data/infra/roadrunner.Dockerfile b/data/infra/roadrunner.Dockerfile new file mode 100644 index 00000000..8520b92d --- /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 --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 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: diff --git a/docker/config/shlink_in_docker.local.php b/docker/config/shlink_in_docker.local.php index 4fba24b6..9dc99351 100644 --- a/docker/config/shlink_in_docker.local.php +++ b/docker/config/shlink_in_docker.local.php @@ -6,11 +6,14 @@ namespace Shlinkio\Shlink; use Shlinkio\Shlink\Common\Logger\LoggerType; +use function Shlinkio\Shlink\Config\runningInRoadRunner; + return [ 'logger' => [ 'Shlink' => [ 'type' => LoggerType::STREAM->value, + 'destination' => runningInRoadRunner() ? 'php://stderr' : 'php://stdout', ], ], diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index f1c4c495..a1cc03be 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -31,6 +31,15 @@ if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then /usr/sbin/crond & fi -# 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 +# RoadRunner config needs these to have been set, so falling back to default values if not set yet +export PORT="${PORT:-"8765"}" +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 diff --git a/indocker b/indocker index 789386ac..03061e2f 100755 --- a/indocker +++ b/indocker @@ -1,7 +1,7 @@ #!/usr/bin/env bash # 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 fi diff --git a/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php b/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php index 6fadaa5d..907b3d9c 100644 --- a/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php +++ b/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php @@ -5,10 +5,11 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\EventDispatcher\Event; 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]; } + + public static function fromPayload(array $payload): self + { + return new static($payload['visitId'] ?? ''); + } } diff --git a/module/Core/src/EventDispatcher/Event/ShortUrlCreated.php b/module/Core/src/EventDispatcher/Event/ShortUrlCreated.php index 9786808f..b6ab1a0c 100644 --- a/module/Core/src/EventDispatcher/Event/ShortUrlCreated.php +++ b/module/Core/src/EventDispatcher/Event/ShortUrlCreated.php @@ -5,8 +5,9 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\EventDispatcher\Event; 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) { @@ -18,4 +19,9 @@ final class ShortUrlCreated implements JsonSerializable 'shortUrlId' => $this->shortUrlId, ]; } + + public static function fromPayload(array $payload): self + { + return new self($payload['shortUrlId'] ?? ''); + } } diff --git a/module/Core/src/EventDispatcher/Event/UrlVisited.php b/module/Core/src/EventDispatcher/Event/UrlVisited.php index 02452a3e..c57d59d6 100644 --- a/module/Core/src/EventDispatcher/Event/UrlVisited.php +++ b/module/Core/src/EventDispatcher/Event/UrlVisited.php @@ -6,8 +6,18 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event; 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; } } diff --git a/module/Core/src/EventDispatcher/LocateVisit.php b/module/Core/src/EventDispatcher/LocateVisit.php index 197ce9a0..8708fb8a 100644 --- a/module/Core/src/EventDispatcher/LocateVisit.php +++ b/module/Core/src/EventDispatcher/LocateVisit.php @@ -41,7 +41,7 @@ class LocateVisit return; } - $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress, $visit); + $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit); $this->eventDispatcher->dispatch(new VisitLocated($visitId)); } diff --git a/module/Core/src/Visit/VisitsTracker.php b/module/Core/src/Visit/VisitsTracker.php index 3aef46df..585a0f86 100644 --- a/module/Core/src/Visit/VisitsTracker.php +++ b/module/Core/src/Visit/VisitsTracker.php @@ -72,6 +72,6 @@ class VisitsTracker implements VisitsTrackerInterface $this->em->persist($visit); $this->em->flush(); - $this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->remoteAddress)); + $this->eventDispatcher->dispatch(UrlVisited::withOriginalIpAddress($visit->getId(), $visitor->remoteAddress)); } } diff --git a/module/Core/test/EventDispatcher/LocateVisitTest.php b/module/Core/test/EventDispatcher/LocateVisitTest.php index 406e8146..09a8086d 100644 --- a/module/Core/test/EventDispatcher/LocateVisitTest.php +++ b/module/Core/test/EventDispatcher/LocateVisitTest.php @@ -193,7 +193,7 @@ class LocateVisitTest extends TestCase { $ipAddr = $originalIpAddress ?? $visit->getRemoteAddr(); $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); $flush = $this->em->flush()->will(function (): void { diff --git a/module/Rest/src/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddleware.php b/module/Rest/src/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddleware.php index 8eb98153..73a6ac69 100644 --- a/module/Rest/src/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddleware.php +++ b/module/Rest/src/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddleware.php @@ -11,14 +11,14 @@ use Psr\Http\Server\RequestHandlerInterface; 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 { /** @var array $body */ - $body = $request->getParsedBody(); + $body = $request->getParsedBody() ?? []; $request = $request->withQueryParams($this->sanitizeDomainFromPayload($request->getQueryParams())) ->withParsedBody($this->sanitizeDomainFromPayload($body));