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
Comment | File | Size | Author |
---|---|---|---|
#70 | 2830295-70.patch | 11.92 KB | amoebanath |
#67 | 2830295-64.patch | 11.92 KB | Gauravvvv |
| |||
#65 | interdiff_64-65.txt | 579 bytes | Ratan Priya |
#65 | 2830295-65.patch | 11.98 KB | Ratan Priya |
#64 | interdiff_61-64.txt | 3.61 KB | Ratan Priya |
Comments
Comment #2
hchonovComment #3
catchProtection 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.
Comment #5
catchAlso it's so, so good that we can have test coverage for concurrent AJAX, nice one!
Comment #6
hchonovComment #7
cspitzlayA 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.
Comment #8
hchonovThen 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.Comment #16
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commentedHad the same issue, I've implemented what was discussed and amended the failing test
Comment #17
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commentedfixed some issue with the previous patch
Comment #18
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commentedfor those of use who need this in D8
Comment #19
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commentedComment #21
sinn CreditAttribution: sinn for European Commission and European Union Institutions, Agencies and Bodies commented#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.
Comment #22
joevagyok CreditAttribution: joevagyok for European Commission and European Union Institutions, Agencies and Bodies commentedI tested manually and it works. I made a code review on the patch as follows:
This should be rephrased.
Class name could refer to the comment and follow the rest of the test class namings with Ajax like: AjaxFormResponseTest.php
Let's simplify this annotation to Tests concurrent ajax form requests and provide more explanation as a comment in the function.
This could become: testConcurrentAjaxRequests()
$web_assert should become $assert_session.
This comment should start with capital letter.
Request should be singular here.
We have the $page variable to use here.
I think we don't need t() here.
This comment is not required.
We have the $page variable to use here.
Comment #23
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commentedComment #24
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commented@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.
Comment #25
sardara CreditAttribution: sardara at Randstad Digital for European Commission and European Union Institutions, Agencies and Bodies commentedI 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.
The name is little representative of what the field widget is for.
But anyway, I would remove the current approach altogether, see below.
We are not testing the response at all though, we are testing concurrency.
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.
Comment #26
sardara CreditAttribution: sardara at Randstad Digital for European Commission and European Union Institutions, Agencies and Bodies commentedI 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 thesuccess
method, the one defined in theajax.options
. At that point the response has been validated and the various response commands are processed insideDrupal.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.Comment #27
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commentedComment #28
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commentedand one for d8
Comment #29
Andras_Szilagyi CreditAttribution: Andras_Szilagyi for European Commission and European Union Institutions, Agencies and Bodies commentedComment #30
sardara CreditAttribution: sardara at Randstad Digital for European Commission and European Union Institutions, Agencies and Bodies commentedComment #31
sardara CreditAttribution: sardara at Randstad Digital for European Commission and European Union Institutions, Agencies and Bodies commentedI 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.
Comment #32
sardara CreditAttribution: sardara at Randstad Digital for European Commission and European Union Institutions, Agencies and Bodies commentedComment #33
sardara CreditAttribution: sardara at Randstad Digital for European Commission and European Union Institutions, Agencies and Bodies commentedActually 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.
Comment #34
sardara CreditAttribution: sardara at Randstad Digital for European Commission and European Union Institutions, Agencies and Bodies commentedI 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.
Comment #39
darvanenThis 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.
Comment #40
catchWe 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.
Comment #41
sardara CreditAttribution: sardara for European Commission and European Union Institutions, Agencies and Bodies commentedI 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.
Comment #42
catchI'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.
Comment #44
Chewie CreditAttribution: Chewie at Arhs for European Commission and European Union Institutions, Agencies and Bodies commentedAdded references to some related issues.
Comment #45
Chewie CreditAttribution: Chewie at Arhs for European Commission and European Union Institutions, Agencies and Bodies commentedPatch #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.
Comment #46
bogdan.dinu CreditAttribution: bogdan.dinu as a volunteer and at United Nations commentedThis is a big issue on content types with a large paragraph structure.
I've updated the patch to work with D9.5
Comment #47
Manoj Raj.R CreditAttribution: Manoj Raj.R as a volunteer and at ITT Digital commentedPatch #33 works well for me.
Comment #48
Gauravvvv CreditAttribution: Gauravvvv at Axelerant for Drupal India Association commentedI have attached the patch for d10. Please review
Comment #49
bogdan.dinu CreditAttribution: bogdan.dinu as a volunteer and at United Nations commentedThe 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.
Comment #50
smustgrave CreditAttribution: smustgrave at Mobomo commented@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.
Comment #51
bogdan.dinu CreditAttribution: bogdan.dinu as a volunteer and at United Nations commented@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.
Comment #52
catchThis 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.
Comment #53
smustgrave CreditAttribution: smustgrave at Mobomo commentedWill move over to #2920251: Offer concurrent editing protection for all entities by including the changed timestamp to each entity form
Comment #54
pbouchereau CreditAttribution: pbouchereau as a volunteer commentedThis 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.
Comment #55
pbouchereau CreditAttribution: pbouchereau as a volunteer commentedOk, 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?
Comment #56
pbouchereau CreditAttribution: pbouchereau as a volunteer commentedRerolled patch #33.
Comment #57
pbouchereau CreditAttribution: pbouchereau as a volunteer commentedComment #58
pbouchereau CreditAttribution: pbouchereau as a volunteer commentedHere's patch #33 re-rolled without mysterious filename change...
Comment #59
smustgrave CreditAttribution: smustgrave at Mobomo commentedCan the issue summary be updated to include why this is different then the related issue. + remaining tasks, etc.
Also please include any interdiff
Comment #60
smustgrave CreditAttribution: smustgrave at Mobomo commentedComment #61
Gauravvvv CreditAttribution: Gauravvvv at Axelerant for Drupal India Association commentedFixed build error and attached interdiff for same. and updated issue summary
Comment #62
smustgrave CreditAttribution: smustgrave at Mobomo commentedPatch needs to be updated for 10.1.x-dev,
Maybe even 11.x-dev at this point.
Comment #64
Ratan Priya CreditAttribution: Ratan Priya at OpenSense Labs for DrupalFit commentedRe-rolled patch #61 for 10.1.x-dev.
Needs review.
Comment #65
Ratan Priya CreditAttribution: Ratan Priya at OpenSense Labs for DrupalFit commentedTried to fix failed custom commands.
Comment #66
smustgrave CreditAttribution: smustgrave at Mobomo commentedWe really should target the latest development branch and backport
Comment #67
Gauravvvv CreditAttribution: Gauravvvv at Axelerant for Drupal India Association commentedI have re-rolled the patch #61, for 11.x-dev. please review
Comment #68
smustgrave CreditAttribution: smustgrave at Mobomo commentedCan 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.
Comment #69
amoebanath CreditAttribution: amoebanath commentedRerolled patch #64 for 10.2.x, as it doesn't apply there
Comment #70
amoebanath CreditAttribution: amoebanath commentedUploaded the wrong copy -_-
Rerolled patch #64 for 10.2.x, as it doesn't apply there