Guard Cache::pool() against reentrant construction of the same pool - #19587
Merged
Merged
Conversation
Asking for a pool that is still being constructed recursed without bound. The registry only holds the engine once the build has finished, so the reentrant call found nothing registered, started another build, and repeated until the process exhausted its stack or memory. The reachable path is a cache engine that cannot connect and logs that failure. RedisEngine::init() calls Log::error() from inside its connect path, and if any configured log engine reaches for a cache-backed resource on its way to writing the message, it lands back in Cache::pool() for the pool that is mid-build. A database schema metadata cache is the usual way this closes, since log engines that write to a table go through the connection's schema collection. The NullEngine fallback already in _buildEngine() does not help here: it runs from the catch around $registry->load(), and the recursion happens inside that call, before the RuntimeException can propagate. Track which pools are mid-build and hand a NullEngine to reentrant callers, so the inner consumer degrades instead of looping. The marker is cleared in a finally, so a failed build does not mark the pool unbuildable for the rest of the process. Without this, the added test crashes the PHP process outright rather than failing an assertion.
ADmad
reviewed
Aug 6, 2026
Contributor
|
I agree we should fix the issue at the lowest level that is reasonable rather than addressing a symptom by patching RedisEngine specifically. I know of at least two projects where userland code has extended RedisEngine, so it would be nice to have the patch in the core Cache facade. |
Member
Author
|
Well, my recommendation: |
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.
Asking for a cache pool that is still being constructed recurses without bound.
The cycle
Cache::pool()returns early only once the registry holds the engine, and the registry is populated by_buildEngine()after$registry->load()returns. A call that arrives while that build is still in progress therefore finds nothing registered, starts another build, and repeats.The reachable path is a cache engine that cannot connect and logs it.
RedisEngine::init()callsLog::error()from inside its connect path, and if a configured log engine reaches for a cache-backed resource on the way to writing that message, it lands back inCache::pool()for the pool that is mid-build. A schema metadata cache is the usual way this closes, because a log engine writing to a table goes through the connection's schema collection:The
NullEnginefallback already in_buildEngine()does not help. It runs from thecatcharound$registry->load(), and the recursion happens inside that call, before theRuntimeExceptioncan propagate out of it.Each round allocates, so the process grows until it dies. With a small stack it segfaults first.
The change
Track which pools are mid-build, and hand reentrant callers a
NullEngineinstead of starting another build:The inner consumer degrades to an uncached lookup, which is the correct outcome when the cache backend is unreachable anyway. The outer build continues and registers whatever it resolves to, so the caller that actually asked for the pool is unaffected.
Clearing the marker in a
finallymatters: a build that throws must not leave the pool permanently unbuildable for the rest of the process. There is a test for that case specifically.Notes
RedisEngineinstead by not logging frominit(), but that only covers the one engine that happens to do it today and removes log output people may rely on. Guarding the construction path covers any engine and keeps the logging.Cache.phpchange reverted,tests/TestCase/Cache/CacheTest.php --filter Reentrantcrashes the PHP process rather than failing an assertion.