Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
In https://www.drupal.org/node/2222293 it was stated that the replacement for DRUPAL_CACHE_NONE was to set max_age = 0.
This does not work in a cache bubbling enabled caching world.
Proposed resolution
- Introduce
#cache['max-age']
- Bubble
max-age
just like we bubble cache tags - Ensure render arrays with
max-age === 0
aren't render cached
Remaining tasks
Reviews.
User interface changes
None.
API changes
API addition for Render API: #cache['max-age']
Comment | File | Size | Author |
---|---|---|---|
#31 | cache_max_age-2443073-28.patch | 24.69 KB | joshtaylor |
#28 | cache_max_age-2443073-24.patch | 24.85 KB | joshtaylor |
Comments
Comment #1
Fabianx CreditAttribution: Fabianx commentedComment #2
effulgentsia CreditAttribution: effulgentsia commentedWhy not? Because render arrays don't have a
#cache['max_age']
support? Should they? In which case, we could bubble that up, right (where merge = min())? In which case, would setting that to 0 be preferable to an 'uncacheable' context?Comment #3
Fabianx CreditAttribution: Fabianx commented#2: Oh even better :).
Yes, I have been of the opinion that max-age should be bubbling and it sounds like a great way to make that happen automatically.
I would suggest to also implement downstream-ttl at the same time, which is an Akamai concept, but very interesting - especially because it comes from their ESI spec:
Lets start with an example, lets say we have a file short-time.jpg, which is exchanged every 10 min.
Now this file sets a header of: Cache-Control: public, max-age=600
Now this means however that the browser is able to cache this for 600 seconds as well, so in the worst case, the file was cached by the browser at a time when it was just about to expire from Akamai.
So then the browser has it for 600+599 seconds == 1199 seconds, which means it gets the new file 599 seconds too late.
Of course you can half now the time to max-age = 300 and it will mostly be correct, but in the best case be just 300 seconds.
To solve this problem Akamai does a trick (which can be done in Varnish, too):
- It sets the max-age to the expiration time, so in our example above the browser would get a max-age of:
max-age=1
And then after it expires, the new version with e.g. max-age=596
Thus the caches are synchronized correctly.
To make it possible to cache, long.jpg, which can be cached for a year or two that doesn't matter, you can send a downstream-ttl header, like:
Edge-Control: downstream-ttl=6000000000
which means that the max-age for the browser is exactly put to this number and we end up with the old case, where the max time is doubled the amount of time.
Hence while we can implement max-age first in a very simple way, I think what we ultimately want is not only the set max-age, but also the current 'age' (or adjusting max-age), so that we can calculate the correct real TTL.
But +1 to bubbling max-age and having it in #cache, render_cache in D7 already supports max-age and downstream-ttl as a cache properties, so definitely part of my todo list for D8 as well :).
Comment #4
Wim LeersOk, so let's do
#cache['max-age']
, per #2 & #3.This is a first, rough iteration. I won't continue until I get some initial feedback whether we indeed want to continue down this road.
Comment #6
Wim LeersFailures don't matter, this needs a review.
Comment #7
larowlanCode makes sense to me. Can we get an issue summary update? Are we tackling downstream-ttl here too?
The constructor is starting to get hairy here, and given all arguments have default values, a builder-pattern might be of merit for DX here
This is the first argument in the constructor that isn't type-hinted. I think we should be doing some validation of the incoming value here and throwing an Exception if something unexpected is encountered. We're doing min() comparisons with the value at a later date, so before we get that far we should be ensuring the incoming value is sane
:)
unrelated?
Same story here re validation?
Comment #8
BerdirThere's an existing issue about being able to prevent caching by setting a flag, sounds like we can close that as a duplicate of this... doing so now: #2344243: Support a kill flag in $render_array[#cache']
Comment #9
Wim Leers#7: thanks for the general +1 and the actual review! Now rerolling.
#8: cool — thanks!
Now rerolling to be more than just a quick POC.
Comment #10
Wim LeersFirst: a straight reroll with the conflict caused by #2445761: Add a X-Drupal-Cache-Contexts header to aid in debugging and testing fixed.
Comment #12
Wim Leers#7
FilterProcessResult
.::createFromRenderArray()
helper.But writing the above actually convinced me of your point! :D
The thing is: you want
BubbleableMetadata
to support chained calls to set various things, but…FilterProcessResult
already has this! So if we: A) move all the get/set/add methods fromFilterProcessResult
toBubbleableMetadata
and B) makeFilterProcessResult
extendBubbleableMetadata
… then we have what you wanted. And we even end up with less code overall! And without an API change! (With the exception of removingFilterProcessResult::getBubbleableMetadata()
, but that's something that affects the .1%, the rest of the API affects the 99.9%.)string[]
, notarray
. But you're right that that at least gives a certain level of assurance.In the next reroll, I'm going to change the constructor of
BubbleableMetadata
, so that we're forced to use the "builder pattern" everywhere.Comment #13
Wim LeersTests still needed, but besides that, this should be mostly done.
Comment #15
Wim LeersThis is the right interdiff for #13.
Comment #17
Wim LeersComment #18
Wim LeersComment #19
Wim LeersUpdated IS to match #2 + #3.
Comment #20
Wim LeersComment #21
Fabianx CreditAttribution: Fabianx commentedI opened #2449749: Add #cache['downstream-ttl'] to force expiration after a certain time and fix #cache['max-age'] logic by adding #cache['age'] as a follow-up to discuss the scope of cache hierarchy's and ttls, so this one can go in as the easy thing.
Comment #22
Fabianx CreditAttribution: Fabianx commentedI reviewed this again and this is RTBC!
Nice work! Could use a beta evaluation and needs a change record ...
Comment #23
larowlan+1 RTBC
Comment #24
joshtaylor CreditAttribution: joshtaylor commentedThis was broken with a recent core update, reroll attached.
Comment #25
joshtaylor CreditAttribution: joshtaylor commentedComment #26
Fabianx CreditAttribution: Fabianx commentedBack to RTBC, thanks for the re-roll.
Comment #27
webchickBoink!
Comment #28
joshtaylor CreditAttribution: joshtaylor commentedReroll due to #2381217: Views should set cache tags on its render arrays, and bubble the output's cache tags to the cache items written to the Views output cache.
Comment #29
joshtaylor CreditAttribution: joshtaylor commentedComment #31
joshtaylor CreditAttribution: joshtaylor commentedWrong patch, sorry.
Comment #32
Fabianx CreditAttribution: Fabianx commentedBack to RTBC - if tests still pass.
Comment #33
alexpottI wonder if it is just quicker to do the array_filter all the time rather then searching for Cache::PERMANENT?
Comment #34
Wim LeersYou're right when dealing with very large sets of max-ages (100):
in_array()
: http://3v4l.org/kFF5V/perf#tabsBut not when dealing with smaller ones (10) — and we almost always are handling only 2 values.
in_array()
: http://3v4l.org/UHAfY/perf#tabs ()(But the differences are very small.)
Comment #35
Wim LeersThat was actually a bad example. This comparison is better. 10000 times running a set of typically encountered max-age values, in the different (typical) combinations, to exercise all code paths of this function. That's a much fairer comparison.
in_array()
: http://3v4l.org/6pKmr/perf#tabs (137/141/128/115 ms user time on PHP 5.4/5.5/5.6/7)in_array()
: http://3v4l.org/0ptco/perf#tabs (144/168/151/130 ms user time on PHP 5.4/5.5/5.6/7)And that apparently leads to the same conclusion. So I think no changes are necessary.
Comment #36
Wim Leershttps://www.drupal.org/node/2451555
Comment #37
catchHmm this is micro-optimization but I wonder if a straight foreach() would be quicker.
Also it's not clear from a quick scan how this affects:
1. Internal page cache
2. Max age header for external caches.
I think it does not affect the internal page cache, but it does affect external caches.
Comment #38
Wim Leers#37
We do bubble up the max-age to the page-level header:
… but interestingly, it doesn't have any effect right now! Because we still conflate "page cache settings" with "internal page cache" (i.e. simple built-in reverse proxy for anon users). Consequently, in
FinishResponseSubscriber::onRespond()
, we do this:So even with the internal page cache turned on, the "page cache maximum age" setting (
cache.page.max_age
) at/admin/config/development/performance
trumps whatever max-age was bubbled.But… I think that belongs in a separate issue.
This issue is about marking it possible to mark parts of the page uncacheable, and propagating that uncacheability. The patch does that just fine. Making sure it also bubbles to the response level as expected (it is bubbled to response, but overwritten by the above) can be a separate issue, because it also relates to the internal page cache. Doing that separately would be just like we did e.g. #2445761: Add a X-Drupal-Cache-Contexts header to aid in debugging and testing separately.
Comment #39
catchThis was what I meant in terms of the comparison, but also not anything in it: http://3v4l.org/EYE5S/perf#tabs
The internal cache overriding is consistent with what we do now so yes separate issue if we decide to change the behaviour - I'm not sure we should anyway.
Committed/pushed to 8.0.x, thanks!
Comment #41
Wim LeersAwesome, now #2099137: Entity/field access and node grants not taken into account with core cache contexts is unblocked! :) And we now have a much more sensible Render API, which now finally is starting to map to HTTP closely :)
Published the CR.
Comment #42
Wim LeersOops: #2453891: Renderer::getCacheableRenderArray() does not include max-age.