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!

Comments

dagmar created an issue. See original summary.

dagmar’s picture

Title: Cache expire time » getRelated is using wrong cache tags
Status: Active » Needs review
StatusFileSize
new1.23 KB

I 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 means node_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:30 instead of node_list.

A module like Slushi Cache is still recommended to purge expired entries, but this entries will be fewer than the current implementation.

Status: Needs review » Needs work

The last submitted patch, 2: 2828639-cachetags-for-get-related-2.patch, failed testing.

dagmar’s picture

Title: getRelated is using wrong cache tags » [BUGFIX] getRelated is using wrong cache tags
Category: Feature request » Bug report
Status: Needs work » Needs review
StatusFileSize
new2.7 KB

Status: Needs review » Needs work

The last submitted patch, 4: 2828639-cachetags-for-get-related-4.patch, failed testing.

dagmar’s picture

Status: Needs work » Needs review
StatusFileSize
new2.74 KB

This should fix the tests.

dagmar’s picture

Issue summary: View changes

Updated issue summary now I have more information about the issue.

e0ipso’s picture

I like where this is going, but we should not construct the cacheable metadata strings ourselves.

  1. +++ b/src/Resource/EntityResource.php
    @@ -253,14 +253,24 @@ class EntityResource implements EntityResourceInterface {
    +    $tags = [
    ...
    +    $response->getCacheableMetadata()->setCacheTags($tags);
    

    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:

    $response
      ->getCacheableMetadata()
      ->addCacheableDependency($related_entity);
    
  2. +++ b/tests/src/Kernel/Resource/EntityResourceTest.php
    @@ -359,7 +366,11 @@ class EntityResourceTest extends JsonapiKernelTestBase {
    -    $this->assertEquals(['config:user_role_list'], $response
    

    Good! Entity list tags should only be present where the lists are dynamic, not editorially controlled.

e0ipso’s picture

Status: Needs review » Needs work
dagmar’s picture

Status: Needs work » Needs review
StatusFileSize
new2.55 KB

Thanks @e0ipso

Here is the new patch

Status: Needs review » Needs work

The last submitted patch, 10: 2828639-cachetags-for-get-related-10.patch, failed testing.

dagmar’s picture

Status: Needs work » Needs review
StatusFileSize
new4.72 KB

Fixed failing tests and adding more coverage to test node relationships.

dagmar’s picture

StatusFileSize
new4.73 KB

Fixed some spaces.

wim leers’s picture

Status: Needs review » Needs work
  1. +++ b/src/Resource/EntityResource.php
    @@ -260,7 +260,19 @@ class EntityResource implements EntityResourceInterface {
    +    // When listing related entities to an entity, the list of cachetags should
    +    // include the id of the related entities and the id of the origin entity.
    +    $response->getCacheableMetadata()->addCacheableDependency($entity);
    +    foreach ($field_list as $field_item) {
    +      /* @var \Drupal\Core\Entity\EntityInterface $related_entity */
    +      $related_entity = $field_item->entity;
    +      $response->getCacheableMetadata()->addCacheableDependency($related_entity);
    +    }
    

    Hm, all of this reminds me of \Drupal\Core\Entity\EntityInterface::referencedEntities():

      /**
       * Gets a list of entities referenced by this entity.
       *
       * @return \Drupal\Core\Entity\EntityInterface[]
       *   An array of entities.
       */
      public function referencedEntities();
  2. +++ b/tests/src/Kernel/Resource/EntityResourceTest.php
    @@ -359,7 +378,26 @@ class EntityResourceTest extends JsonapiKernelTestBase {
    -    $this->assertEquals(['config:user_role_list'], $response
    +    $this->assertEquals([
    +        'config:user.role.test_role_one',
    +        'config:user.role.test_role_two',
    +        'user:' . $this->user->id(),
    +      ], $response
    +      ->getCacheableMetadata()
    

    The reason this is changing is that EntityResource::getRelated() used to call EntityResource::respondWithCollection(), but no longer does that.

    AFAICT the real bug is in EntityResource::getRelated():

        // 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);
    

    That setCacheTags() call is overwriting all cache tags with just the list cache tag. That should become a addCacheTags() call, which will add it to the set of cache tags collected so far.

dagmar’s picture

Status: Needs work » Needs review

Thanks @Wim Leers.

That setCacheTags() call is overwriting all cache tags with just the list cache tag. That should become a addCacheTags() call, which will add it to the set of cache tags collected so far.

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.

dagmar’s picture

Any comments on this?

Status: Needs review » Needs work

The last submitted patch, 13: 2828639-cachetags-for-get-related-13.patch, failed testing.

e0ipso’s picture

Title: [BUGFIX] getRelated is using wrong cache tags » [PP-1] [BUGFIX] getRelated is using wrong cache tags
e0ipso’s picture

This 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.

e0ipso’s picture

Title: [PP-1] [BUGFIX] getRelated is using wrong cache tags » [BUGFIX] getRelated is using wrong cache tags
e0ipso’s picture

Status: Needs work » Needs review
StatusFileSize
new3.64 KB

Re-rolled and changed strategy a bit.

e0ipso’s picture

Added 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.

wim leers’s picture

Looking at the patch, I see what you're fixing/changing:

  1. +++ b/src/RequestHandler.php
    @@ -145,6 +145,9 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
    +        // The serializer modifies the $cacheable_metadata, which is referenced
    +        // by the $response object, effectively adding the metadata to the
    +        // response.
    

    The serializer doesn't modify this. It's normalizers that refine the cacheability metadata.

    Specifically, \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer::normalize and \Drupal\jsonapi\Normalizer\RelationshipItemNormalizer::normalize refine 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.

  2. +++ b/src/Resource/EntityResource.php
    @@ -252,19 +253,18 @@ class EntityResource implements EntityResourceInterface {
    +      $cacheable_metadata->addCacheableDependency($entity_item);
    

    So this is the thing that was missing!

  3. +++ b/src/Resource/EntityResource.php
    @@ -252,19 +253,18 @@ class EntityResource implements EntityResourceInterface {
    -    array_walk($access_info, function ($access) use ($response) {
    -      $response->addCacheableDependency($access);
    -    });
    

    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_cache grows 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 user cache context are being cached by Dynamic Page Cache. This is a problem in \Drupal\jsonapi\Controller\EntityResource::getIndividual for example, but \Drupal\jsonapi\Controller\EntityResource::getCollection and \Drupal\jsonapi\Controller\EntityResource::getRelated seem 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.

dagmar’s picture

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.)

Well, 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.

wim leers’s picture

But 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.

dagmar’s picture

The root cause is that the default cache back-end (the database-powered one) pretends to be unlimited in size. We have #2526150: Make CACHE_PERMANENT configurable in the database cache — currently allows unlimited growth: cache DB tables of gigabytes! to fix that.

Agree, partially... Without the patch you have this situation:

  1. 10 listings cached
  2. New node (even if is not related or listed in the previous nodes), invalidates all related nodes because of node_list
  3. 10 listing expired + 10 listing cached
  4. New node (even if is not related or listed in the previous nodes), invalidates all related nodes because of node_list
  5. 10 listing expired + 10 listing expired + 10 active listings

With the patch you have:

  1. 10 listings cached
  2. New node (doesn't invalidate the previous listings)
  3. 10 listing cached
  4. Relate the new node to one of those listings
  5. 11 listings cached

So basically, you have less cache invalidations and less exponential grow.

e0ipso’s picture

It would cause correct invalidation (thanks to adding the missing cache tags)

For 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.

Status: Needs review » Needs work

The last submitted patch, 27: 2828639--cachetags-for-get-related--27.patch, failed testing.

dagmar’s picture

Wow, 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.

+    // to-many relationship.
+    $response = $entity_resource->getRelated($this->node3, 'field_relationships', $this->request->reveal());
+    $this->assertInstanceOf(DocumentWrapper::class, $response
+      ->getResponseData());
+    $this->assertInstanceOf(EntityCollectionInterface::class, $response
+      ->getResponseData()
+      ->getData());
+    $this->assertEquals([
+        'node:1',
+        'node:2',
+        'node:3',
+      ], $response
e0ipso’s picture

Assigned: Unassigned » e0ipso
e0ipso’s picture

I re-rolled the existing patch.

e0ipso’s picture

Status: Needs work » Needs review
e0ipso’s picture

Added extra test as requested from @dagmar.

Merging if green.

  • e0ipso committed ffb7191 on 8.x-1.x
    fix(Cacheability) Fix wrong cache tags in related responses (#2828639 by...
e0ipso’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

apupiales’s picture

Hi @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.