From e890106010129e424b127787bb68194ab3138e36 Mon Sep 17 00:00:00 2001 From: Buster Neece Date: Fri, 22 Dec 2023 22:00:40 -0600 Subject: [PATCH] Implement Roadrunner PHP application server for high-performance PHP and Centrifugo event hooks. --- Dockerfile | 8 + composer.json | 2 + composer.lock | 499 +++++++++++++++++- config/routes/api.php | 3 - .../Api/Internal/CentrifugoAction.php | 76 --- src/Service/Centrifugo.php | 24 +- src/Service/Centrifugo/EventHandler.php | 105 ++++ src/Service/ServiceControl.php | 2 +- util/docker/dev/setup/php-spx.sh | 1 - util/docker/web/centrifugo/config.yaml.tmpl | 4 +- util/docker/web/nginx/azuracast.conf.tmpl | 52 +- util/docker/web/php/www.conf.tmpl | 53 -- util/docker/web/roadrunner/rr.yaml.tmpl | 30 ++ util/docker/web/scripts/run_php_fpm | 20 - util/docker/web/scripts/run_roadrunner | 20 + .../{php-fpm.conf => roadrunner.conf} | 4 +- util/docker/web/setup/php.sh | 8 +- util/docker/web/setup/roadrunner.sh | 5 + .../docker/web/startup_scripts/06_php_conf.sh | 5 + util/rr_centrifugo_worker.php | 27 + util/rr_http_worker.php | 44 ++ 21 files changed, 772 insertions(+), 220 deletions(-) delete mode 100644 src/Controller/Api/Internal/CentrifugoAction.php create mode 100644 src/Service/Centrifugo/EventHandler.php delete mode 100644 util/docker/web/php/www.conf.tmpl create mode 100644 util/docker/web/roadrunner/rr.yaml.tmpl delete mode 100644 util/docker/web/scripts/run_php_fpm create mode 100644 util/docker/web/scripts/run_roadrunner rename util/docker/web/service.full/{php-fpm.conf => roadrunner.conf} (81%) create mode 100644 util/docker/web/setup/roadrunner.sh create mode 100644 util/docker/web/startup_scripts/06_php_conf.sh create mode 100644 util/rr_centrifugo_worker.php create mode 100644 util/rr_http_worker.php diff --git a/Dockerfile b/Dockerfile index c79124d8f..8b726e0d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,11 @@ FROM ghcr.io/azuracast/azuracast.com:builtin AS docs # FROM ghcr.io/azuracast/icecast-kh-ac:latest AS icecast +# +# Roadrunner build step +# +FROM ghcr.io/roadrunner-server/roadrunner:2023.3.8 AS roadrunner + # # Final build image # @@ -49,6 +54,9 @@ COPY --from=mariadb /etc/apt/trusted.gpg.d/mariadb.gpg /etc/apt/trusted.gpg.d/ma COPY --from=icecast /usr/local/bin/icecast /usr/local/bin/icecast COPY --from=icecast /usr/local/share/icecast /usr/local/share/icecast +# Add Roadrunner +COPY --from=roadrunner /usr/bin/rr /usr/local/bin/rr + # Run base build process COPY ./util/docker/common /bd_build/ diff --git a/composer.json b/composer.json index 950b594f4..c0d1bda43 100644 --- a/composer.json +++ b/composer.json @@ -66,10 +66,12 @@ "psr/simple-cache": "^3.0", "ramsey/uuid": "^4.0", "rlanvin/php-ip": "dev-master", + "roadrunner-php/centrifugo": "^2.0", "skoerfgen/acmecert": "^3.2", "slim/http": "^1.1", "slim/slim": "^4.2", "spatie/flysystem-dropbox": "^3", + "spiral/roadrunner-http": "^3.3", "spomky-labs/otphp": "^11", "supervisorphp/supervisor": "dev-main", "symfony/cache": "^6", diff --git a/composer.lock b/composer.lock index af34eadc7..aae7ec6b2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "937d84fd61dbb782e05b56a7d49ee79e", + "content-hash": "4613de83e017a7053418e8b1903b34d2", "packages": [ { "name": "aws/aws-crt-php", @@ -2192,6 +2192,50 @@ ], "time": "2023-11-06T15:42:03+00:00" }, + { + "name": "google/protobuf", + "version": "v3.25.1", + "source": { + "type": "git", + "url": "https://github.com/protocolbuffers/protobuf-php.git", + "reference": "1fb247e72df401c863ed239c1660f981644af5db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/1fb247e72df401c863ed239c1660f981644af5db", + "reference": "1fb247e72df401c863ed239c1660f981644af5db", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": ">=5.0.0" + }, + "suggest": { + "ext-bcmath": "Need to support JSON deserialization" + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Protobuf\\": "src/Google/Protobuf", + "GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "proto library for PHP", + "homepage": "https://developers.google.com/protocol-buffers/", + "keywords": [ + "proto" + ], + "support": { + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v3.25.1" + }, + "time": "2023-11-15T21:36:03+00:00" + }, { "name": "graham-campbell/guzzle-factory", "version": "v7.0.1", @@ -5932,6 +5976,151 @@ }, "time": "2022-03-02T08:51:37+00:00" }, + { + "name": "roadrunner-php/centrifugo", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/centrifugo.git", + "reference": "9b48bfa1d6aee0c889ea77bb2afe8f733e0ad8e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/centrifugo/zipball/9b48bfa1d6aee0c889ea77bb2afe8f733e0ad8e7", + "reference": "9b48bfa1d6aee0c889ea77bb2afe8f733e0ad8e7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "google/protobuf": "^3.7", + "php": ">=8.1", + "roadrunner-php/roadrunner-api-dto": "^1.0", + "spiral/goridge": "^4.0", + "spiral/roadrunner": "^2023.1", + "spiral/roadrunner-worker": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "phpunit/phpunit": "^10.0", + "vimeo/psalm": ">= 5.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "RoadRunner\\Centrifugo\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Pavel Buchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "email": "alexey.gagarin@spiralscout.com" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + }, + { + "name": "Kirill Nesmeyanov (SerafimArts)", + "email": "kirill.nesmeyanov@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/spiral/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner: Centrifugo bridge", + "homepage": "https://roadrunner.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://roadrunner.dev/docs", + "forum": "https://forum.roadrunner.dev/", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/centrifugo/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2023-04-13T11:38:58+00:00" + }, + { + "name": "roadrunner-php/roadrunner-api-dto", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/roadrunner-api-dto.git", + "reference": "82e0889171086b485bd4c31a3575b8ffa531e6d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/roadrunner-api-dto/zipball/82e0889171086b485bd4c31a3575b8ffa531e6d1", + "reference": "82e0889171086b485bd4c31a3575b8ffa531e6d1", + "shasum": "" + }, + "require": { + "google/protobuf": "^v3.22", + "php": "^8.1" + }, + "conflict": { + "temporal/sdk": "<2.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Temporal\\": "generated/Temporal", + "RoadRunner\\": "generated/RoadRunner", + "GPBMetadata\\": "generated/GPBMetadata" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pavel Butchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "email": "alexey.gagarin@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner PHP API", + "homepage": "https://roadrunner.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://roadrunner.dev/docs", + "forum": "https://forum.roadrunner.dev/", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/roadrunner-api-dto/tree/1.4.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2023-10-11T13:36:31+00:00" + }, { "name": "skoerfgen/acmecert", "version": "3.3.1", @@ -6371,6 +6560,314 @@ ], "time": "2023-09-25T07:13:36+00:00" }, + { + "name": "spiral/goridge", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/goridge.git", + "reference": "d955f58be1c51daa1eb94a5ddaf4c2daf64ee14e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/goridge/zipball/d955f58be1c51daa1eb94a5ddaf4c2daf64ee14e", + "reference": "d955f58be1c51daa1eb94a5ddaf4c2daf64ee14e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-sockets": "*", + "php": ">=8.1", + "spiral/roadrunner": "^2023" + }, + "require-dev": { + "google/protobuf": "^3.22", + "infection/infection": "^0.26.1", + "jetbrains/phpstorm-attributes": "^1.0", + "phpunit/phpunit": "^10.0", + "rybakit/msgpack": "^0.7", + "vimeo/psalm": "^5.9" + }, + "suggest": { + "ext-msgpack": "MessagePack codec support", + "ext-protobuf": "Protobuf codec support", + "google/protobuf": "(^3.0) Protobuf codec support", + "rybakit/msgpack": "(^0.7) MessagePack codec support" + }, + "type": "goridge", + "autoload": { + "psr-4": { + "Spiral\\Goridge\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Pavel Buchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "High-performance PHP-to-Golang RPC bridge", + "homepage": "https://spiral.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://roadrunner.dev/docs", + "forum": "https://forum.roadrunner.dev/", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/goridge/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2023-10-03T18:40:43+00:00" + }, + { + "name": "spiral/roadrunner", + "version": "v2023.3.8", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-server/roadrunner.git", + "reference": "80eff8aa42c59bc2d2cc9a2fab4e02398fd94994" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-server/roadrunner/zipball/80eff8aa42c59bc2d2cc9a2fab4e02398fd94994", + "reference": "80eff8aa42c59bc2d2cc9a2fab4e02398fd94994", + "shasum": "" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov / Wolfy-J", + "email": "wolfy.jd@gmail.com" + }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner: High-performance PHP application server and process manager written in Go and powered with plugins", + "homepage": "https://roadrunner.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://roadrunner.dev/docs", + "forum": "https://forum.roadrunner.dev/", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-server/roadrunner/tree/v2023.3.8" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2023-12-14T15:48:20+00:00" + }, + { + "name": "spiral/roadrunner-http", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/http.git", + "reference": "7a1f7d0729ad483e9c637ee276f6e910def33f4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/http/zipball/7a1f7d0729ad483e9c637ee276f6e910def33f4d", + "reference": "7a1f7d0729ad483e9c637ee276f6e910def33f4d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "psr/http-factory": "^1.0.1", + "psr/http-message": "^1.0.1 || ^2.0", + "spiral/roadrunner": "^2023.3", + "spiral/roadrunner-worker": "^3.1.0" + }, + "require-dev": { + "buggregator/trap": "^1.0", + "jetbrains/phpstorm-attributes": "^1.0", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.2 || ^7.0", + "vimeo/psalm": "^5.9" + }, + "suggest": { + "spiral/roadrunner-cli": "Provides RoadRunner installation and management CLI tools" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spiral\\RoadRunner\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Pavel Buchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner: HTTP and PSR-7 worker", + "homepage": "https://spiral.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://roadrunner.dev/docs", + "forum": "https://forum.roadrunner.dev/", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/http/tree/3.3.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2023-12-05T20:45:38+00:00" + }, + { + "name": "spiral/roadrunner-worker", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/worker.git", + "reference": "53f81c3e138a650c4605fcd60cae7c89ef24061f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/worker/zipball/53f81c3e138a650c4605fcd60cae7c89ef24061f", + "reference": "53f81c3e138a650c4605fcd60cae7c89ef24061f", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "ext-json": "*", + "ext-sockets": "*", + "php": ">=8.1", + "psr/log": "^2.0|^3.0", + "spiral/goridge": "^4.1.0", + "spiral/roadrunner": "^2023.1" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.0", + "phpunit/phpunit": "^10.0", + "symfony/var-dumper": "^6.3 || ^7.0", + "vimeo/psalm": "^5.9" + }, + "suggest": { + "spiral/roadrunner-cli": "Provides RoadRunner installation and management CLI tools" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spiral\\RoadRunner\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Pavel Buchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner: PHP worker", + "homepage": "https://spiral.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://roadrunner.dev/docs", + "forum": "https://forum.roadrunner.dev/", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/worker/tree/3.3.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2023-12-05T19:53:37+00:00" + }, { "name": "spomky-labs/otphp", "version": "11.2.0", diff --git a/config/routes/api.php b/config/routes/api.php index ce69b7e78..318fb2db9 100644 --- a/config/routes/api.php +++ b/config/routes/api.php @@ -69,9 +69,6 @@ return static function (RouteCollectorProxy $app) { $group->post('/sftp-event', Controller\Api\Internal\SftpEventAction::class) ->setName('api:internal:sftp-event'); - $group->post('/centrifugo', Controller\Api\Internal\CentrifugoAction::class) - ->setName('api:internal:centrifugo'); - $group->get('/relays', Controller\Api\Internal\RelaysController::class) ->setName('api:internal:relays') ->add(Middleware\RequireLogin::class); diff --git a/src/Controller/Api/Internal/CentrifugoAction.php b/src/Controller/Api/Internal/CentrifugoAction.php deleted file mode 100644 index dbe903dd5..000000000 --- a/src/Controller/Api/Internal/CentrifugoAction.php +++ /dev/null @@ -1,76 +0,0 @@ -getParsedBody()); - $this->logger->debug('Centrifugo connection body', $parsedBody); - - $channels = array_filter( - $parsedBody['channels'] ?? [], - fn($channel) => str_starts_with($channel, 'station:') || $channel === Centrifugo::GLOBAL_TIME_CHANNEL - ); - - $allInitialData = []; - - $router = $request->getRouter(); - $router->buildBaseUrl(false); - - foreach ($channels as $channel) { - $initialData = []; - - if ($channel === Centrifugo::GLOBAL_TIME_CHANNEL) { - $initialData['time'] = time(); - } elseif (str_starts_with($channel, 'station:')) { - $stationName = substr($channel, 8); - $np = $this->npCache->getForStation($stationName); - if (!($np instanceof NowPlaying)) { - continue; - } - - $np->resolveUrls($router->getBaseUrl()); - $np->update(); - - $initialData['np'] = $np; - $initialData['triggers'] = []; - } - - $allInitialData[] = [ - 'channel' => $channel, - 'pub' => [ - 'data' => $initialData, - ], - ]; - } - - return $response->withJson([ - 'result' => [ - 'user' => '', - 'channels' => $channels, - 'data' => $allInitialData, - ], - ]); - } -} diff --git a/src/Service/Centrifugo.php b/src/Service/Centrifugo.php index 645d0f724..904239f15 100644 --- a/src/Service/Centrifugo.php +++ b/src/Service/Centrifugo.php @@ -30,27 +30,37 @@ final class Centrifugo 'method' => 'publish', 'params' => [ 'channel' => self::GLOBAL_TIME_CHANNEL, - 'data' => [ - 'time' => time(), - ], + 'data' => $this->buildTimeMessage(), ], ]); } + public function buildTimeMessage(): array + { + return [ + 'time' => time(), + ]; + } + public function publishToStation(Station $station, mixed $message, array $triggers): void { $this->send([ 'method' => 'publish', 'params' => [ 'channel' => $this->getChannelName($station), - 'data' => [ - 'np' => $message, - 'triggers' => $triggers, - ], + 'data' => $this->buildStationMessage($message, $triggers), ], ]); } + public function buildStationMessage(mixed $message, array $triggers = []): array + { + return [ + 'np' => $message, + 'triggers' => $triggers, + ]; + } + private function send(array $body): void { $this->client->post( diff --git a/src/Service/Centrifugo/EventHandler.php b/src/Service/Centrifugo/EventHandler.php new file mode 100644 index 000000000..eeec66059 --- /dev/null +++ b/src/Service/Centrifugo/EventHandler.php @@ -0,0 +1,105 @@ +centrifugo->isSupported()) { + throw new RuntimeException('Centrifugo is not supported.'); + } + + $this->router->buildBaseUrl(false); + } + + public function __invoke(RequestInterface $request): ?ResponseInterface + { + if ($request instanceof Invalid) { + $e = $request->getException(); + $this->logger->error( + sprintf('Centrifugo error: %s', $e->getMessage()), + [ + 'exception' => $e, + ] + ); + return null; + } + + try { + if ($request instanceof Connect) { + return $this->onConnect($request); + } + } catch (Throwable $e) { + $request->error($e->getCode(), $e->getMessage()); + } + + return null; + } + + private function onConnect(Connect $request): ?ResponseInterface + { + $channels = array_filter( + $request->channels, + fn($channel) => str_starts_with($channel, 'station:') || $channel === Centrifugo::GLOBAL_TIME_CHANNEL + ); + + if (empty($channels)) { + return null; + } + + $allInitialData = []; + + foreach ($channels as $channel) { + if ($channel === Centrifugo::GLOBAL_TIME_CHANNEL) { + $initialData = $this->centrifugo->buildTimeMessage(); + } elseif (str_starts_with($channel, 'station:')) { + $stationName = substr($channel, 8); + $np = $this->npCache->getForStation($stationName); + if (!($np instanceof NowPlaying)) { + continue; + } + + $np->resolveUrls($this->router->getBaseUrl()); + $np->update(); + + $initialData = $this->centrifugo->buildStationMessage($np); + } else { + continue; + } + + $allInitialData[] = [ + 'channel' => $channel, + 'pub' => [ + 'data' => $initialData, + ], + ]; + } + + return new ConnectResponse( + user: '', + data: $allInitialData, + channels: $channels + ); + } +} diff --git a/src/Service/ServiceControl.php b/src/Service/ServiceControl.php index ee1ab3579..fce7d9345 100644 --- a/src/Service/ServiceControl.php +++ b/src/Service/ServiceControl.php @@ -80,7 +80,7 @@ final class ServiceControl 'cron' => __('Runs routine synchronized tasks'), 'mariadb' => __('Database'), 'nginx' => __('Web server'), - 'php-fpm' => __('PHP FastCGI Process Manager'), + 'roadrunner' => __('Roadrunner PHP Server'), 'php-nowplaying' => __('Now Playing manager service'), 'php-worker' => __('PHP queue processing worker'), 'redis' => __('Cache'), diff --git a/util/docker/dev/setup/php-spx.sh b/util/docker/dev/setup/php-spx.sh index eba5f9674..e9b772a89 100644 --- a/util/docker/dev/setup/php-spx.sh +++ b/util/docker/dev/setup/php-spx.sh @@ -20,4 +20,3 @@ apt-get remove --purge -y php${PHP_VERSION}-dev zlib1g-dev build-essential echo "extension=spx.so" > /etc/php/${PHP_VERSION}/mods-available/30-spx.ini ln -s /etc/php/${PHP_VERSION}/mods-available/30-spx.ini /etc/php/${PHP_VERSION}/cli/conf.d/30-spx.ini -ln -s /etc/php/${PHP_VERSION}/mods-available/30-spx.ini /etc/php/${PHP_VERSION}/fpm/conf.d/30-spx.ini diff --git a/util/docker/web/centrifugo/config.yaml.tmpl b/util/docker/web/centrifugo/config.yaml.tmpl index bfa8a446f..caf76d076 100644 --- a/util/docker/web/centrifugo/config.yaml.tmpl +++ b/util/docker/web/centrifugo/config.yaml.tmpl @@ -8,8 +8,10 @@ websocket_disable: true uni_websocket: true uni_sse: true uni_http_stream: true -proxy_connect_endpoint: http://localhost:6010/api/internal/centrifugo +proxy_connect_endpoint: grpc://127.0.0.1:6300 proxy_connect_timeout: 10s +grpc_api: true +grpc_api_port: 6301 allowed_origins: - "*" diff --git a/util/docker/web/nginx/azuracast.conf.tmpl b/util/docker/web/nginx/azuracast.conf.tmpl index ba4831b9d..9e39e6692 100644 --- a/util/docker/web/nginx/azuracast.conf.tmpl +++ b/util/docker/web/nginx/azuracast.conf.tmpl @@ -1,9 +1,5 @@ -upstream php-fpm-internal { - server unix:/var/run/php-fpm-internal.sock; -} - -upstream php-fpm-www { - server unix:/var/run/php-fpm-www.sock; +upstream roadrunner { + server 127.0.0.1:6090; } upstream centrifugo { @@ -16,40 +12,6 @@ upstream vite { } {{end}} -# Internal connection handler for PubSub and internal API calls -server { - listen 127.0.0.1:6010; - - root /var/azuracast/www/web; - index index.php; - - server_name localhost; - - # Default clean URL routing - location / { - try_files $uri @clean_url; - } - - location @clean_url { - rewrite ^(.*)$ /index.php last; - } - - location ~ ^/index\.php(/|$) { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - - fastcgi_pass php-fpm-internal; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $realpath_root; - include fastcgi_params; - - fastcgi_read_timeout 600; - fastcgi_buffering off; - - internal; - } -} - server { listen {{ default .Env.AZURACAST_HTTP_PORT "80" }}; listen {{ default .Env.AZURACAST_HTTPS_PORT "443" }} default_server http2 ssl; @@ -134,18 +96,10 @@ server { {{end}} location ~ ^/index\.php(/|$) { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - - # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini - fastcgi_pass php-fpm-www; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $realpath_root; include fastcgi_params; - + fastcgi_pass roadrunner; fastcgi_read_timeout {{ default .Env.NGINX_TIMEOUT "1800" }}; fastcgi_buffering off; - internal; } diff --git a/util/docker/web/php/www.conf.tmpl b/util/docker/web/php/www.conf.tmpl deleted file mode 100644 index 17b80fe81..000000000 --- a/util/docker/web/php/www.conf.tmpl +++ /dev/null @@ -1,53 +0,0 @@ -[global] -error_log = /dev/stderr -daemonize = no - -[www] -user = azuracast -group = azuracast - -listen = /var/run/php-fpm-www.sock -listen.owner = azuracast -listen.group = www-data -listen.mode = 0660 - -pm = ondemand -pm.max_children = {{ default .Env.PHP_FPM_MAX_CHILDREN "20" }} -pm.start_servers = 2 -pm.min_spare_servers = 2 -pm.max_spare_servers = 4 -pm.max_requests = 200 -pm.status_path = /status -pm.process_idle_timeout = 60s - -chdir = / -clear_env=No -catch_workers_output = yes -decorate_workers_output = no - -{{if eq .Env.PROFILING_EXTENSION_ENABLED "1"}} -process.dumpable = yes -{{end}} - -[internal] -user = azuracast -group = azuracast - -listen = /var/run/php-fpm-internal.sock -listen.owner = azuracast -listen.group = www-data -listen.mode = 0660 - -pm = ondemand -pm.max_children = 10 -pm.start_servers = 2 -pm.min_spare_servers = 2 -pm.max_spare_servers = 4 -pm.max_requests = 50 -pm.process_idle_timeout = 60s - -chdir = / - -clear_env=No -catch_workers_output = yes -decorate_workers_output = no diff --git a/util/docker/web/roadrunner/rr.yaml.tmpl b/util/docker/web/roadrunner/rr.yaml.tmpl new file mode 100644 index 000000000..1e98626f9 --- /dev/null +++ b/util/docker/web/roadrunner/rr.yaml.tmpl @@ -0,0 +1,30 @@ +version: '3' + +rpc: + listen: tcp://127.0.0.1:6401 + +server: + relay: pipes + command: "php util/rr_http_worker.php" + +logs: + mode: {{if eq .Env.APPLICATION_ENV "development"}}development{{else}}production{{end}} + +http: + address: 0.0.0.0:6010 + fcgi: + address: tcp://127.0.0.1:6090 + pool: + debug: {{if eq .Env.APPLICATION_ENV "development"}}true{{else}}false{{end}} + max_jobs: 50 + +centrifuge: + proxy_address: tcp://127.0.0.1:6300 + grpc_api_address: tcp://127.0.0.1:6301 + use_compressor: true + name: "azuracast" + pool: + debug: {{if eq .Env.APPLICATION_ENV "development"}}true{{else}}false{{end}} + command: "php util/rr_centrifugo_worker.php" + num_workers: 1 + max_jobs: 250 diff --git a/util/docker/web/scripts/run_php_fpm b/util/docker/web/scripts/run_php_fpm deleted file mode 100644 index 9e213fdf9..000000000 --- a/util/docker/web/scripts/run_php_fpm +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -source /etc/php/.version - -# Set up PHP config -dockerize -template "/etc/php/${PHP_VERSION}/fpm/05-azuracast.ini.tmpl:/etc/php/${PHP_VERSION}/fpm/conf.d/05-azuracast.ini" \ - -template "/etc/php/${PHP_VERSION}/fpm/www.conf.tmpl:/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf" \ - cp /etc/php/${PHP_VERSION}/fpm/conf.d/05-azuracast.ini /etc/php/${PHP_VERSION}/cli/conf.d/05-azuracast.ini - -# Wait for services to spin up. -gosu azuracast php /var/azuracast/www/bin/uptime_wait || exit 1 - -# Initialize before running FPM -gosu azuracast azuracast_cli azuracast:setup --init || exit 1 - -# Run initial Acme check -gosu azuracast azuracast_cli azuracast:acme:get-certificate || true - -# Run PHP-FPM -exec /usr/sbin/php-fpm${PHP_VERSION} -F --fpm-config /etc/php/${PHP_VERSION}/fpm/php-fpm.conf -c /etc/php/${PHP_VERSION}/fpm/ diff --git a/util/docker/web/scripts/run_roadrunner b/util/docker/web/scripts/run_roadrunner new file mode 100644 index 000000000..10d087bfd --- /dev/null +++ b/util/docker/web/scripts/run_roadrunner @@ -0,0 +1,20 @@ +#!/bin/bash + +if [ $(whoami) != 'azuracast' ]; then + echo 'This script must be run as the "azuracast" user. Rerunning...' + exec gosu azuracast run_roadrunner "$@" +fi + +dockerize -template "/var/azuracast/rr.yaml.tmpl:/var/azuracast/rr.yaml" + +# Wait for services to spin up. +php /var/azuracast/www/bin/uptime_wait || exit 1 + +# Initialize before running FPM +azuracast_cli azuracast:setup --init || exit 1 + +# Run initial Acme check +azuracast_cli azuracast:acme:get-certificate || true + +# Run PHP-FPM +exec rr serve -c /var/azuracast/rr.yaml -w /var/azuracast/www diff --git a/util/docker/web/service.full/php-fpm.conf b/util/docker/web/service.full/roadrunner.conf similarity index 81% rename from util/docker/web/service.full/php-fpm.conf rename to util/docker/web/service.full/roadrunner.conf index df91f944d..1aae328a9 100644 --- a/util/docker/web/service.full/php-fpm.conf +++ b/util/docker/web/service.full/roadrunner.conf @@ -1,5 +1,5 @@ -[program:php-fpm] -command=run_php_fpm +[program:roadrunner] +command=run_roadrunner priority=300 numprocs=1 autostart=true diff --git a/util/docker/web/setup/php.sh b/util/docker/web/setup/php.sh index 5bfe1471c..719a86445 100644 --- a/util/docker/web/setup/php.sh +++ b/util/docker/web/setup/php.sh @@ -12,7 +12,7 @@ echo "deb-src [signed-by=/etc/apt/keyrings/php.gpg] https://ppa.launchpadcontent apt-get update -apt-get install -y --no-install-recommends php${PHP_VERSION}-fpm php${PHP_VERSION}-cli php${PHP_VERSION}-gd \ +apt-get install -y --no-install-recommends php${PHP_VERSION}-cli php${PHP_VERSION}-gd \ php${PHP_VERSION}-curl php${PHP_VERSION}-xml php${PHP_VERSION}-zip \ php${PHP_VERSION}-gmp php${PHP_VERSION}-mysqlnd php${PHP_VERSION}-mbstring php${PHP_VERSION}-intl \ php${PHP_VERSION}-redis php${PHP_VERSION}-maxminddb @@ -20,11 +20,7 @@ apt-get install -y --no-install-recommends php${PHP_VERSION}-fpm php${PHP_VERSIO # Copy PHP configuration echo "PHP_VERSION=$PHP_VERSION" >> /etc/php/.version -mkdir -p /run/php -touch /run/php/php${PHP_VERSION}-fpm.pid - -cp /bd_build/web/php/php.ini.tmpl /etc/php/${PHP_VERSION}/fpm/05-azuracast.ini.tmpl -cp /bd_build/web/php/www.conf.tmpl /etc/php/${PHP_VERSION}/fpm/www.conf.tmpl +cp /bd_build/web/php/php.ini.tmpl /etc/php/${PHP_VERSION}/cli/05-azuracast.ini.tmpl # Enable FFI (for StereoTool inspection) echo 'ffi.enable="true"' >> /etc/php/${PHP_VERSION}/mods-available/ffi.ini diff --git a/util/docker/web/setup/roadrunner.sh b/util/docker/web/setup/roadrunner.sh new file mode 100644 index 000000000..88ce552e0 --- /dev/null +++ b/util/docker/web/setup/roadrunner.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +set -x + +cp /bd_build/web/roadrunner/rr.yaml.tmpl /var/azuracast/rr.yaml.tmpl diff --git a/util/docker/web/startup_scripts/06_php_conf.sh b/util/docker/web/startup_scripts/06_php_conf.sh new file mode 100644 index 000000000..a11b5e723 --- /dev/null +++ b/util/docker/web/startup_scripts/06_php_conf.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +source /etc/php/.version + +dockerize -template "/etc/php/${PHP_VERSION}/cli/05-azuracast.ini.tmpl:/etc/php/${PHP_VERSION}/cli/conf.d/05-azuracast.ini" diff --git a/util/rr_centrifugo_worker.php b/util/rr_centrifugo_worker.php new file mode 100644 index 000000000..83582e3ea --- /dev/null +++ b/util/rr_centrifugo_worker.php @@ -0,0 +1,27 @@ +getContainer(); + +/** @var App\Service\Centrifugo\EventHandler $centrifugo */ +$centrifugo = $di->get(App\Service\Centrifugo\EventHandler::class); + +$worker = Spiral\RoadRunner\Worker::create(); +$requestFactory = new RoadRunner\Centrifugo\Request\RequestFactory($worker); +$centrifugoWorker = new RoadRunner\Centrifugo\CentrifugoWorker($worker, $requestFactory); + +while ($request = $centrifugoWorker->waitRequest()) { + $response = $centrifugo->__invoke($request); + if (null !== $response) { + $request->respond($response); + } +} diff --git a/util/rr_http_worker.php b/util/rr_http_worker.php new file mode 100644 index 000000000..523f2e79d --- /dev/null +++ b/util/rr_http_worker.php @@ -0,0 +1,44 @@ + false, +]); +$diBuilder = AppFactory::createContainerBuilder($environment); + +$httpFactory = new App\Http\HttpFactory(); +$worker = \Spiral\RoadRunner\Worker::create(); +$psr7Worker = new Spiral\RoadRunner\Http\PSR7Worker($worker, $httpFactory, $httpFactory, $httpFactory); + +while (true) { + try { + $request = $psr7Worker->waitRequest(); + if ($request === null) { + break; + } + } catch (\Throwable $e) { + $psr7Worker->respond($httpFactory->createResponse(400)); + continue; + } + + try { + $di = AppFactory::buildContainer($diBuilder); + $app = AppFactory::buildAppFromContainer($di); + + $response = $app->handle($request); + $psr7Worker->respond($response); + } catch (\Throwable $e) { + $psr7Worker->respond($httpFactory->createResponse(500, 'Critical error')); + $psr7Worker->getWorker()->error((string)$e); + } + + gc_collect_cycles(); +}