Loading entity during save will cache old version

Created on 17 September 2024, about 2 months ago
Updated 19 September 2024, about 2 months ago

Problem/Motivation

When entity is being saved, a well-timed load for given entity will cause old version of it to be cached.

This can (and likely will) cause data loss.

There seems to be several tickets made that describes the behavior how I met this issue, and more real-life like scenario might contain something like messages with tokens that will eventually cause this bug to happen.

Steps to reproduce

Complete version using user entity as example

  1. Create new user, should be ID 2 with vanilla standard installation
  2. Have one terminal open running drush scr bugsaver.php
  3. Have few terminals open running drush scr bugloader.php - even one instance will eventually find the perfect timing but it's significantly faster to run 3 to 6 instances
  4. Once the saver script prints out the mismatch, feel free to close the loader scripts
  5. Open the edit form for the new user as admin - the user name is now old
  6. (Option A) To "fix" the entity, clear caches & refresh the pagedrush cr
  7. (Option B) Add a role to the user and save - the new user name is now fully gone

Sample snippet for saving

// bugsaver.php
$entityId = 2;
$entityType = 'user';
$field = 'name';

$entityTypeManager = \Drupal::entityTypeManager();
$uuidGenerator = \Drupal::service('uuid');

$entityStorage = $entityTypeManager->getStorage($entityType);
$entity = $entityStorage->load($entityId);
$current = $match = $uuidGenerator->generate();

$entity->set($field, $current)->save();
$entity->save();

while (TRUE) {
  $entityStorage = $entityTypeManager->getStorage($entityType);
  $entity = $entityStorage->load($entityId);

  $current = $entity->get($field)->value;
  if ($current != $match) {
    echo "uh oh. {$current} != {$match}" . PHP_EOL;
    exit;
  }

  $match = $uuidGenerator->generate();
  $entity->set($field, $match)->save();
}

Sample snippet for loading

$entityId = 2;
$entityType = 'user';

$entityTypeManager = \Drupal::entityTypeManager();
while (TRUE) {
  $entityStorage = $entityTypeManager->getStorage($entityType);
  $entity = $entityStorage->load($entityId);
}

Proposed resolution

TBD

  • Semaphore to prevent entity being loaded while it is being saved
  • State that prevents entity being cached while it is being saved
  • ????

Remaining tasks

TBD

User interface changes

TBD

Introduced terminology

TBD

API changes

TBD

Data model changes

TBD

Release notes snippet

TBD

🐛 Bug report
Status

Active

Version

11.0 🔥

Component
Entity 

Last updated about 19 hours ago

Created by

🇫🇮Finland dropa

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @dropa
  • 🇫🇮Finland dropa

    Seems to reproduce with latest vanilla as well

  • 🇺🇸United States luke.leber Pennsylvania

    This sounds a lot like a very rare and hard to reproduce issue we've seen on our production Acquia sites. In our case, this manifested as the incorrect revision being marked as the default revision, and showing stale information to end-users on the front-end.

    Following, and will try to reproduce in stage! Thanks for filing this issue! It could help to explain the "that can't happen" thing that certainly happens 🤣!

  • 🇫🇮Finland dropa

    In our case, this manifested as the incorrect revision being marked as the default revision

    This is exactly how it showed up in our case as well, but after digging (far) deeper into it turned out it doesn't matter whether entity supports revisions or not.

    showing stale information to end-users on the front-end.

    I'm sure majority of us has been there and just cleared cache without thinking twice. "Luckily" in our case the outcome was more site-breaking by affecting entities that are not manually editable. Looking back with what I know now, I'm sure I've met the same issue before.

Production build 0.71.5 2024