Skip to content

Commit db88816

Browse files
authored
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.
1 parent ceda914 commit db88816

1 file changed

Lines changed: 95 additions & 6 deletions

File tree

src/Services/System/UpdateExecutor.php

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,79 @@ public function disableMaintenanceMode(): void
207207
}
208208
}
209209

210+
/**
211+
* Reset PHP OPcache for the web server process.
212+
*
213+
* OPcache in PHP-FPM is separate from CLI. After updating code files,
214+
* PHP-FPM may still serve stale cached bytecode, causing constructor
215+
* mismatches and 500 errors. This method creates a temporary PHP script
216+
* in the public directory, invokes it via HTTP to reset OPcache in the
217+
* web server context, then removes the script.
218+
*
219+
* @return bool Whether OPcache was successfully reset
220+
*/
221+
private function resetOpcache(): bool
222+
{
223+
$token = bin2hex(random_bytes(16));
224+
$resetScript = $this->project_dir . '/public/_opcache_reset_' . $token . '.php';
225+
226+
try {
227+
// Create a temporary PHP script that resets OPcache
228+
$scriptContent = '<?php '
229+
. 'if (function_exists("opcache_reset")) { opcache_reset(); echo "OK"; } '
230+
. 'else { echo "NO_OPCACHE"; } '
231+
. '@unlink(__FILE__);';
232+
233+
$this->filesystem->dumpFile($resetScript, $scriptContent);
234+
235+
// Try to invoke it via HTTP on localhost
236+
$urls = [
237+
'http://127.0.0.1/_opcache_reset_' . $token . '.php',
238+
'http://localhost/_opcache_reset_' . $token . '.php',
239+
];
240+
241+
$success = false;
242+
foreach ($urls as $url) {
243+
try {
244+
$context = stream_context_create([
245+
'http' => [
246+
'timeout' => 5,
247+
'ignore_errors' => true,
248+
],
249+
]);
250+
251+
$response = @file_get_contents($url, false, $context);
252+
if ($response === 'OK') {
253+
$this->logger->info('OPcache reset via ' . $url);
254+
$success = true;
255+
break;
256+
}
257+
} catch (\Throwable $e) {
258+
// Try next URL
259+
continue;
260+
}
261+
}
262+
263+
if (!$success) {
264+
$this->logger->info('OPcache reset via HTTP not available, trying CLI fallback');
265+
// CLI opcache_reset() only affects CLI, but try anyway
266+
if (function_exists('opcache_reset')) {
267+
opcache_reset();
268+
}
269+
}
270+
271+
return $success;
272+
} catch (\Throwable $e) {
273+
$this->logger->warning('OPcache reset failed: ' . $e->getMessage());
274+
return false;
275+
} finally {
276+
// Ensure the temp script is removed
277+
if (file_exists($resetScript)) {
278+
@unlink($resetScript);
279+
}
280+
}
281+
}
282+
210283
/**
211284
* Validate that we can perform an update.
212285
*
@@ -434,12 +507,20 @@ public function executeUpdate(
434507
], 'Warmup cache', 120);
435508
$log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart);
436509

437-
// Step 13: Disable maintenance mode
510+
// Step 13: Reset OPcache (if available)
511+
$stepStart = microtime(true);
512+
$opcacheResult = $this->resetOpcache();
513+
$log('opcache_reset', $opcacheResult
514+
? 'Reset PHP OPcache for web server'
515+
: 'OPcache reset skipped (not available or not needed)',
516+
true, microtime(true) - $stepStart);
517+
518+
// Step 14: Disable maintenance mode
438519
$stepStart = microtime(true);
439520
$this->disableMaintenanceMode();
440521
$log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart);
441522

442-
// Step 14: Release lock
523+
// Step 15: Release lock
443524
$stepStart = microtime(true);
444525
$this->releaseLock();
445526

@@ -494,6 +575,9 @@ public function executeUpdate(
494575
], 'Clear cache after rollback', 120);
495576
$log('rollback_cache', 'Cleared cache after rollback', true);
496577

578+
// Reset OPcache after rollback
579+
$this->resetOpcache();
580+
497581
} catch (\Exception $rollbackError) {
498582
$log('rollback_failed', 'Rollback failed: ' . $rollbackError->getMessage(), false);
499583
}
@@ -682,12 +766,17 @@ function ($entry) use ($log) {
682766
$this->runCommand(['php', 'bin/console', 'cache:warmup'], 'Warm up cache');
683767
$log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart);
684768

685-
// Step 6: Disable maintenance mode
769+
// Step 6: Reset OPcache
770+
$stepStart = microtime(true);
771+
$this->resetOpcache();
772+
$log('opcache_reset', 'Reset PHP OPcache', true, microtime(true) - $stepStart);
773+
774+
// Step 7: Disable maintenance mode
686775
$stepStart = microtime(true);
687776
$this->disableMaintenanceMode();
688777
$log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart);
689778

690-
// Step 7: Release lock
779+
// Step 8: Release lock
691780
$this->releaseLock();
692781

693782
$totalDuration = microtime(true) - $startTime;
@@ -817,7 +906,7 @@ public function startBackgroundUpdate(string $targetVersion, bool $createBackup
817906
'create_backup' => $createBackup,
818907
'started_at' => (new \DateTime())->format('c'),
819908
'current_step' => 0,
820-
'total_steps' => 14,
909+
'total_steps' => 15,
821910
'step_name' => 'initializing',
822911
'step_message' => 'Starting update process...',
823912
'steps' => [],
@@ -890,7 +979,7 @@ public function executeUpdateWithProgress(
890979
bool $createBackup = true,
891980
?callable $onProgress = null
892981
): array {
893-
$totalSteps = 12;
982+
$totalSteps = 13;
894983
$currentStep = 0;
895984

896985
$updateProgress = function (string $stepName, string $message, bool $success = true) use (&$currentStep, $totalSteps, $targetVersion, $createBackup): void {

0 commit comments

Comments
 (0)