Problem/Motivation

Execute two ajax requests - the first one has a processing time of e.g. 5 seconds and the second one of 1 second.
What now happens is that the form is processed for the first and for the second request with the initially requested form, but the second request has to be processed with the form generated by the first request, otherwise the new form array and form state generated by the second request will not have the data from the first request and caching them will overwritte the first request which on submitting the form will result in user data loss.

Issue is the chaotic situation that arises on the frontend due to this bug. When the bug occurs, the entire paragraph or brick subform is replaced by unrelated fields. The potential loss in this case is not the saved data, but rather the contributed content.

The main concern discussed in the title and summary is related to the backend aspect of the problem, which should be addressed through validation.

Attaching a patch demonstrating the problem.

Proposed resolution

Subsequent ajax requests have to wait on the previous ones before executing - however we have to still offer the ability that ajax requests could be executed regardless if another ajax request is currently running or not, only ajax requests modifying the form/the form build id have to be put in a queue or something similiar.

We need to solve this both in FormBuilder (not allowing processing of the same form build id twice) on the server side and in ajax.js (wait submitting with the same form build id) on the client side.

Remaining tasks

User interface changes

API changes

Data model changes

CommentFileSizeAuthor
#70 interdiff-64-70.patch3.4 KBamoebanath
#70 2830295-70.patch11.92 KBamoebanath
#69 interdiff-64-69.patch3.4 KBamoebanath
#69 2830295-69-2.patch3.54 KBamoebanath
#67 2830295-64.patch11.92 KBGauravvvv
#65 interdiff_64-65.txt579 bytesRatan Priya
#65 2830295-65.patch11.98 KBRatan Priya
#64 interdiff_61-64.txt3.61 KBRatan Priya
#64 2830295-64.patch12.01 KBRatan Priya
#61 interdiff-58_61.txt1.21 KBGauravvvv
#61 2830295-61.patch15.16 KBGauravvvv
#58 2830295-58.patch15.19 KBpbouchereau
#56 2830295-56.patch15.19 KBpbouchereau
#48 2830295-48.patch3.71 KBGauravvvv
#46 2830295_46.patch6.85 KBbogdan.dinu
#33 interdiff-27-31.txt13.66 KBsardara
#33 interdiff-31-33.txt7.88 KBsardara
#33 2830295-33.patch15 KBsardara
#31 interdiff-27-31.patch13.66 KBsardara
#31 2830295-31.patch14.19 KBsardara
#29 interdiff_23-27.txt17.2 KBAndras_Szilagyi
#29 interdiff_23-28.txt5.84 KBAndras_Szilagyi
#28 2830295-concurrent_ajax_d8-28.patch11.25 KBAndras_Szilagyi
#27 2830295-concurrent_ajax-27.patch11.28 KBAndras_Szilagyi
#23 2830295-concurrent_ajax_d8-23.patch12.98 KBAndras_Szilagyi
#23 2830295-concurrent_ajax-23.patch13.26 KBAndras_Szilagyi
#18 2830295-concurrent_ajax_d8-18.patch13.16 KBAndras_Szilagyi
#17 2830295-concurrent_ajax-17.patch13.44 KBAndras_Szilagyi
#16 2830295-concurrent_ajax-16.patch11.9 KBAndras_Szilagyi
concurrent_ajax_form_posts_cause_data_loss.patch6.42 KBhchonov
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

hchonov created an issue. See original summary.

hchonov’s picture

Issue summary: View changes
catch’s picture

Priority: Critical » Major

Protection against this sounds good, but we don't count a lost form submission as critical per https://www.drupal.org/core/issue-priority. Also is this significantly different from two non-AJAX forms being submitted for say config save - the last one to hit the server will win in that case.

Status: Needs review » Needs work

The last submitted patch, concurrent_ajax_form_posts_cause_data_loss.patch, failed testing.

catch’s picture

Also it's so, so good that we can have test coverage for concurrent AJAX, nice one!

hchonov’s picture

Issue summary: View changes
cspitzlay’s picture

A queue for ajax requests could be problematic as the response of the first request could alter the DOM in a way that makes the sending of the second one wrong.
For example, if the button that triggers the second request is removed by the response to the first one it would not make sense or even be a logical error to still execute the second request.

hchonov’s picture

Then we should not allow the execution of following ajax requests but only the ones that could alter the DOM - that would be submits. However we have to allow for certain submits to be executed even if currently one is running, e.g. the autosave_form module will otherwise break but for it to run during other ajax requests are running would be safe as it has a logic preventing it from altering the form build id on the page and is also not altering the form in any way.

I would say in \Drupal\Core\Render\Element\RenderElement::preRenderAjaxForm for submit form elements we have to set
$element['#ajax']['blocking'] = TRUE; and then add it to the drupalSettings for the ajax element and then in ajax.js do not allow the concurrent execution of ajax instances having both the blocking property set.

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.

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.

Andras_Szilagyi’s picture

Had the same issue, I've implemented what was discussed and amended the failing test

Andras_Szilagyi’s picture

fixed some issue with the previous patch

Andras_Szilagyi’s picture

for those of use who need this in D8

Andras_Szilagyi’s picture

Status: Needs work » Needs review

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.

sinn’s picture

#18 works for me - when ajax request is in progress, pressing on another button does nothing and concurrent ajax request isn't triggered.

I would propose to disable ajax buttons (make them inactive) if there is some ajax request is in progress - it can be unclear for users why they press the button but nothing happens.

joevagyok’s picture

I tested manually and it works. I made a code review on the patch as follows:

  1. +++ b/core/misc/ajax.es6.js
    @@ -726,6 +737,12 @@
    +    // An ajax callback can creating disrupting changes so
    +    // we don't permit concurrency if its a blocking request.
    

    This should be rephrased.

  2. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +class FormAjaxTest extends WebDriverTestBase {
    

    Class name could refer to the comment and follow the rest of the test class namings with Ajax like: AjaxFormResponseTest.php

  3. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +   * Tests that sending a second ajax request while the first one is still
    +   * running the form will be rebuilt and saved correctly.
    

    Let's simplify this annotation to Tests concurrent ajax form requests and provide more explanation as a comment in the function.

  4. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +  public function testAjaxRequestBeforeCompletingPreviousAjaxRequest() {
    

    This could become: testConcurrentAjaxRequests()

  5. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +    $web_assert = $this->assertSession();
    

    $web_assert should become $assert_session.

  6. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +    // check that we can't submit the second ajax request.
    

    This comment should start with capital letter.

  7. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +    // Ensure second requests have been processed.
    

    Request should be singular here.

  8. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +      $page_field_value = $this->getSession()->getPage()->findField($locator)->getValue();
    

    We have the $page variable to use here.

  9. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +    $this->drupalPostForm(NULL, [], t('Save'));
    

    I think we don't need t() here.

  10. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +    // Assert that all the fields contain the value that we have set.
    

    This comment is not required.

  11. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormAjaxTest.php
    @@ -0,0 +1,144 @@
    +      $page_field_value = $this->getSession()->getPage()->findField($locator)->getValue();
    

    We have the $page variable to use here.

Andras_Szilagyi’s picture

@joevagyok, @sinn, thank you both for your feedback, I've implemented above. About the suggestion with disabling the buttons, I've thought about it.. for now I would not do it for 2 concerns: once this is merged other elements will be able to call blocking requests, and potentially, not all buttons will be blocking. It can be done but I think for now the visual aids like throbber and the waiting message are sufficient.

sardara’s picture

Status: Needs review » Needs work
  1. +++ b/core/misc/ajax.es6.js
    @@ -12,6 +12,13 @@
    +  drupalSettings.ajaxAllowed = true;
    

    I don't think this belongs inside the drupalSettings object. I'd put it inside the Drupal.ajax object, which olds the ajax instances already and some other data.

  2. +++ b/core/modules/field/tests/modules/field_plugins_test/src/Plugin/Field/FieldWidget/TestFieldWaitingWidget.php
    @@ -0,0 +1,29 @@
    + *   label = @Translation("Text field waiting"),
    

    The name is little representative of what the field widget is for.
    But anyway, I would remove the current approach altogether, see below.

  3. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormResponseTest.php
    @@ -0,0 +1,143 @@
    +class AjaxFormResponseTest extends WebDriverTestBase {
    

    We are not testing the response at all though, we are testing concurrency.

  4. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormResponseTest.php
    @@ -0,0 +1,143 @@
    +  protected static $modules = ['node', 'field_plugins_test'];
    

    We shouldn't write the test depending on node and fields. We should use the already existing AJAX test module to provide a new route with the slow AJAX response behaviour.

The patch works well, I agree with the UX issue of clicking other buttons but nothing happens. I also agree that it's a bit limited by the throbber presence. We can see what UX experts think.

sardara’s picture

+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -544,6 +544,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
+        'blocking' => FALSE,

I checked better why we are doing this and I noticed it's needed because an AJAX response triggers additional AJAX requests.
I think the solution should be adding the ajaxAllowed = true also inside the success method, the one defined in the ajax.options. At that point the response has been validated and the various response commands are processed inside Drupal.Ajax.prototype.success. So we can allow subsequent requests to start. The one in the complete method should be kept as it will allow resetting the values after responses with errors.

Andras_Szilagyi’s picture

Andras_Szilagyi’s picture

Andras_Szilagyi’s picture

FileSize
5.84 KB
17.2 KB
sardara’s picture

Assigned: Unassigned » sardara
sardara’s picture

I reworked the tests to cover more scenarios.
The provided patch here has some tests that are meant to fail to unravel some bugs present in 27.
In #25 I asked to not depend on node for the tests, which I did in this patch but I admit now the tests is a bit more obscure, as it doesn't show the data loss as clearly as using the widget. I could add some visual output of the current counters, but at the same time this is a test so not sure if it's needed.
There's still a scenario that is not covered and I'm not sure how to tackle it. A "slow, non-blocking" request could trigger a subsequent blocking request. If any other requests that change the form have been started during the "slow, non-blocking" request but before the subsequent blocking request, the form would be outdated. I think that the developer should mark the ""slow, non-blocking" trigger element correctly to prevent this scenario.

sardara’s picture

Status: Needs work » Needs review
sardara’s picture

Assigned: sardara » Unassigned
FileSize
15 KB
7.88 KB
13.66 KB

Actually non-blocking requests should always be allowed, even if other requests are being executed. I refactored the tests and the code to mark this behaviour.
As usual I uploaded the previous interdiff with the wrong extension, so I'm re-uploading it here.

sardara’s picture

+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ConcurrentAjaxRequestsTest.php
@@ -0,0 +1,104 @@
+    $this->clickLink('Non-blocking request');
+    // Wait for the non-blocking request to complete successfully.
+    $this->assertTrue($assert_session->waitForText('Current theme: stark', 2000));

I don't like this part as we are waiting an arbitrary 2 seconds, which could cause random failures. The thing is we need to have the slow request run long enough for this request to be over, to test that non-blocking requests do not reset the js flag of blocking requests as it was happening in #31.

The last submitted patch, 31: 2830295-31.patch, failed testing. View results

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.

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.

darvanen’s picture

This is not limited to AJAX. I have experienced this via graphQL as well.

I think a better place for any fixes of this sort of thing would be in the Entity API, perhaps some kind of lock like you get in databases where if you read with the intention of saving the result it locks any further reads of the same kind.

catch’s picture

We already have EntityChangedConstraint/EntityChangedConstraintValidator in core, however there are some outstanding issues for translations and revisions (#2920889: EntityChangedConstraintValidator doesn't take adding, removing and reverting translations into account and related).

I think the entity system is the better place to continue things like this - even if we added locking to the AJAX system, you can have conflicts between AJAX and non AJAX forms, or web services etc. So adding locking AJAX just at the AJAX level could lead to a false sense of security.

sardara’s picture

I don't like too much the "locking" thing either, but the issue is present also at form level for non-entity forms.
The test I attached in #33 uses a normal form to prove the bug. So I am not sure that fixing this at entity system level would help the remaining form scenarios.

catch’s picture

Category: Bug report » Feature request
Priority: Major » Normal

I'm moving this to a feature request for the forms system.

For entities we need protection at the entity level, since they're not only updated via forms. Other subsystems where this is a problem could also implement validation. Adding it at the form API level might an idea but doesn't really feel like the cause of actual bugs which is dependent on what's being submitted.

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.

Chewie’s picture

Patch #33 works well for me. It solves my problem.
I reproduced the original issue with the node where I have paragraphs (paragraphs module) with multiple nested paragraph entities and when I clicked (with a very short time delay) "Collapse" and add new item (paragraph entity) buttons. As result, I experienced showing different not related fields.
After applying the patch I cannot do this anymore (trigger other ajax requests), so I don't have such a problem. As I understand it is related to an incorrect update of the form build id.

bogdan.dinu’s picture

This is a big issue on content types with a large paragraph structure.
I've updated the patch to work with D9.5

Manoj Raj.R’s picture

Patch #33 works well for me.

Gauravvvv’s picture

I have attached the patch for d10. Please review

bogdan.dinu’s picture

The patch I submitted earlier breaks paragraphs forms. DO NOT USE!
Also, after doing some more tests, I realized the issue has been fixed in the core and there is no more need for the patch.

smustgrave’s picture

Status: Needs review » Needs work

@bogdan.dinu you said this has been resolved? Do you know where it was fixed? So we can track down the ticket and move over credit for users who helped here.

bogdan.dinu’s picture

@smustgrave unfortunately no. I don't know where this was fixed. I just tried locally on D9.4 without the patch and saw that there is no more data loss.

catch’s picture

This issue is pretty old, so it might have been #2837022: Concurrently editing two translations of a node may result in data loss for non-translatable fields or a similar issue.

We could also mark it duplicate of #2920251: Offer concurrent editing protection for all entities by including the changed timestamp to each entity form maybe which is still active and covering similar ground.

smustgrave’s picture

pbouchereau’s picture

Category: Feature request » Bug report
Status: Closed (duplicate) » Needs work

This is not fixed, I'm still experiencing this on a Drupal 9.4.9 project with bricks and on a Drupal 9.5.9 project with paragraphs.
And how could this be marked as a duplicate of an issue tied to the entity system component? That's disregarding the comment in #41 about non-entity forms.
I also disagree about the category change. To me, if a form breaks when clicking on a button, it's a bug (sorry if there's something I misunderstood).

Hiding patch per #49.

pbouchereau’s picture

Ok, I think I see.
The issue's title and summary focus on the backend part of the problem, which should indeed be handled by validation.
But what brought me here is the mess caused by this bug on frontend: when it occurs, the whole paragraph/brick subform gets replaced by other unrelated fields.
What can be lost is not saved data, just contributed content.
Unless I miss something, I don't think this will get fixed with #2920251: Offer concurrent editing protection for all entities by including the changed timestamp to each entity form, will it?
If not, what's the best way to handle this, change this issue's title and summary or create a new one?

pbouchereau’s picture

Rerolled patch #33.

pbouchereau’s picture

Status: Needs work » Needs review
pbouchereau’s picture

FileSize
15.19 KB

Here's patch #33 re-rolled without mysterious filename change...

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update

Can the issue summary be updated to include why this is different then the related issue. + remaining tasks, etc.

Also please include any interdiff

smustgrave’s picture

Gauravvvv’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
15.16 KB
1.21 KB

Fixed build error and attached interdiff for same. and updated issue summary

smustgrave’s picture

Status: Needs review » Needs work

Patch needs to be updated for 10.1.x-dev,

Maybe even 11.x-dev at this point.

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.

Ratan Priya’s picture

Version: 11.x-dev » 10.1.x-dev
Status: Needs work » Needs review
FileSize
12.01 KB
3.61 KB

Re-rolled patch #61 for 10.1.x-dev.
Needs review.

Ratan Priya’s picture

smustgrave’s picture

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

We really should target the latest development branch and backport

Gauravvvv’s picture

I have re-rolled the patch #61, for 11.x-dev. please review

smustgrave’s picture

Status: Needs review » Needs work

Can the issue summary be updated with proposed solution.

"We need to solve this both in FormBuilder" but how is it being solved? Helps the reviewers.

amoebanath’s picture

Rerolled patch #64 for 10.2.x, as it doesn't apply there

amoebanath’s picture

FileSize
11.92 KB
3.4 KB

Uploaded the wrong copy -_-

Rerolled patch #64 for 10.2.x, as it doesn't apply there