Laravel 多租户应用程序中的单个共享队列工作者 [英] Single shared queue worker in Laravel multi-tenant app

查看:35
本文介绍了Laravel 多租户应用程序中的单个共享队列工作者的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

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

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

  • 与上面类似,但在单独的 PHP 线程中运行作业,而不是使用 RunKit Sandbox

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

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

关于如何解决这个问题有什么建议吗?

解决方案

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

服务提供商

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

__sleep() 和 __wakeup() 的特性

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

trait BootsTenant{受保护的 $tenantId;/*** 为序列化准备实例.** @return 数组*/公共函数 __sleep(){$this->tenantId = env('TENANT_ID');返回 array_keys(get_object_vars($this));}/*** 恢复ENV,运行服务提供者*/公共函数 __wakeup(){//我们需要设置 TENANT_ID env,并再次强制 BootTenantServiceProvider\Dotenv::makeMutable();\Dotenv::setEnvironmentVariable('TENANT_ID', this->tenantId);app()->register(BootTenantServiceProvider::class, [], true);}}

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

队列作业

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

class MyJob 实现 SelfHandling、ShouldQueue {使用 BootsTenant;受保护的 $userId;公共函数 __construct($userId){$this->userId = $userId;}公共函数句柄(){//此时作业已经从队列中反序列化,//trait __wakeup() 方法已经恢复了 TENANT_ID//服务提供者已经为我们设置好了!$user = User::find($this->userId);//用 $user 做一些事情}}

与 SerializesModel 冲突

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

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

使用 queue:listen 而不是 --daemon

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

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

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 Illuminate\Foundation\Bootstrap\LoadConfiguration). 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.

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:

  • 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.)

  • similar to the above, but run the job in a separate PHP thread instead of a separate process using 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.)

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:

Service Provider

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.

Trait with __sleep() and __wakeup()

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);
    }
}

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.

Queue jobs

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
    }
}

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.

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.

Use queue:listen instead of --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.

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.

这篇关于Laravel 多租户应用程序中的单个共享队列工作者的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆