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:

  1. page caching is enabled
  2. a #pre_render callback that sets a cache tag ($build['#cache']['tags']['thing'][] = '<id>';)
  3. 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

  1. 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.
  2. In drupal_render(), after executing all #pre_render callbacks, do unset($elements['#pre_render']). This prevents running the #pre_render callbacks twice: once in drupal_render(), once in drupal_render_collect_cache_tags().

Remaining tasks

None.

User interface changes

None.

API changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Status: Active » Needs review
Issue tags: +Spark, +sprint
FileSize
4.51 KB
moshe weitzman’s picture

Does this make #pre_render callbacks less useful, since they can't influence $element anymore (because they operate on a copy)?

jessebeach’s picture

Does this make #pre_render callbacks less useful, since they can't influence $element anymore (because they operate on a copy)?

I think it means that we're potentially running some subset of pre_render functions twice.

It's given me an idea though.

jessebeach’s picture

What if we actually just process all the pre_renders all at once #2099131-77: Use #pre_render pattern for entity render caching?

Wim Leers’s picture

#2: No, #pre_render callbacks run just like before. Only when there are callbacks that haven't run yet when drupal_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" when drupal_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.

znerol’s picture

we do the "apply #pre_render callbacks on a copy of the render array" thing, only for the purpose of collecting cache tags.

I 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?

Wim Leers’s picture

#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.

Wim Leers’s picture

@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.

znerol’s picture

@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.

Wim Leers’s picture

#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: rendering some things that aren't part of the current page, but are being rendered and cached for the next page load would break.

I'd rather not introduce new assumptions or globals unless we have no other choice.

catch’s picture

This 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?

Wim Leers’s picture

#11: Wouldn't it then be better to just return

array(
  '#markup' => 'the previous return value of drupal_render()',
  '#cache' => array('tags' => array(…)),
)

?

catch’s picture

@Wim probably. I'm slightly wary that despite us discouraging direct use of drupal_render(), we still have 274 calls in core.

znerol’s picture

#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:

// Old style:
print drupal_render($element);

// New style:
$result = drupal_render($element);
print $result->getMarkup();
var_dump($result->getTags());
var_dump($result->getAttached());
Wim Leers’s picture

#14: I like that!

catch, thoughts?

znerol’s picture

Perhaps we even could reuse (or extend) \Drupal\Core\Page\HtmlFragment? It already implements CacheableInterface.

catch’s picture

Have 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 :)

jessebeach’s picture

Turning 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.

jessebeach’s picture

Status: Needs review » Needs work

Cool, I think we have a direction and znerol has a mandate to explore their suggestion. Let's mark this as Needs Work for now.

msonnabaum’s picture

I think something like #14 is the way to go. I don't see this being workable unless we have some object to encapsulate it.

znerol’s picture

Status: Needs work » Needs review

I 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 of drupal_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 (say drupal_render_to_fragment returns a HTMLFragment - or even better into a method on a helper class). Then use something like this for backward compatibility:

function drupal_render(...) {
  return drupal_render_to_fragment(...)->getContent();
}

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 to drupal_page_set_cache. I wonder whether it would be justifiable to get rid of FinishResponseSubscriber::onRespond and just move that over to HtmlViewSubscriber::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.

Wim Leers’s picture

I 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 of drupal_render() can then get at those cache tags easily, because $elements is updated by reference by drupal_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 in DefaultHtmlFragmentRenderer::preparePage(), so I removed that.

catch’s picture

This looks a lot better to me, thanks!

The last submitted patch, 22: cache_tags_pre_render_page_cache-2215719-22.patch, failed testing.

Status: Needs review » Needs work
Wim Leers’s picture

Now the patch should be green.

  • Hunk 1 is just an improvement, to not unnecessarily collect cache tags.
  • Hunk 2 fixes all the test failures in #22.
  • Hunk 3 fixes the two exceptions in #22.

The last submitted patch, 26: cache_tags_pre_render_page_cache-2215719-26.patch, failed testing.

Status: Needs review » Needs work
Wim Leers’s picture

msonnabaum’s picture

Status: Needs review » Reviewed & tested by the community

Looks quite reasonable to me.

Status: Reviewed & tested by the community » Needs work
Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community

Back to RTBC — this was the patch that was expected to fail.

Fabianx’s picture

There 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.

Wim Leers’s picture

#33: that's definitely unrelated.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.x, thanks!

Wim Leers’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.