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
The internal page cache still cannot be swapped out easily.
Proposed resolution
Move the remaining procedural code from the following functions to the page cache stack middleware. Also move the code responsible for storing a page in the internal cache from FinishResponseSubscriber
to the new middleware.
Remaining tasks
User interface changes
None.
API changes
Removed functions:
- drupal_page_cache_get_cid()
- drupal_page_get_cache()
- drupal_page_set_cache()
- drupal_serve_page_from_cache()
- DrupalKernel::handlePageCache()
Removed DRUPAL_BOOTSTRAP_PAGE_CACHE
.
Beta phase evaluation
Issue category | Task |
---|---|
Issue priority | Major because of contrib page level caches like Boost and Authcache dependencies |
Disruption | Disruptive for contributed and custom modules because it will require an internal refactoring after commit |
Follow-up to #2257695: [meta] Modernize the page cache
Comment | File | Size | Author |
---|---|---|---|
#66 | interdiff.txt | 826 bytes | znerol |
#66 | 2348679-move-page-cache-to-stack-middleware-66.patch | 29.17 KB | znerol |
#64 | interdiff.txt | 850 bytes | znerol |
#64 | 2348679-move-page-cache-to-stack-middleware-64.patch | 29.15 KB | znerol |
#62 | 2348679-move-page-cache-to-stack-middleware-62.patch | 29.2 KB | Wim Leers |
Comments
Comment #1
znerol CreditAttribution: znerol commentedThis will fail due to the fact that
session_name()
is not initialized at the point where theNoSessionOpen
request policy is instantiated. This is resolvable by #2347877: Move DrupalKernel::initializeCookieGlobals() into a SessionConfiguration service.Comment #3
tim.plunkettMiddlewar sounds pretty cool :)
Comment #6
znerol CreditAttribution: znerol commentedReroll.
Comment #8
Wim LeersThis was blocked on #2347877: Move DrupalKernel::initializeCookieGlobals() into a SessionConfiguration service, which has been committed yesterday!
Comment #9
BerdirTest fails look like there's an issue with serializing non-serializable responses, like files, among other things.
I assume we somehow lost this part?
Comment #10
BerdirCan we move drupal_page_cache_get_cid() into this as well (it is not used anywhere else I think) or at least wrap it in a method that is easy to override?
Goal: Solve #2062463: allow page cache cid to be alterable by making it easy to subclass this and replace the service.
Same here, move this part into a helper method, like writeInternalCache() would make it easier to explore some ideas I had with having a quickly expiring edge cache + a long-living page cache that uses cache tags for invalidation.
Extending on #9, I think we are missing the use for those two interfaces?
Comment #11
znerol CreditAttribution: znerol commentedAgree, done.
Right, fixed.
What about extracting a storage class instead?
Comment #12
Wim LeersCan we get an interdiff? :)
Comment #14
znerol CreditAttribution: znerol commentedOh, sorry. Forgot to attach.
Comment #16
Wim LeersThis is a misnomer; we call the parts that make up a cache ID "cache keys", but this returns the "cache ID", not "cache keys".
I bet it's the fact that this can easily be looked at as a key/value store type thing that made you think of this name, that'd make perfect sense in that context? :)
Other than that, this patch looks awesome! I think we want some profiling here to ensure we don't make things slower? But IIRC that already happened somewhere else, before #2257695: [meta] Modernize the page cache got split up into many child issues? Since so much time has passed, we probably want to do new profiling anyway though?
Comment #17
dawehnerIt would be pretty slick if we could stop including the page cache middleware, if its not configured to be executed.
Comment #18
BerdirPossibly, but that would require a container rebuild when you change the setting.
That said, *if* we would that, we could drop the settings check there *and* the weird setting, because we would always check if enabled, so that might be quite interesting for performance. So maybe a follow-up to investigate that?
Comment #19
Wim LeersWow, that sounds fascinating!
dawehner++
Berdir++
Comment #20
znerol CreditAttribution: znerol commentedTook me some time to fix the remaining test fails... Will address the pending feedback (thanks!) in a subsequent patch.
Comment #21
znerol CreditAttribution: znerol commentedComment #22
znerol CreditAttribution: znerol commentedAdressing #10 and #16.
Comment #23
znerol CreditAttribution: znerol commentedI actually meant to pass through
$allow_invalid
.Comment #24
dawehnerReally great work!
@znerol
What do you think about the follow up to pass the configuration on compiler dump time?
Why do we not just call ->forward in places we call pass?
Is there a reason we don't inject it atm. already?
Comment #25
znerol CreditAttribution: znerol commentedRe #24:
forward()
is also called from withinfetch()
. Inliningpass()
will result in a situation where a subclass would be incapable of telling the reason for a call to kernelhandle()
.Comment #26
dawehnerOkay, that is fair!
One thing which changed now, is that we inject the configFactory, so even if you have set
Settings::get('page_cache_without_database'
the db is initialialized, additional to all other dependencies of the config manager. Not sure whether we should deal with it, given the idea of #17/#18
which would solve that problem again.
Comment #27
BerdirThat's a very good point. Maybe we should not inject the config factory but add a @todo on a follow-up for the dynamic service definition idea? Definitely needs to be profiled.
Comment #28
znerol CreditAttribution: znerol commentedAddresses #26, indeed very good point. Note that config is also accessed from within
fetch()
. Thus a follow-up in the spirit of #17 will likely need to convert the compression-setting into a container parameter.Comment #29
BerdirI think we have an open issue for that already somewhere, not quite sure, however.
Comment #30
Wim LeersLeaving at NR; just nitpicks for the fix of #16 :)
s/id/ID/
s/key/ID/
Comment #31
znerol CreditAttribution: znerol commentedAttempting to perform some benchmarks without database access. Baseline is 8.0.x
HEAD
with the mockcache module enabled and properly configured, database server turned off, PHP 5.6.4 with Zend OPcache v7.0.4-dev.$ ab -k -c1 -n1000 http://localhost:3000/
(see attachments for the full log):HEAD
patch (db turned off)
Turns out that the early exit in
HEAD
sidesteps the call to$kernel->terminate()
in the front controller. Since subscribers of the terminate event wish to access all kinds of stuff, the database connection is initiated there (and if the db is not available the request blows up in a weird way #2408435: Uncaught exception message is shown when exception is thrown during the terminate event).patch (db accessible)
The results above are with the db turned off and exceptions thrown all over the place, they are worse when the db is accessible:
patch (terminate removed)
When simply removing
$kernel->terminate()
from the front controller, the results are as follows:I think the tiny regression is acceptable given it is well within the margin of error (= standard deviation multiplied by 2).
Just how we can get rid of
$kernel->terminate()
for cached responses?Comment #32
Wim LeersDo we understand why there is a regression?
That's fair, but if it's consistently reproducible to have a lower throughput, then that argument would be less applicable.
Also note that the standard deviation is lower than that of HEAD, which is good. Can we also consistently reproduce that lower standard deviation? If we can, that'd be another reason in favor of the patch.
Comment #33
znerol CreditAttribution: znerol commentedCurrently
DrupalKernel
checks whether the kernel has been booted before forwardingterminate
to the http kernel. We simply could check whetherpreHandle()
was run instead.Comment #34
BerdirCan we add a comment here why we are doing this?
With that I think this is ready.
Will try to do something profiling on this as well.
Comment #36
dawehner+1 for the wrapping of \Drupal::config() for now.
@znerol
I think one approach we could indeed do is to use config to fill the compression setting, or lets be honest, this is not really something you need to configure through the UI IMHO.
Comment #37
znerol CreditAttribution: znerol commentedRe #32 found the reason: Now that everything moved to the middleware, the response policy is also instantiated when cached requests are delivered (~ +200 function calls).
Comment #38
znerol CreditAttribution: znerol commentedOh, and another fun thing: All middlewares are constructed as soon as the http kernel is instantiated. It follows that performance of the page cache depends on the dependencies of all middlewares - unless perhaps if those with heavy dependencies are constructed lazily.
Comment #39
znerol CreditAttribution: znerol commentedI've reviewed usage of the
needs_destruction
tag. In core it is only used forCacheCollector
and derivative classes (e.g. theme registry, alias whitelist) in order to update caches which accumulate entries over time. Therefore I think it is okay to simply fix the test here.Also addressed #34.
Comment #40
Wim Leers+1
#37: awesome!
#38: I'd hope #1973618: DIC: Lazy instantiation of service dependencies (ProxyManager for "proxy services") helps with that? :)
Comment #41
neclimdulre: #37, why was that not being called prior to this patch? Did we add the code using this service to improve performance characteristics in some way? I don't see a discussion of it in this thread.
Being as its sort of hard to pick them out, are there any other code changes that should be noted that aren't just moving code around that we could document in the summary?
Comment #42
znerol CreditAttribution: znerol commentedI cannot follow, please elaborate.
This patch does not contain any functional changes. Even the change to
DrupalKernel
regardingterminate()
is just compensating for the earlyexit
we had before.The reason for the slight regression visible from #31 is that response policy is prematurely constructed as a result of injecting it. That being said, we could make the response policy lazy (as proposed by wim) after #2408357: The ProxyBuilder includes parent interfaces, which causes php errors. gets in. Benchmark looks like this then:
Head is
5.873 [ms]
(see #31), margin of error is 2.6. Difference between HEAD and patch with lazy loaded response policy in terms of function calls is 26.Comment #43
neclimdulYeah, I understood that injecting the service was causing this regression. I should have been more clear though. Injecting the services lead to the regression but if that code was being used previously it should have had similar characteristics that made me ask why we started injecting it. So I found the code calling this service which is just this one block of code.
This code did not exist previously so why was it added?
Also made me curious if there was any other code that was added to deal with the code being called differently or something.
Comment #44
znerol CreditAttribution: znerol commentedOh, I see. That code still exists in
FinishResponseSubscriber
where this was extracted from. That's why it does not show up in the patch. The response policy is evaluated two times now, once for the internal cache in the middleware and once when setting headers for external caches. Note that this was already the case for the request policy before.Comment #45
znerol CreditAttribution: znerol commentedComment #46
znerol CreditAttribution: znerol commentedCR draft
Comment #47
Wim LeersComment #48
Wim LeersDraft CR looks good.
Do you see any way to reduce that increase, or even turn it around into a decrease?
Comment #49
znerol CreditAttribution: znerol commentedThe increase is due to the fact that the class loader needs to find the response policy. Removing it from constructor arguments results in 6 function calls fewer than
8.0.x
.no response policy
lazy response policy
Comment #50
znerol CreditAttribution: znerol commentedComment #51
znerol CreditAttribution: znerol commented#2408357: The ProxyBuilder includes parent interfaces, which causes php errors. went in.
Comment #52
neclimdulRe: my other questions. I see now how the combination of the request policy check in handle() and the response policy check roughly match the logic in FinalResponseSubscriber.
Looking some more, can we merge these into a single method? They seem to do the same thing.
Comment #53
znerol CreditAttribution: znerol commentedSee #24ff.
Comment #54
neclimdulThere doesn't seem to be a distinction in the documentation. The interface is our contract and both essentially document "pass the request though to the backend" so I'm still not clear why we need two. At the very least we need to document why we need two methods and what makes each useful.
Comment #55
znerol CreditAttribution: znerol commentedI failed to come up with sane documentation. Moreover, if two experienced core contributors stumble over the same lines it is very likely that something is not quite right there.
How about removing
PageCache::forward()
entirely and instead inline$this->httpKernel->handle()
? I do not see any reason to override theforward()
method in a subclass because the same effect can be achieved by simply adding another middleware between the page cache and the wrapped kernel.Comment #57
znerol CreditAttribution: znerol commentedCannot reproduce the test-failure in
UserAccountLinksTests
.Comment #58
znerol CreditAttribution: znerol commentedReupload of #55.
Comment #59
neclimdulThat works for me. I don't see anything at this point that's a red flag and this is definitely where we where trying to go with the kernel. If Wim doesn't having anything else I'd support RTBC'ing this.
Hopefully we can start looking at tweaking logic to cut those calls out as follow ups too. Things like #1818628: Use Composer's optimized ClassLoader for Core/Component classes might minimize it at least.
PS - Thanks for putting up my the late questions and confusion! Great work!
Comment #60
neclimdulDang-it I lied. We should fix this.
Comment #61
znerol CreditAttribution: znerol commentedRight.
PS - Thanks for the review, much appreciated.
Comment #62
Wim LeersRepeatingn znerol's test methodology of #31 on my system, to see whether I can reproduce. With one major difference: I'm not using the MockCache module, so I'm testing actual responses, of the actual D8 frontpage. PHP 5.5.11, no XHProf, no XDebug, with OpCache, with DB.
$ ab -k -c1 -n1000 http://tres/
.The difference is negligible: the min, mean, sd and median are identical when comparing HEAD with the patch; only in the max is there a significant difference, but since all other numbers are the same, that doesn't really have any noteworthy effect.
Therefore, together with #59: RTBC. Great work, znerol! :)
Found these nitpicks, and fixed them:
Nit: s/preHandle/preHandle()/
These docs hadn't been updated accordingly.
Comment #63
alexpottLet's not do the config load if we don't have too. Yes we probably have already loaded system.performance - but we might not have so
Comment #64
znerol CreditAttribution: znerol commentedComment #66
znerol CreditAttribution: znerol commentedznerol--
Comment #67
Wim LeersOohh, well-spotted, Alex!
Comment #68
alexpottCommitted 1e985b0 and pushed to 8.0.x. Thanks!
Thanks for adding the beta evaluation.
Comment #70
BerdirFollow-up: #2368987: Move internal page caching to a module to avoid relying on config get on runtime