【问题标题】:Single shared queue worker in Laravel multi-tenant appLaravel 多租户应用程序中的单个共享队列工作程序
【发布时间】:2017-02-16 11:33:56
【问题描述】:

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

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

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

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

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

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

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

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

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

【问题讨论】:

    标签: php laravel laravel-5 architecture queue


    【解决方案1】:

    要扩展 @jszobody 他的答案,请参阅由多租户 Laravel 包构建的 TenantAwareJob trait。

    这正是您想要的,在睡眠编码您的租户之前,当唤醒它启动您的租户。

    这个特征也适用于SerializesModels,所以你可以传递你的模型。

    更新

    自 Laravel 6 以来,这不再起作用。 SerializeModels trait 覆盖了 __serialize__unserialize functions

    新方法是注册一个服务提供者并加入队列。这样,您可以在处理之前添加有效负载数据并启动 tentant。示例:

        public function boot()
        {
            $this->app->extend('queue', function (QueueManager $queue) {
                // Store tenant key and identifier on job payload when a tenant is identified.
                $queue->createPayloadUsing(function () {
                    return ['tenant_id' => TenantManager::getInstance()->getTenant()->id];
                });
    
                // Resolve any tenant related meta data on job and allow resolving of tenant.
                $queue->before(function (JobProcessing $jobProcessing) {
                    $tenantId = $jobProcessing->job->payload()['tenant_id'];
                    TenantManager::getInstance()->setTenantById($tenantId);
                });
    
                return $queue;
            });
        }
    

    灵感来自 laravel-multitenancytanancy queue driver

    【讨论】:

    • 谢谢你,我正在寻找这个确切的特征。它已从当前版本的 Hyn 中删除。我的设置就像一个魅力(每个租户的单个数据库)。
    【解决方案2】:

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

    服务提供者

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

    具有 __sleep() 和 __wakeup() 的特征

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

    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() 方法将恢复环境变量并运行服务提供者。

    队列作业

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

    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 trait 提供了自己的 __sleep__wakeup 方法。我还没有完全弄清楚如何让这两个特征一起工作,或者即使有可能。

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

    使用 queue:listen 代替 --daemon

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

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

    【讨论】:

    • 感谢您非常详细的回答。我没有意识到queue:listen 实际上非常接近我的需要,但我使用它的问题是我需要在启动过程的早期设置租户才能覆盖所有配置,这意味着我需要以某种方式设置租户,然后以某种方式在应用程序中再次通过所有引导运行。出于与您概述的完全相同的原因,我选择不在命令中分配租户 ID。我没有使用设置租户的自定义特征,而是使用自定义队列连接器将租户 ID 保存在元数据中。
    • 那你会很难过。在框架完全启动之前,队列有效负载不会被反序列化。我建议重新审视你的租户启动过程,弄清楚如何在框架启动后让它工作。
    • 我知道 :) 不幸的是,我认为我最终可能需要在由守护队列工作程序创建的单独线程/进程中运行实际作业,因为这将允许完全控制引导一个新的具有正确配置的应用程序。我希望一旦我设置了一个租户,也许有一个聪明的解决方案可以重新启动一个已经实例化的应用程序,但我认为这很可能不像我希望的那么容易。
    • 对我来说非常重要的是,这个解决方案对第三方库创建的队列作业完全透明,所以我需要以这样的方式实现它,对它们来说,它的运行方式与单个库的运行方式相同-租户应用程序。我会将您的答案标记为已接受,因为对于寻求类似实现的其他人来说,这是一个非常好的解决方案。感谢您对此的帮助。
    • 很高兴我能帮上忙,反正部分是。不过我很好奇,如果队列作业是由第三方创建的,你怎么知道它属于哪个租户?
    猜你喜欢
    • 1970-01-01
    • 2020-05-08
    • 1970-01-01
    • 2018-06-17
    • 2015-12-30
    • 1970-01-01
    • 1970-01-01
    • 2011-05-03
    • 1970-01-01
    相关资源
    最近更新 更多