feat: Add configurable max_requests for PHP threads#2292
feat: Add configurable max_requests for PHP threads#2292nicolas-grekas wants to merge 1 commit intophp:mainfrom
Conversation
45cffba to
bebcd7a
Compare
|
I would propose we either make this a global setting only or a php_server block setting only. I currently don't see why this should be a worker setting. |
|
See #280 As I wrote: this can be dealt with on the PHP side, but this is an ops concern, so it would make sense to have this in the Caddyfile, don't you think? |
b30002c to
9f13975
Compare
|
Actually I had a closer look and realized we should do the same full reset on worker threads too - something userland PHP cannot do. Both worker and module mode now do full ZTS cleanup via That complements user-side refreshes that clean only application state. |
13baffd to
ae8d6ed
Compare
|
PR description updated, patch ready and green 🚀 |
I just meant that I don't think the setting should exist in more than one place in the Caddyfile config. I'm leaning towards it only being a php_server setting (which would affect all workers in that block too). |
|
What about having only |
|
Would be fine, although I think that we generally want almost all options to be inside php_server blocks. I think workers should have never been in global scope either. |
|
BTW, what about defaulting to eg 100? |
|
I don't think we should. Default should stay 0 for performance and because most apps don't actually have memory issues in non-worker mode. |
ae8d6ed to
c635f17
Compare
|
Done, there's not a single max_requests under frankenphp, default to 0 = unlimited. |
|
Can we get a quick vote on 👍 for frankenphp, 👎 for php_server |
|
My reasoning for going with only |
2b5961b to
f2a3936
Compare
|
Thanks for the review @henderkes, should be all addressed now. (just waiting for the CI now) |
f2a3936 to
e29b6a9
Compare
Hmm, that's a good point. |
henderkes
left a comment
There was a problem hiding this comment.
All good from my side now, but I'll look into properly testing for the restart log message tomorrow, if you don't get to it first.
Thanks for the addition and your work on Symfony!
e29b6a9 to
5c53ffb
Compare
8492c94 to
966326f
Compare
|
Thanks for the thorough review! Applied all simplifications. Two items I kept:
Or did I miss something? |
d66b8ff to
cf16368
Compare
AlliBalliBaba
left a comment
There was a problem hiding this comment.
Not sure this needs its own section in the docs, otherwise looking good from my side 👍
bc300c6 to
e7f2c85
Compare
right, removed |
da41d6c to
d12a6b3
Compare
|
I've added an entry back in the doc to expand a bit on the tension this option creates.
/cc @dunglas |
There was a problem hiding this comment.
Code-wise, this looks good to me.
However, I'm still not entirely convinced by the use case. The question is: do we want to introduce and maintain this complexity because some (proprietary) extensions have issues or not.
Normal code shouldn't leak, and open source code can and should be patched instead of using such workarounds.
WDYT @php/frankenphp-collaborators?
I think in an optimal world, we wouldn't have to maintain this. However, in the world we're living in, Blackfire and some other profilers do still have issues and they're slow to fix them. PHP-FPM has also had this setting forever, so we wouldn't really be going out of line there. I think an advantage of having this PR is at least having a mechanism in place to restart a regular thread. The maintenance added by this is acceptable to me. |
|
I agree with Marc. While extension maintainers should of course fix their leaks, the addition is lightweight and it's opt-in, making it not-that-easy to discover (which is good news and should always stay as-is). I'm really torn between the idea that "other languages don't need to restart like that, developers are careful about memory leaks and deal with it because it's part of the job" and the reality of the situation. Even though it’s not ideal, I’m leaning toward a positive opinion on this addition. |
|
Perhaps we could also mark the setting |
|
+1 for merging this as "experimental" |
28baba3 to
898064b
Compare
|
I like this addition since rebooting threads has other potential use cases. But as @dunglas mentioned the downside is that it might encourage extensions not to fix their leaks. +1 for experimental |
|
PR updated, experimental note added. FTR, I talked with blackfire engineers: they aren't able to prioritize looking at this because frankenphp is a very very small share of their customer.s And since frankenphp isn't stable with blackfire, their customers are not going to adopt frankenphp either. Chicken and egg. |
And even worse, they're not going to get any customers who care about performance, either, by not properly supporting FrankenPHP or Swoole. You can take a guess why we chose not to adopt Blackfire 😅 |
|
What are the other use case do you have in mind @AlliBalliBaba ? |
|
I think we could also use similar logic to restart all threads when watching or on Another side effect is that this allows configuring I'm indifferent to the leak prevention though, since this has never been a problem that I have personally observed. |
PHP-FPM recycles worker processes after a configurable number of requests (
pm.max_requests), preventing memory leaks from accumulating over time. FrankenPHP keeps PHP threads alive indefinitely, so any leak in PHP extensions (e.g. ZTS builds of profiling tools like Blackfire) or application code compounds over hours/days. In production behind reverse proxies like Cloudflare, this can lead to gradual resource exhaustion and eventually 502 errors with no visible warnings in logs.This PR adds a
max_requestsoption in the globalfrankenphpblock that automatically restarts PHP threads after a given number of requests, fully cleaning up the thread's memory and state. It applies to both regular (module mode) and worker threads.When a thread reaches the limit it exits the C thread loop, triggering a full cleanup including any memory leaked by extensions. A fresh thread is then booted transparently. Other threads continue serving requests during the restart.
This cannot be done from userland PHP: restarting a worker script from PHP only resets PHP-level state, not the underlying C thread-local storage where extension-level leaks accumulate. And in module mode (without workers), there is no userland loop to count requests at all.
Default is
0(unlimited), preserving existing behavior.Usage:
{ frankenphp { max_requests 500 } }Changes:
max_requestsCaddyfile directive in the globalfrankenphpblockWithMaxRequestsfunctional optionRebootingandRebootReadystates in the thread state machine for restart coordinationthreadregular.gothreadworker.goshutdown()waits for in-flight reboots to complete before draining threads