[RFC] [Discussion] Measuring maximum execution time based on wall-time

php.internals

Máté Kocsis

5 years ago
Hi Everyone, Thanks for all the insightful feedbacks so far, I went ahead, and wrote a proper RFC: https://wiki.php.net/rfc/max_execution_wall_time Regards: Máté

Rowan Collins

5 years ago
Hi Máté, On 13 December 2020 00:25:10 GMT+00:00, "Máté Kocsis" <kocsismate90@gmail.com> wrote:
>RFC: https://wiki.php.net/rfc/max_execution_wall_time
I wonder if you could give some more detailed examples of what you would use this for. You write:
> [The current functionality] can have serious consequences for distributed systems with high traffic, where finishing a request in a timely manner is essential for avoiding cascading failures.
It feels like "finishing in a timely manner" is rather different from "being forcefully killed after a fixed time limit". I'm struggling to picture when I'd want a hard wall-time limit, rather than: - checking the script's duration at key points where I know it can gracefully exit, ensure a consistent state, and return an appropriate message to the caller - enforcing a network timeout on the calling end so that I don't need to rely on all services cooperatively exiting in good time Regards,
-- Rowan Tommins [IMSoP]

Benjamin Eberlei

5 years ago
Hi Máté we talked about this before and I think it's a good addition. Two suggestions to improve the RFC: The RFC could include more details on how the behavior works exactly in combination with long running I/O. Lets say the default socket timeout being 60 seconds, andt the max execution wall time being 10 seconds. What happens if an HTTP call taking 60 seconds via file_get_contents gets started at 9 seconds in the request? I think you should mention that you also need to configure the timeouts of all I/O operations for an effective strategy, so that one gets a good picture how this puzzle piece fits into production. greetings Benjamin On Sun, Dec 13, 2020 at 1:25 AM Máté Kocsis <kocsismate90@gmail.com> wrote:

Máté Kocsis

5 years ago
Hi Rowan and Benjamin, we talked about this before and I think it's a good addition. Two
> suggestions to improve the RFC: >
Thanks for the ideas, I'll incorporate more details about the interaction of these settings. And I've just collected a few examples when my proposal would be useful: https://gist.github.com/kocsismate/dbcc4ba81b27cfb2e25b949723bb7c79 . Their intention is to illustrate that no matter how tight the individual timeout settings are, if the number of external calls is high enough during the same request, nothing (*) can prevent the response time to skyrocket until all the workers become busy, causing outage because the web server can't serve any new connections. *except for some more complex, or non-clean mechanisms, which are outlined in the RFC, I'm struggling to picture when I'd want a hard wall-time limit, rather than:
> - checking the script's duration at key points where I know it can > gracefully exit, ensure a consistent state, and return an appropriate > message to the caller > - enforcing a network timeout on the calling end so that I don't need to > rely on all services cooperatively exiting in good time
In my opinion, the main problem with the first suggestion is that in most cases, non-trivial production code can barely do anything like "checking the script's duration at key points". If there are enough levels of abstraction, the outermost layers (e.g. controllers) can't do any checks, they simply have not enough control. On the other hand, having assumptions about the request duration in any inner layers (e.g. model if we take MVC as an example) is a bad idea according to my experience. I admit though that there are some cases when it is possible to add the checks in question, but I believe that would result in a very noisy code, while a simple ini config would do a much better service. Do the above examples and this reasoning make sense to you? Also, I'll improve the wording I used when describing the consequences. Regards, Máté

Rowan Collins

5 years ago
Hi Máté, On 15/12/2020 23:12, Máté Kocsis wrote:
> I've just collected a few examples when my proposal would be useful: > https://gist.github.com/kocsismate/dbcc4ba81b27cfb2e25b949723bb7c79 > > [...] > > Do the above examples and this reasoning make sense to you? Also, I'll > improve the wording I used when describing the consequences.
The part I'm curious about is not so much when the setting would have an effect, as when it would be the desired solution. Your point about the server not being able to serve any more connections is reasonable, and I can see this being a useful last resort mechanism to keep the server responding. I'd be likely to treat it similarly to the Linux OOM killer: if it was ever actually invoked, I would be investigating how to fix my application. The wording in your e-mails and RFC suggests you see it as something you'd use instead of application-level solutions, rather than as well as them, and that's what I'm trying to picture the use case for. One scenario I can think of would look a bit like this: foreach ( $ordersToProcess as $orderDetails ) {     $result = send_order_to_remote_system($orderDetails);     save_confirmation_to_db($orderDetails->customerOrderRef, $result->supplierOrderRef); } If the remote system responded very slowly, that loop might take many times the expected duration. If we simply kill the process when a fixed wall time has elapsed, we're very likely to create an order on the remote system, but exit without saving its reference. It is however easy to see where in that loop we could safely call a routine like throw_exception_if_time_limit_reached(). If rather than placing orders, the loop was just gathering search results, killing the process would be less dangerous, but cleanly exiting would still be preferable, because we could return a warning and partial results, rather than a 500 error. Other scenarios I can think of have similar problems: * If the database locks up inside a transaction, killing the process probably won't roll that transaction back cleanly * If the process was working through a large import of data, it may have written a bunch of temporary data to disk or holding tables which won't be cleaned up In other words, I can only think of cases where "the cure would be worse than the disease". Regards,
-- Rowan Tommins [IMSoP]

Máté Kocsis

5 years ago
Hi Rowan, I'd be likely to treat it similarly to
> the Linux OOM killer: if it was ever actually invoked, I would be > investigating how to fix my application. >
I think this is mostly not about application-level, rather than infrastructure-level issues, at least according to our use-cases. For example, if your application is under heavy DOS attack, or there are other kind of network connection problems, then your service(s) may experience slow database/cache/API response times. It is also possible that a 3rd party API you depend on faces such issues. All these scenarios could severely harm the availability of your application, unless you have a hard, wall-clock time based timeout as a way to short-circuit too slow responses. So we are not talking about application (design) issues only, like the n+1 query problem. If the remote system responded very slowly, that loop might take many
> times the expected duration. If we simply kill the process when a fixed > wall time has elapsed, we're very likely to create an order on the > remote system, but exit without saving its reference. It is however easy > to see where in that loop we could safely call a routine like > throw_exception_if_time_limit_reached().
In my opinion, if the proposed ini setting causes consistency issues for an application, then they are already vulnerable to other factors which can make their application halt execution at random places: fatal errors, power outages, etc. I think developers of distributed systems should be aware of this - and I think they usually are, let's just take the CAP theorem -, so they have to accept and consider these risks. Please also note that "max_execution_time" already measures wall-time on a few platforms, so we already have precedence for the proposed behavior.
> If rather than placing orders, the loop was just gathering search > results, killing the process would be less dangerous, but cleanly > exiting would still be preferable, because we could return a warning and > partial results, rather than a 500 error. >
If returning a 50x response with a custom message is a requirement, then sure, developers can just ignore the new ini setting. Although, i.e. apache and nginx already allow custom error pages, and I think that should just be good enough for most use-cases. * If the database locks up inside a transaction, killing the process
> probably won't roll that transaction back cleanly >
Since there can be many other causes of a killed process, I think this particular problem is unrelated to my proposal, and if such a thing happens, then it's a bug in the database server. Also, please be aware that the timeout is a clean shutdown mechanism, so shutdown handlers and the already mentioned RSHUTDOWN functions are triggered. On the other hand, fpm's timeout doesn't invoke any of them. In other words, I can only think of cases where "the cure would be worse
> than the disease".
To be honest, my impression is that you either underestimate the "disease", or overestimate the current "cures". Speaking about the latter, nginx + fpm, one of the most popular web server setups (if not the most popular one) doesn't provide an easy-to-use and safe way to shutdown execution after a dynamically configurable amount of time. While one can use the "if (time() - $startTime > $timeout) { /* ... */ }" based approach instead, this won't scale well when used in non-trivial codeses. Thus, I believe my suggestion offers a more convenient, safer, and more universal way to solve the underlying problem of controlling the real execution time, than what the currently available options do. Regards: Máté

Rowan Collins

5 years ago
Hi Máté, On 18/12/2020 16:43, Máté Kocsis wrote:
> I think this is mostly not about application-level, rather than > infrastructure-level issues, at least > according to our use-cases.
I think we may actually just be saying the same thing in different terms: in this message, you refer to a "heavy DOS attack", which I totally agree is the kind of scenario where "just kill some processes and hope that's enough to ride out the storm" would be useful. Some of your earlier messages, though, made it sound like you were relying on it as an every day thing - you talked about seeing "regressions" during your migration, for instance. But maybe I just read too much into that wording?
> In my opinion, if the proposed ini setting causes consistency issues > for an application, then they are already > vulnerable to other factors which can make their application halt > execution at random places: fatal errors, power outages, > etc.
Certainly, I would *like* my application to be robust to a power outage in the middle of a request; but I would prioritise that robustness based on how likely it is to happen. If one request in a billion is terminated unexpectedly, I will probably take certain risks; if it's going to happen on a regular basis, I'm going to have to spend a lot more time on that set of problems. So, again, it comes down to whether this is a "last resort" setting, or something you'd expect to be invoked regularly.
> I think developers of distributed systems should be aware of this - > and I think they usually are
Actually, this may be part of the confusion: I'm not sure what you're referring to by "distributed systems", so don't know whether I'm included in the set of developers you're picturing here.
> If returning a 50x response with a custom message is a requirement, > then sure, developers can just ignore the new ini setting. > Although, i.e. apache and nginx already allow custom error pages, and > I think that should just be good enough for most use-cases.
You missed the key part of the quote you replied to here, which is "return partial results". My point was not about the *formatting* of the error, but that its *content* might need to be application-specific.
> Also, please be aware that the timeout is a clean shutdownmechanism, > so shutdown handlers and the already mentioned RSHUTDOWN functions are > triggered.
It might be useful to expand on this in the RFC, remembering that "RSHUTDOWN" doesn't mean anything to a userland developer. Will "finally" blocks be run? Destructors? Will the error handler be invoked? I know the answers are probably "the same as the current timeout setting", but spelling it out would help to picture when this feature might or might not be useful. Regards,
-- Rowan Tommins [IMSoP]

Peter Bowyer

5 years ago
On Fri, 18 Dec 2020 at 16:43, Máté Kocsis <kocsismate90@gmail.com> wrote:
> Also, please > be aware that the timeout is a clean shutdown > mechanism, so shutdown handlers and the already > mentioned RSHUTDOWN functions are triggered. On the other hand, fpm's > timeout doesn't invoke any of them. >
Tangentially, can this be considered a bug in FPM's handling? I appreciate the speed boost FPM brought over CGI, but the more I work with it the less I like the way it functions (but that is a separate conversation). Peter

Máté Kocsis

5 years ago
Hi Rowan and Peter, Tangentially, can this be considered a bug in FPM's handling? I appreciate
> the speed boost FPM brought over CGI, but the more I work with it the less > I like the way it functions (but that is a separate conversation). >
No, I don't think so, since FPM terminates the child process by sending a SIGTERM (rather than a SIGKILL), it seems that it does the right thing based on the source code. But I'd appreciate it if someone more familiar with FPM or process management could verify my assumptions. Actually, this may be part of the confusion: I'm not sure what you're
> referring to by "distributed systems", so don't know whether I'm > included in the set of developers you're picturing here.
My definition of "distributed system" is when there is a network boundary between components of an application. Practically speaking, this is the case when the application code and the database/cache/sessions etc. are on different servers. So, again, it comes down to whether this is a "last resort" setting, or
> something you'd expect to be invoked regularly.
Yes, I certainly imagine it as a last resort possibility which could supersede FPM's "request_terminate_timeout" or the other external timeout mechanisms that are currently in use. In fact, "max_execution_wall_timeout" is not effective alone, it has to be used in cooperation with other timeout settings either on the caller (PHP) or the callee (e.g. database) side, since external calls cannot be cancelled midway by PHP. That said, setting cURL, socket etc. timeouts is still highly encouraged as a regular countermeasure. My purpose with this RFC is to improve the last safety net we have (real execution timeout), which I find suboptimal in its current form. You missed the key part of the quote you replied to here, which is
> "return partial results". My point was not about the *formatting* of the > error, but that its *content* might need to be application-specific.
Ah, sorry, I did really miss it! I think my answer still holds though: if there is such a requirement, developers can ignore this ini setting (or explicitly set its value to 0, if necessary). Probably this is a good example why we should not make wall-clock time based measurement the default of "max_execution_time". Otherwise, I think my proposal would offer a solution for the 90% of the problems we currently have due to the way how "max_execution_time" works. It might be useful to expand on this in the RFC, remembering that
> "RSHUTDOWN" doesn't mean anything to a userland developer. Will > "finally" blocks be run? Destructors? Will the error handler be invoked? > I know the answers are probably "the same as the current timeout > setting", but spelling it out would help to picture when this feature > might or might not be useful.
I absolutely agree, and I'll try to elaborate as soon as I touch the RFC again, most probably today. Your answer is true, it is "the same as the current timeout setting". However, I was considering the possibility to throw an exception in case of the new timeout, but I think this task would be more suitable for a PHP 9.0 release where we could convert fatal errors to exceptions where it would make sense. Máté

Nikita Popov

5 years ago
On Sun, Dec 13, 2020 at 1:25 AM Máté Kocsis <kocsismate90@gmail.com> wrote:
> Hi Everyone, > > Thanks for all the insightful feedbacks so far, I went ahead, and wrote a > proper > RFC: https://wiki.php.net/rfc/max_execution_wall_time > > Regards: > Máté >
Something potentially worth pointing out (and assuming I'm inferring the correct behavior here): If max_execution_wall_time is exceeded during an internal function call (which seems quite likely, as that's where there is the most potential for something to hang) and the function does not return within hard_timeout seconds, then the a process abort will be triggered. The hard_timeout is 2s by default. If any of the individual call timeouts are >= 2s, then it's not unlikely that this situation occurs. Regards, Nikita

Máté Kocsis

5 years ago
Hi Nikita, Something potentially worth pointing out (and assuming I'm inferring the
> correct behavior here): If max_execution_wall_time is exceeded during an > internal function call (which seems quite likely, as that's where there is > the most potential for something to hang) and the function does not return > within hard_timeout seconds, then the a process abort will be triggered. > The hard_timeout is 2s by default. If any of the individual call timeouts > are >= 2s, then it's not unlikely that this situation occurs. >
Thanks for bringing this to my attention, I didn't realize this consequence. I'm wondering though if it would be a sane idea to use the current, CPU time based timer for hard_limit even when max_execution_wall_time times out? This way the process abort could be avoided in case of most functions in question, if I got it right. Nevertheless, I'll update the RFC with its relation to hard_timeout in the coming days. As I'd like to proceed with this RFC soon, I'd appreciate any review and constructive feedback. I have two questions for example: - Is the max_execution_wall_time really the best setting name we can come up with? What about something like max_execution_real_time? - Would anyone miss a set_wall_time_limit() function? Regards: Máté