From 2fbb3794359df7691188f8731ef4c9a1a9dbb96a Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 19 Jun 2026 09:41:05 -0400 Subject: [PATCH 1/7] feat(FOUR-31695): solving conflicts --- .gitignore | 5 +- .../Repositories/SettingsConfigRepository.php | 2 +- composer.json | 7 +- composer.lock | 177 ++++++++++++++++++ 4 files changed, 186 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 7a47d39bab..f30ac80d98 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,7 @@ devhub/pm-font/dist test-db-snapshot.db snapshot_*.db storage/transitions -.envrc \ No newline at end of file +.envrc +**/caddy +frankenphp +frankenphp-worker.php diff --git a/ProcessMaker/Repositories/SettingsConfigRepository.php b/ProcessMaker/Repositories/SettingsConfigRepository.php index 2f4cf6e0b2..f2707faf2f 100644 --- a/ProcessMaker/Repositories/SettingsConfigRepository.php +++ b/ProcessMaker/Repositories/SettingsConfigRepository.php @@ -39,7 +39,7 @@ public function get($key, $default = null) if ($key === 'session.lifetime') { $settingValue = $this->getFromSettings($key); - return $settingValue ?? $default; + return $settingValue ?: Arr::get($this->items, $key) ?: $default ?: 120; } if (Arr::has($this->items, $key)) { diff --git a/composer.json b/composer.json index 1bd4939f7f..474148e089 100644 --- a/composer.json +++ b/composer.json @@ -25,8 +25,9 @@ "guzzlehttp/psr7": "^2.12.3", "igaster/laravel-theme": "^2.0", "jenssegers/agent": "^2.6", - "laravel/framework": "^13.13", - "laravel/horizon": "^5.47", + "laravel/framework": "^13.0", + "laravel/horizon": "^5.45", + "laravel/octane": "^2.17", "laravel/pail": "^1.2", "laravel/passport": "^13.7", "laravel/scout": "^11.1", @@ -253,4 +254,4 @@ "ignore": [] } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 7424e10bfa..b507253c75 100644 --- a/composer.lock +++ b/composer.lock @@ -2896,6 +2896,94 @@ ], "time": "2020-06-13T08:05:20+00:00" }, + { + "name": "laminas/laminas-diactoros", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "60c182916b2749480895601649563970f3f12ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/60c182916b2749480895601649563970f3f12ec4", + "reference": "60c182916b2749480895601649563970f3f12ec4", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~3.1.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-12T15:31:36+00:00" + }, { "name": "laravel/framework", "version": "v13.13.0", @@ -3200,6 +3288,95 @@ }, "time": "2026-06-03T15:11:37+00:00" }, + { + "name": "laravel/octane", + "version": "v2.17.5", + "source": { + "type": "git", + "url": "https://github.com/laravel/octane.git", + "reference": "058ae4d7109eed40836dc42960f9388b9bf71f73" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/octane/zipball/058ae4d7109eed40836dc42960f9388b9bf71f73", + "reference": "058ae4d7109eed40836dc42960f9388b9bf71f73", + "shasum": "" + }, + "require": { + "laminas/laminas-diactoros": "^3.0", + "laravel/framework": "^10.10.1|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "nesbot/carbon": "^2.66.0|^3.0", + "php": "^8.1.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0|^8.0" + }, + "conflict": { + "spiral/roadrunner": "<2023.1.0", + "spiral/roadrunner-cli": "<2.6.0", + "spiral/roadrunner-http": "<3.3.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.6.1", + "inertiajs/inertia-laravel": "^1.3.2|^2.0", + "laravel/scout": "^10.2.1", + "laravel/socialite": "^5.6.1", + "livewire/livewire": "^2.12.3|^3.0", + "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0", + "orchestra/testbench": "^8.21|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.1.7", + "phpunit/phpunit": "^10.4|^11.5|^12.0|^13.0", + "spiral/roadrunner-cli": "^2.6.0", + "spiral/roadrunner-http": "^3.3.0" + }, + "bin": [ + "bin/roadrunner-worker", + "bin/swoole-server" + ], + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Octane": "Laravel\\Octane\\Facades\\Octane" + }, + "providers": [ + "Laravel\\Octane\\OctaneServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Octane\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Supercharge your Laravel application's performance.", + "keywords": [ + "frankenphp", + "laravel", + "octane", + "roadrunner", + "swoole" + ], + "support": { + "issues": "https://github.com/laravel/octane/issues", + "source": "https://github.com/laravel/octane" + }, + "time": "2026-06-04T09:05:08+00:00" + }, { "name": "laravel/pail", "version": "v1.2.6", From f4126faafa22b3c8327a0e677473bfb13fe21de6 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 26 Jun 2026 08:41:15 -0400 Subject: [PATCH 2/7] feat(FOUR-32024): review and fix the calculation of DB timing when loaded by octane --- .../Middleware/ServerTimingMiddleware.php | 2 ++ .../Providers/ProcessMakerServiceProvider.php | 8 +++++++ tests/Feature/ServerTimingMiddlewareTest.php | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/ProcessMaker/Http/Middleware/ServerTimingMiddleware.php b/ProcessMaker/Http/Middleware/ServerTimingMiddleware.php index 96ade2bded..2a15e84a6d 100644 --- a/ProcessMaker/Http/Middleware/ServerTimingMiddleware.php +++ b/ProcessMaker/Http/Middleware/ServerTimingMiddleware.php @@ -28,6 +28,8 @@ public function handle(Request $request, Closure $next): Response return $next($request); } + ProcessMakerServiceProvider::beginRequestTiming(); + // Start time for controller execution $startController = microtime(true); diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php index 762fd6cd0b..706bba7625 100644 --- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php +++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php @@ -511,6 +511,14 @@ public static function getBootTime(): ?float return self::$bootTime; } + /** + * Reset per-request query timing metrics. + */ + public static function beginRequestTiming(): void + { + self::$queryTime = 0; + } + /** * Get the query time for the request. * diff --git a/tests/Feature/ServerTimingMiddlewareTest.php b/tests/Feature/ServerTimingMiddlewareTest.php index b9449ebfed..040401d227 100644 --- a/tests/Feature/ServerTimingMiddlewareTest.php +++ b/tests/Feature/ServerTimingMiddlewareTest.php @@ -6,6 +6,8 @@ use Illuminate\Support\Facades\Route; use ProcessMaker\Http\Middleware\ServerTimingMiddleware; use ProcessMaker\Models\User; +use ProcessMaker\Providers\ProcessMakerServiceProvider; +use ReflectionClass; use Tests\Feature\Shared\RequestHelper; use Tests\TestCase; @@ -41,6 +43,28 @@ public function testServerTimingHeaderIncludesAllMetrics() $this->assertStringContainsString('db;dur=', $serverTiming[2]); } + public function testBeginRequestTimingClearsAccumulatedQueryTime() + { + $reflection = new ReflectionClass(ProcessMakerServiceProvider::class); + $property = $reflection->getProperty('queryTime'); + $property->setAccessible(true); + $property->setValue(null, 500); + + Route::middleware(ServerTimingMiddleware::class)->get('/timing-reset-test', function () { + DB::select('SELECT 1'); + + return response()->json(['message' => 'Timing reset test']); + }); + + $response = $this->get('/timing-reset-test'); + $serverTiming = $this->getHeader($response, 'server-timing'); + + preg_match('/db;dur=([\d.]+)/', implode(',', $serverTiming), $matches); + $dbTime = (float) ($matches[1] ?? 500); + + $this->assertLessThan(500, $dbTime); + } + public function testQueryTimeIsMeasured() { // Mock a route with a query From 1a44a4e1ec0803410cc88665ee0e105d2b810148 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 29 Jul 2026 10:11:13 -0400 Subject: [PATCH 3/7] feat(FOUR-32353): solving conflicts --- .env.dusk.testing | 2 + .env.example | 3 ++ .../Listeners/HandleRedirectListener.php | 11 +++++ .../Providers/ProcessMakerServiceProvider.php | 48 +++++++++++++++++++ config/app.php | 4 ++ config/octane.php | 32 +++++++++++++ 6 files changed, 100 insertions(+) create mode 100644 config/octane.php diff --git a/.env.dusk.testing b/.env.dusk.testing index 7b284846d8..a5434bf44a 100644 --- a/.env.dusk.testing +++ b/.env.dusk.testing @@ -17,6 +17,8 @@ DATA_DB_PASSWORD=secret APP_URL=http://127.0.0.1 +OCTANE_ENABLED=false + SAUCELABS_BROWSER_TESTING=false SAUCELABS_USERNAME=processmaker SAUCELABS_ACCESS_KEY=eb78836b-b7c9-4800-95b4-69ef4be96106 diff --git a/.env.example b/.env.example index c3a094135c..38cd7f8cfb 100644 --- a/.env.example +++ b/.env.example @@ -70,3 +70,6 @@ KEYCLOAK_CLIENT_SECRET= KEYCLOAK_BASE_URL= KEYCLOAK_USERNAME= KEYCLOAK_PASSWORD= +# Enable Octane compatibility listeners when running under `php artisan octane:start` +OCTANE_ENABLED=false +OCTANE_MAX_REQUESTS=500 diff --git a/ProcessMaker/Listeners/HandleRedirectListener.php b/ProcessMaker/Listeners/HandleRedirectListener.php index 78491809a4..7679a73572 100644 --- a/ProcessMaker/Listeners/HandleRedirectListener.php +++ b/ProcessMaker/Listeners/HandleRedirectListener.php @@ -20,6 +20,17 @@ protected function setRedirectTo(ProcessRequest $processRequest, string $method, self::$redirectionParams = $params; } + /** + * Reset the static state for Octane compatibility. + * This prevents data leaks between requests in long-running workers. + */ + public static function reset(): void + { + self::$processRequest = null; + self::$redirectionMethod = ''; + self::$redirectionParams = []; + } + public static function sendRedirectToEvent() { $method = self::$redirectionMethod; diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php index 706bba7625..331ae6d2a7 100644 --- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php +++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php @@ -43,6 +43,7 @@ use ProcessMaker\ImportExport\SignalHelper; use ProcessMaker\Jobs\SmartInbox; use ProcessMaker\LicensedPackageManifest; +use ProcessMaker\Listeners\HandleRedirectListener; use ProcessMaker\Managers; use ProcessMaker\Managers\MenuManager; use ProcessMaker\Managers\ScreenCompiledManager; @@ -107,6 +108,9 @@ public function boot(): void $this->checkConfigCache(); + // Register Octane listeners if Octane is enabled + $this->registerOctaneListeners(); + // Hook after service providers boot self::$bootTime = (microtime(true) - self::$bootStart) * 1000; // Convert to milliseconds } @@ -529,6 +533,15 @@ public static function getQueryTime(): float return self::$queryTime; } + /** + * Reset the query time for Octane compatibility. + * This prevents timing metrics from leaking between requests. + */ + public static function resetQueryTime(): void + { + self::$queryTime = 0; + } + /** * Set the boot time for service providers. * @@ -576,6 +589,41 @@ public static function getPackageBootTiming(): array return self::$packageBootTiming; } + /** + * Register Octane-specific listeners for request lifecycle management. + * + * When OCTANE_ENABLED is true, this method registers: + * - A 'requestHandled' listener to reset per-request static state + * - A flush list for stateful singletons + * - A tick listener for memory monitoring + */ + private function registerOctaneListeners(): void + { + if (!config('app.octane_enabled', false) || !class_exists('\Laravel\Octane\Octane')) { + return; + } + + $octane = \Laravel\Octane\Octane::class; + + // Reset per-request static state after each request + $octane::on('requestHandled', function ($request, $result) { + self::resetQueryTime(); + HandleRedirectListener::reset(); + }); + + // Flush stateful singletons per request + $octane::flush(config('octane.flush', [])); + + // Monitor memory usage every 30 seconds + $octane::tick('octane-memory-monitor', function () { + $memory = memory_get_usage(true); + $threshold = 128 * 1024 * 1024; // 128MB + if ($memory > $threshold) { + Log::warning('Octane worker memory high: ' . round($memory / 1024 / 1024, 2) . 'MB'); + } + })->seconds(30); + } + /** * Find the tenant based on the environment variable */ diff --git a/config/app.php b/config/app.php index b3c5891f98..72088b34ab 100644 --- a/config/app.php +++ b/config/app.php @@ -266,6 +266,10 @@ 'force_https' => env('FORCE_HTTPS', true), + // Enable Octane compatibility listeners when running under `php artisan octane:start`. + 'octane_enabled' => filter_var(env('OCTANE_ENABLED', false), FILTER_VALIDATE_BOOLEAN), + 'octane_max_requests' => (int) env('OCTANE_MAX_REQUESTS', 500), + 'nayra_docker_network' => env('NAYRA_DOCKER_NETWORK', 'host'), 'nayra_port' => env('NAYRA_PORT', 8080), diff --git a/config/octane.php b/config/octane.php new file mode 100644 index 0000000000..13fe63eb5e --- /dev/null +++ b/config/octane.php @@ -0,0 +1,32 @@ + [ + // Services with mutable state that must be recreated per request + ProcessMaker\Models\AnonymousUser::class, + ProcessMaker\ImportExport\Extension::class, + ProcessMaker\ImportExport\SignalHelper::class, + ProcessMaker\Managers\MenuManager::class, + ], + + 'warm' => [ + // Services to pre-resolve on worker start + ProcessMaker\Managers\PackageManager::class, + ProcessMaker\Managers\LoginManager::class, + ProcessMaker\Managers\IndexManager::class, + ], +]; From c7bb47d861a2ffa48d533223ae86d29e8e9cdb80 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 29 Jul 2026 11:02:25 -0400 Subject: [PATCH 4/7] feat(FOUR-32353): solving conflicts --- .env.dusk.testing | 2 - .env.example | 3 -- .../Providers/ProcessMakerServiceProvider.php | 45 +++++++------------ config/app.php | 4 -- config/octane.php | 4 +- 5 files changed, 19 insertions(+), 39 deletions(-) diff --git a/.env.dusk.testing b/.env.dusk.testing index a5434bf44a..7b284846d8 100644 --- a/.env.dusk.testing +++ b/.env.dusk.testing @@ -17,8 +17,6 @@ DATA_DB_PASSWORD=secret APP_URL=http://127.0.0.1 -OCTANE_ENABLED=false - SAUCELABS_BROWSER_TESTING=false SAUCELABS_USERNAME=processmaker SAUCELABS_ACCESS_KEY=eb78836b-b7c9-4800-95b4-69ef4be96106 diff --git a/.env.example b/.env.example index 38cd7f8cfb..c3a094135c 100644 --- a/.env.example +++ b/.env.example @@ -70,6 +70,3 @@ KEYCLOAK_CLIENT_SECRET= KEYCLOAK_BASE_URL= KEYCLOAK_USERNAME= KEYCLOAK_PASSWORD= -# Enable Octane compatibility listeners when running under `php artisan octane:start` -OCTANE_ENABLED=false -OCTANE_MAX_REQUESTS=500 diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php index 331ae6d2a7..c4d09de820 100644 --- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php +++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php @@ -17,12 +17,14 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\URL; use Laravel\Horizon\Horizon; use Laravel\Horizon\SystemProcessCounter; use Laravel\Horizon\WorkerCommandString; +use Laravel\Octane\Events\RequestTerminated; use Laravel\Passport\Client as PassportClient; use Lavary\Menu\Menu; use OpenApi\Analysers\AttributeAnnotationFactory; @@ -264,7 +266,7 @@ protected static function registerEvents(): void { // Listen to the events for our core screen // types and add our javascript - Facades\Event::listen(ScreenBuilderStarting::class, function ($event) { + Event::listen(ScreenBuilderStarting::class, function ($event) { // Add any extensions to form builder // and renderer from packages $event->manager->addPackageScripts($event->type); @@ -283,7 +285,7 @@ protected static function registerEvents(): void }); // Log Notifications - Facades\Event::listen(NotificationSent::class, function ($event) { + Event::listen(NotificationSent::class, function ($event) { $id = $event->notifiable->id; $notifiable = get_class($event->notifiable); $notification = get_class($event->notification); @@ -292,24 +294,24 @@ protected static function registerEvents(): void }); // Log Broadcasts (messages sent to laravel-echo-server and redis) - Facades\Event::listen(BroadcastNotificationCreated::class, function ($event) { + Event::listen(BroadcastNotificationCreated::class, function ($event) { $channels = implode(', ', $event->broadcastOn()); Log::debug('Broadcasting Notification ' . $event->broadcastType() . 'on channel(s) ' . $channels); }); // Fire job when task is assigned to a user - Facades\Event::listen(ActivityAssigned::class, function ($event) { + Event::listen(ActivityAssigned::class, function ($event) { $task_id = $event->getProcessRequestToken()->id; // Dispatch the SmartInbox job with the processRequestToken as parameter SmartInbox::dispatch($task_id); }); - Facades\Event::listen(MadeTenantCurrentEvent::class, function ($event) { + Event::listen(MadeTenantCurrentEvent::class, function ($event) { event(new TenantResolved($event->tenant)); }); - Facades\Event::listen(TenantNotFoundForRequestEvent::class, function ($event) { + Event::listen(TenantNotFoundForRequestEvent::class, function ($event) { if (config('app.multitenancy') === false || self::actuallyRunningInConsole()) { // This is expected if multitenancy is disabled. // We also need to check if we are running in a console command because @@ -334,7 +336,7 @@ protected static function registerEvents(): void } }); - Facades\Event::listen(function (CommandStarting $event) { + Event::listen(function (CommandStarting $event) { if ($event->command === 'l5-swagger:generate') { // Set the analyser to use the legacy DocBlockAnnotationFactory. This must // be set here because this config value is not serializable and cannot be cached. @@ -590,38 +592,23 @@ public static function getPackageBootTiming(): array } /** - * Register Octane-specific listeners for request lifecycle management. + * Reset per-request static state between Octane requests. * - * When OCTANE_ENABLED is true, this method registers: - * - A 'requestHandled' listener to reset per-request static state - * - A flush list for stateful singletons - * - A tick listener for memory monitoring + * Octane workers stay alive across requests, so static properties must be + * cleared to avoid leaking data from one request into the next. Singletons + * holding mutable state are handled by the 'flush' list in config/octane.php, + * which Octane applies on its own. */ private function registerOctaneListeners(): void { - if (!config('app.octane_enabled', false) || !class_exists('\Laravel\Octane\Octane')) { + if (!class_exists(RequestTerminated::class)) { return; } - $octane = \Laravel\Octane\Octane::class; - - // Reset per-request static state after each request - $octane::on('requestHandled', function ($request, $result) { + Event::listen(RequestTerminated::class, function () { self::resetQueryTime(); HandleRedirectListener::reset(); }); - - // Flush stateful singletons per request - $octane::flush(config('octane.flush', [])); - - // Monitor memory usage every 30 seconds - $octane::tick('octane-memory-monitor', function () { - $memory = memory_get_usage(true); - $threshold = 128 * 1024 * 1024; // 128MB - if ($memory > $threshold) { - Log::warning('Octane worker memory high: ' . round($memory / 1024 / 1024, 2) . 'MB'); - } - })->seconds(30); } /** diff --git a/config/app.php b/config/app.php index 72088b34ab..b3c5891f98 100644 --- a/config/app.php +++ b/config/app.php @@ -266,10 +266,6 @@ 'force_https' => env('FORCE_HTTPS', true), - // Enable Octane compatibility listeners when running under `php artisan octane:start`. - 'octane_enabled' => filter_var(env('OCTANE_ENABLED', false), FILTER_VALIDATE_BOOLEAN), - 'octane_max_requests' => (int) env('OCTANE_MAX_REQUESTS', 500), - 'nayra_docker_network' => env('NAYRA_DOCKER_NETWORK', 'host'), 'nayra_port' => env('NAYRA_PORT', 8080), diff --git a/config/octane.php b/config/octane.php index 13fe63eb5e..5a9a9c642a 100644 --- a/config/octane.php +++ b/config/octane.php @@ -8,7 +8,7 @@ | | 'flush' — Services with mutable state that must be recreated per request. | These singletons will be flushed (re-bound) on each request - | when OCTANE_ENABLED=true. + | automatically while the application runs under Octane. | | 'warm' — Services to pre-resolve once when an Octane worker starts, | avoiding lazy-resolution overhead on the first request. @@ -24,6 +24,8 @@ ], 'warm' => [ + ...Laravel\Octane\Octane::defaultServicesToWarm(), + // Services to pre-resolve on worker start ProcessMaker\Managers\PackageManager::class, ProcessMaker\Managers\LoginManager::class, From c8a6a04584a112b47ed22d2cb0501b600fac34d6 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 29 Jul 2026 11:12:20 -0400 Subject: [PATCH 5/7] feat: add test for octane --- .../Providers/ProcessMakerServiceProvider.php | 16 +--- .../Octane/ResetRequestStateTest.php | 84 +++++++++++++++++++ 2 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 tests/unit/ProcessMaker/Octane/ResetRequestStateTest.php diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php index c4d09de820..4d6040591f 100644 --- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php +++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php @@ -45,13 +45,13 @@ use ProcessMaker\ImportExport\SignalHelper; use ProcessMaker\Jobs\SmartInbox; use ProcessMaker\LicensedPackageManifest; -use ProcessMaker\Listeners\HandleRedirectListener; use ProcessMaker\Managers; use ProcessMaker\Managers\MenuManager; use ProcessMaker\Managers\ScreenCompiledManager; use ProcessMaker\Models; use ProcessMaker\Multitenancy\Tenant; use ProcessMaker\Observers; +use ProcessMaker\Octane\ResetRequestState; use ProcessMaker\PolicyExtension; use ProcessMaker\Providers\PermissionServiceProvider; use ProcessMaker\Repositories\SettingsConfigRepository; @@ -535,15 +535,6 @@ public static function getQueryTime(): float return self::$queryTime; } - /** - * Reset the query time for Octane compatibility. - * This prevents timing metrics from leaking between requests. - */ - public static function resetQueryTime(): void - { - self::$queryTime = 0; - } - /** * Set the boot time for service providers. * @@ -605,10 +596,7 @@ private function registerOctaneListeners(): void return; } - Event::listen(RequestTerminated::class, function () { - self::resetQueryTime(); - HandleRedirectListener::reset(); - }); + Event::listen(RequestTerminated::class, ResetRequestState::class); } /** diff --git a/tests/unit/ProcessMaker/Octane/ResetRequestStateTest.php b/tests/unit/ProcessMaker/Octane/ResetRequestStateTest.php new file mode 100644 index 0000000000..03b4cf73a2 --- /dev/null +++ b/tests/unit/ProcessMaker/Octane/ResetRequestStateTest.php @@ -0,0 +1,84 @@ +assertGreaterThan(0, ProcessMakerServiceProvider::getQueryTime()); + + $listener = new ResetRequestState(); + $listener->handle(new RequestTerminated( + $this->app, + $this->app, + Request::create('/'), + new Response() + )); + + $this->assertSame(0.0, ProcessMakerServiceProvider::getQueryTime()); + } + + public function test_it_prevents_redirect_state_from_leaking_into_the_next_request(): void + { + Event::fake([RedirectToEvent::class]); + + $redirectListener = new RedirectStateProbe(); + $redirectListener->queue(ProcessRequest::factory()->create()); + + $listener = new ResetRequestState(); + $listener->handle(new RequestTerminated( + $this->app, + $this->app, + Request::create('/'), + new Response() + )); + + HandleRedirectListener::sendRedirectToEvent(); + + Event::assertNotDispatched(RedirectToEvent::class); + } + + public function test_octane_request_termination_automatically_resets_request_state(): void + { + Event::fake([RedirectToEvent::class]); + + $redirectListener = new RedirectStateProbe(); + $redirectListener->queue(ProcessRequest::factory()->create()); + + event(new RequestTerminated( + $this->app, + $this->app, + Request::create('/first-request'), + new Response() + )); + + HandleRedirectListener::sendRedirectToEvent(); + + Event::assertNotDispatched(RedirectToEvent::class); + } +} + +class RedirectStateProbe extends HandleRedirectListener +{ + public function queue(ProcessRequest $processRequest): void + { + $this->setRedirectTo($processRequest, 'processUpdated'); + } +} From a5a035802f391198824ee10e9844bdf907a2c709 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 29 Jul 2026 11:21:08 -0400 Subject: [PATCH 6/7] add octane resetRequestState class --- ProcessMaker/Octane/ResetRequestState.php | 17 +++++++++++++++++ .../Octane/ResetRequestStateTest.php | 16 +++------------- 2 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 ProcessMaker/Octane/ResetRequestState.php diff --git a/ProcessMaker/Octane/ResetRequestState.php b/ProcessMaker/Octane/ResetRequestState.php new file mode 100644 index 0000000000..45071e97ad --- /dev/null +++ b/ProcessMaker/Octane/ResetRequestState.php @@ -0,0 +1,17 @@ +assertGreaterThan(0, ProcessMakerServiceProvider::getQueryTime()); $listener = new ResetRequestState(); - $listener->handle(new RequestTerminated( - $this->app, - $this->app, - Request::create('/'), - new Response() - )); + $listener->handle(); $this->assertSame(0.0, ProcessMakerServiceProvider::getQueryTime()); } @@ -43,12 +38,7 @@ public function test_it_prevents_redirect_state_from_leaking_into_the_next_reque $redirectListener->queue(ProcessRequest::factory()->create()); $listener = new ResetRequestState(); - $listener->handle(new RequestTerminated( - $this->app, - $this->app, - Request::create('/'), - new Response() - )); + $listener->handle(); HandleRedirectListener::sendRedirectToEvent(); @@ -75,7 +65,7 @@ public function test_octane_request_termination_automatically_resets_request_sta } } -class RedirectStateProbe extends HandleRedirectListener +final class RedirectStateProbe extends HandleRedirectListener { public function queue(ProcessRequest $processRequest): void { From 741db2999ac6bb58519a7ecaddf8bf212183d02e Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 31 Jul 2026 10:26:43 -0400 Subject: [PATCH 7/7] feat: update config/octane --- config/octane.php | 222 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 210 insertions(+), 12 deletions(-) diff --git a/config/octane.php b/config/octane.php index 5a9a9c642a..598df4b7c6 100644 --- a/config/octane.php +++ b/config/octane.php @@ -1,20 +1,143 @@ env('OCTANE_SERVER', 'roadrunner'), + + /* + |-------------------------------------------------------------------------- + | Force HTTPS + |-------------------------------------------------------------------------- | - | 'warm' — Services to pre-resolve once when an Octane worker starts, - | avoiding lazy-resolution overhead on the first request. + | When this configuration value is set to "true", Octane will inform the + | framework that all absolute links must be generated using the HTTPS + | protocol. Otherwise your links may be generated using plain HTTP. | */ + 'https' => env('OCTANE_HTTPS', false), + + /* + |-------------------------------------------------------------------------- + | Octane Listeners + |-------------------------------------------------------------------------- + | + | All of the event listeners for Octane's events are defined below. These + | listeners are responsible for resetting your application's state for + | the next request. You may even add your own listeners to the list. + | + */ + + 'listeners' => [ + WorkerStarting::class => [ + EnsureUploadedFilesAreValid::class, + EnsureUploadedFilesCanBeMoved::class, + ], + + RequestReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + ...Octane::prepareApplicationForNextRequest(), + // + ], + + RequestHandled::class => [ + // + ], + + RequestTerminated::class => [ + // FlushUploadedFiles::class, + ], + + TaskReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TaskTerminated::class => [ + // + ], + + TickReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TickTerminated::class => [ + // + ], + + OperationTerminated::class => [ + FlushOnce::class, + FlushTemporaryContainerInstances::class, + // DisconnectFromDatabases::class, + // CollectGarbage::class, + ], + + WorkerErrorOccurred::class => [ + ReportException::class, + StopWorkerIfNecessary::class, + ], + + WorkerStopping::class => [ + CloseMonologHandlers::class, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Warm / Flush Bindings + |-------------------------------------------------------------------------- + | + | The bindings listed below will either be pre-warmed when a worker boots + | or they will be flushed before every new request. Flushing a binding + | will force the container to resolve that binding again when asked. + | + */ + + 'warm' => [ + ...Octane::defaultServicesToWarm(), + // Services to pre-resolve on worker start + ProcessMaker\Managers\PackageManager::class, + ProcessMaker\Managers\LoginManager::class, + ProcessMaker\Managers\IndexManager::class, + ], + 'flush' => [ // Services with mutable state that must be recreated per request ProcessMaker\Models\AnonymousUser::class, @@ -23,12 +146,87 @@ ProcessMaker\Managers\MenuManager::class, ], - 'warm' => [ - ...Laravel\Octane\Octane::defaultServicesToWarm(), + /* + |-------------------------------------------------------------------------- + | Octane Swoole Tables + |-------------------------------------------------------------------------- + | + | While using Swoole, you may define additional tables as required by the + | application. These tables can be used to store data that needs to be + | quickly accessed by other workers on the particular Swoole server. + | + */ - // Services to pre-resolve on worker start - ProcessMaker\Managers\PackageManager::class, - ProcessMaker\Managers\LoginManager::class, - ProcessMaker\Managers\IndexManager::class, + 'tables' => [ + 'example:1000' => [ + 'name' => 'string:1000', + 'votes' => 'int', + ], ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Cache Table + |-------------------------------------------------------------------------- + | + | While using Swoole, you may leverage the Octane cache, which is powered + | by a Swoole table. You may set the maximum number of rows as well as + | the number of bytes per row using the configuration options below. + | + */ + + 'cache' => [ + 'rows' => 1000, + 'bytes' => 10000, + ], + + /* + |-------------------------------------------------------------------------- + | File Watching + |-------------------------------------------------------------------------- + | + | The following list of files and directories will be watched when using + | the --watch option offered by Octane. If any of the directories and + | files are changed, Octane will automatically reload your workers. + | + */ + + 'watch' => [ + 'app', + 'bootstrap', + 'config/**/*.php', + 'database/**/*.php', + 'public/**/*.php', + 'resources/**/*.php', + 'routes', + 'composer.lock', + '.env', + ], + + /* + |-------------------------------------------------------------------------- + | Garbage Collection Threshold + |-------------------------------------------------------------------------- + | + | When executing long-lived PHP scripts such as Octane, memory can build + | up before being cleared by PHP. You can force Octane to run garbage + | collection if your application consumes this amount of megabytes. + | + */ + + 'garbage' => 50, + + /* + |-------------------------------------------------------------------------- + | Maximum Execution Time + |-------------------------------------------------------------------------- + | + | The following setting configures the maximum execution time for requests + | being handled by Octane. You may set this value to 0 to indicate that + | there isn't a specific time limit on Octane request execution time. + | + */ + + 'max_execution_time' => 30, + ];