Repository navigation
fix: destroy persistent resources before a PHP thread exits - #2812
Merged
dunglas merged 1 commit intoNov 5, 2026
Merged
Conversation
Member
|
LGTM, thanks! |
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.
I hit this in a Laravel Octane app with
watchenabled: after about 62 reboots PostgreSQL refused new connections (remaining connection slots are reserved for roles with the SUPERUSER attribute). The app usesPDO::ATTR_PERSISTENT, and every reboot left one idle connection per worker thread behind. 8 workers,max_connections500, so that adds up quickly.The cause is php/php-src#13381 (PHP 8.4). Since then
ts_free_thread()doesn't destroy the thread's persistent list anymore,zend_shutdown()does it for the threads that are still registered in TSRM. FrankenPHP threads callts_free_thread()when they exit, so their lists never get destroyed. That happens on every thread restart: the reboot (watcher,RestartWorkers(), admin API, probably also the OPcache restart from #2568, I didn't test that one) andmax_requests.PHP 8.2 and 8.3 have a different problem in the same spot.
ts_free_thread()still destroys the list there, but with apfsockopen()socket the reboot aborts withtcache_thread_shutdown(): unaligned tcache chunk detected. I only found that because the new test crashed on 8.3.The fix destroys the persistent list right before
ts_free_thread(), while everything is still in place. That's basically what php-src master does now withzend_thread_free_handler(php/php-src#24428), but it isn't in any release branch, so the patch is active for everything below 8.7.zend_destroy_rsrc_list()andzend_init_rsrc_plist()aren'tZEND_API, so I call the hash functions they wrap. Re-initializing the list afterwards keeps the second destroy on 8.2/8.3 harmless, and would do the same if the master change ever gets backported.The tests open a socket with
pfsockopen()and count how many get closed afterRestartWorkers()and aftermax_requests. Without the fix they fail on 8.4 and 8.5 (no socket gets closed) and abort on 8.2 and 8.3. With the fix they pass on all four. The whole root package passes on 8.2, 8.3 and 8.5, the caddy module on 8.5 (-race, official 1.13.4 builder images). There's no 8.6 builder image yet, so I couldn't try that one.I also checked it with the real thing: 4 workers with one persistent pgsql connection each, three reboots through the watcher. The official 1.13.4 image goes from 4 to 16 connections, with this patch it stays at 4. Same with
max_requests(4 → 18 without, 4 with). Workers that just return and start again in the same thread still reuse their connection.