Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

suppress self-stuttering #70

Merged
merged 2 commits into from
Jan 15, 2024
Merged

Conversation

loreanvictor
Copy link
Contributor

Summary

With current implementation of realtime collaboration, there was an issue of self-stuttering. This PR suppresses such stuttering by signing issued change operations and locally discarding broadcast changes that a client optimistically anticipates will be overwritten.

Problem Description

Current implementation of realtime collaboration relies on strongly idempotent changes being broadcast by clients, temporally sorted by the server and re-broadcast to all clients (including the client who issued the change initially).

  • Changes are strongly idempotent: a client applying a change would get the same result regardless of whether they have already, at some point in the past, applied the change or not, or how many times.
  • This yields eventual consistency, as each client in the end applies the same sequence of changes, broadcast by the server, overriding changes they have applied locally.

While this provides a simple and straight-forward solution for achieving eventual consistency, it has a "self-stuttering" side effect:

  1. client A applies change X locally, then broadcasts it
  2. client B applies change Y locally, then broadcasts it. change Y has the same scope as change X.
  3. both clients receive change X from the server and (re)apply it. Both their local states becomes the result of change X.
  4. both clients receive change Y from the server and (re)apply it. Both their local states becomes the result of change Y.

In this scenario, client A observes the state change as I -> X -> X -> Y. Since reapplying a change doesn't affect the state (idempotency), this is equivalent to observing the state sequence I -> X -> Y, which is correct.

Client B however, observes the sequence I -> Y -> X -> Y, i.e. the state changing and then reverting again (stuttering). I call this self-stuttering because it is caused by them eagerly applying changes originated by themself (change Y). The desired sequence would be I -> Y.

This is particularly an issue as it can occur even without another client issuing changes on the same scope. Imagine the client issuing constant changes on the same scope (for example, by dragging an element). The following sequence of events is possible (and probable):

  1. The client issues change X.
  2. The client issues change Y.
  3. The server broadcasts change X, the client (re)applies it.
  4. The server broadcasts change Y, the client (re)applies it.

In this scenario, the client will observe the state sequence I -> X -> Y -> X -> Y, without interference from any other collaboration peer.

Solution Details

The solution relies on each client optimistically assessing whether an incoming change from the server would be overridden by a following change in near future or not.

  • In reliable network conditions, a client can assume that any change they broadcast will be broadcast back at some point in near future.
  • Subsequently, if they receive another change on the same scope from the server before receiving their own change, they can assume that it will be overridden in near future.
  • The client can hence keep track of changes they have issued for a scope and discard incoming changes on that scope until they receive their own issued change.

In case of current implementation, JSON Patch's replace operation is not only strongly idempotent, but also the main cause of self-stuttering. Its scope can be tracked by each client by tracking changes done to a certain path. To ensure the same change is broadcast back by the server, each client can sign each operation with a unique hash and check the hash of incoming operations.

A PatchVerifier utility class has been added for this purpose:

// webapp/src/main/utils/patch-verifier.ts

export class PatchVerifier {
  public signOperation(op: Operation): SignedOperation;
  public sign(patch: Patch): SignedPatch;

  public isVerifiedOperation(op: Operation): boolean;
  public verified(patch: Patch): Patch;
}

signOperation() method checks if the operation is a replace operation, and in that case signs it with a unique hash and records its path and hash pair in a mapping. sign() signs all operations inside a patch.

isVerifiedOperation() checks whether some incoming operation is a signed replace operation or not. If it is, it checks whether its path is recorded in the aforementioned mapping. If not, then it verifies the operation (it can be applied without causing stuttering), otherwise it denies the operation (it can be optimistically discarded). If the hash of a signed replace operation with a matching path also matches, then the client has received its own issued change back from the server, and the path, hash pair is removed from the mapping (following changes on the path will be verified and applied).

verified() method clears up a patch, leaving only verified operations.

@matthiaslehnertum matthiaslehnertum merged commit b8a66ec into main Jan 15, 2024
1 check passed
@matthiaslehnertum matthiaslehnertum deleted the feature/supress-self-stuttering branch January 15, 2024 09:57
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.

2 participants