Problem/Motivation
When using resource/ID/related_field endpoints the result is processed as a collection. Response collections have this piece of code
// When a new change to any entity in the resource happens, we cannot ensure
// the validity of this cached list. Add the list tag to deal with that.
$list_tag = $this->entityTypeManager->getDefinition($entity_type_id)
->getListCacheTags();
$response->getCacheableMetadata()->setCacheTags($list_tag);
This means a list of related entities with defined ids will be treated like a collection of items. And collections are tagged with tags that invalidate the cache wherever a new entity is created/updated. (the tag node_list is used for related nodes)
If you have a site with a lot of nodes (with new entries every day) and related content you can end (like we did) with a cache_dynamic_page_cache grow table of 7GB in a few days.
Proposed resolution
Use the ids of each related entity and the id of the source entity to tag the response instead of using the default from collections.
Remaining tasks
None
User interface changes
None
API changes
None
Data model changes
Original report:
We have a situation where cache_dynamic_page_cache grow up to 7GB in a few days. Most of the cache entries have this structure:
response:[request_format]=api_json:[route]=api.dynamic.node--article.relatedd5d35b6d0fc215403b86fdb67830073b0ef3c1c77646b87e24f2150b014d4b21
Where the part that varies is the hash at the end.
While this behavior could be correct based on the from a point of view of cache invalidation, we found all those cache entries are set to expire -1 which means they will not be invalidated never.
So the question here is, Does jsonapi provides any way to set the expire time to a value different that -1?
Also, this seems like another bug, related articles entries are invalidated every time an article is updated. Any help about how to debug this will be appreciated
Thanks!
| Comment | File | Size | Author |
|---|---|---|---|
| #37 | 2828639-cachetags-for-get-related-37.patch | 2.8 KB | apupiales |
| #33 | 2828639--cachetags-for-get-related--33.patch | 16.21 KB | e0ipso |
| #33 | 2828639--interdiff--31-33.txt | 13.29 KB | e0ipso |
| #31 | 2828639--cachetags-for-get-related--31.patch | 4.41 KB | e0ipso |
| #27 | 2828639--interdiff--22-27.txt | 1.74 KB | e0ipso |
Comments
Comment #2
dagmarI realized that the root cause why entities are duplicated so often is when you list the content for a relationship, it is tagged with the
$this->entityTypeManager->getDefinition($entity_type_id)->getListCacheTags();tag. For nodes this meansnode_list.So, even if you don't edit a node, or its related articles, by just creating a new node or editing a new one, it will invalidate all the related endpoints. If you have a high volume site this will generate a lot of entries on the cache_dynamic_page_cache and cache_render tables.
This patch changes the tags used to tag related listings. Instead of using node_list it will use the entity itself and the ids of the related entities, for example:
node:12 node:20 node:30instead ofnode_list.A module like Slushi Cache is still recommended to purge expired entries, but this entries will be fewer than the current implementation.
Comment #4
dagmarComment #6
dagmarThis should fix the tests.
Comment #7
dagmarUpdated issue summary now I have more information about the issue.
Comment #8
e0ipsoI like where this is going, but we should not construct the cacheable metadata strings ourselves.
We should not be limited by tags. What if there is an uncacheable entity in there, we want to detect that as well.
We want to use something like:
Good! Entity list tags should only be present where the lists are dynamic, not editorially controlled.
Comment #9
e0ipsoComment #10
dagmarThanks @e0ipso
Here is the new patch
Comment #12
dagmarFixed failing tests and adding more coverage to test node relationships.
Comment #13
dagmarFixed some spaces.
Comment #14
wim leersHm, all of this reminds me of
\Drupal\Core\Entity\EntityInterface::referencedEntities():The reason this is changing is that
EntityResource::getRelated()used to callEntityResource::respondWithCollection(), but no longer does that.AFAICT the real bug is in
EntityResource::getRelated():That
setCacheTags()call is overwriting all cache tags with just the list cache tag. That should become aaddCacheTags()call, which will add it to the set of cache tags collected so far.Comment #15
dagmarThanks @Wim Leers.
Ok, makes sense (I created this issue #2836409: [BUGFIX] Improve cacheable metadata for collections to track that bug), but even with that solution, this doesn't fix the problem described in the issue. As soon the list tag is added to the response all the related content lists will be invalidated as soon a new entity is created.
Comment #16
dagmarAny comments on this?
Comment #18
e0ipsoComment #19
e0ipsoThis is blocked by getting tests on #2836409: [BUGFIX] Improve cacheable metadata for collections.
I like the feature in this patch. However this has the potential to yield a VERY long list of cache tags, such long list may have problems with some CDNs. As fubhy highlighted on the GraphQL module.
Comment #20
e0ipsoComment #21
e0ipsoRe-rolled and changed strategy a bit.
Comment #22
e0ipsoAdded a code comment to explain why the individual requests don't have the cache metadata for the requested entity.
Added #2841851: Audit cacheable metadata generation to see if we can be more focused in the metadata generation.
Comment #23
wim leersLooking at the patch, I see what you're fixing/changing:
The serializer doesn't modify this. It's normalizers that refine the cacheability metadata.
Specifically,
\Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer::normalizeand\Drupal\jsonapi\Normalizer\RelationshipItemNormalizer::normalizerefine it.So I'd rephrase this to:
The serializer receives the response's cacheability metadata object as serialization context. Normalizers called by the serializer then refine this cacheability metadata, and thus they are effectively updating the response object's cacheability.So this is the thing that was missing!
We still need cacheability metadata of access results. We should keep this.
but I'm not convinced it will fix the original reported issue. It would cause correct invalidation (thanks to adding the missing cache tags), but you're not making changes that would cause Dynamic Page Cache to stop growing so much.
Because the issue reported originally is that
dynamic_page_cachegrows to enormous sizes. I don't understand how that's possible just yet. Unless we're talking hundreds of thousands of nodes and they're all being requested in many different ways.The explanation in #2 makes no sense to me: the presence or absence of a certain cache tag cannot cause more or less cache items to be created. Cache tags are used only for invalidation. And invalidation happens only at runtime: if a cache item X has a tag that's been invalidated since that cache item X was created, then X will *not* be removed from the cache until it's requested again. Only when it's been requested, we check whether tags have been invalidated. (This was done for scalability reasons. The details behind this are out of scope here.)
When I started looking into this, I noticed that entity access results are not added to the cacheability metadata for a response. That would definitely be an explanation: it could mean that even responses that vary by the
usercache context are being cached by Dynamic Page Cache. This is a problem in\Drupal\jsonapi\Controller\EntityResource::getIndividualfor example, but\Drupal\jsonapi\Controller\EntityResource::getCollectionand\Drupal\jsonapi\Controller\EntityResource::getRelatedseem to get it right for example.So, +1 for the changes in this patch (well, once my feedback is addressed), but I don't see how it would solve the originally reported problem.
Comment #24
dagmarWell, that depends on the type of site. We implemented this on a newspaper where each day 100 nodes are created every night, and other 50 are created during the day. So, each time a node is created, all the older related nodes listings (remember, 150 per day) are invalidated. So in a few days you end with a significant number of invalidated items.
Combine this with the fact that caches never expires, you get the behavior I described.
Comment #25
wim leersBut as you said, the caches never expire. #2 does not solve that?
The root cause is that the default cache back-end (the database-powered one) pretends to be unlimited in size. We have #2526150: Database cache bins allow unlimited growth: cache DB tables of gigabytes! to fix that.
Comment #26
dagmarAgree, partially... Without the patch you have this situation:
With the patch you have:
So basically, you have less cache invalidations and less exponential grow.
Comment #27
e0ipsoFor the record, this was already being correctly invalidated via the entity list tag. However it was invalidating too much.
I addressed the feedback on #23.
Comment #29
dagmarWow, this is a lot of new code... So, in #13 I provided some extra text coverage that I would like to see in this new patch too. That code coverage ensures my case described in #2 is covered.
Comment #30
e0ipsoComment #31
e0ipsoI re-rolled the existing patch.
Comment #32
e0ipsoComment #33
e0ipsoAdded extra test as requested from @dagmar.
Merging if green.
Comment #35
e0ipsoComment #37
apupiales commentedHi @dagmar
We are using Jsonapi (1.x-dev) with 2828639-cachetags-for-get-related-6.patch and works good but due to https://www.drupal.org/node/2723323 (When deleting an entity, references to the deleted entity remain in entity reference fields) I would like to complement your patch with an additional validation in order to avoid an error when a referenced node is deleted (Error: Call to a member function id() on null en Drupal\jsonapi\Resource\EntityResource->getRelated()).
Note: I will create a new issue to share the equivalent patch for the last version of jsonapi.
Thanks.