Updated: Comment #0
Problem/Motivation
Note: page caching happens *outside* of the Render Cache API. (Specifically, DefaultHtmlFragmentRenderer::render()
must call drupal_render_collect_cache_tags()
before it can call drupal_render()
on $page_array['page_top']
and $page_array['page_bottom']
. Because once drupal_render()
is called, only a string remains. Yet the page cache must also know the cache tags for its contents. Therefore it must call drupal_render_collect_cache_tags()
before drupal_render()
.
When:
- page caching is enabled
- a
#pre_render
callback that sets a cache tag ($build['#cache']['tags']['thing'][] = '<id>';
) - then the cache tags set by that
#pre_render
callback will not bubble up to the page cache entry.
The cause is simple: the #pre_render
callbacks simply haven't run yet. But for good reason: see the note above.
This blocks #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags!
Proposed resolution
- Let
drupal_render_collect_cache_tags()
run the#pre_render
callbacks on the copy of the render array it receives. This ensures cache tags added by#pre_render
callbacks are available. - In
drupal_render()
, after executing all#pre_render
callbacks, dounset($elements['#pre_render'])
. This prevents running the#pre_render
callbacks twice: once indrupal_render()
, once indrupal_render_collect_cache_tags()
.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Comment | File | Size | Author |
---|---|---|---|
#29 | cache_tags_pre_render_page_cache-2215719-29_test_only-FAIL.patch | 2.43 KB | Wim Leers |
#29 | interdiff.txt | 3.14 KB | Wim Leers |
#29 | cache_tags_pre_render_page_cache-2215719-29.patch | 12.01 KB | Wim Leers |
Comments
Comment #1
Wim LeersComment #2
moshe weitzman CreditAttribution: moshe weitzman commentedDoes this make #pre_render callbacks less useful, since they can't influence $element anymore (because they operate on a copy)?
Comment #3
jessebeach CreditAttribution: jessebeach commentedI think it means that we're potentially running some subset of
pre_render
functions twice.It's given me an idea though.
Comment #4
jessebeach CreditAttribution: jessebeach commentedWhat if we actually just process all the pre_renders all at once #2099131-77: Use #pre_render pattern for entity render caching?
Comment #5
Wim Leers#2: No,
#pre_render
callbacks run just like before. Only when there are callbacks that haven't run yet whendrupal_render_collect_cache_tags()
is being executed, we do the "apply#pre_render
callbacks on a copy of the render array" thing, only for the purpose of collecting cache tags. The#pre_render
callbacks will still run "properly" whendrupal_render()
gets called on them.#4: No, this is completely independent of that issue. This only affects page caching, or more generally: anything that needs to be able to retrieve the cache tags of a render array without using the render system's built in caching ("render caching"), of which page caching is the only example in Drupal core.
Comment #6
znerol CreditAttribution: znerol commentedI cannot help but wonder whether perhaps the cache-tag architecture needs an overhaul? Maybe it would make sense when
drupal_render()
would assign the cache-tags it encounters to a (request-scope?) service. Then the page cache service just may collect them from there?Comment #7
Wim Leers#6: That wouldn't work: Multiple subsets of a render array may mark themselves as cacheable (e.g. entities, blocks). Therefor,
drupal_render()
should be able to store them in the render cache. If all cache tags are collected in a single (global, because request-scope) set of cache tags, then that's effectively impossible.I'm the first to agree that we need to rethink Drupal's entire approach to rendering, but we can't do that in Drupal 8 anymore. We must ship D8, not rearchitect it. I'm afraid we're down to ugly work-arounds now, at least in this area.
Comment #8
Wim Leers@all: please look at the patch, it comes with new test coverage. Remove any single line of the code changes, and the test would fail. I'm really changing as little as possible.
Comment #9
znerol CreditAttribution: znerol commented@Wim Leers, there is no need to alter the way
drupal_render()
bubbles up the cache tags for its own usage. I think it would be enough when it additionally would pass them to a service where page cache could fetch them from.Comment #10
Wim Leers#9: That could work in theory. However, that would create a new assumption: that everything rendered during the current request is going to be served as part of the current page.
So use cases like:
would break.I'd rather not introduce new assumptions or globals unless we have no other choice.
Comment #11
catchThis has come up before in relation to #attached - needing a way for the calling function to get those back out of drupal_render().
It's not a fantastic API, but
drupal_render(&$element, &$attached = NULL, &$cache_tags = NULL);
We could then fill those values in if they're passed, no API change.
Or add them to a key on $elements since that's passed by ref anyway?
Comment #12
Wim Leers#11: Wouldn't it then be better to just return
?
Comment #13
catch@Wim probably. I'm slightly wary that despite us discouraging direct use of drupal_render(), we still have 274 calls in core.
Comment #14
znerol CreditAttribution: znerol commented#14 What about returning an object with a __toString() magic method? Then we could retain backward compatibility and still it would be possible to access rendering metadata:
Comment #15
Wim Leers#14: I like that!
catch, thoughts?
Comment #16
znerol CreditAttribution: znerol commentedPerhaps we even could reuse (or extend)
\Drupal\Core\Page\HtmlFragment
? It already implementsCacheableInterface
.Comment #17
catchHave to be careful with __toString() because you can't throw exceptions, but in this case we'll have the string already there and just return it, so yes that seems fine :)
Comment #18
jessebeach CreditAttribution: jessebeach commentedTurning the return of
drupal_render
into an object will give me additional options to explore in #2099131: Use #pre_render pattern for entity render caching as well.Comment #19
jessebeach CreditAttribution: jessebeach commentedCool, I think we have a direction and znerol has a mandate to explore their suggestion. Let's mark this as Needs Work for now.
Comment #20
msonnabaum CreditAttribution: msonnabaum commentedI think something like #14 is the way to go. I don't see this being workable unless we have some object to encapsulate it.
Comment #21
znerol CreditAttribution: znerol commentedI had a quick stab on #14 but regrettably
__toString
does not help us out of this. The problem is that calling code does all sorts of things with the result ofdrupal_render
. In some cases the result is straightly encoded into JSON and ends up as object instead of as a string there. That leads to leads to all sorts of UI-regressions, including broken contextual links etc.One solution which might work is extracting the inners of
drupal_render
into another function (saydrupal_render_to_fragment
returns aHTMLFragment
- or even better into a method on a helper class). Then use something like this for backward compatibility:However that does not yet really solve the original issue yet. We still have the problem, that at the moment there is no sane way to pass tags collected in
HtmlViewSubscriber::onHtmlPage
todrupal_page_set_cache
. I wonder whether it would be justifiable to get rid ofFinishResponseSubscriber::onRespond
and just move that over toHtmlViewSubscriber::onHtmlPage
?Though if we go that way, some content types will not be cached anymore. Especially Ajax responses handled by
ViewSubscriber
. However the lack of cache tag handling over there indicates that we need to address this anyway in order to make #2124957: Replace 'content' cache tag with 'rendered' and use it sparingly happen. Otherwise we might end up with stale cached Ajax responses delivered through the page cache.Given that this issue blocks #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags and there is no obvious easy way out, I think that #1 should go in.
Comment #22
Wim LeersI discussed this with catch in IRC.
catch is more favorable to fix it in the code responsible for building pages, roughly like the third paragraph in #21 says. The simple solution is to let
drupal_render()
always collect cache tags and store it in$elements['#cache']['tags']
. The caller ofdrupal_render()
can then get at those cache tags easily, because$elements
is updated by reference bydrupal_render()
.So far the simple theory. In practice, it's quite a bit more complicated because of the incomprehensible mess that
Html(Fragment|Page|ViewSubscriber|ControllerBase|PageController)
is.In doing so, I also found that
template_preprocess_html()
was duplicating the work already being done inDefaultHtmlFragmentRenderer::preparePage()
, so I removed that.Comment #23
catchThis looks a lot better to me, thanks!
Comment #26
Wim LeersNow the patch should be green.
Comment #29
Wim LeersGreenification.
Comment #30
msonnabaum CreditAttribution: msonnabaum commentedLooks quite reasonable to me.
Comment #32
Wim LeersBack to RTBC — this was the patch that was expected to fail.
Comment #33
Fabianx CreditAttribution: Fabianx commentedThere is also the issue the preprocess and templates can't set cache tags or attach assets but especially preprocess might render referenced data ...
Not sure this belongs here, but its an issue for sure.
Comment #34
Wim Leers#33: that's definitely unrelated.
Comment #35
catchCommitted/pushed to 8.x, thanks!
Comment #36
Wim LeersHURRAY! :)
This unblocks #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags :)