Vector clocks: detecting concurrency (Fidge and Mattern 1988)#

A Lamport timestamp orders events but cannot tell whether two events are causally related. Fidge and Mattern independently gave each process a vector of counters, one per process. Entry q of an event’s vector counts the events at process q that happened before or at it, so comparing two vectors entry by entry answers exactly whether one event caused the other or the two are concurrent.

For every pair of events, vectors and Lamport timestamps never disagree on a causal pair, but only vectors recognize the concurrent pairs. Concurrent updates are the ones a replicated system must reconcile; two conflicting transactions broadcast at the same time are concurrent in exactly this sense.

import itertools

import matplotlib.pyplot as plt
import numpy as np

import blockchainkit as bk

The same history as the Lamport example#

history = [
    [("send", "m1"), ("local", "a"), ("receive", "m3")],
    [("receive", "m1"), ("send", "m2")],
    [("local", "b"), ("receive", "m2"), ("send", "m3")],
]
vectors = bk.network.vector_timestamps(history)
lamport = bk.network.lamport_timestamps(history)
events = [(p, i) for p, row in enumerate(history) for i in range(len(row))]
names = [f"P{p}:{history[p][i][1]}" for p, i in events]
for name, (p, i) in zip(names, events, strict=True):
    print(f"{name:7s} Lamport {lamport[p][i]}  vector {vectors[p][i]}")
P0:m1   Lamport 1  vector (1, 0, 0)
P0:a    Lamport 2  vector (2, 0, 0)
P0:m3   Lamport 6  vector (3, 2, 3)
P1:m1   Lamport 2  vector (1, 1, 0)
P1:m2   Lamport 3  vector (1, 2, 0)
P2:b    Lamport 1  vector (0, 0, 1)
P2:m2   Lamport 4  vector (1, 2, 2)
P2:m3   Lamport 5  vector (1, 2, 3)

Classify every pair#

relation = np.zeros((len(events), len(events)))
for (x, (p, i)), (y, (q, j)) in itertools.product(enumerate(events), repeat=2):
    a, b = vectors[p][i], vectors[q][j]
    if bk.network.happened_before(a, b):
        relation[x, y] = 1
        assert lamport[p][i] < lamport[q][j]  # Causal order implies Lamport order.
    elif bk.network.happened_before(b, a):
        relation[x, y] = -1
concurrent = int((relation[np.triu_indices(len(events), 1)] == 0).sum())
print(concurrent, "concurrent pairs")
assert bk.network.concurrent(vectors[0][1], vectors[2][0])  # "a" and "b".

fig, ax = plt.subplots(figsize=(6, 5))
ax.imshow(relation, cmap="coolwarm", vmin=-1, vmax=1)
ax.set_xticks(range(len(events)), names, rotation=60)
ax.set_yticks(range(len(events)), names)
ax.set_title("row -> column (red), column -> row (blue), concurrent (grey)")
fig.tight_layout()

plt.show()
row -> column (red), column -> row (blue), concurrent (grey)
8 concurrent pairs

Exercise#

A vector clock grows with the number of processes. Why can no scalar clock capture concurrency exactly? Hint: in this history, find events x, y, z with x concurrent to y and y concurrent to z, but x before z. Could “equal timestamps” ever mean “concurrent”?

A worked solution is in Exercises: networking.

Total running time of the script: (0 minutes 0.103 seconds)

Gallery generated by Sphinx-Gallery