Advance and Retreat Mechanism

distributed-systemsalgorithmsvisualization
Vpretreat(e)Vp{e}advance(e)Vp{e}V_p \xrightarrow{\text{retreat}(e)} V_p \setminus \{e\} \xrightarrow{\text{advance}(e')} V_p \cup \{e'\}

Statement

The eg-walker processes events in topological order but must interpret each event's operation in the context of its parents — not the current state. The advance/retreat mechanism solves this by maintaining two simultaneous views of the document:

  • Prepare version VpV_p: can move forward (advance) and backward (retreat)
  • Effect version VeV_e: only moves forward (via apply)

Before applying event ee, the algorithm adjusts VpV_p to match Events(e.parents)\text{Events}(e.\text{parents}):

Vpretreat(e1),,retreat(ek)Vpadvance(e1),,advance(ej)Events(e.parents)V_p \xrightarrow{\text{retreat}(e_1), \ldots, \text{retreat}(e_k)} V_p' \xrightarrow{\text{advance}(e'_1), \ldots, \text{advance}(e'_j)} \text{Events}(e.\text{parents})

The Three Operations

Apply(e)(e)

Precondition: Vp=Events(e.parents)V_p = \text{Events}(e.\text{parents}) and eVee \notin V_e.

Effect: Updates both versions: VpVp{e}V_p \leftarrow V_p \cup \{e\}, VeVe{e}V_e \leftarrow V_e \cup \{e\}.

Output: The transformed operation — how the document at VeV_e changes.

Retreat(e)(e)

Precondition: eVpe \in V_p.

Effect: VpVp{e}V_p \leftarrow V_p \setminus \{e\}. The effect version VeV_e is unchanged.

Makes the prepare version behave as if ee had not yet happened.

Advance(e)(e)

Precondition: eVpe \notin V_p but eVee \in V_e.

Effect: VpVp{e}V_p \leftarrow V_p \cup \{e\}. The effect version VeV_e is unchanged.

Restores a previously retreated event to the prepare version without re-applying it.

Visualization

Processing events e1,,e8e_1, \ldots, e_8 in topological order with the causal structure shown:

graph LR
    e1 --> e2
    e2 --> e3
    e3 --> e4
    e2 --> e5
    e5 --> e6
    e6 --> e7
    e4 --> e8
    e7 --> e8

The algorithm proceeds:

StepActionVpV_p afterVeV_e after
1apply(e1e_1){e1}\{e_1\}{e1}\{e_1\}
2apply(e2e_2){e1,e2}\{e_1, e_2\}{e1,e2}\{e_1, e_2\}
3apply(e3e_3){e1,e2,e3}\{e_1, e_2, e_3\}{e1,e2,e3}\{e_1, e_2, e_3\}
4apply(e4e_4){e1,,e4}\{e_1, \ldots, e_4\}{e1,,e4}\{e_1, \ldots, e_4\}
5retreat(e4e_4), retreat(e3e_3){e1,e2}\{e_1, e_2\}{e1,,e4}\{e_1, \ldots, e_4\}
6apply(e5e_5){e1,e2,e5}\{e_1, e_2, e_5\}{e1,,e5}\{e_1, \ldots, e_5\}
7apply(e6e_6){e1,e2,e5,e6}\{e_1, e_2, e_5, e_6\}{e1,,e6}\{e_1, \ldots, e_6\}
8apply(e7e_7){e1,e2,e5,e6,e7}\{e_1, e_2, e_5, e_6, e_7\}{e1,,e7}\{e_1, \ldots, e_7\}
9advance(e3e_3), advance(e4e_4){e1,,e7}\{e_1, \ldots, e_7\}{e1,,e7}\{e_1, \ldots, e_7\}
10apply(e8e_8){e1,,e8}\{e_1, \ldots, e_8\}{e1,,e8}\{e_1, \ldots, e_8\}

Note step 9: we advance e3,e4e_3, e_4 without retreating e5,e6,e7e_5, e_6, e_7 — concurrent events coexist in the prepare version when the target event (e8e_8) has both branches as parents.

Computing the Retreat/Advance Sets

Given old prepare version GoldG_{\text{old}} and target Gnew=Events(e.parents)G_{\text{new}} = \text{Events}(e.\text{parents}):

Retreat set=GoldGnew(processed in reverse topological order)\text{Retreat set} = G_{\text{old}} \setminus G_{\text{new}} \quad \text{(processed in reverse topological order)}
Advance set=GnewGold(processed in topological order)\text{Advance set} = G_{\text{new}} \setminus G_{\text{old}} \quad \text{(processed in topological order)}

Efficient computation uses a priority queue traversal:

  1. Insert frontier events of both GoldG_{\text{old}} and GnewG_{\text{new}} into a max-priority-queue (ordered by topological position)
  2. Pop the maximum element; tag it as "retreat" (from GoldG_{\text{old}} only) or "advance" (from GnewG_{\text{new}} only)
  3. If popped from both — it's a common ancestor; enqueue its parents
  4. Stop when all remaining elements are common ancestors

Index Transformation

The two-version state enables index transformation:

  1. Locate: Use the prepare version (sp=Inss_p = \text{Ins} count) to find the ii-th visible character — this is where the operation's author saw the character
  2. Map: Translate that record's position to the effect version (se=Inss_e = \text{Ins} count) — this gives the correct index in the current document
graph TD
    subgraph "Prepare Version (author's view)"
        P["Index i → find i-th record with s_p = Ins"]
    end
    subgraph "Effect Version (current doc)"
        E["Count records with s_e = Ins before target → output index j"]
    end
    P --> |"position mapping"| E

If the target record has se=Dels_e = \text{Del}, the deletion is a no-op (the character was already deleted by a concurrent operation).

Connections

The retreat/advance mechanism maintains the Internal State MachineInternal State Machinesp{NotInsertedYet,Ins,Del1,Del2,}s_p \in \{\text{NotInsertedYet}, \text{Ins}, \text{Del}_1, \text{Del}_2, \ldots\}The per-character state machine that tracks insertion/deletion across prepare and effect versions in eg-walker.Read more → invariants. Correctness of the overall algorithm (the Event Graph Convergence ProofEvent Graph Convergence Proofσ1,σ2TopoSorts(G):replay(G,σ1)=replay(G,σ2)\forall \sigma_1, \sigma_2 \in \text{TopoSorts}(G): \text{replay}(G, \sigma_1) = \text{replay}(G, \sigma_2)Formal proof that eg-walker produces identical document states regardless of topological sort order — strong eventual consistency.Read more →) depends on retreat and advance being exact inverses for the prepare version. The Eg-walker: Event Graph WalkerEg-walker: Event Graph Walkerdoc=replay(G)where G=(E,)\text{doc} = \text{replay}(G) \quad \text{where } G = (E, \rightarrow)A collaboration algorithm for text editing that combines CRDT convergence with OT-competitive memory and merge performance.Read more → overview provides the broader algorithmic context.