Since #1463656: Add a Drupal kernel; leverage HttpFoundation and HttpKernel commit, I get these errors on my environement: Notice: ob_flush(): failed to flush buffer. No buffer to flush in Drupal\Core\EventSubscriber\RequestCloseSubscriber->onTerminate() (line 44 of /var/www/d8-git/www/core/lib/Drupal/Core/EventSubscriber/RequestCloseSubscriber.php).

This seems to be a problem due to the fact that ob_flush() is called even when the output buffer has not been started (typically during redirects for example).

Reading the ob_end_flush() it made me realize that anything else done after the ob_flush() call should never appear on screen, it's basically a set of cleanup tasks and various userland shutdown handlers. When calling ob_end_flush() you actually release the stream and potentially close the connection with the HTTPd, which makes you page gone to the client faster, this is a potential performance improvement.

But, sadly, I experienced the same error using ob_end_flush(), so I continued looking for a solution, and found one thanks to this post:

      while (ob_get_level() > 0) {
        ob_end_flush();
      }

This supposedly ensures that the function is being called only if you really have a buffer to flush. This ensures both a secure flush (no PHP warnings) and early stream release (faster delivery to the client).

Reading the PHP documentation page, a lot of people seem to complain about potential bugs, but I tested with various browsers and have never been able to reproduce those, plus, all those complains predates 2011, they almost all have more than 2 years, which means no one seem to complain since.

I think it's a safe move to do.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

pounard’s picture

Tested with:
PHP-5.3.10 / FPM / Nginx / With zlib.output_compression / FF / Chromium: OK
PHP-5.3.10 / FPM / Nginx / Without zlib.output_compression / FF / Chromium: OK

neclimdul’s picture

I hesitate to RTBC it without someone else reviewing but it looks reasonable. I talked to pounard on IRC and he wasn't able to reproduce the zlib errors and I can only find references to it with php 5.2 so I think we're ok there.

pounard’s picture

As per RobLoach suggestion, added docblocks for posterity and comprehension.

RobLoach’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me. I don't have an nginx server handy to test this on, but it's working just fine on Apache! The doc block is also quite helpful and it makes sense when reading about the buffer stack.

Crell’s picture

This seems logical to me. I suspect there was some undocumented magic happening with buffer balancing before, and because it was undocumented we inadvertently broke it with the kernel patch. This is a tidier way of handling it anyway.

pounard’s picture

Hum, after doing some research I found out about fastcgi_finish_request() which actually handles the performance side of this. In current Symfony head, they are handling it seamlessly in the Response::send() method.

May be we could move this code side by side with this one, and suggest a PR to Symfony (then we would be able to drop our own support for this and have a nicer result).

Before doing this, we need to get this patch commited anyway because we can't let PHP warnings happen.

Dries’s picture

Status: Reviewed & tested by the community » Fixed

Comitted to 8.x. Thanks.

pounard’s picture

Thanks.

After some reading (a lot) there is no clean way with a vanilla PHP to detach the process and continue in the background without an explicit Connection: Close HTTP headers (which is probably a bad thing to do for the sake of keep-alive).

We can still improve what I done by adding a flush(); call just after the while() loop: this triggers a real and early buffer send (without detaching PHP from the HTTPd), and allow a finer buffer handling for layer uppers (HTTPd, Proxies, etc...). That's what we should PR to SF guys. I don't have a lot of time right now so not doing it, but we should keep that in mind.

Damien Tournoud’s picture

Status: Fixed » Needs work

So we committed this without even diving into this, looking which code paths are affected and where we don't start an output buffer while maybe we should? Oh, and without any tests?

At the minimum, let's fix the documentation, as this is just completely false:

+        // Using ob_end_flush() instead of ob_flush() will close the output
+        // buffer. This means that potential later errors won't get to the user
+        // and the HTTPd might release the connection sooner.
Damien Tournoud’s picture

I suspect there was some undocumented magic happening with buffer balancing before, and because it was undocumented we inadvertently broke it with the kernel patch.

The "undocumented magic" was (1) we open the buffer when started generating the page, (2) we flush it at the end. Very magic.

neclimdul’s picture

@Damien No, the magic was we didn't always start the buffer but we magically didn't didn't clean the buffer in those code paths either. That's the balancing he was referring to and I think this happened in places where we exited the code flow early like drupal_goto and things that called drupal_exit(). Since we are removing those code paths and having a consistent exit point we need to handle when the buffer hasn't been initialized here. Fairly straight forward.

I personally looked through some of the code paths and since its a simple cleanup I don't know that its reasonable to ask that we review everything for a notice fix since the code should behave the same. Also, Crell is more familiar with anyone in the community with the new request routing and he OK'd it and that should be good enough.

I can see where the early termination comment is incorrect, we should have fixed that. That's an easy follow up.

pounard’s picture

Output buffering and connection handling: there is magic

In Drupal, there is only one code path: we deliver a response. This was already true in Drupal 7, this is explicitely true in Drupal 8 since we have the Response object. This is always true, whether we deliver an error page (error response), not found page (which is a special case of the error one), a file, a redirection, or a normal page.

Now in PHP side of things everything will be sent directly into the standard output it is attached too, until we start the output buffer. But in some cases, there is actually magic, especially when enabling the PHP buffer gziping (on some environments) but I suspect that PHP could create itself other layers of buffers depending on the environment it is running in. Regarding connection handling, this is a bit more complex, you can read the PHP documentation as a start: http://php.net/manual/en/features.connection-handling.php

We also know that PHP will flush itself all opened output buffer when the script exits, which actually doesn't mean the browser will close the connection (for example, in Apache context, conection handling is handled by Apache, not PHP, which allows it to handle the HTTP keep-alive while that in any CGI gateway use case, it will close the connection to the gateway, leaving the HTTPd upper handling another connection by itself to the client, which is not the PHP connection).

Opening ourself an output buffer is the most sane thing to do, we need it as it is explained there it will improve performances when we render our templates.

Now please consider reading that page: http://developer.yahoo.com/performance/rules.html/ There is there a special paragraph about Flush the Buffer Early. I think that Drupal 7 attempts to achieve this by calling ob_flush(), but this is wrong. The only way to ensure the buffer is really flushed is calling the flush() method (see both documentations in http://www.php.net/manual/en/function.ob-flush.php and http://www.php.net/manual/en/function.flush.php

In cases where we are in CGI gateway, for exemple, we already have two connections layers upper, which means that buffer bloat is easy, and response time will increase if keep our own buffer back while proceding to shutdown.

Drupal 7 attempts to flush the buffer early using ob_flush(), but it actually doesn't push it to the upper layer (HTTPd or CGI gateway): this piece of code is particularely useless by itself, plus, it doesn't take into account that we might have multiple output buffers since PHP can in real life magically open some behind our backs.

The "undocumented magic" was (1) we open the buffer when started generating the page, (2) we flush it at the end. Very magic.

Yes, magically, not that there is any magic in it, but because we can't try to extrapolate all possible uses cases people will use Drupal in. So, first answer to you, there is real magic here in our standpoint (even though we all agree, magic does not exist).

Drupal 7 stable is broken

There is another point to consider: we should not leave errors pass to the client once the full page is rendered, for many reasons. First, some of those errors could be insignificant and happen, for exemple, during some non essential shutdown operations. Plus, errors coming into the buffer once all the HTML is rendered could break rendering. And last but not the least, we could send errors into redirect response, which may break them (and that is bad). Note that we will eventually log all errors in the watchdog, whether or not we display those, so it's a safe thing to turn off the output buffering at this point.

Ensuring that buffers are not just only flushed, but closed will avoid those errors to get back to it, in order to ensure that, we need to use ob_flush_end() instead of ob_flush().

Now considering the last two paragraphs and the fact that PHP can magically add output buffers behind our backs, that Drupal 7 code is useless because the ob_flush() is inneficient and broken because we could potentially send errors after the full page rendering thus breaking it that we have to fix it. We just have to replace the ob_flush() by:

while (0 < ob_get_level()) {
  ob_flush_end();
}

Flush the Buffer Early

Because we want to improve performances, we need to proceed to the real flush() call, so the code above might be improved by writing:

while (0 < ob_get_level()) {
  ob_flush_end();
}
flush();

Which does the trick and ensures that the full buffer is pushed to the upper layer, ensuring that the gateway we use (either direct HTTPd or CGI) will manage its own buffer instead of waiting for us to have finished our userspace shutdown operations to finish and thus decreasing the response time in all cases, no exception.

Now, we also now that FPM is quite used now, hopefully we can force an early connection release by calling the fastcgi_finish_request(), so Drupal 7 code should be fixed as this:

if (function_exists('fastcgi_finish_request')) {
  fastcgi_finish_request();
}
else {
  while (0 < ob_get_level()) {
    ob_flush_end();
  }
  flush();
}

If you don't like the actual commited comment, please feel free to quote this post into comments into the code above for the next patch.

All this algorithm is the actual result of actual one full day of working and testing, and I will have monday loading stress tests results too which will confirm or infirm, or non be non significant. Whatever those tests will say, we have either to remove this broken and useless (and confusing) Drupal 7 code, or fix it as expressed above.

Drupal 8 and Symfony 2

Symfony 2 doesn't seem to care about all other environments than FPM, since they just call the fastcgi_finish_request() if exists, but I think they are wrong about this one and their code could be improved. Anyway, their buffer release code is into the Response object, at the very end of the send() method, right after the class specific implementation content sending (which might be only HTTP headers). This means that like us, they will do it in every code path, their framework is supposed to always send a response.

This also mean they actually do it even earlier than us!

We should PR them for adding the exact same code as I exposed above, but in their response object. That way, we would not need to keep our own implementation and leave the onTerminate listener alone for normal shutdown operations, not buffering control, which would be cleaner.

Last answer

At the minimum, let's fix the documentation, as this is just completely false:

I demonstrated upper that is only partially false. Actually only and the HTTPd might release the connection sooner is wrong. We should write something like this: Using ob_end_flush() instead of ob_flush() will close the output buffer. This means that potential later errors won't get to the user. Flushing the buffers followed by the flush() call will force buffers to be pushed upper layer (HTTPd or CGI gateway) allowing it to send it to the client faster.

So we committed this without even diving into this, looking which code paths are affected and where we don't start an output buffer while maybe we should? Oh, and without any tests?

Flushing all the buffers at the kernel terminason and before doing additional shutdown operations is generally a good idea and exactly what most frameworks do, even SF does with FPM, just earlier than us: I don't see where additional tests could have actually helped us here. The only problem here is that SimpleTest actually did not catched our PHP warnings, and that in a sense is broken.

pounard’s picture

pounard’s picture

Status: Needs work » Needs review
FileSize
2.2 KB

Here is the D8 proper patch.

pounard’s picture

And the D7 proper patch.

Status: Needs review » Needs work

The last submitted patch, 1651010-15-d7-output_buffer_handling.patch, failed testing.

pounard’s picture

Status: Needs work » Needs review
Issue tags: +needs backport to 6.x

This could also be backported to D6.

Damien Tournoud’s picture

Status: Needs review » Needs work

I demonstrated upper that is only partially false. Actually only and the HTTPd might release the connection sooner is wrong. We should write something like this: Using ob_end_flush() instead of ob_flush() will close the output buffer. This means that potential later errors won't get to the user. Flushing the buffers followed by the flush() call will force buffers to be pushed upper layer (HTTPd or CGI gateway) allowing it to send it to the client faster.

Flushing the buffer early is a good thing, but it never (and has never) closes any connection context, neither on the PHP side (even on FastCGI) nor on the web server side of things. You can *always* start a new buffer or outputting things directly. A five minute research should have convinced you of that.

So let's remove this non-sense from the code comments.

Damien Tournoud’s picture

@neclimdul: I really don't see what you are talking about. Drupal 7 unconditionally opens the output buffer in _drupal_bootstrap_page_header() and unconditionally flushes it in drupal_page_footer() (optionally catching the buffer and saving it to cache in there).

There is very much zero magic involved in this process.

pounard’s picture

@#18 It's fixed in #12 I said this part was wrong. If you read my 2 patches you will see I fixed it in both.

EDIT:

A five minute research should have convinced you of that.

Half an hour of reseach actually convinced me that it can be done using some messy tricks playing with HTTP headers to force the client to close the connection for us. That's why I wrote #8 -some hours before yours- and #12. In FPM context, the fastcgi_finish_request() actually closes the gateway connection. I played with both quite a bit, and even if making the hacky solution is quite easy, I'm sure this is something we don't want to do because we would mess up with other because playing too deep with HTTP connection handling headers may interact with upper layers. I think I don't need the 5 minutes you suggest anymore.

There is very much zero magic involved in this process.

There is some underlaying PHP magic under, read #12. Drupal 7 stable code is both broken and useless. EDIT: Not all Drupal 7, only the ob_flush() call, of course.

pounard’s picture

About Drupal 7 patch I made: there is still something to fix, because the drupal_session_commit() might attempt to send the session cookie to the user, we need the session to be started if needed before the flush() call.

Because the session commit is a costy task, I'd suggest to still doing it after the flush. We can still do that: testing if $_SESSION is empty, starting the session if not, then flush() then delaying the session commit after that should work fine. I have testing code on a patched site actually that works nicely.

I will update the patch soon.

cweagans’s picture

pounard’s picture

A while ago I PR'ed SF2 https://github.com/symfony/symfony/pull/4914 and this piece of code evolved since, because some people managed to find out some bugs I missed until it reached https://github.com/symfony/symfony/issues/5033 all of those being merged.

We now need to revert current D8 patch, and need to fix the D7 version using the SF2 current algorithm, and we we then may be able to call this a win.

aspilicious’s picture

Status: Needs work » Fixed

I think this is fixed by chx his code. I grepped for ob_flush and couldn't find anything.

pounard’s picture

Status: Fixed » Needs work

Please link to the patch, I spotted other regressions than just ob_flush()'ing, I want to be sure they are fixed.

aspilicious’s picture

pounard’s picture

Thanks, I will review this as soon as I can.

Crell’s picture

Is there anything to do here still, or can we re-close?

pounard’s picture

Actually, I went for a round of debugging recently to ensure we don't miss at least the lazy cookie send before we close the output buffer, and it seems that it works. I'd like to have some other pair of eyes verifying this, because in the earlier version of the patch this actually was one of the drawbacks.

I'd also like, if the D8 version is OK, to attempt a Drupal 7 clean port for this, case in which it'd loose the WSCCI tag, but for this the D7 maitainers need to give the go.

Crell’s picture

Issue tags: -WSCCI

OK, dropping out of my queue then. I defer on the D7 question to David.

pounard’s picture

Sounds the right thing to do.

xxm’s picture

Is there a solution for d7?
ugly this server errors:-(

pounard’s picture

Yes there is.

xxm’s picture

Where?

pounard’s picture

Is there a solution for d7?
Ok, patch to be made. I did an ugly one for a custom project, I'll do a proper one for here.

pounard’s picture

Version: 8.x-dev » 7.x-dev
Assigned: Unassigned » pounard
Status: Needs work » Needs review
FileSize
2.2 KB

Here is a D7 version (seems to be working on my local box) finger crossed that tests pass.

Switching back to D7 since the fix is applied de facto into Symfony's Symfony\Component\HttpFoundation\Response::send() method so applied per default to D8.

Status: Needs review » Needs work

The last submitted patch, 1651010-35-better_flush-d7.patch, failed testing.

pounard’s picture

Fails seems to be irrevelant to the patch. I will try some debugging when I'll have some time for it.

pounard’s picture

Status: Needs work » Needs review
Issue tags: -Needs backport to D7, -symfony, -needs backport to 6.x

#36: 1651010-35-better_flush-d7.patch queued for re-testing.

Status: Needs review » Needs work
Issue tags: +Needs backport to D7, +symfony, +needs backport to 6.x

The last submitted patch, 1651010-35-better_flush-d7.patch, failed testing.

pounard’s picture

Ok maybe fails are related...

pounard’s picture

Status: Needs work » Needs review
FileSize
2.2 KB

Retrying.

Status: Needs review » Needs work

The last submitted patch, 1651010-42-better_flush-d7.patch, failed testing.

pounard’s picture

Status: Needs work » Needs review
FileSize
3 KB

I think I might found some of the errors cause: the session commit must happen after the hook_exit() run. Did some improvements in documentation and added credits to copy/pasted Symfony code (this code is actually being used in Drupal 8).

Hope this new patch passes.

pounard’s picture

Title: HttpKernel patch introduces ob_flush() related PHP warnings on my environement, potential patch and performance improvement » Unification of drupal_exit() and drupal_page_footer() and performance boost for FPM enabled environments

Status: Needs review » Needs work

The last submitted patch, 1651010-44-better_flush-d7.patch, failed testing.

pounard’s picture

Status: Needs work » Needs review

I have absolutely no idea why the update tests are failing.

mgifford’s picture

Status: Needs review » Needs work
Issue tags: -needs backport to 6.x +Needs tests

Hopefully this will help someone look at the tests.

  • Dries committed 48a0b69 on 8.3.x
    - Patch #1651010 by pounard: HttpKernel patch introduces ob_flush()...

  • Dries committed 48a0b69 on 8.3.x
    - Patch #1651010 by pounard: HttpKernel patch introduces ob_flush()...

  • Dries committed 48a0b69 on 8.4.x
    - Patch #1651010 by pounard: HttpKernel patch introduces ob_flush()...

  • Dries committed 48a0b69 on 8.4.x
    - Patch #1651010 by pounard: HttpKernel patch introduces ob_flush()...

  • Dries committed 48a0b69 on 9.1.x
    - Patch #1651010 by pounard: HttpKernel patch introduces ob_flush()...