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=0on 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
- 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:
- a new
CacheableStreamedResponseInterface - the
PageCachemust implementTerminableInterfaceso that it can read streamed responses after they have finished streaming, so it can then still cache them
Remaining tasks
Comprehensive test coverage.
User interface changes
None.
API changes
- Addition:
CacheableStreamedResponseInterface(needs CR) - Addition: Page Cache supports streamed responses (because it now implements
TerminableInterface)
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #12 | interdiff-11-12.txt | 578 bytes | effulgentsia |
| #12 | page_cache_streamed_responses-2795209-12.patch | 14.8 KB | effulgentsia |
| #11 | interdiff-9-11.txt | 456 bytes | effulgentsia |
| #11 | page_cache_streamed_responses-2795209-11.patch | 14.8 KB | effulgentsia |
| #9 | page_cache_streamed_responses-2795209-9.patch | 14.76 KB | wim leers |
Comments
Comment #2
wim leersComment #3
fabianx commentedRTBC - Looks great to me!
Comment #4
xjmI'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.
Comment #5
xjmIs the reason for marking this interface internal that:
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.
IIRC API.d.o does not pick up this syntax, but OTOH I think it also doesn't parse
@seein inline comments anyway.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
$responsebeing 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.)
Comment #6
wim leersPer #2657684-92: 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, agreed that this should be done in 8.3 at this point.
Comment #7
effulgentsia commentedRe #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 onKernelEvents::RESPONSEhaving 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@apitag to it. If we want to leave room to change that, then let's keep it@internaland 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().
Comment #8
effulgentsia commentedSetting 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:
The trait isn't in this patch, so this line needs to be removed.
Comment #9
wim leersThis. Added that additional documentation.
Addressed everything.
Comment #10
effulgentsia commentedAdding review credit to @Fabianx and @xjm.
Comment #11
effulgentsia commentedI 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.
Comment #12
effulgentsia commentedOne more trivial fix for a Coder complaint.
Comment #13
effulgentsia commentedI'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:
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,
CacheableStreamedResponseInterfacecould also be moved to the BigPipe namespace for 8.2.Comment #14
effulgentsia commentedNote, however, that that would not be compatible with a contrib/custom module that swaps out
http_middleware.page_cachefor 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.Comment #15
xjmDon'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!
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.)
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.
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.
Comment #16
wim leersI 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 :)
Comment #17
znerol commentedTrying to understand the problem and the implications. Some random notes:
StreamedResponsecould 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.readfileexample in the original PR. I wouldn't want to store that in the internal page cacheAlso:
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?Comment #18
wim leersStreamedResponse. That class is pretty much useless.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.page_cacheis installed, then after the response is streamed, i.e. whenterminate()is called, Page Cache ends up storing a non-streamed response based on the streamed response. Whenpage_cacheis 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).Comment #19
znerol commentedI 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.
Comment #20
wim leersCan you describe which part you find convoluted?
Which fake response? There is no fake response. There is a response that is streamed. And after it is streamed,
terminate()is called (byindex.php'sinate($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 ownHttpCachedoesn'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.Yes, that's how it was implemented initially. But that ties BigPipe to Page Cache. Which has two severe flaws:
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.Comment #21
wim leersTo 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
HttpFoundationinfrastructure 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".
Comment #22
fabianx commentedAlso 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.
Comment #23
dawehner@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?
Comment #24
znerol commentedThat'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.
Comment #25
wim leers#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
HttpKernelworkflow. 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:The first point we can solve, because in
index.php(and any other Symfony front controller), you have this:The
KernelEvents::TERMINATEevent 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.
Comment #26
dawehnerThank you @Wim Leers for explaining that kind of problem.
Comment #27
wim leersVery glad that it helped! :)
Comment #28
znerol commentedI did read through the following material today:
My observations/thoughts so far:
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, subclassingStreamedResponseis deprecated in Symfony 3 since that class will be final in Symfony 4.FrameworkBundle/Controller/Controller.phpshows an example on how the callback-approach is supposed to work. I think BigPipe could do something similar.Comment #29
fabianx commentedI 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.
Comment #30
wim leersComment #31
wim leers#28
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.
CacheableStreamedResponseInterfaceCacheableStreamedResponseInterfacelike #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 ofPageCacheis that it's just a poor man's Varnish — i.e. that it doesn't have any special knowledge.Drupal <> PageCache <> reverse proxybut notDrupal <> 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: — 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.
For B), there's no clear choice.
Our options for proceeding:
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.$settings['reverse_proxy'] !== TRUEOR$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 insettings.phpand it introduces difficult-to-debug failure modes for BigPipe when used with CDNsonly let BigPipe stream responses for anonymous users IF the response would not be cacheable by Page Cache anywayImpossible, 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 donebig_pipe_sessionlessor 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).Comment #32
wim leersCross-posted #28 (znerol) + #31 to #2657684-101: 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 to make the discussion easier to follow.
Thanks again @znerol!