Repository navigation
Give each coroutine its own Blade compiler and Blade engine - #25
Open
adhikjoshi wants to merge 1 commit into
Open
adhikjoshi wants to merge 1 commit into
adhikjoshi wants to merge 1 commit into
Conversation
The cloned view factory still held the worker's EngineResolver, so every coroutine in a worker rendered through one CompilerEngine and one BladeCompiler. BladeCompiler::compile() stores the view it is compiling in $this->path, reads the source (a hooked file call, so the coroutine yields), then writes to getCompiledPath($this->path). A concurrent request that compiled another view in between moved the path, and one view's compiled PHP was written into the other view's cache file. The bad file stayed until the compiled views were cleared. On redcell.sh this put Inertia's <x-inertia::head> code into the cache file of <x-inertia::app />, so every page printed its head tags where the app root belongs: a white screen until the container restarted. Inline component views whose source and compiled file share an mtime second are rechecked by each new worker, so every max_requests restart reopened the window. RequestScope now binds 'blade.compiler' to a clone of the boot-time compiler with empty per-compile state, and gives the coroutine's view factory a cloned EngineResolver whose Blade engine is a clone of the worker's engine pointed at that compiler, with empty render and expiry caches. Boot-time directives, components and precompilers carry over, and an engine subclass from a package is kept. The worker's compiler, engine and resolver are never changed. The new test renders ten pairs of new inline templates concurrently with SWOOLE_HOOK_ALL. Without the fix 19 of 20 renders fail (missing compiled file or another view's code); with it all 20 render their own output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPrqAsHV6fVfweg9jbkyhM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Incident
On 2026-10-18 redcell.sh (Octane coroutine worker,
maxScale=1) showed a white screen on every page for hours. The HTML had the SSR head, but where<x-inertia::app />should print<script data-page>+<div id="app">, it printed the head tags again (body == head). The Laravel side and the Node SSR server were both fine: the same page JSON rendered correctly through a local SSR server. A fresh revision (new container, empty compiled-view cache) fixed it.Cause
RequestScope::createViewFactory()shallow-clones the viewFactory, so every coroutine in a worker still shares oneEngineResolver→CompilerEngine→BladeCompiler.BladeCompiler::compile($path)does:setPath($path)(shared$this->path)$this->files->get($this->getPath()): a hooked file read underSWOOLE_HOOK_ALL, so the coroutine yieldsgetCompiledPath($this->getPath())→ writeIf a second request compiles another view during step 2, it moves
$this->path. One view's compiled PHP is then written into the other view's cache file. Nothing recompiles it after that, because the bad write makes the compiled file newer than its source. Inertia's two inline components (Head,App) compile back to back on the first render, soApp's cache file gotHead's code:@if($response) {!! $response->head !!}.Compiler::isExpired()uses>=on mtimes. Inline component views whose.blade.phpand compiled.phpwere written in the same second count as expired for each new worker, so everymax_requestsrestart reopens the window. This explains why a container that worked for two days broke at a random worker restart.The shared
CompilerEnginealso mixeslastCompiled(the view named in error messages) andcompiledOrNotExpiredacross requests.Fix
Same pattern as the Vite change in #23:
blade.compiler/BladeCompiler::classare request-scoped. Each coroutine gets a clone of the boot-time compiler, with empty per-compile state (path,footer,rawBlocks,firstCaseInSwitch,forElseCounter,lastSection,lastFragment). Boot-time directives, components, precompilers and echo handlers carry over.Blade::render()/Blade::compileString()in a request use the clone, because facades are uncached and resolve through the proxy.EngineResolver. Itsbladeengine is built lazily as a clone of the worker's engine (so a package'sCompilerEnginesubclass is kept), pointed at the coroutine's compiler, with emptylastCompiled/compiledOrNotExpired. The worker's engine is resolved once as a template. This avoids running the framework's resolver for each request, which on Laravel 10 would add a terminating callback to the worker app for every request. Other engines (php,file) stay shared because they hold no per-render state.I chose a per-coroutine clone over a worker-level mutex around
compile(). Shared state is not only incompile():compileString()andCompilerEnginebookkeeping are also shared, andComponentTagCompilerdoes hooked filesystem checks mid-compile. A lock would have to cover all of those and would serialize compiles with I/O inside the lock. The clone matches how this package already isolates view, Vite, translator and the managers.Test
tests/Unit/RequestScopeBladeCompilerIsolationTest.phpSWOOLE_HOOK_ALL, it renders ten pairs of new inline templates (the shape of Inertia's head and app) concurrently throughBlade::render().File does not exist at path .../compiled/<hash>.php, and the one that renders has stray code (<title>head 9</title>head-9/ ?>).Full suite with PHP 8.5 + Swoole 6.2.2: 227 tests OK. The 29 skips and the deprecations are unchanged from
main.Not changed (noted)
Illuminate\View\Component::$factoryis static, so the first coroutine's factory servesexists()/addNamespace()for others. Both go to the shared finder, so I found no wrong output from it.CompilesComponents::$componentHashStackis static, but it is only pushed and popped withincompileStatements(), which does not yield.🤖 Generated with Claude Code
https://claude.ai/code/session_01XPrqAsHV6fVfweg9jbkyhM
Need help on this PR? Tag
@codesmith-botwith what you need.