Laravel 多租户应用程序中的单个共享队列工作程序

2022-01-21 00:00:00 queue php laravel laravel-5 architecture

我正在构建一个多租户 Laravel 应用程序(在 Laravel 5.3 上),它允许每个租户为任何受支持的 Laravel 设置拥有自己的一组配置.目前这是通过使用我自己的实现覆盖默认的 Laravel Application 来实现的,该实现提供了一个自定义配置加载器(覆盖了默认的 IlluminateFoundationBootstrapLoadConfiguration).应用程序在引导时从环境(PHP 的 $_ENV.env 文件)中检测当前租户,然后为检测到的租户加载适当的配置文件.

I'm building a multi-tenant Laravel application (on Laravel 5.3) that allows each tenant to have its own set of configurations for any supported Laravel settings. This is currently achieved by overriding the default Laravel Application with my own implementation that provides a custom configuration loader (overrides the default IlluminateFoundationBootstrapLoadConfiguration). The application detects the current tenant from the environment (either PHP's $_ENV or the .env file) on bootstrap and then loads the appropriate configuration files for the detected tenant.

上述方法适用于 HTTP 和控制台内核,其中每个请求/命令的生命周期有限,但我不确定如何处理队列工作程序.我想为所有租户使用一个队列工作器,并且我已经实现了一个自定义队列连接器,以便在安排队列作业时添加额外的元数据,以便在工作器接收到租户时识别租户.

The above approach works great for both the HTTP and Console kernels where each request/command has a limited life-cycle but I'm not sure how to approach the queue worker. I would like to have a single queue worker for all the tenants and I've already implemented a custom queue connector to add additional metadata when a queue job is scheduled, to make it possible to identify the tenant when the worker receives it.

我正在寻求您帮助的部分是如何在隔离环境中运行每个队列作业,我可以使用我的自定义配置进行引导.

The part on which I'm looking for your help is how to run each queue job in an isolated environment which I can bootstrap with my custom configuration.

我看到的一些可能的解决方案是:

A few possible solutions that I see would be:

  • 运行一个作为守护进程运行的自定义队列工作者,并从队列中获取作业,但在单独的 PHP 进程中执行作业(通过 exec() 创建);一旦作业执行完毕,worker 收集结果(状态、异常等)并在父进程中完成作业(例如删除作业等)

  • to run a custom queue worker that runs as a daemon and gets the job from the queue, but executes the job in a separate PHP process (created via exec()); once the job is executed, the worker gathers the results (status, exceptions, etc.) and finishes the job in the parent process (e.g. deletes the job, etc.)

与上述类似,但使用 RunKit Sandbox

实施一个解决方案,在收到队列作业后重新启动"应用程序(例如,重新加载当前租户的配置、重置任何已解决的依赖项等)

implement a solution that "reboots" the application once a queue job has been received (e.g. reloads configurations for the current tenant, resets any resolved dependencies, etc.)

重要的是,我希望这个多租户作业的执行对作业本身是透明的,以便那些并非设计为在多租户环境中运行的作业(例如来自 Laravel Scout 等第三方软件包的作业)) 无需任何修改即可处理.

What's important is that I'd like for this multi-tenant job execution to be transparent for the job itself so that jobs that are not designed to run in a multi-tenant environment (e.g. jobs from third party packages like Laravel Scout) can be handled without any modification.

关于如何解决这个问题的任何建议?

Any suggestions on how to approach this?

推荐答案

我们的情况几乎相同.这是我们的方法:

We have pretty much the same situation. Here is our approach:

我们有一个名为 BootTenantServiceProvider 的 ServiceProvider,它在正常的 HTTP/Console 请求中引导租户.它期望存在一个名为 TENANT_ID 的环境变量.这样,它将加载所有适当的配置并设置特定的租户.

We have a ServiceProvider called BootTenantServiceProvider that bootstraps a tenant in a normal HTTP/Console request. It expects an environment variable to exist called TENANT_ID. With that, it will load all the appropriate configs and setup a specific tenant.

我们有一个 BootsTenant 特征,我们将在队列作业中使用它,它看起来像这样:

We have a BootsTenant trait that we will use in our queue jobs, it looks like this:

trait BootsTenant
{
    protected $tenantId;

    /**
     * Prepare the instance for serialization.
     *
     * @return array
     */
    public function __sleep()
    {
        $this->tenantId = env('TENANT_ID');

        return array_keys(get_object_vars($this));
    }

    /**
     * Restore the ENV, and run the service provider
     */
    public function __wakeup()
    {
        // We need to set the TENANT_ID env, and also force the BootTenantServiceProvider again

        Dotenv::makeMutable();
        Dotenv::setEnvironmentVariable('TENANT_ID', this->tenantId);

        app()->register(BootTenantServiceProvider::class, [], true);
    }
}

现在我们可以编写一个使用此特征的队列作业.当作业在队列中被序列化时,__sleep() 方法会在本地存储tenantId.当它被反序列化时,__wakeup() 方法将恢复环境变量并运行服务提供者.

Now we can write a queue job that uses this trait. When the job is serialized on the queue, the __sleep() method will store the tenantId locally. When it is unserialized the __wakeup() method will restore the environment variable and run the service provider.

我们的队列作业只需要使用这个特性:

Our queue jobs simply need to use this trait:

class MyJob implements SelfHandling, ShouldQueue {
    use BootsTenant;

    protected $userId;

    public function __construct($userId)
    {
        $this->userId = $userId;
    }

    public function handle()
    {
        // At this point the job has been unserialized from the queue,
        // the trait __wakeup() method has restored the TENANT_ID
        // and the service provider has set us all up!

        $user = User::find($this->userId);
        // Do something with $user
    }
}

与 SerializesModel 冲突

Laravel 包含的 SerializesModels 特征提供了自己的 __sleep__wakeup 方法.我还没有完全弄清楚如何让这两个特征一起工作,或者即使有可能.

Conflict with SerializesModels

The SerializesModels trait that Laravel includes provides its own __sleep and __wakeup methods. I haven't quite figured out how to make both traits work together, or even if it's possible.

现在我确保我永远不会在构造函数中提供完整的 Eloquent 模型.您可以在上面的示例作业中看到,我只将 ID 存储为类属性,而不是完整的模型.我有 handle() 方法在队列运行时获取模型.那么我根本不需要 SerializesModels 特征.

For now I make sure I never provide a full Eloquent model in the constructor. You can see in my example job above I only store IDs as class attributes, never full models. I have the handle() method fetch the models during the queue runtime. Then I don't need the SerializesModels trait at all.

您需要使用 queue:listen 而不是 queue:work --daemon 来运行队列工作程序.前者为每个队列作业启动框架,后者将启动的框架保持在内存中.

You need to run your queue workers using queue:listen instead of queue:work --daemon. The former boots the framework for every queue job, the latter keeps the booted framework loaded in memory.

至少,假设您的租户启动过程需要全新的框架启动,您需要这样做.如果您能够连续启动多个租户,干净地覆盖每个租户的配置,那么您也许可以摆脱 queue:work --daemon 就好了.

At least, you need to do this assuming your tenant boot process needs a fresh framework boot. If you are able to boot multiple tenants in succession, cleanly overwriting the configs for each, then you might be able to get away with queue:work --daemon just fine.

相关文章