CRDT and OT generalize as
Time Machines

Version 2 (outline)

[ authors ]

Abstract: CRDT and OT are typically seen as competing approaches, but we show that they are instances of a more general concept: a Time Machine.

A Time Machine combines the abilities of both OT and CRDT and achieves things neither could do alone -- we can now create fully P2P systems that guarantee consistency, under arbitrary edits, over arbitrary network conditions, with improved performance, that can prune and forget old history, or share aspects of history in modules, with a common network protocol.

Introduction

If you read hacker news, you might see people contrasting OT and CRDT as approaches to collaborative editing:

You might see people arguing for advantages of one or the other:

This could lead you to think that and one might win over the other:

But the model where programmers must choose between CRDT and OT is inaccurate:

                         Programmer Choice
                                 o
                                / \
                               /   \
                              V     V
                            CRDT    OT

A better model is:

                         System Capabilities

                         CRDT
                          ^
                          |
                          |
                          |
                          +-------------> OT

CRDT and OT are abilities that an algorithm can support. A Time Machine supports both:

                         System Capabilities

                         CRDT
                          ^             • <-- Time Machine
                          |
                          |
                          |
                          +-------------> OT

A Time Machine is a CRDT that can be used to do OT. It features the best of both worlds:

This article explains the Time Machine, its benefits, and demonstrates:

OT and CRDT as Time Machine

The general problem is:

          •
         / \
        A   B
        | \ |
        C   o
         \ /
          o
        o        o        o
        |        |        |
        A        A        B
        |        |/      \|
        C        B'       A'
        |/      \|       \|
        B''      C'       C'

Operational Transform asks the question of how to convert operations onto other lines of time.

OT is Rebase

      o    OH
      |
      o
  .../.\
  . o . o             <-- should it be only one version at a time?
  . | . |
  . o . o
  ..... |
      .....
      .   .
      .   .
      .   .
      .....

CRDT merges through a Collapsed History

Whereas OT operates on Observable History (OH), CRDT converts all observed edits into an internal format, that we call Collapsed History (CH):

      o    OH
      |
      o
     / \
    o   o
    |   |
    o   o
      
   ---|---
      v
           CH
     (*)

These collapses are like "Data Laced with History", they get closer to an observable snapshot of the state than the list of raw operations, but they still contain history.

Then we can compute an observation of the current version. View:CHSnapView : CH \to Snap

Example Collapses:

General rules: [ callout box ]

Problems with CRDT

Time Machine: CRDT that does OT

We want the best of both worlds:

  1. Consistency of CH merges
  2. Abstraction of OH data

And very pragmatic needs:

  1. Final updates as operations:

... in fact, most CRDT libraries implement this OT, but don't document it!

Approach:

What this looks like:

 - [ Trad CRDT        ]
   OH->CH----------->|Snap
   |  ------------>  |

   -[  *  *  *  * *  ]-    <--- time machine comic depiction

 - [ CRD Time Machine ]
   OH-------->CH->OH->Snap

              =[___]=      <--- encapsulated time machine comic (callout to make it big)

Let's point out key aspects:

Putting these together, a CRDT is like a time machine that is:

A much better time machine:

Formalizing Time Machines

Now we formally define these concepts, to prove that a Time Machine is a generalization of OT and CRDT.

Time Machines manipulate Observed History

In Observed History, each event is described as-observed by the peer it originated at. Time Machines merge incoming observations by traveling them to the receiving peer's perspective in time.

We formally define Observed History OHOH as the set of observations oOHo \in OH, for all known mutation events, from all peers. This form of history is called the Partial-Order Log in Baquero, and the XX in Archagon.

Observation

An observation oo is the tuple (e,op,parents,version)(e, op, parents, version), where:

The same event can exist in multiple observations; each at a different parentsparents, displaying a potentially transformed representation of opop.

We also define the functions event(o)event(o), op(o)op(o), parents(o)parents(o), and version(o)version(o) to retrieve the corresponding elements of the tuple.

Event

An event ee marks a point in time, on a peer, when an edit operation occurred. You can also think of it as a “timestamp” in relative spacetime.

Version

Merge

Operation

Each mutation occurs via some opOperationsop \in Operations. Different data types support different sets of OperationsOperations. For instance, a counter might support:

CounterOperations={Increment(count),Reset}CounterOperations = \set{\texttt{Increment(count)}, \texttt{Reset}}

Whereas, a collaborative text editor might support the operations:

TextOperations1={Replace(start, length, string)}TextOperations_1 = \set{\texttt{Replace(start, length, string)}}

Or it might only support explicit insertions and deletions:

TextOperations2={Insert(start, string),Delete(start, length)}TextOperations_2 = \set{\texttt{Insert(start, string)}, \texttt{Delete(start, length)}}

Additionally, we assume that there exists an operation that composes other operations, so multiple operations can be expressed as a single opop.

Snapshot

When we observe the state of a peer, at any point of time, we call it a “snapshot,” and refer to it as snapsnap. Operations apply to a snapshot of state, resulting in a new snapshot:

snapop=snapsnap \bullet op = snap'

Each peer's local time progresses linearly as it applies a sequence of operations to its snapshots. Some operations are generated locally, and some are received remotely, and transformed so that they can be applied locally.

Time Machines implement View() and Travel()

A Time Machine implements two functions over observed history:

The first function, ViewView, produces the observable snapshot snapsnap at a versionversion in time. We sometimes abbreviate View(OH)=View(OH,frontier(OH))View(OH) = View(OH, frontier(OH)). This is what a CRDT does.

The second function, Travel(OH,op,from,to)opTravel(OH, op, from, to) \mapsto op', transforms an operation opop to the equivalent opop', that would be observed when the edit event travels through time from the version fromfrom to the version toto. This is what OT does.

View() can merge

To merge a new observation oo using View, we simply add it to our history OHOH and then View the result:

View(OH{o})=snapView(OH \cup \set{o}) = snap'

This is how a CRDT works—it reproduces the entire state with every merged edit. This makes it easier to define and guarantee consistency— you just constrain yourself to making the View function deterministic.

Travel() can merge

This is how OT works—it updates a snapshot by traveling the remote observation into our local time, and then applying it. We want this in practice because (a) re-generating a large data structure for a small edit can be inefficient, and (b) to update cursor positions and other local state annotations, we need to interpret the differential operation anyway. Practical systems need Travel.

We merge with Travel following the template snapop=snapsnap \bullet op' = snap', where we want to produce the transformed opop' using our Travel function. Consider:

Now we can apply the traveled opop' to our snapsnap, and compute snapsnap' with:

snapTravel(OH{o},op(o),parents(o),frontier(OH))=snapsnap \bullet Travel(OH \cup \set{o}, op(o), parents(o), frontier(OH)) = snap'

They merge equivalently because View() = snap • Travel()

If you ViewView a merger, you should see the same thing as TravelingTraveling the remote opop to the current version as opop' and applying it with snapopsnap \bullet op'.

View(OH{o})=snapTravel(OH{o},op(o),parents(o),frontier(OH))View(OH \cup \set{o}) = snap \bullet Travel(OH \cup \set{o}, op(o), parents(o), frontier(OH))

Both produce the same snapshot.

We create Time Machine Objects to encapsulate history

Practical Time Machines will want to cache, hold, and encapsulate history over time. This allows them to optimize the representation of history according to their needs, by e.g. pruning old history.

So we generally define a Time Machine as an object with fields:

class TimeMachine {

}

The Update(o)Update(o) method incorporates a new observation into the machine's internal_history, which different Time Machines might represent in different ways. The View and Travel methods draw on this history to produce snapshots and transformations.

Now the Time Machine implementation is free to represent history in new ways, such as via a collapse, without impinging on other peers' choices of representation. This e.g. lets peers prune history at different rates.

Time Machines exist throughout Computer Science

These functions can describe an array of systems in CS beyond CRDT/OT, such as MVCC in databases, event sourcing, STM, LFS, VCS, and backup systems which store and manipulate histories of operations and snapshots to implement archiving, collaboration, and parallel processing.

We differentiate a few properties here:

CRDTime Machine: a consistent View and Travel

Here's a relative, consistent time machine. It has an outer CRDT and an inner CRDT. The outer CRDT does not collapse history — it works on pure observed history. But in order to guarantee consistent merging, it encapsulates an interior collapsing CRDT CC that we call the collapse. Since this interior CRDT is encapsulated, each implementation is free to use instantiate it (or not) on different subsets of histories, in different ways, in different cases.

The Outer CRDT

First, we'll define the outer CRDT. This is a CRDT, as defined in Shapiro, that has four additional constraints to its definition, which allows it to simultaneously implement the Time Machine API.

So starting with the CmRDT definition from Shapiro (which he's proved is equivalent to the CvRDT definition):Since Shapiro has shown the CmRDT and CvRDT forms of CRDT equivalent, this is enough to show that we have a CRDT

(S,s0,q,t,u,P)(S, s^0, q, t, u, P)

We add four constraints to implement View and Travel:

Now we have an outer CRDT that implements both View and Travel:

This defines the contract of the outer CRDT with other peers. But it does not explain how to implement ViewView and TravelTravel consistently. We will do that by defining a new internal collapsing CRDT.

The Inner Collapsing CRDT: CC

Now we add one more term to the CmRDT: the inner “collapsing” CRDT CC:

(S,s0,q,t,u,P,C)(S, s^0, q, t, u, P, C)

A CRD Time Machine implements View and Travel in terms of this inner collapsing CRDT CC. It is not required to always use CC — for instance, it can skip it entirely when transforming a non-concurrent (linear) sequence of operations to obtain better performance — but it always can, and its merge-type is defined in terms of CC.

CC defines ViewView and the Merge-Type

We now constrain the Time Machine's View function in terms of the CC query function qq:

This defines the merge-type.

Defining Travel is the remaining challenge

TP0 guarantees consistent Travel in CRDTime Machine

In order to be Consistent, we need View and Travel to be consistent. Well, View will be consistent as long as it is deterministic. That is easy to do. If we have that, we can define Travel's consistency in terms of View. We can do that with the equivalence property from before:

snap1op=snap2snap_1 \bullet op' = snap_2

Given a Travel() function outputting opop' for any merged version, TP0 is true iff applying the opop' to any snap1snap_1 produces a snap2snap_2, that's equivalent to the snapshots produced by View() at versions v1v_1 and v2v_2.

snap1op=snap2View(OH,v1)Travel(OH,op,v1,v2)=View(OH,v2)\begin{alignat}{3} snap_1 &\bullet &op' &= &snap_2\\ View(OH, v_1) &\bullet &Travel(OH, op, v_1, v_2) &= &View(OH, v2) \end{alignat}

TP0 verifies consistency of merges

A great way to test your system for TP0 is to see if it is satisfied for every possible merge, given some observation oo to merge into a base version vv.

We know that:

So we can now express TP0 in terms of View and Travel when merging oo at vv:

TP0    View(OH{o})=View(OH)Travel(OH{o},op(o),parents(o),frontier(OH))TP0 \iff View(OH ∪ \set{o}) = View(OH) \bullet Travel(OH \cup \set{o}, op(o), parents(o), frontier(OH))

Any differential CRDT can satisfy TP0

TP0 allows ambiguous travels

There are often multiple possible opop' to go from snap1snap_1 to snap2snap_2. For instance, the string mutation "XX" \to "XXX" could be achieved by inserting an "X" at any of positions 0, 1, or 2; or even by replacing characters 1 or 2 with the two-byte string "XX".

Example: the diff(snap1,snap2)opdiff(snap_1, snap_2) \mapsto op' function satisfies TP0.

TP0 is different from TP1 and TP2.

TP0 guarantees that a peer's local snapshot updates consistently from Travel's transformed operations. This is all you need to prove that your snapshot-update satisfies Strong Eventual Consistency (SEC), and thus is a CRDT.

Updating cursor locations and annotations with TP½

Surprise, this turns out to be equivalent to TP1 and TP2.

[Todo: incorporate from meeting 77]

The Big Picture: what is CRDT and OT?

Thus, far from OT and CRDT being separate, we will show that not only is a CTM a CRDT, and an OT implementation, but it is also implemented using an internal CRDT, and sometimes with internal OT, which runs top of a history that is itself a CRDT, and can be generated by OT.

     Observed         Observable         Observed
     @ Peer 1         @ Sender           @ Peer 2

         o                o                 o         time
         |               / \                |         |
         o              o   o               o         V
         |              | / |               |
         o              o   o               o
         |               \ /                |
         o                o                 o

                    - Collapse(H) -
                          v
                         (*)

    | Sending Peer    |       | Receiving Peer  |
    | State   | Edit  |  TM   | Rebased | State |
    |---------|-------|-------|---------|-------|
    |         |       |       |         |       |
              |------>|                           this is a trivial crdt
                      |------>|                   this is a crdt
                      |------------------------>| this is a crdt too

                      |------>|                   this can be done with OT TP1, TP2
                      |---------------->|         ""   <-- maybe it should be this
                              |-------->|         this can be done with OT TP0
                      |---------------->|         this is OT

Figure 4.4

We hope the reader can dispel the notion that CRDTs and OTs are competing algorithms. They are rather complementary properties of algorithms that we can strive to implement together within our Time Machines.

Figure 4.4 (above) depicts the flow of information within a Collapsing Time Machine. (Acknowledge Seph.)

Todo: proofs & explanations of each line

\therefore A CRD Time Machine is a CRDT that does OT through a CRDT.

Example Time Machines

Braid-HTTP: standard protocol

Sync9 + Antimatter: prune all history

Sync7 and DT merge over subset of history

Enables Memory Hierarchies

DiffSync: a VCS-style implementation

Simpleton: easier implementation

Modular History

Observed history is capable of replacing a span of history as a module with an equivalent module, without affecting the key properties of:

Examples of what we'll achieve

Peers can do this in order to:

And they can do it differently on each peer, and even change behaviors over time — all without breaking compatibility and consistency.

We can replace a span of history with an alternate subgraph of events.

Summarize(OH,from,to)SubgraphSummarize(OH, from, to) \mapsto Subgraph

Summaries are generated by a set of functions applicable to a CRDTM defined by Semantics = (Ops, View). Each function has a set of properties for that CRDTM:

Examples Summarize:OHOHSummarize : OH \to OH functions:

And all these can interoperate.