1

I'm trying to figure out if it's possible to "hide" the perceived execution time of some end-of-request logging tasks that are page specific and depend on the availability of the Drupal DB API. Essentially I'd like to run these tasks after the page response is returned in full to the browser. I've always just assumed that hook_exit() was the right choice for this kind of thing but after looking at its invocation within drupal_page_footer() in some detail, I'm not so sure. Here's the notable snippet from drupal_page_footer():

module_invoke_all('exit');

// Commit the user session, if needed.
drupal_session_commit();

if (variable_get('cache', 0) && ($cache = drupal_page_set_cache())) {
drupal_serve_page_from_cache($cache);
}
else {
  ob_flush();
}

So it looks like both cached and un-cached responses are not sent to the browser until after all hook_exit() implementations run (flushing the buffer and executing drupal_serve_page_from_cache() both happen after module_invoke_all('exit')). This seems a bit contrary to the hook_exit() documentation which states that, "This hook MUST NOT print anything because by the time it runs the response is already sent to the browser."

Am I perhaps misinterpreting the way hook_exit() implementations work in relation to the response delivery? If not, is there some alternative way to trigger custom logging logic later in the response (while DB access is still available)? Perhaps by registering a shutdown function or something to that effect?

1

You're looking for fastcgi_finish_request(). Recommend using it inside of a shutdown function. If you need something else the HTTPRL module should be able to do what you need with threading.

  • Very interesting. I guess that indirectly confirms my initial conclusion that hook_exit() was not an option for this (despite what the docs may say). By moving things to a shutdown function I guess I can at least get the page markup delivered before the logging task, but it seems that your specific recommendation would be the only way to also get the connection closed before that point? I'm guessing this would ensure that js triggerd by $(document).ready() fires sooner, etc.? – rjacobs Feb 6 '15 at 3:38
  • General rule is that the faster the connection is closed by PHP the faster the browser can do it's thing. – mikeytown2 Feb 6 '15 at 23:27
  • I guess that sums things up pretty nicely. – rjacobs Feb 7 '15 at 1:24
0

I want to follow-up on this with an answer to my own question. This just expands on the already accepted answer.

For anyone wanting to achieve this in a reusable module, that may be implemented on multiple environments, a simple call to fastcgi_finish_request() will of course not be possible as there is no guarantee that FastCGI is being used. That said, it still looks like fastcgi_finish_request() is the best option for this when available.

To still leverage some advantage when FastCGI is not available, I've found it's useful to at least attempt to flush all content to the client before running the logging task. This would allow the browser to begin displaying content, start fetching any render-blocking js, etc., before the logging happens. It would not allow the connection to be closed, as fastcgi_finish_request() would do (so DOM ready events would still be held up), but it still seems better than not doing anything at all.

Here's what I've been testing in my shutdown function (this would only be applicable in a shutdown function and would not work in any hook implementations):

/**
 * Shutdown function used for logging purposes.
 */
function mymodule_shutdown() {
  // Try to ensure that our logging happens after the page content has been sent
  // to the browser, and ideally after the connection is closed.
  // fastcgi_finish_request() is our best option as it's designed for exactly
  // this purpose, but php may not always be running under FastCGI. As an
  // alternative we just force buffers to be flushed (this will not close the
  // connection but it will at least get the page content pushed out).
  if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();
  }
  else {
    flush();
  }
  run_expensive_logging_task();
}

There is some useful info in this post about all this as well. I can't be too sure if calling flush() alone will always do the job as there are some indications that output buffering must be explicitly killed (ob_end_flush()) as well. However, in the context of a Drupal shutdown function it seems like this is unneeded as wrapping-up all the output buffering stuff was likely handled by core already. Of course if fastcgi_finish_request() is used it takes care of all this in one line.

  • See this function for how to do it: httprl_background_processing() – mikeytown2 Feb 20 '15 at 19:22
  • Thanks, that function is a very useful reference! I'm going to make note of those details for sure. However, I think the application of that httprl_background_processing() logic may be a little limited, as it assumes that I have full control of the page headers. If I'm needing to trigger a logging task that fires for all page requests regardless of how the response is built, and when headers are sent (basically a shutdown function that's registered on all pages), I'm assuming the best I can do is flush any buffers? – rjacobs Feb 20 '15 at 21:56
  • The headers you need for sure are Connection: close and Content-Length: ob_get_length(). – mikeytown2 Feb 20 '15 at 22:11
  • Yes, I think that makes sense, as a browser receiving those headers, along with a matching-length data response, would close the connection while things still churn-away on the server. However, unless I am missing something, I don't think I can set those headers without full control of menu callback, or at least some guarantee that no content/headers have been sent yet. Without this control or access to fastcgi_finish_request(), is there actually anything else I can do besides flush buffers? – rjacobs Feb 25 '15 at 22:03
  • hook_page_delivery_callback_alter() might be the way to control the output – mikeytown2 Feb 25 '15 at 22:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.