views:

204

answers:

3

I have a PHP site which runs quite a lot of database queries. With certain combinations of parameters, these queries can end up running for a long time, triggering an ugly timeout message. I want to replace this with a nice timeout message themed according to the rest of my site style.

Anticipating the usual answers to this kind of question:

  1. "Optimise your queries so they don't run for so long" - I am logging long-running queries and optimising them, but I only know about these after a user has been affected.

  2. "Increase your PHP timeout setting (e.g. set_time_limit, max_execution_time) so that the long-running query can finish" - Sometimes the query can run for several minutes. I want to tell the user there's a problem before that (e.g. after 30 seconds).

  3. "Use register_tick_function to monitor how long scripts have been running" - This only gets executed between lines of code in my script. While the script is waiting for a response from the database, the tick function doesn't get called.

In case it helps, the site is built using Drupal (with lots of customisation), and is running on a virtual dedicated Linux server on PHP 5.2 with MySQL 5.

A: 

The connection handling docs are what you need.

Basically, you need to register a shutdown function using register_shutdown_function(). This function will be called whenever a script is finished, regardless of whether it has completed successfully, been cancelled by the user (ESC key), or has timed out.

That shutdown function can then call the connection_status() function. If connection_status() returns 2 (TIMEOUT) and the previous page was the one that runs the troublesome query, you can redirect the user to a page saying "Sorry, but we're experiencing high server load right now." or whatever.

MatW
Tis only relates to the connection between the place where the php code is running (usually a webserver) and browser - not the webserver and DBMS
symcbean
He's not asking for something that communicates with the DBMS, just a mechanism for handling script timeouts gracefully, which is what connection_status() was designed to do. I've edited my answer to provide more info.
MatW
This sounds like the solution I need, but I'm struggling with the redirection. I'm trying to use the PHP header() function, but it's saying headers have already been sent.Even with a simple test case:`set_time_limit(1);register_shutdown_function('clean_shutdown');`while(true) {}function clean_shutdown() { if(connection_status() header('Location: http://www.bbc.co.uk', TRUE, 500); }}I still get a "headers already sent (on line 5)" message, but I can't see where they would have been sent.
Mark B
BTW, ignore the backticks in that code snippet - I'm still getting to grips with Markdown, and SO won't let me edit it any more!
Mark B
9 times out of 10, that error points to the fact that you have already started outputting a response of some kind. It doesn't have to be an actual HTML element or echo statement though; even a whitespace character in your document will trigger the headers to be sent. I've had that error before because the first line of my script read: " <?php". If you've not got any sneaky whitespace characters, check for a BOM (byte order mark): http://www.php.net/manual/en/function.header.php#95864
MatW
+2  A: 

There is no asynchronous mysql calls and no scope for forking lightweight threads.

While you could split your PHP code into two tiers and use a connection between them which you can invoke asynchronously, the problem with this approach is the DB tier will still try to run the query after the upper tier has given up on getting the results back - potentially blocking the DBMS for other users. (you're more likely to get more frequent requests for pages which are timing out).

You'll have the same problem if you push the timeout handling up into a reverse proxy sitting in front of the webserver.

The most sensible place to implement the timeout is the database itself - but AFAIK, mysql does not support that.

So next option is building a proxy between the PHP and the database - this could be self-contained - generating 2 lighweight threads for each request (1 to run the query, 2nd as a watchdog to kill the first if it takes too long) - but this not only requires writing code in a lnaguage which supports lightweight threads, but also defining a protocol for communications with the PHP.

However taking a different approach to the proxy model - you could spawn a separate PHP process using proc_open and set the stdout stream to be non-blocking - that way your PHP can continue to run and check to see if the proxy has run the query. If it times out, then as the parent of the proxy, it can signal it to shutdown (proc_terminate()) which should stop the query running on the database.

Certainly, it's going to mean a lot of development work.

It may prove a lot simpler to set up one or more slave DBMS to run your slow queries against - potentially with smart load balancing. Or look at other ways of making the slow queries go faster - like pre-consolidation.

HTH

C.

symcbean
A: 

Is your server tuned with APC, Memcache, Boost and Drupal Cache? Those are alternate routes that work very well.

Otherwise, what kind of scripts are running in Drupal that would cause this? Just out of curiosity, are you running Views and Panels?

Kevin
This isn't a Drupal issue. We are using Views and Panels, but not on this part of the site. (I know the modules will still be loaded, but we aren't calling them). The problem here is exclusively database access, and it is customer-specific queries which cause the problem. Some customers have more data than others, and some request it over longer time periods than others. I've already created summary tables to speed up many of the queries, but there's still work to be done. I'm looking for something to provide a cleaner user experience while that work is going on.
Mark B