fluffydiscord/roadrunner-symfony-bundle

Symfony 的 Roadrunner 运行时

v2.4.3 2024-09-26 11:43 UTC

README

为 Symfony 和 RoadRunner 提供另一个运行时。

安装

composer require fluffydiscord/roadrunner-symfony-bundle

用法

.rr.yaml 中定义环境变量 APP_RUNTIME 并设置 rpc 插件

.rr.yaml

server:
    env:
        APP_RUNTIME: FluffyDiscord\RoadRunnerBundle\Runtime\Runtime

rpc:
    listen: tcp://127.0.0.1:6001

别忘了将 RR_RPC 添加到你的 .env

RR_RPC=tcp://127.0.0.1:6001

配置

fluffy_discord_road_runner.yaml

fluffy_discord_road_runner:
  # Optional
  # Specify relative path from "kernel.project_dir" to your RoadRunner config file
  # if you want to run cache:warmup without having your RoadRunner running in background,
  # e.g. when building Docker images. Default is ".rr.yaml"
  rr_config_path: ".rr.yaml"
    
  # https://docs.roadrunner.dev/http/http
  http:
    # Optional
    # -----------
    # This decides when to boot the Symfony kernel.
    #
    # false (default) - before first request (worker takes some time to be ready, but app has consistent response times)
    # true - once first request arrives (worker is ready immediately, but inconsistent response times due to kernel boot time spikes)
    #
    # If you use large amount of workers, you might want to set this to true or else the RR boot up might
    # take a lot of time or just boot up using only a few "emergency" workers 
    # and then use dynamic worker scaling as described here https://docs.roadrunner.dev/php-worker/scaling
    lazy_boot: false

  # https://docs.roadrunner.dev/plugins/centrifuge
  centrifugo:
    # Optional
    # -----------
    # See http section
    lazy_boot: false

  # https://docs.roadrunner.dev/key-value/overview-kv
  kv:
    # Optional
    # -----------
    # If true (default), bundle will automatically register all "kv" adapters in your .rr.yaml.
    # Registered services have alias "cache.adapter.rr_kv.NAME"
    auto_register: true

    # Optional
    # -----------
    # Which serializer should be used.
    # By default, "IgbinarySerializer" will be used if "igbinary" php extension 
    # is installed (recommended), otherwise "DefaultSerializer".
    # You are free to create your own serializer, it needs to implement
    # Spiral\RoadRunner\KeyValue\Serializer\SerializerInterface
    serializer: null

    # Optional
    # -----------
    # Specify relative path from "kernel.project_dir" to a keypair file 
    # for end-to-end encryption. "sodium" php extension is required. 
    # https://docs.roadrunner.dev/key-value/overview-kv#end-to-end-value-encryption
    keypair_path: bin/keypair.key

位于负载均衡器或代理后面运行

如果你想将 REMOTE_ADDR 作为受信任的代理使用,请将其替换为 0.0.0.0/0,否则你的受信任头将无法工作。

Symfony 正在使用 $_SERVER['REMOTE_ADDR'] 来查找代理地址,但在 RoadRunner 的上下文中,$_SERVER 只包含环境变量,而 REMOTE_ADDS 是缺失的。

响应/文件流

内置对 Symfony 的 BinaryFileResponseStreamedJsonResponse 的完全支持。要使 StreamedResponse 完全可流式传输,您需要将 callback 改为 \Generator,将所有 echo 替换为 yield。查看示例

use Symfony\Component\HttpFoundation\StreamedResponse;

class MyController
{
    #[Route("/stream")]
    public function myStreamResponse() 
    {
        return new StreamedResponse(
            function () {
                // replace all 'echo' or any outputs with 'yield'
                // echo "data";
                yield "data";
            }
        );
    }
}

会话

目前,Symfony 可能会与工作模式下的会话(例如,丢失登录用户或相反,由于缺少全局变量而导致登录用户会话泄露到另一个请求)苦苦挣扎。(解释见文末)。

捆绑引入了 FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory,该工厂正确处理原生会话。如果您使用 symfony/flex(待 flex 菜谱 PR),则此工厂将默认自动注册。您也可以手动注册它,例如在 framework.yaml 中。

framework:
    session:
        storage_factory_id: FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory

Sentry

内置对 Sentry 的支持。只需像平时一样安装和配置即可。

composer require sentry/sentry-symfony

Centrifugo(WebSocket)

要启用 Centrifugo,您需要添加 roadrunner-php/centrifugo 包。

composer require roadrunner-php/centrifugo

捆绑使用 Symfony 的事件调度器。您可以为任何扩展 FluffyDiscord\RoadRunnerBundle\Event\Centrifugo\CentrifugoEventInterface 的事件创建 事件监听器

  • ConnectEvent 必需 :)
  • InvalidEvent
  • PublishEvent
  • RefreshEvent
  • RPCEvent
  • SubRefreshEvent
  • SubscribeEvent

示例用法

<?php

namespace App\EventListener;

use App\Centrifuge\Event\ConnectEvent;
use RoadRunner\Centrifugo\Payload\ConnectResponse;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: ConnectEvent::class, method: "handleConnect")]
readonly class ChatListener
{
    public function handleConnect(ConnectEvent $event): void
    {
        // original Centrifugo request passed from RoadRunner
        $request = $event->getRequest();
        
        // auth your user or whatever you want
        $authToken = $request->getData()["authToken"] ?? null;
        $user = ...

        // stop propagating to other listeners,
        // you have successfully connected your user
        $event->stopPropagation();

        // send response using the $event->setResponse($myResponse)
        $event->setResponse(new ConnectResponse(
            user: $user->getId(),
            data: [
                "messages" => ... // initial data client receives when connected
            ],
        ));
    }
}

请注意,如果您没有设置任何响应,则捆绑将默认发送 DisconnectResponse

使用 Symfony 和 RoadRunner 进行开发

  • 如果可能,停止在您的服务中使用延迟加载,立即注入服务。
  • 这不再需要,并可能给您带来潜在问题,例如内存泄漏。
  • 不要在您的服务中使用/创建本地类/数组缓存。尝试使它们无状态;如果它们不能是无状态的,请在每个请求之前添加 ResetInterface 来清理。
  • Symfony 表单可能会由于本地缓存而导致数据在请求之间泄漏。确保您的表单 defaultOptions 是无状态的。不要存储任何敏感/重要信息,否则它将在后续请求中泄露。
  • 通过利用 EquatableInterface 和自定义反/序列化逻辑来简化 User 会话序列化。这将防止由于断开的 Doctrine 实体而导致的错误,并且作为额外的奖励,将加快从会话中加载用户的速度。
<?php

namespace App\Entity\User;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface
{
    #[ORM\Id]
    private ?int $id = null;

    #[ORM\Column(type: Types::TEXT, unique: true)]
    private ?string $email = null;

    #[ORM\Column(type: Types::TEXT)]
    private ?string $password = null;

    // serialize ony these three fields
    public function __serialize(): array
    {
        return [
            "id"       => $this->id,
            "email"    => $this->email,
            "password" => $this->password,
        ];
    }

    // unserialize ony these three fields
    public function __unserialize(array $data): void
    {
        $this->id = $data["id"] ?? null;
        $this->email = $data["email"] ?? null;
        $this->password = $data["password"] ?? null;
    }

    // check only the three serialized fields
    public function isEqualTo(mixed $user): bool
    {
        if (!$user instanceof self) {
            return false;
        }

        return $this->id === $user->getId()
            &&
            $this->password === $user->getPassword()
            &&
            $this->email === $user->getEmail()
        ;
    }
}

调试(建议)

使用RoadRunner时,您不能简单地“丢弃并死亡”,因为没有东西会被打印出来。我想介绍Buggregator来解决这个问题。作为额外的好处,它还可以作为mailtrap或本地测试Sentry

致谢

灵感来源于现有的解决方案,如Baldinof的BundleNyholm的Runtime