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.
The way that block cache invalidation works now is wrong for several reasons:
- Cacheable metadata which depends on site alert entity should be applied to each block item build instead of invalidating the cache in SiteAlertForm::save() and SiteAlertDeleteForm::submitForm().
foreach (SiteAlert::loadMultiple() as $alert) { if ($alert->getActive() && $alert->isCurrentlyScheduled()) { $cache_metadata = new CacheableMetadata(); $cache_metadata->addCacheableDependency($alert); $build_item = [ '#theme' => 'site_alert', ..., ]; $cache_metadata->applyTo($build_item); $build[] = $build_item; ... } }
- Also the whole block build cache should be tagged with
\Drupal::entityTypeManager()->getDefinition('site_alert')->getListCacheTags()
. - The block cache
max-age
is 0. That makes the block and the whole page uncacheable. SiteAlertBlock::getCacheTags()
should be removed.- Using
\Drupal::service('page_cache_kill_switch')->trigger();
inSiteAlertController::getUpdatedAlerts()
feels wrong. This means that every time Ajax is loading the alert list is re-building it even nothing has been changed. We probably want here to render the block and not duplicate the code. Also we don't want to respond with a Symfony response, but with aHtmlResponse
which is cacheable, meaning we don't need to render the block build but we'll pass it as render array (=== caching).
Comment | File | Size | Author |
---|---|---|---|
#20 | interdiff.txt | 7 KB | pfrenssen |
#20 | 3111573-20.patch | 51.14 KB | pfrenssen |
|
Comments
Comment #2
claudiu.cristeaComment #3
claudiu.cristeaComment #4
pfrenssenThese issues can be solved the following way:
* The block should get a cache context that produces a unique hash representing the IDs of the visible alerts, or "none" if no alerts are visible. This will allow to cycle alerts without invalidating any existing page caches. The nice things about cache contexts is that if an existing alert is disabled and then enabled again at a later point, the cached versions will still be there. Even the full page cache can just be reused and doesn't need to be regenerated.
* The block should inherit the cache tags of the alerts that are shown in it. In that case if any of the alerts are edited only the cached versions of the block that actually contain this alert will be invalidated, and all cached versions of the block that don't contain it will be untouched.
With these two mechanisms in place the current approach of using AJAX to inject the content into a cached page is no longer needed.
Comment #5
pfrenssenGoing to implement this.
Comment #6
pfrenssenKicking this off with a failing test.
Comment #8
pfrenssenThe test is failing as expected:
This indicates that when there is a site alert on the page and the page is cached, and a new scheduled site alert comes along, the cached version (without the scheduled alert) is still shown.
Now let's fix this by introducing a cache context that will create variations of the site alert block depending on which alerts are being shown.
Comment #9
pfrenssenWork in progress. My progress is being hindered by #2352009: [pp-3] Bubbling of elements' max-age to the page's headers and the page cache which is making my planned approach impossible right now.
Comment #10
pfrenssenI have to work on some other priorities today, here is my to do list so I can pick this back up later:
Comment #11
pfrenssenPosting progress. I have looked into using the Cache Control Override module to see if it could help site builder get properly cached pages for anonymous users with an expiration date which matches the time the next alert would appear, but it doesn't work. This is probably due to the cache max age being ignored. I see the following mention in #2352009: [pp-3] Bubbling of elements' max-age to the page's headers and the page cache even though the Cache Control Override module page doesn't mention this limitation:
In any case we should not introduce a dependency on CCO just to work around a core bug.
We can make this work by falling back to a JS driven approach in case the page cache is enabled.
Comment #12
pfrenssenSmall cleanups.
Comment #13
pfrenssenImplemented the workaround. This still needs test love.
Comment #14
swirt@pfrenssen I am concerned by this comment
I think that would invalidate the value of this module. One of the things that makes this module so useful is that updates to alerts appear without needing a page reload. The AJAX is not simply to be a cache bypass, it is to provide real-time alerts. In my opinion the AJAX (or a socket) is very useful.
Example: An editor working on a long and complex page can get notified about downtime in real-time.
Comment #15
pfrenssen@swirt no worries, the idea was to make the AJAX refreshing optional rather than a hard requirement, it was never the intention to remove it. See #3111964: Make AJAX refreshing user configurable.
Comment #16
swirtThank you for clarifying this. Now I can rest easier ;)
Comment #17
pfrenssenAdded test coverage for the method that returns the cache max age. Found a bug in the query thanks to the test: the base table name should be surrounded with curly braces.
Next up is to write a JS test for the page cache workaround.
Comment #18
pfrenssenAdded the test coverage for the workaround to allow alerts to show on cached pages, until #2352009: [pp-3] Bubbling of elements' max-age to the page's headers and the page cache is fixed.
This is now ready for review.
Comment #19
claudiu.cristeaInstead "pages", I think it's "pages cache".
s/string/string|null
Also, I see below that $this->hash is always computed, or nor used, so I don't see why it should be initialized to NULL.
Very interesting. Nice.
Hm, I see we want that Ajax response is still cached. But then I see we return a Symfony
Response()
. Drupal has its own cacheable responseHtmlResponse
(this also would allow to avoid the renderer service as we can pass the render array to the response). Also, settingmax-age
to this response, might work, as this is not bubbling to the page, no? (I'm still not very sure).Should be in interface
SiteAlertStorageInterface
?
Ideally, the invalidation would occur only when the ACTIVE alerts list would change. I know that we are taking advantage of a core provided cache tag. In that perfect world, we would need a custom cache tag. Is it worth to create a follow-up? No idea.
Comment #20
pfrenssenCacheContextInterface::getCacheableMetadata()
is not clear on this, but the idea is that some cache backends cannot realistically work with cache contexts; for example a static page cache intended for anonymous users, such as Varnish or the core page_cache. But these backends can set the cache lifetime. We can leverage this perfectly since our content is scheduled and we know exactly when to invalidate the caches.Of course if someone is hit by this they are free to create an issue + patch.
Comment #21
claudiu.cristeaGreat. Thank you!
Comment #23
pfrenssen