Reuse AOT DI scripts when compile fingerprint matches#484
Conversation
Skip unconditional Ray\Compiler recompile in PackageInjector::factory()
when {scriptDir}/.compile-fingerprint still matches the source tree.
Fixes ahead-of-time compile being discarded on first request (bearsunday#483).
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 1.x #484 +/- ##
===========================================
Coverage 100.00% 100.00%
- Complexity 252 257 +5
===========================================
Files 51 52 +1
Lines 753 770 +17
===========================================
+ Hits 753 770 +17 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
NaokiTsuchiya
left a comment
There was a problem hiding this comment.
The stamp won't match in the case #483 was about: {tmpDir}/di shipped in an image.
The tests only cover reuse within the same tree. Compiling in one tree and booting from another — the whole point of AOT — is untested, and breaks for two independent reasons:
- Absolute path in the hash.
appDiris hash input, so a different runtime path is a guaranteed mismatch (see line comment). - mtime, not content.
maxMtime()maxesfilemtime()instead of hashing. Any copy that doesn't preserve mtimes —git clone, CI artifacts,rsyncwithout-t— misses. (max()also can't see a file rolled back.)
Fixing the path alone still leaves the mtime problem.
On mismatch, prodInjector() falls through to compile(), which under readOnlyRootFilesystem dies with FileNotWritable: _bindings.log. Impact 2 from #483 isn't just unfixed — the app doesn't boot.
Suggestions
- Hash
src/contents (hash_file,sort()first — iterator order is filesystem-dependent) instead of mtimes. - Make
fileStamp()paths relative toappDir. - Add a test: compile in one dir, copy
src/anddi/elsewhere clobbering mtimes, boot there. Minimal repro of baking into an image.
Longer term, the opt-in from the end of #483 — declaring these scripts were compiled from this tree — is cheaper and safer for containers. With compilation verified in CI the runtime check can go entirely. Content hash as the safe default, opt-in for immutable images, covers both.
| return '0'; | ||
| } | ||
|
|
||
| return $path . ':' . (string) filemtime($path); |
There was a problem hiding this comment.
The absolute path is in the hash input, so appDir is part of the stamp. Compile in /build/app, run from /var/www/html, and it never matches — identical files or not. Should be relative to appDir.
Replace CompileStamp (mtime + composer.lock + .env* fingerprint) with CompileMarker, a boolean "a compile has run" file. The fingerprint could never prove the scripts still matched the source: mtime is not content, appDir was hash input (image path -> guaranteed mismatch), and the fallback compile() died under readOnlyRootFilesystem (bearsunday#483 impact 2 unfixed). The marker only records that a compile ran; freshness is the deploy's responsibility (ship the marker with the scripts, or recompile). factory() boots straight from CompiledInjector when the marker is present and lets a broken-scripts boot throw instead of silently recompiling -- a marker without working scripts is a deploy error, and a runtime recompile would also die on a read-only FS. When the marker is missing the cold path compiles on demand and emits E_USER_NOTICE so a forgotten bin/compile.php is loud. The marker is written after a successful compile (both the on-demand path in factory() and the AOT Compiler::compile() path), so a crashed compile leaves no marker and the next boot recovers via the on-demand path. deleteFiles() now removes dotfiles too (preserving the .placefolder dir sentinel) so a stale marker does not survive a test clean and short-circuit the next boot. Fixes bearsunday#483 (replaces the fingerprint approach in bearsunday#484).
"Run bin/compile.php for production." assumed the 1.21+ skeleton compile entry, which older projects do not have (they use bear.compile or Compiler::fromInjector). Drop the command name from the E_USER_NOTICE and point to the production manual via @see instead, on both prodInjector() and CompileMarker.
The notice now carries a direct link to the Compilation Recommended section, matching the existing cache-failure notice that embeds a URL. A developer who forgets to pre-compile gets the pointer at runtime, not only in the source @see.
Summary
Fixes #483:
PackageInjector::factory()no longer always recompiles prod DI scripts.Replaces the earlier source-fingerprint approach (this PR's first revisions) with a compile marker: a boolean "a compile has run here" file, not a freshness check.
{scriptDir}/.compile-marker.factory()calls, if the marker is present, constructCompiledInjectordirectly (no write, no recompile).E_USER_NOTICE("Not precompiled; ...") so a forgottenbin/compile.phpis loud.Compiler::compile()writes the marker after the final DI scripts (AOT /__invokepath).The marker is written after a successful compile, so a crashed compile leaves no marker and the next boot recovers via the on-demand path.
Why not a fingerprint
A source fingerprint (
src/mtimes +composer.lock+.env*) can never prove the scripts still match the source:mtimeis not content,appDirin the hash makes image-path relocation a guaranteed mismatch, andmax()mtime misses rolled-back files. The fingerprint was the framework trying to be smarter than it can be. The marker changes the question from "do these scripts still match the source?" (unprovable) to "did a compile run here?" (trivially, perfectly knowable). Freshness becomes the deploy's responsibility — the same requirement the existing serialized-injector cache already imposes.Foundation (image reuse / read-only FS)
Reusing AOT output safely requires the output to be complete and portable, which is being fixed in the Ray libraries:
#[ScriptDir]to__DIR__instead of baking the compile-time absolute path ray-di/Ray.Compiler#136 — compiled scripts resolve#[ScriptDir]/InjectorInterfaceto__DIR__instead of baking the build-time absolute path (path portability). Merged.Compiler::compile()re-emits the weaved class file when it is declared in-process but missing on disk, so AOT output includes AOP-woven class definitions (completeness). Without this, reusing AOT output throwsClass not foundfor intercepted resources (incl.@Embed).This PR should land alongside the Ray.Aop release that includes #261 (bump
ray/aop), so reused AOT output is complete.Test plan
composer test,phpstan,psalm,phpcsE_USER_NOTICE