Problem/Motivation

#2502785: Remove support for $form_state->setCached() for GET requests caused forms not be cached for get requests which has led to the following problem that the form might get rebuild with different entity revisions.

  1. Create an entity.
  2. Load the entity edit form - the entity form is build with the entity in revision 1.
  3. Save the entity in the background in a new revision - revision id 2.
  4. Trigger an ajax request causing form rebuild e.g. "Add another item" - the entity form is rebuild with the entity in revision 2.<
  5. Save the entity in the background in a new revision - revision id 3.
  6. Trigger an ajax request causing form rebuild e.g. "Add another item" - the entity form is rebuild with the entity in revision 2.<

Additionally there might occur different problem:

  1. Create an entity with multivalued field with multiple properties and widget where only one of the properties is user-editable.
  2. Go to the entity edit form.
  3. Rearrange the field values in the background and save the entity e.g. from "0 => green rectangle, 1 => blue circle" make "0 => blue circle, 1 => green rectangle".
  4. On the entity edit form click "Add another item".
  5. Suddenly the field is exchanged and just by adding another item the user now sees something completely different and mixed up -> "0 => green circle, 1 => blue rectangle"

This happens because the form gets cached together with the entity only on the first ajax request.

Both problems occur only if the entity gets edited in the background between the form get request and the first ajax request, but will not occur if the entity gets edited in the background between the first and any following ajax requests.

Proposed resolution

We have to ensure the form gets rebuilt with the same entity the form was initially built.

Remaining tasks

User interface changes

API changes

Data model changes

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

hchonov created an issue. See original summary.

hchonov’s picture

Title: Inconsistent form rebuilding between first and following ajax requests with different entity » Inconsistent form rebuilding between first and following ajax requests with different entity revisions

Status: Needs review » Needs work

The last submitted patch, inconsistent_form_rebuilding-FAIL.patch, failed testing.

hchonov’s picture

Priority: Normal » Major
Status: Needs work » Needs review
FileSize
7.8 KB
4.73 KB

The test I've just added shows the following problem:

  1. Create an entity with multivalued field with multiple properties and widget where only one of the properties is user-editable.
  2. Go to the entity edit form.
  3. Rearrange the field values in the background and save the entity e.g. from "0 => green rectangle, 1 => blue circle" make "0 => blue circle, 1 => green rectangle".
  4. On the entity edit form click "Add another item".
  5. Suddenly the field is exchanged and just by adding another item the user now sees something completely different and mixed up -> "0 => green circle, 1 => blue rectangle"

This problem occurs only if the entity gets edited in the background between the form get request and the first ajax request, but will not occur if the entity gets edited in the background between the first and any following ajax requests.

So I guess this is major, right?

hchonov’s picture

Issue summary: View changes
hchonov’s picture

Title: Inconsistent form rebuilding between first and following ajax requests with different entity revisions » Inconsistent form build between initial form generation and the first ajax request
Fabianx’s picture

The thing is:

Does that only happen with AJAX requests?

e.g. turn off Javascript in Chrome dev tools, do the same scenarios.

I would say this is a limitation of FAPI itself.

Status: Needs review » Needs work

The last submitted patch, 4: 2824293-4.patch, failed testing.

hchonov’s picture

The tests are not javascript tests, so yes the problem is in the FAPI.

Fabianx’s picture

Discussed in IRC:

We really do not want to bring back the ability to store form_state on GET requests as that can easily kill a site both with writes on GET requests, have problematic cache consistency issues (form state expired, but page still in Varnish), and cache_form / KV store table growing each time a form is visited (can get HUGE).

Ideal solution: Use revisions and store the revision_id + a signed hash.

But because not everything supports revisions and not even EntityChangedInterface, the original entities need to be stored for saving (pseudo code):

public hook_entity_update($entity, $type) {
  $this->key_value->store('entity_versions:' . $this->getHash($entity->original), $entity->original);
}

Then use the hash of the entity in the form building process and load this exact version from key value store instead of from entity cache.

In case the same entity changes between loads, you could also store the hash in a field of the entity itself and use the same hash for form building and saving to K/V when it is gonna be outdated.

The advantage is that your conflict management will work without any problems and you will store only as many copies of the entities that you need, especially if you have some nice garbage collection using K/V expirable.

hchonov’s picture

Thinking over this again I think it is the right approach but it would not be that easy.

1. When we cache the entity somewhere we have to ensure that the entity is cached together with its referenced entities (thinking of nested entity forms) - as the referenced entities could change independently from the parent entity and be saved in the same revision, if they support revisions. For this to work I have an another issue that I am currently working on - #2824097: Deep serialization for content entities to make an exact snapshot of an entity object's structure based on its current state.

2. Point 1. Leads to the point that after an entity is updated we have to search for all the parent entities of it and make new cache entires for them as well.

3. Point 2. Has some complications - as there might be use cases where the referenced entity is saved as a part of the save of the parent entity we should only collect the information about the updated entities in a service and on destruct or so make the new cache entries - this way we would avoid creating too much unneeded entity cache entries. We have to ensure as well that in this case all the entities are not loaded from the static entity cache, from the persistent entity cache is fine, but not the static entity cache, as the entity there might have changed meanwhile which would lead to a different hash value later.

About the hash function:
As I see it it should consider the referenced entities as well which means for the hash function we would have to compute it from the serialized entity - sure on the get request this might take some additional time, but then we put the hash value into the form array and on subsequent post requests we do not have to compute this hash value anymore. Sure we have the problem here that the value could be manipulated by the user - any concerns here?

Fabianx’s picture

Good points on the approach. One additional challenge is to see who is still using a certain hash of the form. I would suggest a LRU approach for it and writing a TS per entity loaded during the form building to a table during KernelEvents::Terminate.

Overall I think a hash of the serialized entity would work well.

It might be possible to store this hash within the entity itself (unset $entity->hash before recomputing the hash), then the overhead is minimal.

You could store within the entity references, the hash to load as well.

I don't have concerns about the user manipulating the entity hash, but if we have we can sign the value with a : and a HashBase64 that uses the Drupal Private Key.

Wim Leers’s picture

The entity being cached in FormState by \Drupal\Core\Entity\EntityForm and that calling \Drupal\Core\Entity\EntityForm::prepareEntity() has lots of confusing edge cases and AFAICT hasn't worked like it originally did for years. It was never cleaned up, so now we're left with this confusing system.

hchonov’s picture

Thinking over this again pre caching the entity structure before the entity form is requested is problematic because :

1. This special cache have to be emptied on cache rebuild.

2. After an entity is loaded fresh from the entity db tables or from the entity persistent cache EntityStorageBase::postLoad is executed for it which means that if there is some dynamic behavior / logic in Entity::postLoad or hook_entity_load it will break now. One would say then let us just trigger the post load also when retrieving the entity from this special cache but for this we would need to have cached the entity before the post load functions have been already executed on it otherwise we will introduce another inconsistency.

Storing the loaded entity, not the form state, on form get request would be the easiest solution without such consequences and possible issues. Sure we would need some solution as well with hashes in order to prevent creating 100 entries on 100 reloads resulting in the same entity object. However would this be that a performance issue of the same level as storing the form state on form get requests?

Fabianx’s picture

#14 In the ideal case any write requests during GET requests should be avoided. However as this would just create a persistent cache for this entity object, it should be fine to create this cache when actually storing the entity hash in the form's hidden field.

There still would need to be a strategy to expire those items though.

And it would need to be tested that the entity's hash / cache id would really only be dependent on the state at saving, not on any dynamic post load additions as indeed what killed the sites was one form_cache entry per view, in other words: Too many variations.

fago’s picture

I just ran into a very weird issue which sounds like it could be the source for this as well. In my case, I debugged submitting entity changes with an ajaxif-ed button. The changes to $this->entity in the form are lost in the ajax callback somehow. I figured, the form object's entity is somehow re-loaded during serialization in
\Drupal\Core\Form\FormCache::setCache()
>$this->keyValueExpirableFactory->get('form')->setWithExpire($form_build_id, $form, $expire);

Generally this is related to the issue of ContentEntityForm putting the form object into the form array in
$form['#entity_builders']['update_form_langcode'] = [$this, 'updateFormLangcode'];
This might be related to trigger the serialization issues, but generally this is no biggie as the form object is serialized with the form state as part of the build_info arguments anyway. Nevertheless it helps to complicate things.

Actually, in my case the changes lost were in referenced entities (parapraphs) which contained the changes. So maybe there is just a serialization problem around that.

hchonov’s picture

@fago for your problem I have another issue which will solve your problem when you are making changes on referenced entities - #2824097: Deep serialization for content entities to make an exact snapshot of an entity object's structure based on its current state.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

hchonov’s picture

I've implemented one first version where on form GET request the entity will be stored based on its hash in the key value expiable table, but only if it is not contained there. Multiple form GET requests on the same entity form should not lead to multiple writes as long as the entity is not saved modified meanwhile as this will be the first time the hash will change and a new entry in the key value expiable table will be created. I've set the expire to 1 day, but I think it is safe to set it to some higher value. The key value expiable automatically takes care of deleting obsolete entries. Additionally the hash is stored in a hidden field on the form. On form POST requests the entity will be retrieved from the key value expiable store based on the hash from the user input.

The current approach also takes care of entities with dynamic post loads which are manipulating the entity on load and adding some dynamic information as in this case this will generate a different entity hash.

I intentionally clone the entity and the form in order to let the form sleep method run before the entity is cached as this method might be used to prepare the entity for serialization e.g. like in #2824097: Deep serialization for content entities to make an exact snapshot of an entity object's structure based on its current state and #2844229: [PP-1] Serializing content entity form objects should deeply serialize the entity in case of nested/inline entity forms.

I've made the patch for 8.3.x, but will adjust it for 8.5.x as well.

The last submitted patch, 20: 2824293-20-8.3.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 20: 2824293-20-test-only-8.3.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

hchonov’s picture

We cannot support this feature in tests where we directly post data on the form and the entity_sync_hash_key is not present in the user input, therefore we have to explicitly check for its existence.

hchonov’s picture

The last submitted patch, 23: 2824293-23-8.3.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 24: 2824293-24-8.3.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

hchonov’s picture

Status: Needs work » Needs review
FileSize
44.4 KB
5.4 KB

Of course we could not do this for new entities and the tests creating an entity from through new EntityForm should be adjusted because of the new class constructor.

@todo
It might happen that the garbage collector has removed the entity from the storage or that it has expired and is no longer returned by the storage. We need a solution to extend the expire if e.g. there is only remaining the half of the used expire.

Status: Needs review » Needs work

The last submitted patch, 27: 2824293-27-8.3.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

zviryatko’s picture

This issue also happening with Inline Entity Form or Paragraph (or other similar entity reference field widget) and Translations enabled, could be reproduced easily:
1. Create node type with translations and with entity reference field which also translatable and set inline entity form widget to it.
2. Add node English version with some entity in referenced field.
3. Add French translation and save.
4. Edit French version and click to Edit on referenced content (don't save yet).
5. Edit English version, click to edit referenced content, and Save.
6. Go back to tab with from step 4 and click to Save.

It will show a validation error that entity is outdated, but this doesn't happens when you didn't touch referenced field inline form (or any other form rebuild or ajax). For sure it is inconsistent behavior for ajaxed form and not ajaxed. The root of the issue lays in FormCache which stores `$this->entity` in case of EntityForm and later it trying to use that outdated entity object.

Right solution as I think should be next: instead of caching old entity on form rebuild, it should take the latest entity version and re-apply all values that was submitted in form. It should be strong concept of applying field values one by one to original entity, and it should help for normal concurrent edit and identifying conflicts during form submit, and if there no conflicts it should be saved normally. Like auto-merges in Git.

Here a dirty example of what I meaning:

diff --git a/lib/Drupal/Core/Form/FormBuilder.php b/lib/Drupal/Core/Form/FormBuilder.php
--- a/lib/Drupal/Core/Form/FormBuilder.php
+++ b/lib/Drupal/Core/Form/FormBuilder.php	(date 1642003564406)
@@ -10,6 +10,8 @@
 use Drupal\Core\Access\AccessResultInterface;
 use Drupal\Core\Access\CsrfTokenGenerator;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Entity\EntityFormInterface;
+use Drupal\Core\Entity\TranslatableInterface;
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\Exception\BrokenPostRequestException;
@@ -262,6 +263,16 @@
     $check_cache = isset($input['form_id']) && $input['form_id'] == $form_id && !empty($input['form_build_id']);
     if ($check_cache) {
       $form = $this->getCache($input['form_build_id'], $form_state);
+      $form_obj = $form_state->getFormObject();
+      if ($form && $form_arg instanceof EntityFormInterface && $form_obj instanceof EntityFormInterface) {
+        $route_entity = $form_arg->getEntity();
+        $form_entity = $form_obj->getEntity();
+        if ($route_entity instanceof TranslatableInterface && $form_entity instanceof TranslatableInterface && $route_entity->hasTranslation($form_entity->language()->getId())) {
+          $route_entity = $route_entity->getTranslation($form_entity->language()->getId());
+        }
+        $form_obj->setEntity($route_entity);
+        $form_obj->buildEntity($form, $form_state);
+      }
     }

     // If the previous bit of code didn't result in a populated $form object, we

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

zviryatko’s picture

Just got an idea with more elegant way instead of patching core: add `EntityFormCache` service as wrapper to `@form_cache` service and override `getCache()` method.

andypost’s picture

I don't think it's good idea to "poison" form builder with entity namespace dependency

+use Drupal\Core\Entity

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

DavorHorvacki’s picture

+1 issue for concurrent edits.

My entity is neither translatable nor revisionable, but fields that are not exposed to form display at all are overwritten in case ajax (file uploads, multiple values fields etc.) is triggered. This can be huge issue that can result in data loss.

I also think entity shouldn't be cached at all or at least we should create partial save that will save only fields on form display, this can also boost performance in some drupal projects.

Let's say products stock is managed by external service, it will be overwritten if someone is editing product even if stock is not exposed to form edit.