Skip to content

Instantly share code, notes, and snippets.

@warpech
Last active August 29, 2015 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save warpech/a66d992720d30d0507f7 to your computer and use it in GitHub Desktop.
Save warpech/a66d992720d30d0507f7 to your computer and use it in GitHub Desktop.
Client-server versioning in Puppets

Optional version control

Because of the asynchronous nature of bidirectional client-server communication (caused by network latency, server side push, etc), it can be assumed that client and the server state is always out of sync.

To solve the problem expressing changes in the view-model, optional server and client state versioning is introduced:

{
  "_ServerVersion": 0, 
  "_ClientVersion$": 0, 
  /* ... */
}

_ServerVersion and _ClientVersion$ are known to both parties. Both numbers are sent at the beginning of every batch of patches.

The first patch in the batch is the test operation, which value is the last version of the receipient known to the sender.

The second patch in the batch is the replace operation, which value is a new version number of the sender, consecutive to the previously sent version number.

Version numbering solves 3 problems:

  1. Patch Queueing solves the problem of messages delivered in wrong order
  2. Path Relevance Validation solves the problem of the client acting on data that is no longer in the view-model
  3. Path Operational Transformation solves the problem of the client acting on data that changed its position in the view-model

A response is not always assumed for a request, and depends whether the receiving party is a client or the server:

  • the server bumps its version number when it receives and processes a batch of patches. A response for client is needed to communicate the new version number (even when there are no other changes in the view-model)
  • the client does not bump its version number after it applies a received batch of patches. It only bumps version number when it generates a patch because of its own will

Patch Queueing

Patch Queueing solves the problem of messages delivered in wrong order.

A batch of patches can be applied only if it contains a remote version number which is greater by 1 than the last known remote version number.

If the remote version number is greater by more than 1 (a "version gap"), it is queued until a version greater exactly by 1 arrives.

If the remote version number is the same or smaller as the already acknowledged, that is a fatal error.

Example

Initial JSON view-model:

{
  "_ServerVersion": 0, 
  "_ClientVersion$": 0,
  "Message$": ""
}

Client sends request #1:

[
  {"op": "test", "path": "/_ServerVersion", "value": 0},
  {"op": "replace", "path": "/_ClientVersion$", "value": 1},
  {"op": "replace", "path": "/Message$", "value": "Hello "}
]

Client sends request #2:

[
  {"op": "test", "path": "/_ServerVersion", "value": 0},
  {"op": "replace", "path": "/_ClientVersion$", "value": 2},
  {"op": "replace", "path": "/Message$", "value": "Hello World"}
]

In case the server receives patch with _ClientVersion$: 2 before _ClientVersion$: 1, it must be queued. Only after _ClientVersion$: 1 is applied over _ClientVersion$: 0, can _ClientVersion$: 2 be applied over _ClientVersion$: 1.

Path Relevance Validation

Path Relevance Validation solves the problem of the client acting on data that is no longer in the view-model

When the recepient receives a patch which applies to an outdated version of its view-model, it is important to validate whether the object at the requested path actually exists in the current view-model. The receipient goes through the log of the view-model changes since that version and checks if the patch should be rejected.

A patch is is rejected if the path is invalid by either of the two following reasons:

  • The index for an array points to an object that no longer exists, by either having been deleted or replaced.
  • The path contains a name that points to an object that have been deleted or replaced.

If the patch is rejected, the recepient triggers a callback which can be registered by an application that wants to feedback an error to the user. This means that by default the error situations are handled "silently", but an application can decide to play a sound or show a message in case of a rejection.

If the patch is not rejected, the receipient performs the Path Operational Transformation.

Patch batches are considered non-atomic, meaning that if one patch gets rejected, the other patches before and after that can still be applied. It goes against the JSON Patch spec (RFC 6902) but is vital in case of a stream of independent UI events. The rejection callback is triggered as many times as there are patch rejections in the batch (one callback for one rejection).

Example

A common scenario for that case is a quiz application which replaces a question on the screen after it receives an answer for the currently displayed question. In case when a user double clicks an answer or clicks two different answers, the second click should be rejected. Most importantly, the second click should NOT be interpreted as an answer to the next question.

Initial JSON view-model:

{
  "_ServerVersion": 0, 
  "_ClientVersion$": 1,
  "Question": "What is the capital of Sweden?",
  "Answers": [{
      "Description": "Stockholm",
      "Select$": false
    },{
      "Description": "Berlin",
      "Select$": false
    }]
}

Client sends request #1 to select Answers:

[
  {"op": "test", "path": "/_ServerVersion", "value": 0},
  {"op": "replace", "path": "/_ClientVersion$", "value": 1},
  {"op": "replace", "path": "/Answers/0/Select$", "value": true}
]

Before the server responds, the client clicks on Berlin by accident which results with request #2:

[
  {"op": "test", "path": "/_ServerVersion", "value": 0},
  {"op": "replace", "path": "/_ClientVersion$", "value": 2},
  {"op": "replace", "path": "/Answers/1/Select$", "value": false}
]

Assuming that client requests arrive at the desired order, when the server receives _ClientVersion$: 1 it replaces the question and answers from the view-model with a next question and answers. In consequence, when _ClientVersion$: 2 arrives, the answer at index 1 is not "Berlin" anymore. It should not pass the path validation. An error callback should be triggered instead.

Path Operational Transformation

Path Operational Transformation solves the problem of the client acting on data that changed its position in a view-model array.

When a patch applies to an outdated version of the view-model and contains one or more array index(es), the recepient goes through the log of the view-model changes since that version and recalculates the indexes to reflect the current version.

Example

A common scenario for that case is a purchase order items list where the client requests to remove multiple product items from the list faster than it receives the confirmation of removal from the server.

Initial JSON view-model:

{
  "_ServerVersion": 0, 
  "_ClientVersion$": 0,
  "Items": [{
      "Description": "Ananas",
      "Remove$": false
    },{
      "Description": "Banana",
      "Remove$": false
    }]
}

Client sends request #1 to remove Ananas:

[
  {"op": "test", "path": "/_ServerVersion", "value": 0},
  {"op": "replace", "path": "/_ClientVersion$", "value": 1},
  {"op": "replace", "path": "/Items/0/Remove$", "value": true}
]

Client sends request #2 to remove Banana:

[
  {"op": "test", "path": "/_ServerVersion", "value": 0},
  {"op": "replace", "path": "/_ClientVersion$", "value": 2},
  {"op": "replace", "path": "/Items/1/Remove$", "value": true}
]

Assuming that client requests arrive at the desired order, when the server receives _ClientVersion$: 1 it removes Ananas from the array. In consequence, when _ClientVersion$: 2 arrives, Banana is already at the array index 0 and at the requested path to array index 1 is not valid anymore.

With Path Operational Transformation, the server looks up what was the array layout at _ServerVersion: 0 and resolves that the requested /Items/1/Remove$ needs to be transformed to /Items/0/Remove$.

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