Problem/Motivation
This is step one of #2257695: [meta] Modernize the page cache.
In Drupal 7 it was necessary to enable internal page caching in order make external page caching work. In Drupal 8 this was reversed and now internal page caching is only possible when external caching is configured (i.e. a max-age value has been set). This interdependence is purely artificial. This is due to the fact that the code responsible for generating appropriate Cache-Control
for external caches originating in Pressflow 6 was just bolted onto the existing internal page cache implementation during the development of Drupal 7.
Proposed resolution
Extract the code responsible for generating Cache-Control
and related headers from drupal_serve_page_from_cache
into the FinishResponseSubscriber
. Ensure that the internal page cache exclusively operates on Symfony requests/responses and stop passing around the cache-object.
Remaining tasks
User interface changes
API changes
- Remove the
$cache
parameter fromdrupal_serve_page_from_cache()
drupal_page_get_cache()
returns a symfony response object instead of a cache-object- drupal_page_set_cache() returns nothing instead of a cache-object
Comment | File | Size | Author |
---|---|---|---|
#13 | 2257709-extract-cache-control-13.patch | 28.27 KB | znerol |
#9 | interdiff.txt | 9.31 KB | znerol |
#9 | 2257709-extract-cache-control-9.patch | 29.04 KB | znerol |
#5 | interdiff.txt | 4.72 KB | Wim Leers |
#5 | 2257709-extract-cache-control-5.patch | 33.34 KB | Wim Leers |
Comments
Comment #1
znerol CreditAttribution: znerol commentedComment #2
znerol CreditAttribution: znerol commentedRegrettably the first patch is unreadable around
drupal_serve_page_from_cache()
, therefore I rearranged the remaining code such that we get a better version. Also used the--histogram
algorithm, hope this is easier to read.Comment #3
Wim LeersAlso tagging with Performance and Fast By Default, because correct HTTP cache revalidation support is essential for good performance.
Great work, as usual :)
I found a few subtle bugs that I fixed (see below) and fixed a bunch of nitpicks. You can use HTTP expert Mark Notthingham's http://redbot.org (also comes as a CLI tool) to verify the correctness of HTTP header usage — it helped me verify that the two bugs I found were actually bugs, and that my fixes actually fixed it.
Oh my … this is *so* much clearer!
This means a 304 response will only be sent if both
If-Modified-Since
andIf-None-Modified
request headers are present. Either of them should be sufficient.Fixed.
This should also allow the
ETag
header to be sent.Fixed.
Comment #5
Wim LeersThe existing test coverage was wrong. It is not required for both
If-Modified-Since
andIf-None-Match
to be specified to trigger a 304 response. If both anETag
and aLast-Modified
response header are specified, then UAs SHOULD use both for HTTP revalidation, but it's not a requirement.Fixed the test coverage, should come back green now.
Comment #6
znerol CreditAttribution: znerol commentedIf we are changing the behavior anyway, I suggest to just use the Response::isNotModified() method. However, as a result the two assertions checking whether revalidation with "obsolete If-Modified-Since date" works will fail. In my opinion we could remove them which would basically mean reverting #327269: drupal_page_cache_header() should compare timestamp.
Comment #7
Crell CreditAttribution: Crell commentedMost of this is already possible directly in the Response object. We don't need to reimplement it.
In fact, here's the entire cache-handling code I had from a recent Silex project.
Symfony takes care of most of this for us...
The initial if() is not necessary, as getExpires() will always return a value. If there is no header the expiration date will always be in the past, so we know that it shouldn't be cached.
There's also no need to convert back down to ints. DateTime can compare against itself just fine.
I'm sure this isn't the issue adding this static, but WTF? This is a completely unnecessary hard dependency. When was that added?
This seems like something to push upstream to Symfony. (Not a blocker here, but a @todo for us.)
$response->setMaxAge().
Why is the timestamp a reasonable ETag? Wouldn't a sha1() of the content be more reasonable? It's the last time the *content* changed, not the last-modified header we're already messing with.
If there isn't already a single $response-> method to do this, we should submit that upstream, too. (I'd be surprised if there isn't.) The prepare() method will do some cache mangling anyway.
Ibid. Although I suppose this one we can leave in, as we have reasons... ;-)
Not introduced by this patch, I know, but I have to ask... why is toolbar module doing its own cache serving? That seems... not good.
Comment #8
sunThat code was invented in admin_menu module, and mostly copied as-is into core by others. I was surprised that it did end up in core almost identically — I always considered it as a gross hack, an acceptable experiment for a contrib module, but never intended that code to be taken over literally into Drupal core (because core is supposed to provide an architecture that makes such hacks unnecessary).
In short, I agree, it's not only bad, it's also a huge can of worms for unexpected bugs/regressions, because that code is (intentionally) stepping out of all regular pipelines. We can and we should do better in D8 now. Prolly a separate issue tho.
Comment #9
znerol CreditAttribution: znerol commented#7.1, #7.6: Let's keep the focus. Rolled changes affecting cache-revalidation from #3 and #5 and opened a followup which will address this separately: #2259489: Leverage symfony for HTTP revalidation
#7.2: Fixed.
#7.5, #7.7: The various response-methods operate on individual directives of the
Cache-Control
header. However, we need to replace the whole header in order to effectively remove any directives previously set on it.Re #7.9 and #8: As shown in #2177461: Refactor page caching into a service we can easily support that use case in a proper way with the right architecture (pluggable policy).
Also fixed a problem where controllers add their own
Cache-Control
headers. Those headers should only be replaced when page caching was cancelled (e.g. by a message).Comment #10
sunThis looks very good to me. Further improvements/adjustments should be performed in separate follow-up issues, I think.
Comment #11
Wim LeersRTBC+1.
Sorry for the derailing in #3 & #5 — I didn't realize you were just moving around existing code. I.e. my remarks were out of scope. My apologies!
Comment #12
Anonymous (not verified) CreditAttribution: Anonymous commentedi was pointed at this patch from here #2259823: Make cache->created be to the millisecond.
just wanted to +1 the RTBC, this looks good to go to me.
Comment #13
znerol CreditAttribution: znerol commentedReroll after #2257807: Remove "Forever" from the Page cache maximum age options
Comment #14
znerol CreditAttribution: znerol commentedComment #15
Crell CreditAttribution: Crell commentedI'm not sure it's really necessary to punt using the Symfony code to a follow up, but hey, progress is progress. Let's get this in and move on to #2259489: Leverage symfony for HTTP revalidation as quickly as possible.
Comment #16
Wim LeersAgreed; baby steps!
Comment #17
catchI see this removed, but I don't see it put back anywhere?
Comment #18
znerol CreditAttribution: znerol commentedWe're storing the whole
Response
in the cache (including status code, headers, everything).Comment #19
Crell CreditAttribution: Crell commentedQuestion answered.
Comment #20
catchYes that makes lots of sense and cleans things up a lot.
Committed/pushed to 8.x, thanks!
Comment #22
Wim LeersSo now on to the next step in #2257695: [meta] Modernize the page cache:
:)
Great work, znerol!