Skip to content

Conversation

@max-nextcloud
Copy link
Collaborator

📝 Summary

When creating a new session
check for all existing sessions
to take those into account
that are only preserved because of unsaved changes.

It there still aren't any sessions
make sure to load a fresh editing session
even if there are unsaved steps.
These steps most likely originate from a race condition when closing the last editing session and pushing steps at the same time.

Background

We were seeing empty editing sessions in production while the markdown files had some content.
This was happening because there were some steps still around even though all sessions had been closed.

When the last session was closed
steps were pushed at the same time.
The push passed the session check before the session was cleared. The session data and the steps were cleared
and only then the new step was added.

This left the database in an inconsistent state:

  • just a few late steps instead of a full history
  • no sessions these steps would belong to

Fix

Instead of trying to prevent this race condition
detect this state and recover from it.

Scenarios

Unsaved changes

Sessions are sometimes aborted without close request in the middle of editing. In this case all steps are present in the database and the session also remains.

The session will not be cleaned up during resetDocument. Instead resetDocument throws DocumentHasUnsavedChagesException.

We can detect this scenario because even removeInactiveSessions also takes remaining steps into account and leaves sessions with steps alone. Therefore sessions will remain in apiService->create(...) and freshSession won't be set.

Leftover steps

Steps are only cleaned up when the last active session is closed. Even if a few steps manage to sneak in after the clean up the session will be removed
and so remainingSessions will be empty during create. Therefore freshSession will be set and the content will be send out. The leftovers won't be cleaned up until the file has been saved again. But that should be fine. They will not impact the editing.

🏁 Checklist

  • Code is properly formatted (npm run lint / npm run stylelint / composer run cs:check)
  • Sign-off message is added to all commits
  • Tests (unit, integration and/or end-to-end) passing and the changes are covered with tests
  • Documentation is not required

@max-nextcloud
Copy link
Collaborator Author

/backport 4ffa4eb to stable26

@cypress
Copy link

cypress bot commented Mar 4, 2023

1 flaky tests on run #8866 ↗︎

0 140 0 0 Flakiness 1

Details:

fix: load fresh session if none are remaining
Project: Text Commit: d307be8713
Status: Passed Duration: 03:22 💡
Started: Mar 4, 2023 4:30 PM Ended: Mar 4, 2023 4:33 PM
Flakiness  cypress/e2e/sync.spec.js • 1 flaky test

View Output Video

Test Artifacts
Sync > recovers from a lost connection Output Screenshots

This comment has been generated by cypress-bot as a result of this project's GitHub integration settings.

@max-nextcloud
Copy link
Collaborator Author

No need to compile as this is server side fix only & cypress tests.

$freshSession = true;
try {
$this->documentService->resetDocument($file->getId(), $forceRecreate);
$freshSession = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, this means that we always start a fresh session here, also if resetDocument() throws a DocumentHasUnsavedChangesException, right? Could this lead to data loss of the unsaved changes? Probably we cannot do anything about it anway, as there is no session to apply the changes to anymore, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - that's exactly what this change is aiming for. Let me explain the reasoning in more detail.

We would not need the session to apply the steps - they are self contained - as long as all of them are present.

However if the session does not exist anymore it must have been removed. If steps are still present this only happens during a close call. So close was called on the session.

If other sessions are still around we won't run into this block anyway. If there were other sessions around during the close they would have been closed as well and would have cleaned up the steps. Therefore if we end up here the session must have been the last active session when it was closed. In that case it will have cleaned up it's steps and the remaining unsaved changes are just leftovers from the race condition and yes... we cannot apply them anymore because we lack the history leading up to them.

$remainingSessions = $this->sessionService->getAllSessions($file->getId());
$freshSession = false;
if ($forceRecreate || count($activeSessions) === 0) {
if ($forceRecreate || count($remainingSessions) === 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the impact of this change: we only create a fresh session now if "no session was found", instead of "no active session was found" before, right? So we will always try to restore sessions, even if they're not active anymore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Sessions become inactive after 5 minutes. But there may still be steps that were created during the session but never persisted. All the cleanup functions will leave those steps and their session in place (unless forceReload is set). This change makes it so we still recover these changes after more than 5 minutes.

When creating a new session
check for all existing sessions
to take those into account
that are only preserved because of unsaved changes.

It there still aren't any sessions
make sure to load a fresh editing session
even if there are unsaved steps.
These steps most likely originate from a race condition
when closing the last editing session and pushing steps at the same time.

We were seeing empty editing sessions in production
while the markdown files had some content.

This was happening because there were some steps still around
even though all sessions had been closed.

When the last session was closed
steps were pushed at the same time.
The push passed the session check before the session was cleared.
The session data and the steps were cleared
and only then the new step was added.

This left the database in an inconsistent state:
* just a few late steps instead of a full history
* no sessions these steps would belong to

Instead of trying to prevent this race condition
detect this state and recover from it.

Sessions are sometimes aborted without close request in the middle of editing.
In this case all steps are present in the database
and the session also remains.

The session will not be cleaned up during `resetDocument`.
Instead `resetDocument` throws `DocumentHasUnsavedChagesException`.

We can detect this scenario because even `removeInactiveSessions`
also takes remaining steps into account and leaves sessions with steps alone.
Therefore sessions will remain in `apiService->create(...)`
and `freshSession` won't be set.

Steps are only cleaned up when the last active session is closed.
Even if a few steps manage to sneak in after the clean up
the session will be removed
and so `remainingSessions` will be empty during `create`.
Therefore `freshSession` will be set and the content will be send out.
The leftovers won't be cleaned up until the file has been saved again.
But that should be fine. They will not impact the editing.

Signed-off-by: Max <[email protected]>
@max-nextcloud max-nextcloud force-pushed the debug/yjs-failing-initial-steps branch from 4ffa4eb to d307be8 Compare March 4, 2023 16:27
Copy link
Member

@mejo- mejo- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the extra explanations. Sounds sensible to me. And awesome that you were able to track down the race condition ❤️

@max-nextcloud max-nextcloud merged commit 65fad01 into main Mar 4, 2023
@delete-merged-branch delete-merged-branch bot deleted the debug/yjs-failing-initial-steps branch March 4, 2023 21:54
@backportbot-nextcloud
Copy link

The backport to stable26 failed. Please do this backport manually.

@juliusknorr
Copy link
Member

Manually backported with #3891

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants