Problem/Motivation

This blocks #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits and was split off from there per #2657684-81: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits (see the very bottom of that comment).

This is necessary to support the following:

BigPipe currently only is enabled for requests with a session. The reasons:

  • Page Cache (or something equivalent, like Varnish) is much faster, since there's zero calculations happening. So that's much better still.
  • Only requests with sessions are personalized anyway. (Either for authenticated users, or for anonymous users with shopping carts for example.)

However, there's one case where BigPipe actually makes sense for requests without a session: in case of a Page Cache miss (or Varnish miss). In that case, we currently make the user wait for the entire page to be rendered, and then we flush it in one go (\Drupal\Core\Render\Placeholder\SingleFlushStrategy). This means the user is staring at a blank screen much longer than necessary. This is exactly the problem that BigPipe can solve… so what if we use BigPipe to stream the response in case of such a cache miss, and then store the entire result in the Page Cache?

As a consequence, a new advanced feature becomes available. If you uninstall Page Cache (and don't have Varnish or something like it), and you configure your site to set Cache-Control: max-age=0 on your responses, you're effectively allowing BigPipe to always stream the response to anonymous users. Which means you could even do things like showing different content on every request, yet still have fast responses thanks to BigPipe's streaming.

Proposed resolution

  1. After the entire response has been streamed, assemble the full, final HTML and ensure it's stored in PageCache.

For point 3, we need an API addition for Page Cache:

  1. a new CacheableStreamedResponseInterface
  2. the PageCache must implement TerminableInterface so that it can read streamed responses after they have finished streaming, so it can then still cache them

Remaining tasks

  1. Comprehensive test coverage.

User interface changes

None.

API changes

  1. Addition: CacheableStreamedResponseInterface (needs CR)
  2. Addition: Page Cache supports streamed responses (because it now implements TerminableInterface)

Data model changes

None.

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

Status: Active » Needs review
StatusFileSize
new14.74 KB
fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC - Looks great to me!

xjm’s picture

I'm really sorry that I did not spot these changes in the BigPipe patch sooner -- these changes would have had a beta deadline for 8.2.x. I don't think marking the interface internal really gets around that.

I definitely agree with splitting them into their own issue, though.

xjm’s picture

Version: 8.2.x-dev » 8.3.x-dev
Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/lib/Drupal/Core/Render/CacheableStreamedResponseInterface.php
    @@ -0,0 +1,27 @@
    + * @internal
    + * @todo Make public once code/modules other than BigPipe start using this.
    

    Is the reason for marking this interface internal that:

    1. We think we will need to change it,
    2. We don't want people to try using it, or
    3. We don't want to break public API by adding it?

    If it's #1, then I think marking it internal is okay. If it's #2 I don't see how the @todo is going to happen since it's internal code. :) If it's #3 only we should definitely just consider adding it as a public interface to 8.3.x, because marking an interface internal doesn't really get around the fact that this change would have needed to go in before beta.

  2. +++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
    @@ -206,6 +226,39 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch
    +    // @see ::terminate()
    

    IIRC API.d.o does not pick up this syntax, but OTOH I think it also doesn't parse @see in inline comments anyway.

  3. +++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
    @@ -228,7 +281,7 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch
    -      return $response;
    +      return FALSE;
    

    So reading this line out of context makes it look like quite a BC break. I'm assuming the actual code flow still results in the $response being returned in most circumstances? Because this is actually moving code that used to be in the main method into a helper method? Is that correct?

I'm moving the issue to 8.3.x now, which catch also suggested was the right thing to do. (I don't believe he's had the chance to look at the actual code here yet.)

wim leers’s picture

effulgentsia’s picture

Re #5.1: This interface also has a @todo pointing to #2577631: Allow HtmlResponse to use a flexible emitter. I think whether it should be marked @internal depends on whether we want to leave the option to change anything about the definition of getCacheableStreamedResponse() as part of that issue or any other possible refactoring. For example, are we confident about wanting to commit to the @return type that's documented on this method and also the docs that say that callers can rely on KernelEvents::RESPONSE having been dispatched? If we are confident about those requirements, then I think we can remove the @internal. Because if all we plan to do in the future is additions to this interface, that's still allowed per https://www.drupal.org/core/d8-bc-policy#interfaces so long as we don't add the @api tag to it. If we want to leave room to change that, then let's keep it @internal and document that we reserve the possibility to remove or change that method definition as part of #2577631: Allow HtmlResponse to use a flexible emitter.

Re #5.2: So that just needs to change to static::terminate(), I think.

Re #5.3: Right, there's no change to what is returned by fetch(), just the moving of code into a helper method with a different @return signature, so that it can be called from terminate() as well as fetch().

effulgentsia’s picture

Status: Needs review » Needs work

Setting to NW primarily for #5.1, which needs something done per #7.

While we're at it, might as well fix #5.2 per #7. And also:

+++ b/core/lib/Drupal/Core/Render/CacheableStreamedResponseInterface.php
@@ -0,0 +1,27 @@
+   * @see \Drupal\Core\Render\CacheableStreamedResponseEventDispatcherTrait

The trait isn't in this patch, so this line needs to be removed.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new2.04 KB
new14.76 KB

If we want to leave room to change that, then let’s keep it @internal and document that we reserve the possibility to remove or change that method definition as part of #2577631: Allow HtmlResponse to use a flexible emitter

This. Added that additional documentation.

Addressed everything.

effulgentsia’s picture

Adding review credit to @Fabianx and @xjm.

effulgentsia’s picture

+++ b/core/modules/page_cache/tests/modules/page_cache_test/src/PageCacheTestCacheableStreamedResponse.php
@@ -0,0 +1,25 @@
+class PageCacheTestCacheableStreamedResponse extends Response implements CacheableStreamedResponseInterface, CacheableResponseInterface {
+
+  use CacheableResponseTrait;
+
+  protected $streamedResponse;
+
+  public function __construct($streamed_response) {
+    $this->streamedResponse = $streamed_response;
+    parent::__construct('', 200, []);
+  }
+
+  public function getCacheableStreamedResponse() {
+    return $this->streamedResponse;
+  }
+}

I was curious if Coder was going to complain about the lack of class or function docs here, but it didn't. All it complained about was a missing newline between the last 2 braces, so here's a fix for that.

effulgentsia’s picture

One more trivial fix for a Coder complaint.

effulgentsia’s picture

I'm moving the issue to 8.3.x now, which catch also suggested was the right thing to do.

I'm happy with #12 and am comfortable committing it to 8.3 once it's re-RTBC'd (interdiffs since #5 reviewed and approved by someone).

I'm sad for this not to make it into 8.2. https://www.drupal.org/core/d8-allowed-changes#beta includes a bullet for "minor API additions or internal API changes to fix bugs (or for other prioritized changes), if the impact outweighs the disruption", and IMO:

  • This qualifies as a prioritized change due to it unblocking #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits for commit to an 8.2 patch release or during 8.2 RC, which if that's allowed for changes to experimental code only, is a nice way to improve performance for the next 6 months on production sites using BigPipe and encountering frequent page cache misses.
  • This patch's new @internal interface qualifies as a minor API addition, if it's even that, and since no one needs to use that interface, has 0 disruption.
  • This patch's change to PageCache to implement TerminableInterface is a backwards-compatible behavior addition that I would expect to have 0 to minimal disruption (while it's theoretically possible for a contrib/custom module to for some reason be relying on it not implementing that interface, that seems like a very brittle assumption, and one we shouldn't be expected to coddle).

However, even if all that is true, we're pretty much at the tail end of the beta period, and RC is imminent, so if the release managers don't feel comfortable with this going into 8.2 so last minute, then I respect that call. In which case, I opened #2795391: Move most of PageCache::fetch() into a storeResponse() helper that can be independently invoked from a subclass in the hopes that at least that much can make it into 8.2 (either before or during RC, or into a patch release), which would allow for BigPipe to implement its own subclass of PageCache and add the terminate() method to that. In which case, CacheableStreamedResponseInterface could also be moved to the BigPipe namespace for 8.2.

effulgentsia’s picture

which would allow for BigPipe to implement its own subclass of PageCache

Note, however, that that would not be compatible with a contrib/custom module that swaps out http_middleware.page_cache for a custom implementation. Either BigPipe or that other module would "win" and likely not accommodate whatever the other one needs done. But that's an edge case that we could leave to that contrib/custom module and the site that uses it to solve, since BigPipe is after all still experimental. In that regard though, committing #12 to 8.2 is cleaner, but that's not necessarily sufficient reason to do it.

xjm’s picture

I'm sad for this not to make it into 8.2

Don't be sad. This makes Drupal 8.3.x better, and now is the very best time to start making Drupal 8.3.x awesome. Also, BigBipe can still be beta in 8.2.0, RC soon, and stable for 8.3.0. It will be the first experimental module in beta and the first one in stable!

This qualifies as a prioritized change due to it unblocking #2657684: Ability to use BigPipe without a session: allow first sessionless (and hence anonymous) response to be delivered by BigPipe, then cached in Page Cache/Varnish/… for subsequent sessionless responses for commit to an 8.2 patch release or during 8.2 RC, which if that's allowed for changes to experimental code only, is a nice way to improve performance for the next 6 months on production sites using BigPipe and encountering frequent page cache misses.

While the BigPipe feature is a great one, that doesn't make that issue a prioritized change. Outside In and Content Moderation were prioritized changes. (I think we might remove those four words from the policy doc for the next beta, since they seem to confuse people about the policy more than they help, and they necessarily meant something different when beta lasted a year.)

we're pretty much at the tail end of the beta period, and RC is imminent, so if the release managers don't feel comfortable with this going into 8.2 so last minute, then I respect that call.

The last-minute-ness definitely doesn't help (nor does the fact that today is a US holiday), but I would have made the same decision even a few weeks ago.

has 0 disruption

Let's avoid saying that in general. A spelling fix in a docblock might be close to 0 disruption within some rounding error. Things that are near 0 disruption also tend to be near 0 impact. :) Also keep in mind improvements to page caching and low-level subsystems inherently carry more risk, even BC ones. Not saying the patch is particularly high risk either, and it's BC with internal API additions, but in any case those are changes we target for minor releases and that therefore have a beta deadline.

Those aren't the main reasons for this to be beta deadline, though. The main reason this is targeted for a minor with a beta deadline is that it adds functionality Drupal did not previously support. It could conceivably be marked as a feature request. Not in a bad way; BigPipe itself is a great feature, and adding functionality that unblocks contrib or experimental modules is a win.

I don't actually think this going into 8.3.x only (as soon as it is ready, which is probably soon indeed) impedes innovation or BigPipe's path to stability. Sites that are running BigPipe in 8.2.x are early adopters choosing the risks of experimental modules regardless, and so they can choose to run a couple patches on their sites too if they want this functionality, especially since those patches will have already been committed to the next branch. BigPipe can still be beta now, and could even be marked as stable in 8.3.x (with this feature included) as soon as (say) the end of October.

wim leers’s picture

I agree with everything in #13: risk is very low. But it's the release managers' call. "Sad" is perhaps the wrong word; "unfortunate" is likely better :)

znerol’s picture

Trying to understand the problem and the implications. Some random notes:

  • StreamedResponse in the Symfony issue queue
  • Original PR with lots of discussion
  • The issue summary suggests that a user without a session potentially needs to wait a long time on a Varnish MISS. I see that BigPipe/StreamedResponse could reduce the time-to-first-byte. However, I do not understand how the internal page cache could help in this situation. If you have Varnish in front of your servers you have the internal page cache disabled anyway.
  • I feel that people may use streamed responses to actually stream responses which are too big to fit into RAM. See, e.g. the readfile example in the original PR. I wouldn't want to store that in the internal page cache

Also:

+++ b/core/modules/page_cache/tests/modules/page_cache_test/src/PageCacheTestCacheableStreamedResponse.php
@@ -0,0 +1,26 @@
+class PageCacheTestCacheableStreamedResponse extends Response implements CacheableStreamedResponseInterface, CacheableResponseInterface {
...
+  public function __construct($streamed_response) {
+    $this->streamedResponse = $streamed_response;
+    parent::__construct('', 200, []);
+  }

What happens if the internal page cache module is uninstalled? If I understand correctly, then there is nobody left who cares to retrieve the inner streamed response from the wrapper (i.e., getCacheableStreamedResponse() is called nowhere outside the page cache module). I'd expect that the user agent will receive an empty 200 in this case?

wim leers’s picture

  • First two bullets: note we do not use Symfony's StreamedResponse. That class is pretty much useless.
  • Third bullet: You don't always have Page Cache disabled when you use Varnish. Page Cache and Varnish cannot help with the MISS, but they can speed up subsequent requests: if the streamed response is cached by Varnish/Page Cache, then they can ensure that requests 2, 3 … N are HITs. Responses for requests 2, 3 … N would not be streamed, and there would not be a need for them to be streamed: they're cached already. The only reason we want to stream response 1 is because it can take a while for it to generate.
  • Fourth bullet: again, that's referring to Symfony's StreamedResponse. I know it's confusing we introduce a "different kind of streamed response", but it's a necessity, because Symfony's is so flawed.
  • RE: "also": if page_cache is installed, then after the response is streamed, i.e. when terminate() is called, Page Cache ends up storing a non-streamed response based on the streamed response. When page_cache is not installed, none of that happens. In either case, what the client receives, is the response streamed by BigPipe (or any other module that sends a response implementing this interface).
znerol’s picture

I really dislike the convoluted logic here. Also I fear that the fake response wrapping the real response is prone to cause problems in the future.

Did you try to just implement the terminate event in BigPipe and store the response from there? On subsequent requests the internal page cache module will happily deliver the stored response. For a first iteration I suggest to just copy over the relevant bits from the page cache middleware.

wim leers’s picture

I really dislike the convoluted logic here.

Can you describe which part you find convoluted?

Also I fear that the fake response wrapping the real response is prone to cause problems in the future.

Which fake response? There is no fake response. There is a response that is streamed. And after it is streamed, terminate() is called (by index.php's inate($request, $response);), at which point the streamed response is converted to a non-streamed response for caching. That's the only way to cache a streamed response, because Symfony is not designed to deal with streamed responses, which is why even Symfony's own HttpCache doesn't support it: https://github.com/symfony/symfony/issues/17036. Hence Page Cache does not support it either. This is the only way to make it work.

Did you try to just implement the terminate event in BigPipe and store the response from there?

Yes, that's how it was implemented initially. But that ties BigPipe to Page Cache. Which has two severe flaws:

  1. Other modules doing streaming would have to duplicate this logic, because the logic actually belongs in Page Cache.
  2. It then wouldn't work for alternative/modified Page Cache implementations.

For a first iteration I suggest to just copy over the relevant bits from the page cache middleware.

Again, that was the first iteration. See the patch at #2657684-9: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits, specifically the \Drupal::service('cache.render')->set($cid, $response, $expire, $tags); bit.

wim leers’s picture

To be clear, I really dislike that this patch even needs to exist. I really dislike these changes.

But … they're necessary, because Symfony is completely oblivious wrt streamed responses. Much of the HttpFoundation infrastructure simply doesn't work.

By association, that means you and I both really dislike Symfony. Dislike Symfony, not this patch. This patch is working around Symfony limitations. There's a whole bunch of issues wrt chunked/streamed responses in the Symfony issue queue, and they're all answered with essentially "won't fix".

fabianx’s picture

Also to answer the Varnish bit:

- The streamed response we store in page cache is the same as what we send to the client. There are no JS placeholders, just chunks of data we stream. That is very important.

Varnish has perfect support in Varnish 4 (now 2 years released) for streamed responses.

It will stream the BigPipe response concurrently to all 1..n clients requesting the same URL and afterwards deliver it from its cache.

So _everyone_ is getting a fast streamed response!

So Varnish already does what we want to do in PageCache.

Stream the response and then store it in the cache.

dawehner’s picture

@Wim Leers
Can you point to the upstream issue where you describe the pain points you have? Is there anything we can do about that? Can upstream help us? Please don't just complain but actually try to collaborate with Symfony. They are by orders of magnitude more responsive than on the average Drupal issue. They have opinions, much like we do have ours.

Does PSR-7's idea of using streams for the response and request body helps in your example?

znerol’s picture

Varnish has perfect support in Varnish 4 (now 2 years released) for streamed responses.

It will stream the BigPipe response concurrently to all 1..n clients requesting the same URL and afterwards deliver it from its cache.

That's exactly what i tried to express. But the issue summary suggests that we must modify the internal page cache in order to profit from streamed responses in Varnish which is nonsense. BigPipe is free to stream responses to Varnish right now, no matter whether there is a session open or not.

wim leers’s picture

#23: I'm not just complaining, I'm observing. All of the Symfony issues asking for better support for streamed/chunked responses have been closed with a negative response. And that is fine! It's absolutely extremely difficult to add support for that. It would be guaranteed to be a massive BC break. So I'm not at all frustrated with them not responding or responding negatively: their responses make sense. But it does mean we have to work a bit outside of their designed architecture, because their architecture is only designed for non-streamed responses. That's all :)

PSR-7 doesn't help. The problem is Symfony's HttpKernel workflow. Specifically, response event subscribers. Response event subscribers (also known as "response filters") are able to add/remove headers, as well as modify the response body. In case of a streamed response, it's $response->send() that will actually generate the response body. But when $response->send() is called, there are at least two huge problems:

  1. Page Cache can at best see a partial response body, at worst an empty response body. So, Stack middlewares like caches cannot function properly for streamed responses
  2. At that point, response event subscribers have already run! So they're not able to modify the actual streamed response.

The first point we can solve, because in index.php (and any other Symfony front controller), you have this:

$response = $kernel->handle($request);
$response->send();

$kernel->terminate($request, $response);

The KernelEvents::TERMINATE event is dispatched. This is how PageCache (and other caches) can be made compatible with streamed responses. At least for those streamed responses that are able to convert themselves to a non-streamed response. Which of course can be done for streamed HTML responses. Which is exactly what this patch (and its parent #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits) does.

#24: I don't see where the issue summary says that. This issue is not at all about modifying Page Cache for the sake of Varnish. This issue is about making Page Cache be able to cache streamed responses. Because the vast majority of users don't have Varnish. If we don't make this change, then a streamed response is never cached by Page Cache, which means subsequent requests cannot be responded to by Page Cache.
This issue is about letting subsequent requests be responded to by Page Cache, nothing else.

dawehner’s picture

Thank you @Wim Leers for explaining that kind of problem.

wim leers’s picture

Very glad that it helped! :)

znerol’s picture

I did read through the following material today:

  1. $ (cd src/symfony && git grep -il streamed | grep -vi test)
    src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
    src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
    src/Symfony/Component/HttpFoundation/CHANGELOG.md
    src/Symfony/Component/HttpFoundation/Response.php
    src/Symfony/Component/HttpFoundation/StreamedResponse.php
    src/Symfony/Component/HttpKernel/CHANGELOG.md
    src/Symfony/Component/HttpKernel/Client.php
    src/Symfony/Component/HttpKernel/EventListener/SaveSessionListener.php
    src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php
    src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php
    src/Symfony/Component/Templating/DelegatingEngine.php
    src/Symfony/Component/Templating/StreamingEngineInterface.php
    
  2. $ (cd src/drupal && git grep -il streamed | grep -vi test)
    core/modules/big_pipe/js/big_pipe.js
    core/modules/big_pipe/src/Render/BigPipeResponse.php
    core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
    core/modules/file/file.module
    core/modules/page_cache/src/StackMiddleware/PageCache.php
    
  3. $ (cd src/drupal && find core/modules/big_pipe -name '*.php' | grep -vi test)
    core/modules/big_pipe/src/Controller/BigPipeController.php
    core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
    core/modules/big_pipe/src/EventSubscriber/NoBigPipeRouteAlterSubscriber.php
    core/modules/big_pipe/src/Render/BigPipe.php
    core/modules/big_pipe/src/Render/BigPipeInterface.php
    core/modules/big_pipe/src/Render/BigPipeMarkup.php
    core/modules/big_pipe/src/Render/BigPipeResponse.php
    core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php
    core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
    
  4. #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits
  5. #2577631: Allow HtmlResponse to use a flexible emitter
  6. https://en.wikipedia.org/wiki/Chunked_transfer_encoding
  7. https://info.varnish-software.com/blog/http-streaming-varnish
  8. https://www.varnish-cache.org/docs/4.1/reference/vcl.html?highlight=do_s...

My observations/thoughts so far:

  1. It looks like Symfony treats streamed responses slightly different than Drupal (might be a bug over here). Especially send() is invoked from within a response listener and thus the callback is executed in the context of a normal request/response cycle. In a Symfony application, the streamed response is already built/sent to the browser when $kernel->handle() returns. Hence a middleware wrapping the kernel could theoretically access that data. However, subclassing StreamedResponse is deprecated in Symfony 3 since that class will be final in Symfony 4.
  2. FrameworkBundle/Controller/Controller.php shows an example on how the callback-approach is supposed to work. I think BigPipe could do something similar.
  3. BigPipe is marked as experimental and I think nobody will deny that the code still has some rough edges. It looks like the emitter idea could help cleaning it up. How much cleaning up is required until we can drop the experimental label from BigPipe? If cleaning up results in less changes outside of BigPipe, then let's do that first.
  4. I do not quite understand how the problem that cacheability metadata cannot be communicated properly to external proxies stated in #2657684-25: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits is resolved in the current patch. I understand that this is only tangibly related to the problem here. Maybe this is solvable with trailers as described in RFC 7230 4.1.2. Not sure whether varnish parses trailers though.
  5. Generally I think that we should first try to store big pipe responses in the dynamic page cache. IMHO this is a natural fit (and that's what is already implemented for big pipe responses with a session AFAIK). That approach will work as well for the advanced use-case described in the issue summary.
fabianx’s picture

Status: Needs review » Closed (won't fix)

I have decided to think about this again and I think it is best to won't fix this, but just solve it for BigPipe specifically:

#2795391-7: Move most of PageCache::fetch() into a storeResponse() helper that can be independently invoked from a subclass

allows BigPipe to properly subclass it and override the method.

I think a sub class is best to start with, because per Wim logic (TM) BigPipe is just one use case of a cached response and we need at least 2, to consider a generic solution for core - usually.

Thanks for all your research, #28 and interesting that there are trailers. I did not know that.

wim leers’s picture

#28

  1. Thanks for pointing us to trailers in the RFC! I've always wondered about that, but had never heard of it before. https://www.varnish-cache.org/trac/wiki/HTTPFeatures suggests Varnish supports it. But https://github.com/whatwg/fetch/issues/34 shows that it's a small miracle that Varnish supports it. Many CDNs support it according to the feedback in that GitHub issue. See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer and https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/TE for more details about how it's supposed to be used.
    However, Symfony's HTTP Foundation has precisely zero support for trailers. :( And, nobody has ever even mentioned let alone requested it: https://github.com/symfony/symfony/search?utf8=%E2%9C%93&q=trailers.
    A) Trailers + CacheableStreamedResponseInterface
    I think @znerol is entirely right that this is the correct solution from a HTTP spec perspective. If we'd use this, then we wouldn't need CacheableStreamedResponseInterface like #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits and this issue propose!

    So I think it's clear that we should not modify PageCache: anything it does must rely on the contents of the HTTP request & response only, otherwise it ends up doing things that no other reverse proxy can do. The whole point of PageCache is that it's just a poor man's Varnish — i.e. that it doesn't have any special knowledge.

    B) Streamed responses + cache tags
    @znerol is also entirely right in his implicit concern/claim that it's silly that we'd still support Drupal <> PageCache <> reverse proxy but not Drupal <> reverse proxy. As #2657684-28: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits explained, the former would still work because request 1 streams it and stores it in Page Cache, request 2 then has a page cache hit, and request 3 is a reverse proxy hit. It is thanks to Page Cache that we can get the streamed response to be cached by the reverse proxy with all its cache tags. In the latter case, it's impossible to get all cache tags for the streamed response to be known to the reverse proxy (unless it supported trailers).

    This also means I disagree with @Fabianx' comment at #22: So Varnish already does what we want to do in PageCache. Stream the response and then store it in the cache. — this is misleading, because it's omitting the fact that Varnish will not have cache tags for the streamed placeholder fragments, but Page Cache will. Which means Varnish will not be able to invalidate when possible, but Page Cache will.

    Once #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits lands, you won't be able to use BigPipe + CDN without also using Page Cache.

    Conclusion: our options for proceeding
    For A), I'd vote to remove that interface altogether, and just let the BigPipe module write a response to the Page Cache directly, just like @znerol asked in #19, and with which I disagreed in #20. We shouldn't embed logic that's not using data in a HTTP request/response in Page Cache, because that makes it no longer be an actual HTTP middleware.

    For B), there's no clear choice.

    Our options for proceeding:

    1. Proceed with the old plan, and just document that you MUST use Page Cache when using a reverse proxy that uses tag-based invalidation. This seems unwise.
    2. only let BigPipe stream responses for anonymous users IF $settings['reverse_proxy'] !== TRUE OR $settings['reverse_proxy'] === TRUE && module_exists('page_cache')… i.e. option 1, but enforced via code instead of documentation. This means it's still fairly difficult to support, because people forget to set that in settings.php and it introduces difficult-to-debug failure modes for BigPipe when used with CDNs
    3. only let BigPipe stream responses for anonymous users IF the response would not be cacheable by Page Cache anyway Impossible, because in many cases, it depends on a response policy to determine whether something can be stored in Page Cache, i.e. we need the response to be generated, i.e. it's too late to choose to use BigPipe to deliver the HTML, because all the work has already been done
    4. move the entire "BigPipe streaming + PageCache" logic to a contrib module: big_pipe_sessionless or something like that… but this would require significant extending/overriding of "core BigPipe". But at least supporting "core BigPipe", which provides 95% of the benefits, could be absolutely rock-solid (as it has been until now — effectively no bug reports).
  2. Storing in Dynamic Page Cache is what we already do, both for anonymous and authenticated responses. The point of this issue (and #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits) is to make it possible to have anonymous responses be served with a single flush upon a cache miss, but to stream them instead. Subsequent requests as the anonymous user would then again benefit from the Page Cache. What you're suggesting is that we never cache those responses in Page Cache, which is correct, but leads to worse performance overall: the first response is faster, but all responses after that are slower. So that would then clearly miss the point :)
wim leers’s picture