Foundationপ্রথম নীতি থেকে
LEVEL 9লেসন ৪/১৯কঠিন৫০ মিনিট

Logical Clock — Lamport আর Vector Clock

Logical Clocks: Lamport and Vector Clocks

Wall-clock ভরসা করা যায় না বলে সময়-মাপা ছেড়ে causal ক্রম ট্র্যাক করি — Lamport clock একমুখী promise দেয়, vector clock উল্টোটাও জানিয়ে concurrency exact ধরে।

এই লেসন শেষে আপনি পারবেন

  • Lamport clock-এর তিনটা rule (local, send, receive) প্রয়োগ করে event-এ timestamp বসাতে পারবেন, আর `a → b ⇒ L(a) < L(b)` one-way guarantee প্রমাণ করতে পারবেন
  • Lamport timestamp দিয়ে concurrent event-দুটোকে ভুলভাবে ordered মনে হওয়ার concrete counterexample দেখিয়ে ব্যাখ্যা করতে পারবেন কেন guarantee-র converse সত্য নয়
  • Vector clock-এর per-node counter আর merge rule ব্যবহার করে `V(a) <= V(b)` componentwise compare চালিয়ে happens-before, concurrent নাকি equal — নির্ণয় করতে পারবেন
  • LamportClock আর VectorClock class লিখে বহু-process message-exchange simulate করে দুই ঘড়ির output পাশাপাশি রেখে Lamport-এর ভুল সংখ্যায় ধরিয়ে দিতে পারবেন
  • Lamport clock বনাম vector clock-এর খরচ (O(1) বনাম O(nodes)) তুলনা করে কোথায় কোনটা বাস্তবে ব্যবহার হয় তা যুক্তি দিয়ে বাছতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনে (physical-time) হিসাবটা ভেঙে গিয়েছিল: NTP যতই টিউন করুন, দুই node-এর ঘড়ির মধ্যে clock skew কখনো শূন্য হয় না, আর monotonic clock-ও শুধু নিজ node-এর ভেতরে ভরসাযোগ্য — cross-node তুলনায় অচল। তাহলে event A, event B — কে আগে? প্রশ্নটার উত্তর কীভাবে দেব, যখন replication log সাজাতে হয়, stale write ধরতে হয়, বা production বাগ ডিবাগ করতে হয়?

server-1 log:  09:14:03.001  write(x=5)
server-2 log:  09:14:02.998  write(x=7)

কে আগে লিখল? wall-clock বলে server-2 — কিন্তু ঘড়ি দুইটা sync-ই
নয়, ৩ms পার্থক্য skew-এর ভেতরেই পড়ে। এই সংখ্যা দিয়ে সিদ্ধান্ত
নেওয়া মানেই ভুল সিদ্ধান্তের ঝুঁকি নেওয়া।

আজকের চাল সহজ কিন্তু র‍্যাডিকাল: সময়-মাপা ছেড়ে দিন। কে কত সেকেন্ড আগে ঘটল সেটা না জেনেও, শুধু causal ক্রম — কে কার কারণ হতে পারত — ট্র্যাক করা সম্ভব। সেটাই logical clock-এর কাজ। দুইটা ডিজাইন দেখব: Lamport clock — এক সংখ্যা, সস্তা, কিন্তু অন্ধ একদিকে; আর vector clock — এক vector, দামি, কিন্তু অন্ধত্বহীন।

মূল ধারণা

Lamport clock — তিনটা rule

প্রতিটা process নিজের কাছে একটা scalar counter L রাখে, শুরু 0 থেকে। তিনটা নিয়মই পুরো যন্ত্র:

Ruleকখনকী করে
১Local eventL ← L + 1
২Message sendL ← L + 1, তারপর সেই L-ই message-এ সেঁটে পাঠাও
৩Message receiveL ← max(L, L_msg) + 1

Receive-rule-টাই আসল কাজ করে: receiver নিজের জ্ঞান (L) আর sender-এর জ্ঞান (L_msg)-এর মধ্যে যেটা এগিয়ে সেটা বেছে নেয়, তারপর +1 করে নিশ্চিত করে receive event সবসময় sender-এর send event-এর কঠোরভাবে পরে বসে।

One-way guarantee — আর কেন converse মিথ্যা

এই তিন rule থেকে একটা প্রমাণযোগ্য ফলাফল আসে:

a → b  ⇒  L(a) < L(b)

Induction সহজ: program-order-এ rule ১ counter বাড়ায়ই; message-edge-এ rule ৩-এর +1 নিশ্চিত করে receive সবসময় sender-timestamp-এর চেয়ে বড়; transitivity chain-জুড়ে ধরে রাখে। তাই causal হলে timestamp অবশ্যই ছোট-থেকে-বড় ক্রমে বসবে।

কিন্তু converse মিথ্যা — L(a) < L(b) দেখে a → b বলা যায় না। Concrete counterexample: দুইটা process P1, P2 — কোনো message কখনো বিনিময় হয়নি, সম্পূর্ণ independent। P1 করল দুইটা local event (L=1,2), P2 করল তিনটা (L=1,2,3)। P1-এর শেষ event-এর timestamp 2, P2-এর শেষ event-এর timestamp 3 — 2 < 3, দেখতে মনে হয় P1-এর event P2-এর event-এর causal অতীতে। বাস্তবে দুটো সম্পূর্ণ concurrent — কোনো path নেই, শুধু P2 বেশি কাজ করেছিল বলে counter বেশি এগিয়েছে। Timestamp বলছে “গতি”, causality নয়।

Total order দরকার হলে — tiebreak trick

কোথাও কোথাও (leader election-এর tie-breaking, log-compaction-এর deterministic ক্রম) causal সত্য না হলেও একটা total order দরকার হয় — প্রতিটা event-জোড়ায় সিদ্ধান্ত চাই, “undetermined” না। তখন জোড়া-key ব্যবহার হয়: (L, node_id), lexicographic compare। এটা সবসময় একটা নিশ্চিত ক্রম দেয় — কিন্তু মনে রাখা জরুরি: এটা artificial total order, causality না। node_id টাই-ব্রেক করছে বলেই ক্রম আসছে, কার্যকারণ নয়।

Vector clock — per-node counter

Lamport-এর অন্ধত্ব ঠিক করতে হলে “কার কাছ থেকে কী জ্ঞান এসেছে” আলাদা রাখতে হবে — এক scalar-এ সব মিশিয়ে ফেললে সেই তথ্য হারায়। সমাধান: প্রতিটা process N-node cluster-এ একটা N-লম্বা vector V রাখে, সব শূন্য থেকে শুরু। নিয়ম প্রায় Lamport-এরই মতো, শুধু scalar-এর বদলে vector-এ:

Ruleকখনকী করে
১Local eventনিজের index V[self] ← V[self] + 1
২Message sendনিজের index বাড়াও, পুরো V-ই message-এ সেঁটে পাঠাও
৩Message receiveV ← componentwise max(V, V_msg), তারপর নিজের index +1

Compare rule — চার রায়

দুই vector timestamp V(a), V(b) তুলনায় এক লাইনেই সিদ্ধান্ত:

a → b   ⟺   V(a) <= V(b) componentwise  এবং  V(a) != V(b)
b → a   ⟺   V(b) <= V(a) componentwise  এবং  V(b) != V(a)
V(a) == V(b)                            ⟺   equal (একই event)
কোনো দিকেই comparable নয়                ⟺   concurrent

এখানেই Lamport-এর অন্ধত্ব সারে: componentwise compare প্রতিটা node-এর জ্ঞান আলাদা রাখে বলে “কারো জ্ঞান কারো ভেতরে সম্পূর্ণ ঢুকেছে কি না” সঠিকভাবে বলা যায় — কোনো দিকেই না ঢুকলে সেটাই আসল প্রমাণ concurrency-র, timestamp-এর গতির উপর অনুমান না।

খরচ — আর node-churn-এর কাঁটা

Lamport clock-এর প্রতি event O(1) — এক integer। Vector clock-এর প্রতি event O(nodes) — পুরো vector বহন করতে হয় প্রতিটা message-এ। N ছোট থাকলে সমস্যা না, কিন্তু cluster বড় হলে বা node ঘন ঘন join/leave করলে (churn) vector-ও ফুলতে থাকে, আর কোনো node-এর entry নিরাপদে মুছে ফেলাও সহজ না — তার causal অবদান ইতিহাসে থেকেই যায়। এই সমস্যার আংশিক সমাধানে version vector আর dotted version vector-এর মতো variant এসেছে — pruning-friendly, কিন্তু নিখুঁত না। এখানে শুধু flag করে রাখলাম; সমাধান এই লেসনের কাজ না।

ভেতরে কী ঘটছে

Receive-rule-এর max + 1 কেন এভাবেই

max(L, L_msg) + 1 — শুধু max নিলে receive event আর sender-এর send event সমান timestamp পেতে পারত, causal হলেও strict < হারিয়ে যেত (আর irreflexivity-ও ভাঙত)। +1 যোগ করে নিশ্চিত হয় receive সবসময় নিজের আগের সবকিছুর — নিজের ইতিহাস আর sender-এর ইতিহাস দুই-ই — কঠোরভাবে পরে বসে। Vector clock-এ একই যুক্তি: merge (componentwise max) নিজে increment করে না, তাই receive-এর ঠিক পরে নিজের index-এ +1 আলাদা ধাপ — নইলে receive event নিজের আগের local event-এর সমান vector পেয়ে যেত।

Tiebreak trick-এর সীমা, একটু গভীরে

(L, node_id) জোড়া মনে রাখুন কীসের প্রতিস্থাপন না: এটা consensus না, এটা এমনকি “কে প্রথম শুরু করেছিল” সেটাও বলে না — শুধু একটা deterministic, reproducible tie-break যা সব node একই সিদ্ধান্তে পৌঁছায়। Leader-election module-এ (পরের অধ্যায়ে) দেখবেন এই ধরনের trick term/epoch-number-এর সাথে মিলিয়ে ব্যবহার হয় — কিন্তু সেখানেও কাজটা causal সত্য বলা না, শুধু agreement।

Vector clock-এর merge — কেন componentwise max-ই যথেষ্ট

Merge-এর সময় প্রতিটা index আলাদাভাবে max নেওয়া মানে “যে node-ই যত জেনেছে, তার সেরা জ্ঞানটা রাখো” — কোনো index-এ কমানো হয় না, কারণ knowledge কখনো পিছায় না (monotonic)। এই monotonic-max merge-ই পরের অধ্যায়ের CRDT (gossip-crdts লেসনে বিস্তারিত)-এর join-semilattice ভিত্তি — merge always-safe, order-independent, idempotent। এখানে শুধু বীজটা বপন করে রাখলাম।

একটা event stamp হওয়ার মুহূর্ত — দুই ঘড়ি পাশাপাশি
  1. event ঘটলlocal / send / receive — যেকোনো একটা
  2. Lamport updatelocal/send: +1 ; receive: max(L, L_msg)+1
  3. Vector updatelocal/send: নিজের index+1 ; receive: componentwise max তারপর নিজের index+1
  4. attach (send হলে)দুই timestamp-ই message-payload-এ সেঁটে যায়
  5. পরের compareLamport: শুধু < ; Vector: <=componentwise+!= চার-রায়ের একটা

উদাহরণ

হাতে-হিসাব — একটা send-receive-send-receive chain

দুই process X, Y: X message m পাঠাল, Y জবাবে m2 পাঠাল। মাঝে দুজনেরই একটা করে independent local event।

eventprocessকাজLV (X, Y)
x1Xlocal1(1, 0)
x2Xsend m2(2, 0)
y1Ylocal1(0, 1)
y2Yreceive m3(2, 2)
y3Ysend m24(2, 3)
x3Xreceive m25(3, 3)

y2-এ merge কীভাবে হলো, হাতে দেখি: y1-এর vector (0,1), m-এ attached x2-এর vector (2,0)। Componentwise max (max(0,2), max(1,0)) = (2,1); তারপর Y-এর নিজের index +1 → (2,2)। ঠিক এই দুই-ধাপ (merge, তারপর নিজের index bump) — hood সেকশনের নিয়মটাই এখানে সংখ্যায়।

লক্ষ্য করুন x2 আর y1 — V(x2)=(2,0), V(y1)=(0,1), কোনো দিকেই componentwise <= মেলে না → concurrent, ঠিক যেমনটা হওয়ার কথা: y1 ঘটেছে Y-এর নিজের সময়রেখায়, x2-এর কোনো জ্ঞান তখনও Y-এ পৌঁছায়নি। এখানে Lamport সংখ্যাও (1 < 2) ভুল দিক নির্দেশ করছে না শুধু কাকতালীয়ভাবে — পরের সেকশনে দেখব ঠিক এই কাকতালই সবসময় সৌভাগ্যক্রমে ঘটে না।

ছবিতে

        X                          Y
        │                          │
 x1 ────●  L=1 V=(1,0)             │
        │                          │
 x2 ────●  L=2 V=(2,0)             │
        │╲                         │
        │ ╲ m                y1 ───●  L=1 V=(0,1)
        │  ╲                       │
        │   ╲                y2 ───●  L=3 V=(2,2)
        │                          │╲
        │                          │ ╲ m2
        │                          │  ╲            y3 ── ● L=4 V=(2,3)
 x3 ────●  L=5 V=(3,3) ◄───────────┴───┘

x2 ∥ y1  — কোনো path নেই, componentwise incomparable
x2 → y2 → y3 → x3 — message-chain + transitivity
X-Y chain-এর space-time ছবি — diagonal-ই message; প্রতিটা event-এ Lamport(L) আর Vector(V) পাশাপাশি লেখা।
   P3                              P1
    │                               │
 c1 ●  L=1  V=(0,0,1)               │
    │                               │
    │        কোনো path নেই ──╳──    ●  a2   L=2  V=(2,0,0)
    │                               │

Lamport দেখে বলবে: 1 < 2  ⇒  "c1 আগে, a2 পরে"?  — ভুল সিদ্ধান্ত
Vector দেখে বলবে: (0,0,1) আর (2,0,0) — কোনো দিকেই <= না ⇒ CONCURRENT
অন্ধত্বের anatomy — কোনো arrow নেই তবু Lamport সংখ্যা ক্রমে বসে যায় (build সেকশনের c1 বনাম a2 pair)।

নিজে চালিয়ে দেখুন

EXPERIMENT

সম্পূর্ণ independent দুই process — তবু Lamport 'ক্রম' দেখায়

macOS / Linux / Windows (Python 3)· ১০ মিনিট
# blind.py — Lamport-এর একমুখী অন্ধত্ব, সংখ্যায় প্রমাণিত
class LamportClock:
    def __init__(self):
        self.L = 0
    def tick(self):
        self.L += 1
        return self.L

def run(n1, n2):
    """P1 করে n1টা local event, P2 করে n2টা — কোনো message কখনো নেই।"""
    l1, l2 = LamportClock(), LamportClock()
    a = [l1.tick() for _ in range(n1)]
    b = [l2.tick() for _ in range(n2)]
    return a, b

for n1, n2 in [(2, 3), (5, 2), (1, 1)]:
    a, b = run(n1, n2)
    ordered_pairs = sum(1 for x in a for y in b if x < y or y < x)
    total_pairs = len(a) * len(b)
    tied = sum(1 for x in a for y in b if x == y)
    print(f"P1 করল {n1}টা local event -> L={a}")
    print(f"P2 করল {n2}টা local event -> L={b}")
    print(f"  কোনো message কখনো যায়নি -> প্রতিটা pair সত্যিই CONCURRENT")
    print(f"  Lamport strict order দাবি করছে {ordered_pairs}/{total_pairs} pair-এ"
          f" (tie: {tied})")
    print()

প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):

P1 করল 2টা local event -> L=[1, 2]
P2 করল 3টা local event -> L=[1, 2, 3]
  কোনো message কখনো যায়নি -> প্রতিটা pair সত্যিই CONCURRENT
  Lamport strict order দাবি করছে 4/6 pair-এ (tie: 2)

P1 করল 5টা local event -> L=[1, 2, 3, 4, 5]
P2 করল 2টা local event -> L=[1, 2]
  কোনো message কখনো যায়নি -> প্রতিটা pair সত্যিই CONCURRENT
  Lamport strict order দাবি করছে 8/10 pair-এ (tie: 2)

P1 করল 1টা local event -> L=[1]
P2 করল 1টা local event -> L=[1]
  কোনো message কখনো যায়নি -> প্রতিটা pair সত্যিই CONCURRENT
  Lamport strict order দাবি করছে 0/1 pair-এ (tie: 1)

তিনটা run-এই causal সত্য একই — শূন্য (কোনো message নেই বলে সব pair concurrent) — অথচ Lamport-এর “strict order দাবি করা” pair-সংখ্যা 4/6 থেকে 8/10-এ লাফায় শুধু কে কত দ্রুত local tick করল তার ওপর ভিত্তি করে। শেষ run-এ (1,1) দুই timestamp সমান — tie, কোনো দাবিই নেই; কিন্তু tie মানেও concurrent প্রমাণ হয় না, শুধু কাকতালীয় সমতা। Lamport সংখ্যা দিয়ে causality অনুমান করা যায় না, শুধু causal হলে সংখ্যা কী হবে তা আন্দাজ করা যায় — দিকটা একমুখী।

এটা কী প্রমাণ করে

দুই process-এর মধ্যে একটা message-ও কখনো না গেলে সব pair সত্যিকারভাবেই concurrent — অথচ Lamport timestamp-এর strict `<` তুলনা প্রায় সব pair-কেই 'ordered' দেখায়। Order আসছে independent local-tick-এর গতি থেকে, causality থেকে না।

নিজে বানান

BUILD IT

LamportClock আর VectorClock — 3-process simulation-এ পাশাপাশি

Python · ●●●○○
  1. LamportClock: tick/send/receive — তিন rule-ই তিনটা method
  2. VectorClock: tick/send/receive — একই তিন rule, vector-এ; compare() static method যা BEFORE/AFTER/CONCURRENT/EQUAL ফেরত দেয়
  3. ৩-process (P1,P2,P3) message-exchange design করুন — কিছু event সত্যিকার causal chain-এ, কিছু genuinely concurrent
  4. প্রতিটা event-এ দুই timestamp পাশাপাশি print করুন
  5. শেষে টেবিল বানান: কোন pair-এ Lamport-এর < আসল causal সম্পর্কের সাথে দ্বিমত করে

লক্ষ্য: একই simulation দুই ঘড়ি দিয়ে চালিয়ে দেখানো — Lamport কোথায় কোথায় ভুল দিক নির্দেশ করে, vector clock সেই ভুল কীভাবে ধরে।

# clocks.py — LamportClock, VectorClock, 3-process simulation
class LamportClock:
    def __init__(self):
        self.L = 0
    def tick(self):
        self.L += 1
        return self.L
    def send(self):
        self.L += 1
        return self.L
    def receive(self, L_msg):
        self.L = max(self.L, L_msg) + 1
        return self.L

class VectorClock:
    def __init__(self, node_id, nodes):
        self.node_id = node_id
        self.V = {n: 0 for n in nodes}
    def tick(self):
        self.V[self.node_id] += 1
        return dict(self.V)
    def send(self):
        self.V[self.node_id] += 1
        return dict(self.V)
    def receive(self, V_msg):
        for k in self.V:
            self.V[k] = max(self.V[k], V_msg.get(k, 0))
        self.V[self.node_id] += 1
        return dict(self.V)
    @staticmethod
    def compare(Va, Vb):
        keys = set(Va) | set(Vb)
        le = all(Va.get(k, 0) <= Vb.get(k, 0) for k in keys)
        ge = all(Va.get(k, 0) >= Vb.get(k, 0) for k in keys)
        if le and ge: return "EQUAL"
        if le: return "BEFORE"
        if ge: return "AFTER"
        return "CONCURRENT"

NODES = ["P1", "P2", "P3"]
lam = {n: LamportClock() for n in NODES}
vec = {n: VectorClock(n, NODES) for n in NODES}
log = []  # (event_id, node, L, V)

def local(node, eid):
    L = lam[node].tick()
    V = vec[node].tick()
    log.append((eid, node, L, V))

def send(node, eid):
    L = lam[node].send()
    V = vec[node].send()
    log.append((eid, node, L, V))
    return (L, V)

def receive(node, eid, msg):
    L = lam[node].receive(msg[0])
    V = vec[node].receive(msg[1])
    log.append((eid, node, L, V))

# --- scenario: P1 -> P2 -> P3 -> P1 chain, প্লাস independent local events ---
local("P1", "a1")
m1 = send("P1", "a2")
local("P1", "a3")

local("P2", "b1")
receive("P2", "b2", m1)
m2 = send("P2", "b3")

local("P3", "c1")
receive("P3", "c2", m2)
m3 = send("P3", "c3")

receive("P1", "a4", m3)

print("event   node   L    V")
for eid, node, L, V in log:
    vstr = "{" + ", ".join(f"{k}:{V[k]}" for k in NODES) + "}"
    print(f"{eid:<7} {node:<6} {L:<4} {vstr}")

print()
print("pairs where Lamport-order disagrees with true causal relation:")
events = {eid: (node, L, V) for eid, node, L, V in log}
pairs = [("c1", "a2"), ("c1", "a3"), ("c1", "b2"), ("c1", "b3"),
         ("b1", "a2"), ("b1", "a3")]
for x, y in pairs:
    _, Lx, Vx = events[x]
    _, Ly, Vy = events[y]
    rel = VectorClock.compare(Vx, Vy)
    lamport_says = "x->y" if Lx < Ly else ("y->x" if Ly < Lx else "tie")
    print(f"  {x}(L={Lx}) vs {y}(L={Ly})  lamport suggests {lamport_says:<5}"
          f"  vector says {rel}")

প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):

event   node   L    V
a1      P1     1    {P1:1, P2:0, P3:0}
a2      P1     2    {P1:2, P2:0, P3:0}
a3      P1     3    {P1:3, P2:0, P3:0}
b1      P2     1    {P1:0, P2:1, P3:0}
b2      P2     3    {P1:2, P2:2, P3:0}
b3      P2     4    {P1:2, P2:3, P3:0}
c1      P3     1    {P1:0, P2:0, P3:1}
c2      P3     5    {P1:2, P2:3, P3:2}
c3      P3     6    {P1:2, P2:3, P3:3}
a4      P1     7    {P1:4, P2:3, P3:3}

pairs where Lamport-order disagrees with true causal relation:
  c1(L=1) vs a2(L=2)  lamport suggests x->y   vector says CONCURRENT
  c1(L=1) vs a3(L=3)  lamport suggests x->y   vector says CONCURRENT
  c1(L=1) vs b2(L=3)  lamport suggests x->y   vector says CONCURRENT
  c1(L=1) vs b3(L=4)  lamport suggests x->y   vector says CONCURRENT
  b1(L=1) vs a2(L=2)  lamport suggests x->y   vector says CONCURRENT
  b1(L=1) vs a3(L=3)  lamport suggests x->y   vector says CONCURRENT

তিন মুহূর্ত পড়ার সময় লক্ষ করুন: (১) মূল chain a2 → b2 → b3 → c2 → c3 → a4 — প্রতিটা ধাপে Lamport-ও বাড়ছে, vector-ও componentwise বড় হচ্ছে, দুই ঘড়ি একমত; (২) c1 আর b1 — process শুরুর independent local event — এদের সাথে অন্য process-এর পরের যেকোনো event তুলনা করলেই Lamport বিভ্রান্ত করে (৬টা pair-ই তালিকায়), কারণ c1, b1-এর ছোট সংখ্যা শুধু বোঝায় “কম কাজ করা প্রক্রিয়া”, causal আগে হওয়া না; (৩) vector compare প্রতিটাতেই সঠিক রায় দেয় — CONCURRENT, কোনো ব্যতিক্রম ছাড়া। এটাই সেই জায়গা যেখানে vector clock টাকা উশুল করে — ঠিক এই ৬টা pair-ই Lamport-only display বা delivery-logic-এ ভুল ক্রম হিসেবে বেরিয়ে আসত।

নিজে বাড়ান:

  1. VectorClock.compare-এ EQUAL case-টা কখনো ট্রিগার হয় কি এই scenario-তে? না হলে, কী পরিবর্তন করলে হতো — চিন্তা করুন
  2. Node-সংখ্যা 3 থেকে 6-এ বাড়িয়ে চালান — misleading-pair তালিকা কীভাবে বাড়ে বা কমে দেখুন
  3. (L, node_id) tiebreak যোগ করে একটা total_order() function লিখুন — তারপর সেই order আর vector-compare-এর causal রায় পাশাপাশি রেখে কোথায় দ্বিমত হয় খুঁজুন

বাস্তব সিস্টেমে

Distributed tracing। Single trace-এর ভেতর parent-span → child-span সম্পর্ক সরল, একরৈখিক — সেখানে Lamport-ধাঁচের monotonic sequence-ই যথেষ্ট; ভারী vector বহনের দরকার পড়ে না (distributed-tracing লেসনে বিস্তারিত)।

Dynamo/Riak/Voldemort-ধাঁচের key-value store। Concurrent write দুই ভিন্ন replica-তে একই key-তে আঘাত করলে vector clock (বা তার descendant — version vector) দিয়ে sibling detect হয়; scalar Lamport এখানে অন্ধ, কারণ ঠিক এই concurrency-ই ধরতে হবে।

CRDT। Vector clock-এর componentwise-max merge-ই CRDT-র join-semilattice ভাবনার সরাসরি পূর্বসূরি — gossip-crdts লেসনে এটা পুরোপুরি বিস্তৃত হবে, এখানে শুধু সংযোগটা চিহ্নিত রাখলাম।

যে ভুলগুলো সবাই করে

“Lamport timestamp ছোট মানেই ঘটনাটা সত্যিই আগে ঘটেছে।”

Guarantee একমুখী: a → b হলে L(a) < L(b) নিশ্চিত, কিন্তু L(a) < L(b) দেখে a → b বলা যায় না। Experiment সেকশনে দেখানো হলো — দুই সম্পূর্ণ independent process-এ ছোট-বড় সংখ্যা আসে শুধু কে কত দ্রুত local tick করল তা থেকে, causality থেকে না।

“(L, node_id) দিয়ে বানানো total order-ই আসল causal order।”

Tiebreak trick প্রতিটা pair-এর জন্য একটা deterministic সিদ্ধান্ত দেয় ঠিকই — কিন্তু সিদ্ধান্তটা artificial, node_id-এর ওপর নির্ভর করে। কার্যকারণ concurrent pair-এর মধ্যে কোনো দিক নির্দেশই করে না; tiebreak শুধু “কাউকে একটা ক্রম দিতেই হবে” এমন প্রয়োজনে (leader election-এর মতো) ব্যবহারযোগ্য, causal প্রশ্নে না।

“Vector clock ব্যবহার করা মানেই conflict resolve হয়ে গেছে।”

Vector clock শুধু detect করে — ordered নাকি concurrent, চার রায়ের একটা। দুই write concurrent বেরোলে কোনটা রাখা হবে সেই সিদ্ধান্ত সম্পূর্ণ আলাদা policy-র কাজ (LWW, merge-function, user-prompt)। Detection আর resolution দুই ভিন্ন স্তর — এই লেসন শুধু প্রথমটা বানাল।

“Vector clock-এর সাইজ node-সংখ্যায় স্থির থাকে, বাড়ে না — চিন্তার কিছু নেই।”

Fixed N-এ ঠিকই, কিন্তু বাস্তব cluster-এ node join/leave করে (churn); নতুন node ঢুকলে vector-এর dimension বাড়াতে হয়, আর পুরনো node স্থায়ীভাবে চলে গেলেও তার entry নিরাপদে মুছে ফেলা যায় না — তার causal অবদান ইতিহাসে থেকেই যায়। খরচ O(nodes) স্থির-cluster-এই সত্যি; churn-এ সমস্যা আরও জটিল হয়, version vector/dotted version vector-এর মতো variant-ই সেই কাঁটার আংশিক সমাধান।

বুঝেছেন কি না দেখুন

1

Lamport clock-এর তিনটা rule কী কী, আর receive rule-এ কেন শুধু max না নিয়ে max(...) + 1 নেওয়া হয়?

স্মরণ

Local: L += 1; send: L += 1 তারপর attach; receive: L = max(L, L_msg) + 1। শুধু max নিলে receive event sender-এর send event-এর সমান timestamp পেতে পারত — strict < guarantee আর irreflexivity ভেঙে যেত। +1 নিশ্চিত করে receive সবসময় কঠোরভাবে পরে বসে।

2

Experiment-এ দেখানো হলো দুই independent process-এ Lamport সংখ্যা প্রায়ই strict order দেখায়, যদিও কোনো causal সম্পর্ক নেই। এটা one-way guarantee-র সাথে কীভাবে সামঞ্জস্যপূর্ণ?

যুক্তি

Guarantee বলে causal হলে timestamp অবশ্যই ordered হবে — কিন্তু বিপরীত impliation দেয় না। Timestamp ordered হওয়া causal-হওয়ার প্রমাণ না; দুই independent process নিজ নিজ tick-গতিতে সংখ্যা বাড়ায়, কাকতালীয়ভাবে ordered দেখাতেই পারে।

3

Build-এর simulation-এ c2-এর vector {P1:2, P2:3, P3:2} কীভাবে পাওয়া গেল — merge rule হাতে দেখান।

প্রয়োগ

c2 হলো c1(V={P1:0,P2:0,P3:1})-এর পরে m2 receive; m2-এ attached vector ছিল b3-এর {P1:2,P2:3,P3:0}। Componentwise max → {P1:2,P2:3,P3:1}; নিজের (P3) index +1 → {P1:2,P2:3,P3:2}।

4

Leader election-এ candidate-দের মধ্যে tie ভাঙতে (L, node_id) trick যথেষ্ট, vector clock দরকার নেই কেন?

ডিজাইন

Leader election-এ শুধু একটা deterministic total order দরকার — কে জিতবে — causal semantics লাগে না। (L, node_id) প্রতিটা candidate-এর একটামাত্র তুলনাযোগ্য key দেয়, যথেষ্ট। Vector clock এখানে O(nodes) অতিরিক্ত খরচ যোগ করবে এমন প্রশ্নের জন্য যেটা আদৌ জিজ্ঞাসা করাই হচ্ছে না।

5

Vector clock-এর খরচ O(nodes) কেন, আর node-churn কেন এটাকে আরও জটিল করে?

যুক্তি

প্রতি message-এ পুরো vector (nodes-সংখ্যার সমান entries) বহন করতে হয়; N বাড়লে প্রতিটা event-ই ভারী হয়। Churn-এ নতুন node dimension যোগ করে, আর পুরনো node স্থায়ীভাবে চলে গেলেও তার entry prune করা নিরাপদ না — causal contribution ইতিহাসে থেকে যায়; version vector/ dotted version vector এই pruning-সমস্যার আংশিক সমাধান।

6

কোন ধরনের production system Lamport-ধাঁচের ordering বাছে, কোনটা vector-clock-জাতীয় mechanism — আর বাছাইয়ের যুক্তি কী?

প্রয়োগ

Distributed tracing/logging: single-trace-এর ভেতর causal path সরল (parent→child), scalar যথেষ্ট — Lamport-ধাঁচ। Dynamo/Riak/Voldemort- ধাঁচের key-value store: multi-writer concurrent-write conflict detect করতে হয়, scalar সেখানে অন্ধ — vector clock (বা descendant) লাগে।

এরপর কী

এরপর কী

আজ হাতে এসেছে দুইটা যন্ত্র — Lamport clock (সহজ, একমুখী) আর vector clock (ভারী, কিন্তু concurrency exact ধরে)। Vector clock-এর compare rule দিয়ে এখন যেকোনো event-জোড়ায় বলা যায় — causal নাকি concurrent, নিশ্চিতভাবে। কিন্তু detection একা কিছু বানায় না — পরের লেসনে happens-before relation-টাই আনুষ্ঠানিকভাবে দাঁড় করানো হবে, আর এই detection-যন্ত্র ব্যবহার করে সমাধান হবে real user-visible বাগ — যেমন reply, যে comment-এর জবাবে লেখা, তার আগেই ভেসে ওঠা।

আরও পড়ুন

  • Time, Clocks, and the Ordering of Events in a Distributed System — Leslie Lamport — Communications of the ACM 21(7), 1978 · Logical clock-এর জন্মপত্র — এই লেসনের প্রতিটা rule-এর মূল উৎস
  • Timestamps in Message-Passing Systems That Preserve the Partial Ordering — Colin J. Fidge — Australian Computer Science Communications, 1988 · Vector clock-এর স্বাধীন সহ-উদ্ভাবন — Mattern-এর সাথে একই বছরের কাছাকাছি
  • Virtual Time and Global States of Distributed Systems — Friedemann Mattern — Parallel and Distributed Algorithms, 1989 · Vector clock-এর আরেক সহ-উদ্ভাবক পেপার; merge/compare rule-এর পাঠ্যপুস্তক treatment