Overview
Follow-up to #3490565: Add rudimentary conflict prevention to the preview end-point
In that issue we put in logic to throw an exception if client didn't send the latest auto-save hash for the entity and regions. This prevents 2 users for editing at the same time and wiping out each others changes
Trying to get the e2e tests to pass I discovered that rapid fire requests could cause this exception to be trigger without only 1 user editing the page.
So we decided to scope #3490565 to just the backend parts
In creating the front-end solution we discovered 2 cases which the back-end solution made in #3490565 did not cover
-
A browser session that opened an entity in XB right after the entity was published that therefore has no auto-save data but remains open and inactive while in another browser session the entity is edited and then published again and after that the original browser session makes a request with the stale entity/layout information thus wiping out the changes made since that browser session was open
This is because we cannot determine the difference between
- a client that has no autosave hashes because the entity has just been published and they have the latest changes
- a client that has no autosave hashes because at time the browser made the last request there were no auto-saves because the entity had just been published, but now the version they have locally is 1 or many published versions behind
Because
\Drupal\experience_builder\Controller\ApiLayoutController::getAutoSaveHashes()only returns the hash data for entities that have current auto-save entries, in both scenarios above it will be return an empty array. therefore we don't send enough information back and forth between the client and the server to differentiate between these 2 scenarios.If we don't differentiate between these 2 scenarios then client in the second case could submit a request that would pass
validateAutoSaves()but would save information in the auto-save that would wipe out the published changes and revert back to previous version of the entity. -
Rapid fire client requests, or requests with a slow network
Because in 0.x
\Drupal\experience_builder\Controller\ApiLayoutController::validateAutoSaves()will throw an exception if the client does not have the latest auto-save hashes, 1 client request cannot be fired until the previous client request has returned with the auto-save hashes. Because we don't want to lock the UI until requests come back from the server the current solution will not work
Proposed resolution
- To handle first case in the overview where a client session has old entity/layout/model data AND no auto-saves is able to make a request:
Change
\Drupal\experience_builder\Controller\ApiLayoutController::getClientAutoSaveDatato return nested array with both- hash: the hash from the auto-save entry
- autoSaveRevision: (the latest saved revision if revisionable otherwise a hash of
$entity->toArray()) + changed time if supported
In practice for config entities this would just be the hash of the entity array. For nodes and pages it would be [REVISION_ID-CHANGED_TIME]
This means that
\Drupal\experience_builder\Controller\ApiLayoutController::getAutoSaveHashes()would never return an empty array even if there were no auto-saved entries. Therefore the server can distinguish between a client session that was started 1 or more saved versions ago and a client that was just opened, even if they both have no auto-save hashes -
Handing "Rapid fire client requests, or requests with a slow network" section in the summary:
Because we don't want to lock the UI until the last request returns and we trust the the client to keep track of updated data if requests from the same client session, we don't need to check the hashes are the same. This allows a more seamless UX for users on slow networks. But we don't want a user to be able to switch to different browser tab and have the hashes not be checked because it might not have the latest changes. So for this reason a client session is the same browser tab but not some browser session and different tabs.
With each request to
experience_builder.api.layout.patchandexperience_builder.api.layout.postwe will require a uniqueclientInstanceId. The server will check if this client id matches the lastclient_idin the auto-save entry for all of the entities being edited, page or node + regions. If they all match then the auto-hashes do not need to be checked for matches but theautoSaveRevisionvalues will still be checked. -
In
\Drupal\experience_builder\Controller\ApiLayoutController::validateAutoSaves()remove the special casing for phpunit tests.
User interface changes
If another user has triggered an auto-save since the current user has made changes then the current user will get an error
| Comment | File | Size | Author |
|---|---|---|---|
| #55 | 1127-2.png | 460.56 KB | neha_bawankar |
| #55 | 1127-1.png | 150.9 KB | neha_bawankar |
Issue fork experience_builder-3526907
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
tedbowComment #3
tedbowComment #5
tedbowI brought in the changes from #3490565: Add rudimentary conflict prevention to the preview end-point because I made progress getting the e2e test to pass, so I thought I would the current state
Comment #6
tedbowThe only 2 e2e tests that are failing now are `entity-form-field-types-test.cy.js` which does pass for me locally and `media-library.cy.js` which did fail locally but I didn't see an 409 request errors in the log. So maybe these are unrelated random test errors? Not sure, retesting
Comment #7
larowlanComment #8
wim leersAFAICT this is also a beta blocker? Otherwise we haven't solved the current auto-save-data-loss scenarios described in #3490565: Add rudimentary conflict prevention to the preview end-point?
Comment #9
nagwani commentedComment #10
tedbowI am actually surprised that no existing e2e tests failed 🎉
2 new tests failed
rapid-component-addition.cy.js: I am not sure this can really be test reliability give the random test fails we have in e2e and this test would solely rely on timing. I am removing itauto-save-conflict.cy.js: This was attempt to try to simulate 2 users with 2 different browser sessions having a conflict. I am not sure how to do this in cypress and searched around a little and couldn't get it to work. Someone with more experience might have better luckComment #11
jessebaker commentedI am having a hard time coming up with a robust solution on the client side.
There are two different issues.
The first issue is concurrent editing from two different client instances. That can be either two different users or the same user in two different tabs.
This is solved by the autoSaves hash being passed back and forth and checked and a warning being issued in the instance that a conflict/mismatch arises. ✅
The second issue is proving more difficult to tackle. This issue occurs when a single user in a single client is able to trigger the mismatch scenario by making multiple requests in quick succession.
The issue happens in the following scenario:
Potential solutions (to problem 2)
I've investigated the above solutions and have the following conclusions:
Comment #12
tedbowI think there is pontential problem with #11 5)
Is this true? What if the client makes 3 request in sequence and for some reason request 1 takes a long time to connect? Basically request 2 and 3 are processed and then request 1 gets processed. Wouldn't that wipe out changes in 2 and 3? Is what I suggesting even possible, maybe on randomly sluggish networks? Maybe ok to handle this edge case?
Comment #13
larowlanIt may be that #3492065: Enable concurrent editing: Replace the postPreview action with atomic equivalents is a better use of effort if the approach here (which was meant to be a short-term quick fix) is not possible.
Comment #14
wim leers+1 for re-assessing #3492065: Enable concurrent editing: Replace the postPreview action with atomic equivalents. It's now >6 months old, but still relevant.
Marking and leaving assigned to @tedbow to convey we should first check that avenue.
Comment #16
tedbowre #13 and #14 I am currently a -1 on doing #3492065: Enable concurrent editing: Replace the postPreview action with atomic equivalents because
if we "Enable concurrent editing" there probably will be some very tricky UX problems and we should figure the desired outcomes before we start making the solution
Will concurrent editing just work perfectly and we all agree exactly what that means so there is no describe the UX? 😜
for instance what happens if I am editing props for component, which should show the updates I as type, at the same time another user does another operation that moved that operation at out of view? This would either be by the other user reorder components at the same level as mine or adding another component above it that puts my out of view. Or it could be changing the props of component above mine that changes the height.
If so think a lot of the problems documented in #11 still exist just at a different level down
I think #12 out of sync requests from the same user could still be problem. Especially because we send auto-save requests as you type for single prop of single component
So I think you we wouldn't be saving time but not having to solve #11 it would just be a different level, per component
Comment #17
tedbowJust wanted to make clear my comments in #16 were all with the idea that we have limited time before Beta1 - re #3515932: Milestone 1.0.0-beta1: Enable creation of non-throwaway sites and probably have a lot of other issues. The product decision has been made that we don't need concurrent editing in Beta1. Not sure about GA. but I assume we have a lot of other features that were prioritized in for Beta1
If we had more development resources and there weren't other features that were deemed beta blockers I would be all for getting concurrent editing in before beta1. But also if also if we made that decision won't the first order of business be defining the UX for that?
Comment #18
jessebaker commented@tedbow I've found a bug that I think needs to be addressed server side.
Steps
I suspect the latest autoSave hash on the server side is either being deleted or updated when changes are published but not communicated back to the FE so the client ends up with an out of date hash.
I think that either a) publishing should not update the hash, or b) the response from a publish action needs to include the updated hash to the FE can store it for the next request.
Comment #20
tedbow@jessebaker I found the problem that is causing #18
It is because during Publish we delete the auto-save entry, including the hashes and the client instance id, for the items that were published. This is because there is no auto-saved information at this point. Everything matches what should have been just saved. We don't want this to show up "Review X changes"
So when the client sends the auto-saved hashes when find a mismatch, client has auto-saved hashes and the server doesn't. Now we could change the server side mechanism to stored this information separate from the actual auto-save information we will need to publish but that would make the server side more complex. Also it is also possible that this particular entity will never be edited within XB again, so this keeps us from having to keep auto-saved hash information for any entity that was ever edited in XB forever.
So I was going to suggest that when submit a publish request and get response from the server that you delete the auto-save hashes. That would solve the problem in #18 but I did make me think of another problem, that actually exists right now
I think the effect would be that User 1 would make an auto-saved version that would not be aware of the published changes and so would wipe out those changes when it was published again.
We could solve this by not deleting the auto-saved entry the server-side when publishing, and therefore throwing a conflict error for User 1. But again this would require us to keep auto-save hash information forever for all entities, which could start to add up on larger sites. Because we don't know if there is some browser session somewhere that was left open or not.
So I don't think this is a solution.
I think what we need to do is have the client be aware of the starting point version of the entity it is editing.
So for example for a page or node this would be version number,
vidorchangedproperty.Basically the server would return to the client, along with the "autosaves" key, a "verison_id" key of the last saved version on the server. The client would always return this key.
So in the example above User 1's last request would properly get a 409 error because it's version number would be 1 version number behind. But this would also require the client to request
/xb/api/v0/layout/xb_page/1after publishing. Though after #3516517: [META] Selective Publishing and Reverting the client could try to be more selective and not request if the current page was not publish it would probably be safer to always just request it.There are probably unrelated reasons why the client should always call
/xb/api/v0/layout/xb_page/1after publishing. For example Drupal hashook_entity_presavewhich allows altering an entity before it is saved. Of course XB will not be aware of any changes made in custom code here, therefore if XB doesn't call/xb/api/v0/layout/xb_page/1after publishing the auto-save request could wipe out any changes that were made in anyhook_entity_presavecodeComment #21
tedbowI pushed up 86ce2085 which will return the revision id under "auto" . Probably at least
autoSaves: Record<string, string>;will need to updated on the front-end because this now an object.but it seems to work manually testing
Comment #22
tedbowNote I have not had time to fix the phpunit tests but the e2e test passed so that is good sign. Just to need to update the phpunit tests to expect to a nested structure with revision info from the client.
Comment #23
jessebaker commented@tedbow re: #18 #20
1) I think perhaps we should create/address this in a follow up
2) +1 to using 'changed' or even perhaps 'lastModified' and using a timestamp - I could see that information actually being useful elsewhere in the UI at some point.
3) After publishing I think we already refetch some data so I don't see it being a problem to update the page.
Comment #24
jessebaker commented#20
As of my latest commit the FE now re-requests the page after publishing changes IF the current page is amongst the list of pending changes.
In my testing there is one remaining bug that I can see that is quite tricky to reproduce.
From what I can see, the issue is that the autosave hash has autoSaveRevision but it's always
"1". Nothing I do ever seems to increment that number (that is actually a string).Comment #25
tedbowre #24 ok might know what is happening.....
Comment #26
tedbow@jessebaker thanks for testing this. I was testing this with nodes and it was working.
Basically entity have a seperate revision id but if we create a new revision on every save this won't increment. I guess Page is not incrementing which I guess makes sense because we don't have a way to revert to old revisions or view old revisions AFIACT
So I used the "changed" time also in the revision for the client
Comment #27
tedbowchanging title to make clear we are only handling content entities, node and pages with layouts
We will need this too #3530827: For config entities add client support conflict detection via the `autoSaves' key
Comment #29
tedbowComment #30
effulgentsia commentedComment #31
wim leersHow do I begin reviewing this MR? There's lots of comments here, and the issue summary doesn't talk about the
client_idandclientInstanceIdthis MR is adding.Can we get an up-to-date issue summary to facilitate a review? And ideally also sprinkle
ℹ️ …comments over the MR to make it clearer still 🙏P.S.: if #3512616: Document the reasons for not using Workspaces for saving in XB 1.0.0 had landed, then I'd have asked to update that ADR. In fact … any reason not to do that now, @tedbow? 😇🙏 That'd be even better than an issue summary update!
Comment #32
wim leersComment #33
tedbowre-rolled, added some more code comments and updating the summary but not finished. Leaving assigned to myself as I am working on the summary
Comment #34
tedbowComment #35
tedbowupdated more.... but still more to update
leaving assigned to myself
Comment #36
tedbowComment #37
tedbowChange the MR so that\Drupal\experience_builder\Controller\ApiLayoutController::patchonly validates the region that contains the component being updated.I chatted with @jessebaker, @wimleers and @effulgentsia about this. Right now we are just going to conflict if all entity auto-saves don't match because it forces the user refresh the browser and we can then certain other parts of the front-end are in-sync after the refresh
I need to add test to demonstrate how this works
Comment #38
tedbowAdded test coverage for regions to
\Drupal\Tests\experience_builder\Kernel\ApiLayoutControllerPostTest::testOutdatedAutoSave.\Drupal\Tests\experience_builder\Kernel\ApiLayoutControllerPatchTest::testalready had a test for a conflict with a regionComment #39
tedbow@wim leers Re: #31 I have update the summary. I have had a chance to update #3512616: Document the reasons for not using Workspaces for saving in XB 1.0.0 but I think this issue can be reviewed. Most of the back-end changes are in
\Drupal\experience_builder\Controller\ApiLayoutController::validateAutoSaves.\Drupal\Tests\experience_builder\Kernel\ApiLayoutControllerPostTest::testOutdatedAutoSaveis the most explicit test of this.\Drupal\Tests\experience_builder\Kernel\ApiLayoutControllerPatchTest::test()also catchesConflictHttpExceptionwhich is thrown fromvalidateAutoSaves()Comment #40
tedbowCreated follow-up #3532056: For less conflict errors only validate region auto-save on layout PATCH, in that issue I mention we might just want to wait until #3492065: Enable concurrent editing: Replace the postPreview action with atomic equivalents. The back-end work for #3532056 would be simple but I am not sure about the front-end changes that would be needed. We would need to send different info from the client but we might need to reload differently.
Also added a todo in the code
Comment #41
nagwani commentedComment #42
larowlanLeft a review (which I assume is why this was assigned to me)
Unassigning
Comment #43
tedbowI addressed a couple points in @larowlan's review
Comment #44
larowlanReplied on the issue
There's a few open threads but only one question from me about using ::toArray vs ::normalizeEntity
Comment #45
tedbowAssigning back to @larowlan because I answered his question on the MR
Comment #46
larowlan@tedbow's response regarding using the hash makes sense - thanks
Comment #47
tedbowadding blocker tag as this blocks #3530827: For config entities add client support conflict detection via the `autoSaves' key and #3530258: Add rudimentary conflict prevention to the Config Auto-save endpoint
Comment #48
tedbowComment #49
tedbowI going to RTBC this based on @bnjmnm MR approval and @larowlan saying the backend changes look good
Comment #51
wim leersI trust Lee & Ben, so going ahead and merging! 🚢
Comment #52
wim leersApproved by proxy: since @larowlan +1'd the BE.
Unfortunately:
@tedbow: can you rebase locally & merge once it's green again? 🙏
Comment #54
tedbow🎉
Comment #55
neha_bawankar commentedTested changes on branch 0.x , for following scenarios :
Scenario
Result
Status
Comment #56
wim leersThanks! 🥳
Comment #57
tedbowre #55 @neha_bawankar wow thanks for the detailed testing report!!!