From db8881621c091f2b0d88326ac58808ae8c42ec5b Mon Sep 17 00:00:00 2001 From: Sebastian Almberg <83243306+Sebbeben@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:10:36 +0100 Subject: [PATCH] Add OPcache reset step to update and restore processes (#1288) After cache warmup, create a temporary PHP script in the public directory and invoke it via HTTP to reset OPcache in the PHP-FPM context. This prevents stale bytecode from causing 500 errors when the progress page refreshes after code has been updated. The reset is also performed after rollback and during restore. Uses a random token in the filename for security, and the script self-deletes after execution with a cleanup in the finally block. --- src/Services/System/UpdateExecutor.php | 101 +++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/src/Services/System/UpdateExecutor.php b/src/Services/System/UpdateExecutor.php index 70aea23f..77cf2942 100644 --- a/src/Services/System/UpdateExecutor.php +++ b/src/Services/System/UpdateExecutor.php @@ -207,6 +207,79 @@ class UpdateExecutor } } + /** + * Reset PHP OPcache for the web server process. + * + * OPcache in PHP-FPM is separate from CLI. After updating code files, + * PHP-FPM may still serve stale cached bytecode, causing constructor + * mismatches and 500 errors. This method creates a temporary PHP script + * in the public directory, invokes it via HTTP to reset OPcache in the + * web server context, then removes the script. + * + * @return bool Whether OPcache was successfully reset + */ + private function resetOpcache(): bool + { + $token = bin2hex(random_bytes(16)); + $resetScript = $this->project_dir . '/public/_opcache_reset_' . $token . '.php'; + + try { + // Create a temporary PHP script that resets OPcache + $scriptContent = 'filesystem->dumpFile($resetScript, $scriptContent); + + // Try to invoke it via HTTP on localhost + $urls = [ + 'http://127.0.0.1/_opcache_reset_' . $token . '.php', + 'http://localhost/_opcache_reset_' . $token . '.php', + ]; + + $success = false; + foreach ($urls as $url) { + try { + $context = stream_context_create([ + 'http' => [ + 'timeout' => 5, + 'ignore_errors' => true, + ], + ]); + + $response = @file_get_contents($url, false, $context); + if ($response === 'OK') { + $this->logger->info('OPcache reset via ' . $url); + $success = true; + break; + } + } catch (\Throwable $e) { + // Try next URL + continue; + } + } + + if (!$success) { + $this->logger->info('OPcache reset via HTTP not available, trying CLI fallback'); + // CLI opcache_reset() only affects CLI, but try anyway + if (function_exists('opcache_reset')) { + opcache_reset(); + } + } + + return $success; + } catch (\Throwable $e) { + $this->logger->warning('OPcache reset failed: ' . $e->getMessage()); + return false; + } finally { + // Ensure the temp script is removed + if (file_exists($resetScript)) { + @unlink($resetScript); + } + } + } + /** * Validate that we can perform an update. * @@ -434,12 +507,20 @@ class UpdateExecutor ], 'Warmup cache', 120); $log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart); - // Step 13: Disable maintenance mode + // Step 13: Reset OPcache (if available) + $stepStart = microtime(true); + $opcacheResult = $this->resetOpcache(); + $log('opcache_reset', $opcacheResult + ? 'Reset PHP OPcache for web server' + : 'OPcache reset skipped (not available or not needed)', + true, microtime(true) - $stepStart); + + // Step 14: Disable maintenance mode $stepStart = microtime(true); $this->disableMaintenanceMode(); $log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart); - // Step 14: Release lock + // Step 15: Release lock $stepStart = microtime(true); $this->releaseLock(); @@ -494,6 +575,9 @@ class UpdateExecutor ], 'Clear cache after rollback', 120); $log('rollback_cache', 'Cleared cache after rollback', true); + // Reset OPcache after rollback + $this->resetOpcache(); + } catch (\Exception $rollbackError) { $log('rollback_failed', 'Rollback failed: ' . $rollbackError->getMessage(), false); } @@ -682,12 +766,17 @@ class UpdateExecutor $this->runCommand(['php', 'bin/console', 'cache:warmup'], 'Warm up cache'); $log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart); - // Step 6: Disable maintenance mode + // Step 6: Reset OPcache + $stepStart = microtime(true); + $this->resetOpcache(); + $log('opcache_reset', 'Reset PHP OPcache', true, microtime(true) - $stepStart); + + // Step 7: Disable maintenance mode $stepStart = microtime(true); $this->disableMaintenanceMode(); $log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart); - // Step 7: Release lock + // Step 8: Release lock $this->releaseLock(); $totalDuration = microtime(true) - $startTime; @@ -817,7 +906,7 @@ class UpdateExecutor 'create_backup' => $createBackup, 'started_at' => (new \DateTime())->format('c'), 'current_step' => 0, - 'total_steps' => 14, + 'total_steps' => 15, 'step_name' => 'initializing', 'step_message' => 'Starting update process...', 'steps' => [], @@ -890,7 +979,7 @@ class UpdateExecutor bool $createBackup = true, ?callable $onProgress = null ): array { - $totalSteps = 12; + $totalSteps = 13; $currentStep = 0; $updateProgress = function (string $stepName, string $message, bool $success = true) use (&$currentStep, $totalSteps, $targetVersion, $createBackup): void {