From 83948f0ee28ae9dca1fd4317e191c6c378f3287f Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 14 Oct 2025 14:47:27 -0700 Subject: [PATCH 1/8] Refactor for a single horizon instance to improve memory --- .../MakeQueueTenantAwareAction.php | 27 ------ .../Multitenancy/TenantAwareDispatcher.php | 31 ------- .../Multitenancy/TenantBootstrapper.php | 85 +++++++++++-------- .../Providers/ProcessMakerServiceProvider.php | 51 +++++++++++ config/multitenancy.php | 1 - 5 files changed, 100 insertions(+), 95 deletions(-) delete mode 100644 ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php delete mode 100644 ProcessMaker/Multitenancy/TenantAwareDispatcher.php diff --git a/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php b/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php deleted file mode 100644 index bfb3ff10a5..0000000000 --- a/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php +++ /dev/null @@ -1,27 +0,0 @@ -currentTenantContextKey()); - if (!$tenantId) { - // No need to do anything. Let the job run without a tenant. - return; - } - - parent::bindOrForgetCurrentTenant($event); - } -} diff --git a/ProcessMaker/Multitenancy/TenantAwareDispatcher.php b/ProcessMaker/Multitenancy/TenantAwareDispatcher.php deleted file mode 100644 index 62a20787c6..0000000000 --- a/ProcessMaker/Multitenancy/TenantAwareDispatcher.php +++ /dev/null @@ -1,31 +0,0 @@ -queueResolver); // we need to pass the queueResolver - $this->tenantId = $tenantId; - } - - public function dispatchToQueue($command) - { - $queue = $command->queue; - - // We need to set the default queue here - // because prepending the tenant id means - // it will no longer be empty. - if (empty($queue)) { - $queue = 'default'; - } - $command->queue = 'tenant-' . $this->tenantId . '-' . $queue; - - return parent::dispatchToQueue($command); - } -} diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php index 8e874a8da4..b462d825b4 100644 --- a/ProcessMaker/Multitenancy/TenantBootstrapper.php +++ b/ProcessMaker/Multitenancy/TenantBootstrapper.php @@ -17,20 +17,33 @@ */ class TenantBootstrapper { + private static $landlordValues = []; + private $encrypter = null; private $pdo = null; - private $originalValues = null; + private $app = null; + + public static $landlordKeysToSave = [ + 'APP_URL', + 'APP_KEY', + 'LOG_PATH', + 'DB_USERNAME', + 'DB_PASSWORD', + 'REDIS_PREFIX', + 'CACHE_SETTING_PREFIX', + 'SCRIPT_MICROSERVICE_CALLBACK', + ]; public function bootstrap(Application $app) { if (!$this->env('MULTITENANCY')) { return; } + $this->app = $app; - // We need to save the original values for running horizon - $this->saveOriginalValues(); + self::saveLandlordValues($app); $tenantData = null; @@ -49,29 +62,30 @@ public function bootstrap(Application $app) return; } - $this->setTenantEnvironmentVariables($app, $tenantData); + $this->setTenantEnvironmentVariables($tenantData); // Use tenant's translation files. Doing this here so it's available in cached filesystems.php $app->useLangPath(resource_path('lang/tenant_' . $tenantData['id'])); - $tenantData['original_values'] = $this->getOriginalValue(); + $tenantData['original_values'] = self::$landlordValues; Tenant::setBootstrappedTenant($app, $tenantData); } - private function setTenantEnvironmentVariables($app, $tenantData) + private function setTenantEnvironmentVariables($tenantData) { // Additional configs are set in SwitchTenant.php $tenantId = $tenantData['id']; $config = json_decode($tenantData['config'], true); - $this->set('APP_CONFIG_CACHE', $app->basePath('storage/tenant_' . $tenantId . '/config.php')); + $this->set('APP_CONFIG_CACHE', $this->app->basePath('storage/tenant_' . $tenantId . '/config.php')); // Do not override packages cache path for now. Wait until the License service is updated. - // $this->set('APP_PACKAGES_CACHE', $app->basePath('storage/tenant_' . $tenantId . '/packages.php')); - $this->set('LARAVEL_STORAGE_PATH', $app->basePath('storage/tenant_' . $tenantId)); + // $this->set('APP_PACKAGES_CACHE', $this->app->basePath('storage/tenant_' . $tenantId . '/packages.php')); + $this->set('LARAVEL_STORAGE_PATH', $this->app->basePath('storage/tenant_' . $tenantId)); $this->set('APP_URL', $config['app.url']); $this->set('APP_KEY', $this->decrypt($config['app.key'])); - $this->set('DB_DATABASE', $tenantData['database']); + $value = $tenantData['database']; + $this->set('DB_DATABASE', $value); $this->set('DB_USERNAME', $tenantData['username'] ?? $this->getOriginalValue('DB_USERNAME')); $encryptedPassword = $tenantData['password']; @@ -83,51 +97,45 @@ private function setTenantEnvironmentVariables($app, $tenantData) } $this->set('DB_PASSWORD', $password); - $this->set('REDIS_PREFIX', $this->getOriginalValue('REDIS_PREFIX') . 'tenant-' . $tenantId . ':'); - $this->set('LOG_PATH', $app->basePath('storage/tenant_' . $tenantId . '/logs/processmaker.log')); + // Commenting this out fixes the redis queue, but what about cache???? + // $this->set('REDIS_PREFIX', $this->getOriginalValue('REDIS_PREFIX') . 'tenant-' . $tenantId . ':'); + // $this->debug("Setting log path to " . $this->app->basePath('storage/tenant_' . $tenantId . '/logs/processmaker.log')); + $this->set('LOG_PATH', $this->app->basePath('storage/tenant_' . $tenantId . '/logs/processmaker.log')); } - private function saveOriginalValues() + public static function saveLandlordValues($app) { - if ($this->env('ORIGINAL_VALUES')) { + if ($app->has('landlordValues')) { + self::$landlordValues = $app->make('landlordValues'); + return; } - $toSave = [ - 'APP_URL', - 'APP_KEY', - 'DB_USERNAME', - 'DB_PASSWORD', - 'REDIS_PREFIX', - 'CACHE_SETTING_PREFIX', - 'SCRIPT_MICROSERVICE_CALLBACK', - ]; - $values = []; - foreach ($toSave as $key) { - $values[$key] = $this->env($key); + + foreach (self::$landlordKeysToSave as $key) { + self::$landlordValues[$key] = $_SERVER[$key] ?? ''; } - $this->set('ORIGINAL_VALUES', serialize($values)); } - private function getOriginalValue($key = null) + private function getOriginalValue($key) { - if (!$this->originalValues) { - $this->originalValues = unserialize($this->env('ORIGINAL_VALUES')); - } - if (!$key) { - return $this->originalValues; + if (!isset(self::$landlordValues[$key])) { + // throw new \Exception('Landlord value not found in `landlordValues`: ' . $key); + return ''; } - return $this->originalValues[$key]; + return self::$landlordValues[$key]; } private function env($key, $default = null) { - return Env::get($key, $default); + return $_SERVER[$key] ?? $default; } private function set($key, $value) { - Env::getRepository()->set($key, $value); + // Env::getRepository() is immutable but will use values from $_SERVER and $_ENV + $_SERVER[$key] = $value; + $_ENV[$key] = $value; } private function decrypt($value) @@ -174,4 +182,9 @@ private function executeQuery($query, $params = []) return $stmt->fetch(); } + + private function debug($message) + { + file_put_contents(base_path('storage/debug.log'), $message . PHP_EOL, FILE_APPEND); + } } diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php index fd93a0b222..0b49c8c18d 100644 --- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php +++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php @@ -9,9 +9,12 @@ use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Notifications\Events\BroadcastNotificationCreated; use Illuminate\Notifications\Events\NotificationSent; +use Illuminate\Queue\Events\JobAttempted; +use Illuminate\Queue\Events\JobProcessing; use Illuminate\Support\Arr; use Illuminate\Support\Env; use Illuminate\Support\Facades; +use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Route; @@ -37,6 +40,7 @@ use ProcessMaker\Managers\ScreenCompiledManager; use ProcessMaker\Models; use ProcessMaker\Multitenancy\Tenant; +use ProcessMaker\Multitenancy\TenantBootstrapper; use ProcessMaker\Observers; use ProcessMaker\PolicyExtension; use ProcessMaker\Repositories\SettingsConfigRepository; @@ -62,6 +66,9 @@ class ProcessMakerServiceProvider extends ServiceProvider // Track the query time for each request private static $queryTime = 0; + // Track the landlord values for multitenancy + private static $landlordValues = null; + public function boot(): void { // Track the start time for service providers boot @@ -233,6 +240,50 @@ public function register(): void */ protected static function registerEvents(): void { + Facades\Event::listen(JobProcessing::class, function ($event) { + Context::hydrate($event->job->payload()['illuminate:log:context'] ?? null); + $tenantId = Context::get(config('multitenancy.current_tenant_context_key')); + if ($tenantId) { + if (!method_exists($event->job, 'getRedisQueue')) { + // Not a redis job + return; + } + + // Save the landlord's config values so we can reset them later + if (self::$landlordValues === null) { + foreach (TenantBootstrapper::$landlordKeysToSave as $key) { + self::$landlordValues[$key] = $_SERVER[$key] ?? ''; + } + } + + // Create a new tenant app instance + $_SERVER['TENANT'] = $tenantId; + $_ENV['TENANT'] = $tenantId; + $tenantApp = require app()->bootstrapPath('app.php'); + $tenantApp->instance('landlordValues', self::$landlordValues); + $tenantApp->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + + // Change the job's app service container to the tenant app + $event->job->getRedisQueue()->setContainer($tenantApp); + } + }); + + Facades\Event::listen(JobAttempted::class, function ($event) { + if (!method_exists($event->job, 'getRedisQueue')) { + // Not a redis job + return; + } + + unset($_SERVER['TENANT']); + unset($_ENV['TENANT']); + + // Restore the original values since the tenant boostrapper modified them + foreach (self::$landlordValues as $key => $value) { + $_SERVER[$key] = $value; + $_ENV[$key] = $value; + } + }); + // Listen to the events for our core screen // types and add our javascript Facades\Event::listen(ScreenBuilderStarting::class, function ($event) { diff --git a/config/multitenancy.php b/config/multitenancy.php index b1a5aa5e2f..a909be07a1 100644 --- a/config/multitenancy.php +++ b/config/multitenancy.php @@ -87,7 +87,6 @@ 'actions' => [ 'make_tenant_current_action' => MakeTenantCurrentAction::class, 'forget_current_tenant_action' => ForgetCurrentTenantAction::class, - 'make_queue_tenant_aware_action' => ProcessMaker\Multitenancy\MakeQueueTenantAwareAction::class, 'migrate_tenant' => MigrateTenantAction::class, ], From beaf3ddbd0818485792a8bfb168e24e6620d7e3e Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 14 Oct 2025 14:56:42 -0700 Subject: [PATCH 2/8] Removing comments --- ProcessMaker/Multitenancy/TenantBootstrapper.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php index b462d825b4..c86ef81d67 100644 --- a/ProcessMaker/Multitenancy/TenantBootstrapper.php +++ b/ProcessMaker/Multitenancy/TenantBootstrapper.php @@ -79,8 +79,6 @@ private function setTenantEnvironmentVariables($tenantData) $config = json_decode($tenantData['config'], true); $this->set('APP_CONFIG_CACHE', $this->app->basePath('storage/tenant_' . $tenantId . '/config.php')); - // Do not override packages cache path for now. Wait until the License service is updated. - // $this->set('APP_PACKAGES_CACHE', $this->app->basePath('storage/tenant_' . $tenantId . '/packages.php')); $this->set('LARAVEL_STORAGE_PATH', $this->app->basePath('storage/tenant_' . $tenantId)); $this->set('APP_URL', $config['app.url']); $this->set('APP_KEY', $this->decrypt($config['app.key'])); @@ -97,9 +95,6 @@ private function setTenantEnvironmentVariables($tenantData) } $this->set('DB_PASSWORD', $password); - // Commenting this out fixes the redis queue, but what about cache???? - // $this->set('REDIS_PREFIX', $this->getOriginalValue('REDIS_PREFIX') . 'tenant-' . $tenantId . ':'); - // $this->debug("Setting log path to " . $this->app->basePath('storage/tenant_' . $tenantId . '/logs/processmaker.log')); $this->set('LOG_PATH', $this->app->basePath('storage/tenant_' . $tenantId . '/logs/processmaker.log')); } @@ -119,7 +114,6 @@ public static function saveLandlordValues($app) private function getOriginalValue($key) { if (!isset(self::$landlordValues[$key])) { - // throw new \Exception('Landlord value not found in `landlordValues`: ' . $key); return ''; } From 47453205f47cd4ee70fc5ae2b1d17878303a3547 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 14 Oct 2025 15:21:22 -0700 Subject: [PATCH 3/8] Set the storage path instead of the env var --- ProcessMaker/Multitenancy/TenantBootstrapper.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php index c86ef81d67..257e7300a9 100644 --- a/ProcessMaker/Multitenancy/TenantBootstrapper.php +++ b/ProcessMaker/Multitenancy/TenantBootstrapper.php @@ -62,6 +62,10 @@ public function bootstrap(Application $app) return; } + + // Set storage path + $app->useStoragePath($app->basePath('storage/tenant_' . $tenantData['id'])); + $this->setTenantEnvironmentVariables($tenantData); // Use tenant's translation files. Doing this here so it's available in cached filesystems.php @@ -75,11 +79,9 @@ private function setTenantEnvironmentVariables($tenantData) { // Additional configs are set in SwitchTenant.php - $tenantId = $tenantData['id']; $config = json_decode($tenantData['config'], true); - $this->set('APP_CONFIG_CACHE', $this->app->basePath('storage/tenant_' . $tenantId . '/config.php')); - $this->set('LARAVEL_STORAGE_PATH', $this->app->basePath('storage/tenant_' . $tenantId)); + $this->set('APP_CONFIG_CACHE', $this->app->storagePath('config.php')); $this->set('APP_URL', $config['app.url']); $this->set('APP_KEY', $this->decrypt($config['app.key'])); $value = $tenantData['database']; @@ -95,7 +97,7 @@ private function setTenantEnvironmentVariables($tenantData) } $this->set('DB_PASSWORD', $password); - $this->set('LOG_PATH', $this->app->basePath('storage/tenant_' . $tenantId . '/logs/processmaker.log')); + $this->set('LOG_PATH', $this->app->storagePath('logs/processmaker.log')); } public static function saveLandlordValues($app) @@ -176,9 +178,4 @@ private function executeQuery($query, $params = []) return $stmt->fetch(); } - - private function debug($message) - { - file_put_contents(base_path('storage/debug.log'), $message . PHP_EOL, FILE_APPEND); - } } From 61a6a861911bcdd1366fb7bb1ceeed4a5b973912 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Wed, 15 Oct 2025 10:00:30 -0700 Subject: [PATCH 4/8] Fix boolean values --- ProcessMaker/Multitenancy/TenantBootstrapper.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php index 257e7300a9..44897c4763 100644 --- a/ProcessMaker/Multitenancy/TenantBootstrapper.php +++ b/ProcessMaker/Multitenancy/TenantBootstrapper.php @@ -84,8 +84,7 @@ private function setTenantEnvironmentVariables($tenantData) $this->set('APP_CONFIG_CACHE', $this->app->storagePath('config.php')); $this->set('APP_URL', $config['app.url']); $this->set('APP_KEY', $this->decrypt($config['app.key'])); - $value = $tenantData['database']; - $this->set('DB_DATABASE', $value); + $this->set('DB_DATABASE', $tenantData['database']); $this->set('DB_USERNAME', $tenantData['username'] ?? $this->getOriginalValue('DB_USERNAME')); $encryptedPassword = $tenantData['password']; @@ -124,7 +123,14 @@ private function getOriginalValue($key) private function env($key, $default = null) { - return $_SERVER[$key] ?? $default; + $value = $_SERVER[$key] ?? $default; + if ($value === 'true') { + $value = true; + } elseif ($value === 'false') { + $value = false; + } + + return $value; } private function set($key, $value) From 4c8a6f3449c4f34bb880a59c88718737168a9bac Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Wed, 15 Oct 2025 14:26:43 -0700 Subject: [PATCH 5/8] Bring back MakeQueueTenantAwareAction --- .../MakeQueueTenantAwareAction.php | 17 ++++ .../Providers/ProcessMakerServiceProvider.php | 98 ++++++++++++------- config/multitenancy.php | 1 + 3 files changed, 79 insertions(+), 37 deletions(-) create mode 100644 ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php diff --git a/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php b/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php new file mode 100644 index 0000000000..25118798fd --- /dev/null +++ b/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php @@ -0,0 +1,17 @@ +job->payload()['illuminate:log:context'] ?? null); - $tenantId = Context::get(config('multitenancy.current_tenant_context_key')); - if ($tenantId) { - if (!method_exists($event->job, 'getRedisQueue')) { - // Not a redis job - return; - } + Context::hydrate($event->job->payload()['illuminate:log:context'] ?? null); + $tenantId = Context::get(config('multitenancy.current_tenant_context_key')); + if ($tenantId) { + if (!method_exists($event->job, 'getRedisQueue')) { + // Not a redis job + return; + } - // Save the landlord's config values so we can reset them later - if (self::$landlordValues === null) { - foreach (TenantBootstrapper::$landlordKeysToSave as $key) { - self::$landlordValues[$key] = $_SERVER[$key] ?? ''; - } + // Save the landlord's config values so we can reset them later + if (self::$landlordValues === null) { + foreach (TenantBootstrapper::$landlordKeysToSave as $key) { + self::$landlordValues[$key] = $_SERVER[$key] ?? ''; } + } - // Create a new tenant app instance - $_SERVER['TENANT'] = $tenantId; - $_ENV['TENANT'] = $tenantId; - $tenantApp = require app()->bootstrapPath('app.php'); - $tenantApp->instance('landlordValues', self::$landlordValues); - $tenantApp->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + // Create a new tenant app instance + $_SERVER['TENANT'] = $tenantId; + $_ENV['TENANT'] = $tenantId; + $tenantApp = require app()->bootstrapPath('app.php'); + $tenantApp->instance('landlordValues', self::$landlordValues); + $tenantApp->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); - // Change the job's app service container to the tenant app - $event->job->getRedisQueue()->setContainer($tenantApp); - } - }); + // Change the job's app service container to the tenant app + $event->job->getRedisQueue()->setContainer($tenantApp); + } + } - Facades\Event::listen(JobAttempted::class, function ($event) { - if (!method_exists($event->job, 'getRedisQueue')) { - // Not a redis job - return; - } + private static function resetTenantApp($event): void + { + if (!method_exists($event->job, 'getRedisQueue')) { + // Not a redis job + return; + } - unset($_SERVER['TENANT']); - unset($_ENV['TENANT']); + unset($_SERVER['TENANT']); + unset($_ENV['TENANT']); - // Restore the original values since the tenant boostrapper modified them - foreach (self::$landlordValues as $key => $value) { - $_SERVER[$key] = $value; - $_ENV[$key] = $value; - } + if (!self::$landlordValues) { + return; + } + + // Restore the original values since the tenant boostrapper modified them + foreach (self::$landlordValues as $key => $value) { + $_SERVER[$key] = $value; + $_ENV[$key] = $value; + } + } + + /** + * Register app-level events. + */ + protected static function registerEvents(): void + { + Facades\Event::listen(JobProcessing::class, function (JobProcessing $event) { + self::bootstrapTenantApp($event); + }); + + Facades\Event::listen(JobRetryRequested::class, function (JobRetryRequested $event) { + self::bootstrapTenantApp($event); + }); + + Facades\Event::listen(JobAttempted::class, function (JobAttempted $event) { + self::resetTenantApp($event); }); // Listen to the events for our core screen diff --git a/config/multitenancy.php b/config/multitenancy.php index a909be07a1..b1a5aa5e2f 100644 --- a/config/multitenancy.php +++ b/config/multitenancy.php @@ -87,6 +87,7 @@ 'actions' => [ 'make_tenant_current_action' => MakeTenantCurrentAction::class, 'forget_current_tenant_action' => ForgetCurrentTenantAction::class, + 'make_queue_tenant_aware_action' => ProcessMaker\Multitenancy\MakeQueueTenantAwareAction::class, 'migrate_tenant' => MigrateTenantAction::class, ], From 7d3d8011d36b45eac73af06f39dbb2b15513d825 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Thu, 16 Oct 2025 09:14:16 -0700 Subject: [PATCH 6/8] Fix tenant queues --- .../Admin/TenantQueueController.php | 2 +- .../Http/Middleware/GenerateMenus.php | 8 +++- .../Providers/TenantQueueServiceProvider.php | 42 ++++++++++++++----- config/queue.php | 4 +- .../js/admin/tenant-queues/JobDetails.vue | 7 ++++ .../js/admin/tenant-queues/TenantJobs.vue | 11 +++-- resources/js/admin/tenant-queues/router.js | 4 +- .../views/admin/tenant-queues/index.blade.php | 4 +- 8 files changed, 58 insertions(+), 24 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Admin/TenantQueueController.php b/ProcessMaker/Http/Controllers/Admin/TenantQueueController.php index 5635224eda..4d2b6b094b 100644 --- a/ProcessMaker/Http/Controllers/Admin/TenantQueueController.php +++ b/ProcessMaker/Http/Controllers/Admin/TenantQueueController.php @@ -20,7 +20,7 @@ class TenantQueueController extends Controller public function __construct() { // Check if tenant job tracking is enabled - $enabled = config('queue.tenant_tracking_enabled', false); + $enabled = TenantQueueServiceProvider::enabled(); if (!$enabled) { if (!app()->runningInConsole()) { diff --git a/ProcessMaker/Http/Middleware/GenerateMenus.php b/ProcessMaker/Http/Middleware/GenerateMenus.php index f714b52ac5..e52ee07a44 100644 --- a/ProcessMaker/Http/Middleware/GenerateMenus.php +++ b/ProcessMaker/Http/Middleware/GenerateMenus.php @@ -8,6 +8,7 @@ use Lavary\Menu\Facade as Menu; use ProcessMaker\Models\Permission; use ProcessMaker\Models\Setting; +use ProcessMaker\Providers\TenantQueueServiceProvider; class GenerateMenus { @@ -110,8 +111,13 @@ public function handle(Request $request, Closure $next) 'icon' => 'fa-palette', ]); + $url = 'admin/queues'; + if (TenantQueueServiceProvider::enabled() && $tenantid = app('currentTenant')?->id) { + $url = 'admin/tenant-queues#/tenant/' . $tenantid . '/jobs'; + } + $submenu->add(__('Queue Management'), [ - 'route' => 'queues.index', + 'url' => $url, 'icon' => 'fa-infinity', ]); diff --git a/ProcessMaker/Providers/TenantQueueServiceProvider.php b/ProcessMaker/Providers/TenantQueueServiceProvider.php index 60bd89c45b..f3bcf0a8fb 100644 --- a/ProcessMaker/Providers/TenantQueueServiceProvider.php +++ b/ProcessMaker/Providers/TenantQueueServiceProvider.php @@ -6,10 +6,14 @@ use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Queue\Events\JobQueued; +use Illuminate\Queue\Events\JobQueueing; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Redis; use Illuminate\Support\ServiceProvider; +use Laravel\Horizon\Events\JobPushed; class TenantQueueServiceProvider extends ServiceProvider { @@ -27,9 +31,7 @@ public function register(): void public function boot(): void { // Only register queue event listeners if tenant job tracking is enabled - $enabled = config('queue.tenant_tracking_enabled', false); - - if ($enabled) { + if (self::enabled()) { $this->registerQueueEventListeners(); } } @@ -39,6 +41,10 @@ public function boot(): void */ protected function registerQueueEventListeners(): void { + Event::listen(JobQueued::class, function (JobQueued $event) { + $this->trackJobByTenant($event, 'pushed', $event->id, $event->queue); + }); + // Job pending Queue::before(function (JobProcessing $event) { $this->trackJobByTenant($event->job, 'pending'); @@ -63,7 +69,7 @@ protected function registerQueueEventListeners(): void /** * Track job by tenant in Redis. */ - protected function trackJobByTenant($job, string $status): void + protected function trackJobByTenant($job, string $status, $jobId = null, $queue = null): void { try { $tenantId = $this->extractTenantIdFromJob($job); @@ -72,7 +78,7 @@ protected function trackJobByTenant($job, string $status): void return; } - $jobId = $job->getJobId(); + $jobId = $jobId ?? $job->getJobId(); if (!$jobId) { return; } @@ -82,14 +88,14 @@ protected function trackJobByTenant($job, string $status): void $jobData = [ 'id' => $jobId, 'name' => $this->getJobName($job), - 'queue' => $job->getQueue() ?? 'default', + 'queue' => ($queue ?? $job->getQueue()) ?? 'default', 'status' => $status, - 'created_at' => $payload['pushedAt'], + 'created_at' => $status === 'pushed' ? str_replace(',', '.', microtime(true)) : $payload['pushedAt'], 'completed_at' => $status === 'completed' ? str_replace(',', '.', microtime(true)) : null, 'failed_at' => $status === 'failed' ? str_replace(',', '.', microtime(true)) : null, 'updated_at' => str_replace(',', '.', microtime(true)), 'tenant_id' => $tenantId, - 'attempts' => $job->attempts(), + 'attempts' => $status === 'pushed' ? 0 : $job->attempts(), 'payload' => json_encode($payload['data']), ]; @@ -97,6 +103,10 @@ protected function trackJobByTenant($job, string $status): void $jobData['queued_at'] = str_replace(',', '.', microtime(true)); } + if ($status === 'pushed') { + $jobData['pushed_at'] = str_replace(',', '.', microtime(true)); + } + // Check if job already exists $tenantKey = "tenant_jobs:{$tenantId}:{$jobId}"; $existingJobData = Redis::get($tenantKey); @@ -230,7 +240,7 @@ protected function updateTenantJobCounters(string $tenantId, string $status, str */ protected function handleStatusTransition(string $tenantId, string $jobId, string $newStatus): void { - $statuses = ['pending', 'completed', 'failed', 'exception']; + $statuses = ['pushed', 'pending', 'completed', 'failed', 'exception']; foreach ($statuses as $status) { if ($status === $newStatus) { @@ -306,7 +316,7 @@ public static function getTenantJobs(string $tenantId, string $status = null, in $jobIds = Redis::lrange($listKey, 0, $limit - 1); } else { // Get jobs from all statuses - $statuses = ['pending', 'completed', 'failed', 'exception']; + $statuses = ['pushed', 'pending', 'completed', 'failed', 'exception']; $jobIds = []; foreach ($statuses as $status) { @@ -345,6 +355,7 @@ public static function getTenantJobStats(string $tenantId): array return [ 'total' => (int) ($counters['total'] ?? 0), + 'pushed' => (int) ($counters['pushed'] ?? 0), 'pending' => (int) ($counters['pending'] ?? 0), 'completed' => (int) ($counters['completed'] ?? 0), 'failed' => (int) ($counters['failed'] ?? 0), @@ -362,7 +373,7 @@ public static function getTenantsWithJobs(): array $tenants = []; foreach ($keys as $key) { - $tenantId = str_replace('tenant_job_counters:', '', $key); + $tenantId = preg_replace('/^.*:/', '', $key); $stats = self::getTenantJobStats($tenantId); if ($stats['total'] > 0) { @@ -375,4 +386,13 @@ public static function getTenantsWithJobs(): array return $tenants; } + + public static function enabled(): bool + { + if (!config('app.multitenancy')) { + return false; + } + + return !config('queue.disable_tenant_tracking'); + } } diff --git a/config/queue.php b/config/queue.php index 74a0c5d7e1..7b8030529b 100644 --- a/config/queue.php +++ b/config/queue.php @@ -96,11 +96,13 @@ |-------------------------------------------------------------------------- | | These options configure the behavior of tenant-specific job tracking. + | Job tracking is enabled by default when multitenancy is enabled. | When enabled, jobs will be tracked per tenant in Redis for monitoring | and analytics purposes. | + | Set this to true to disable job tracking for all tenants. */ - 'tenant_tracking_enabled' => env('QUEUE_TENANT_TRACKING_ENABLED', false), + 'disable_tenant_tracking' => env('QUEUE_DISABLE_TENANT_TRACKING', false), ]; diff --git a/resources/js/admin/tenant-queues/JobDetails.vue b/resources/js/admin/tenant-queues/JobDetails.vue index 66bd143c11..5c33f1ad0a 100644 --- a/resources/js/admin/tenant-queues/JobDetails.vue +++ b/resources/js/admin/tenant-queues/JobDetails.vue @@ -104,6 +104,13 @@ {{ job.attempts }} +
+ Pushed At: +
+
+ {{ formatTimestamp(job.pushed_at) }} +
+
Queued At:
diff --git a/resources/js/admin/tenant-queues/TenantJobs.vue b/resources/js/admin/tenant-queues/TenantJobs.vue index c96a807474..67c7d09fd1 100644 --- a/resources/js/admin/tenant-queues/TenantJobs.vue +++ b/resources/js/admin/tenant-queues/TenantJobs.vue @@ -6,7 +6,7 @@ - -