ContentEntityStorageBase::loadRevision() should use the static and the persistent entity cache like ContentEntityStorageBase::load()

Created on 23 November 2015, over 9 years ago
Updated 30 January 2023, about 2 years ago

Problem/Motivation

#597236: Add entity caching to core β†’ added the entity cache to core, but unfortunately it did not consider enabling it when loading an entity by revision ID.
There are contrib modules such as Entity Reference Revisions β†’ , Paragraphs and core modules such as Content Moderation, which rely heavily on loading entity revisions.

A developer might build a system where she is using ContentEntityStorageBase::loadRevision() to load a default entity revision and by doing so one will currently not profit from the entity cache.

ContentEntityStorageBase::loadRevision() is neither using the static entity cache nor the persistent entity cache, which has as a consequence that the function is inconsistent with ContentEntityStorageBase::load(), as if you retrieve an entity multiple times in a single request with the latter one it will always return a reference to the same entity object, but if you retrieve it multiple times with the former one you will always get a new entity object and thus a completely different object reference.

The attached test demonstrates all the problems.

Proposed resolution

  1. Enable static entity caching for ContentEntityStorageBase::loadRevision() for all revisions.
  2. Enable persistent entity caching for ContentEntityStorageBase::loadRevision() for all revisions.
  3. Ensure that ContentEntityStorageBase::load() and ContentEntityStorageBase::loadRevision() return a reference to the same entity object, if loading the default entity revision.
  4. To ensure backwards compatibility $storage::resetCache() will be invalidating all cached revisions. Include an optimization to invalidate only the default revision when the entity has only revisionable fields.

Remaining tasks

Review.

User interface changes

none

API changes

  1. \Drupal\Core\Entity\RevisionableStorageInterface::loadRevisionUnchanged() to load an entity revision bypassing the static cache.
  2. \Drupal\Core\Entity\RevisionableStorageInterface::resetRevisionCache($revision_id) for reseting the static and persistent cache for specific revision IDs
  3. $storage->loadRevision($revision_id) === $storage->loadRevision($revision_id)now evaluates to TRUE
  4. $storage->load($id) === $storage->loadRevision($revision_id)now evaluates to TRUE if $revision_id references the current default revision

Data model changes

none

πŸ“Œ Task
Status

Needs work

Version

10.1 ✨

Component
EntityΒ  β†’

Last updated about 7 hours ago

Created by

πŸ‡©πŸ‡ͺGermany hchonov πŸ‡ͺπŸ‡ΊπŸ‡©πŸ‡ͺπŸ‡§πŸ‡¬

Live updates comments and jobs are added and updated live.
  • Needs issue summary update

    Issue summaries save everyone time if they are kept up-to-date. See Update issue summary task instructions.

  • Needs change record

    A change record needs to be drafted before an issue is committed. Note: Change records used to be called change notifications.

  • needs profiling

    It may affect performance, and thus requires in-depth technical reviews and profiling.

Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • The Needs Review Queue Bot β†’ tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

    Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

    Consult the Drupal Contributor Guide β†’ to find step-by-step guides for working with issues.

  • Status changed to Needs review almost 2 years ago
  • πŸ‡ΊπŸ‡ΈUnited States muriqui

    Reroll of #168, resolving two conflicts:

    • In ContentEntityStorageBase::setPersistentCache, cache tags are now set using the $items array and $this->cacheBackend->setMultiple($items), rather than this->cacheBackend->set() as in #168.
    • In SqlContentEntityStorageTest::testLoadMultiplePersistentCacheMiss, changed $entity->expects($this->any())->method('isDefaultRevision')->will($this->returnValue(TRUE)); to $entity->expects($this->any())->method('isDefaultRevision')->willReturn(TRUE);
  • last update over 1 year ago
    Patch Failed to Apply
  • Status changed to Needs work over 1 year ago
  • πŸ‡§πŸ‡ͺBelgium weseze

    Patch applies correctly to latest releases of both D9 and D10. (haven't tested dev)
    We are using it to fix serious performance issues in a heavy customised layout builder setup. Layout builder pages can contains up to 100 inline blocks. (but really there is no actual hard limit for us)

    Before the patch (while editing and translating pages with LB) we had 5-10.000 SQL queries taking 3-5seconds of loading time. (depending on the complexity of the layout builder setup on the page)
    After the patch this went down to few hundred SQL queries and e few hundred ms of loading time.

    This is a lifesaver for us, so thanks to everyone here for all the work put in!

  • First commit to issue fork.
  • Pipeline finished with Failed
    about 1 year ago
    Total: 178s
    #106060
  • Pipeline finished with Failed
    about 1 year ago
    Total: 201s
    #106067
  • Pipeline finished with Failed
    about 1 year ago
    Total: 176s
    #106140
  • Pipeline finished with Failed
    about 1 year ago
    Total: 562s
    #106194
  • πŸ‡ΊπŸ‡ΈUnited States capysara

    I rerolled patch #172 and moved it into a MR, but it still needs work to get all the tests to pass.

    I'm hiding the patches so that future fixes can continue in the MR.

  • πŸ‡³πŸ‡±Netherlands dennisdk

    Rerolled the patch to 10.3, since the current changes are aimed toward 11.x

  • πŸ‡³πŸ‡±Netherlands dennisdk
  • πŸ‡³πŸ‡±Netherlands basvredeling Amsterdam

    Rerolled patch for 10.3.x

  • Pipeline finished with Failed
    19 days ago
    Total: 135s
    #448757
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Started with a rebase.

    πŸ“Œ Add assertions to OpenTelemetryNodePagePerformanceTest::testNodePageWarmCache() Active got in, which shows that on demo_umami on node pages, we have ~80 node revision queries due to the local tasks block, so that shows how valuable this would be:

         $expected = [
    -      'QueryCount' => 172,
    -      'CacheGetCount' => 247,
    +      'QueryCount' => 94,
    +      'CacheGetCount' => 250,
    

    The reason for this is the active routes, which query and load the most recent revision for edting, instead of the default. I don't think we really understood (I certainly didn't) the cost of introducing that standardization around the getActive() and using it directly on routes. And then with umami you have edit + delete + translations + layout builder ...

    Note that this happens even for anonymous users which do not have access to those routes. But upcasting happens before access checks.

    But, this comes at a price with the current implementation, a massive increase of cache tag lookups:

    -      'CacheTagLookupQueryCount' => 28,
    +      'CacheTagLookupQueryCount' => 57,
    

    I haven't looked at this in many years, but I do believe that this is doing too much and that held it back for that long. I will need to have a better understanding of the current implementation, but I strongly support #132 back from 2018 right now and to split this up and only do static caching first. That should essentially give us the same benefit in this specific scenario without the cost. Does that risk that we might never get persistent caching for revisions? Yes. But it also means that we might get static caching soon as opposed to never.

  • Pipeline finished with Failed
    18 days ago
    Total: 760s
    #448992
  • Pipeline finished with Failed
    18 days ago
    Total: 676s
    #449024
  • Pipeline finished with Failed
    18 days ago
    Total: 663s
    #449027
  • Pipeline finished with Failed
    18 days ago
    Total: 340s
    #449059
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    I've been working on "saving" the persistent cache and reducing the BC impact of this. There are a few interesting advantages of keeping the persistent cache, for example the optimization with loadRevisionUnchanged() that allows to load a revision from static cache and only bypass the static cache, which is very useful when saving an entity, and also just loading the current revision which is what's going on with getActive().

    * I don't see a use case for invalidating a specific revision as an API. The cache should be self-contained, basically the only reason for external calls to resetCache() are tests and we've mostly removed that too thanks to automatically invalidating the memory cache.
    * This allows to remove the default_revision_for cache tag, we should always have the entity ID available and then we can delete that from the cache directly.

    The remaining cache tag is now revision_for.., that still shows up a lot in the performance tests, because of the optimization to also put the default revision with its revision ID into the cache, but that means we need to check the state of those cache tags on cache set.

    I plan to look into either not doing that anymore for now or alternatively remove the cache tag. The number of revisions for an entity is a known quantity. So in cases where we need to invalidate them, we can query the ids and then invalidate the cache. I think that's not necessary that often, basically just when an entity is deleted?

    Looks like the most recent changes broke something, will look into that, off for now.

  • Pipeline finished with Failed
    18 days ago
    Total: 930s
    #449227
  • Pipeline finished with Failed
    18 days ago
    Total: 725s
    #449246
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Fixed the last non-performance test fails now I think. That one was interesting, it was reverting an old revision that was the the default revision back then to the new current revision. The logic here was to invalidate the loaded revision, but that wasn't actually the previous default revision. I extended it to also invalidate $entity->getOriginal()->getRevisionId() which works in this case because the revision we revert to was the default, so the logic in \Drupal\Core\Entity\ContentEntityStorageBase::doPreSave() does not kick in and it does _not_ use loaded revision as source. But if you would revert back to a draft then that wouldn't work. To fix this for that (non-tested) scenario, we'd need to reliably know the previous default revision id.

    I'm also not 100% sure what makes sense as getOriginal() but I think it is in fact the current default revision, because that's what we're changing from.

    Or, we drop this pretty complex optimization logic to avoid invalidating all old revisions when saving an entity. This would get a bit more expensive specifically when we'd combine it with the option in #183 to remove the cache tag and explicitly delete all known revision ids. But you'd probably need many thousands for this to really be an issue.

  • πŸ‡¬πŸ‡§United Kingdom catch

    The remaining cache tag is now revision_for.., that still shows up a lot in the performance tests, because of the optimization to also put the default revision with its revision ID into the cache, but that means we need to check the state of those cache tags on cache set.

    I plan to look into either not doing that anymore for now or alternatively remove the cache tag

    Could we use the entity:$entity_id cache tag instead? Feels like most pages that would request it would do so anyway, and in other situations it wouldn't be much overhead.

    If we can get rid of it by reversing "because of the optimization to also put the default revision with its revision ID into the cache," that also seems fine.

Production build 0.71.5 2024