Logical Clock — Lamport আর Vector Clock
Logical Clocks: Lamport and Vector Clocks
Wall-clock ভরসা করা যায় না বলে সময়-মাপা ছেড়ে causal ক্রম ট্র্যাক করি — Lamport clock একমুখী promise দেয়, vector clock উল্টোটাও জানিয়ে concurrency exact ধরে।
আগে এটা বুঝি
গত লেসনে (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 event | L ← L + 1 |
| ২ | Message send | L ← L + 1, তারপর সেই L-ই message-এ সেঁটে পাঠাও |
| ৩ | Message receive | L ← 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 receive | V ← 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 ঘটলlocal / send / receive — যেকোনো একটা
- Lamport updatelocal/send: +1 ; receive: max(L, L_msg)+1
- Vector updatelocal/send: নিজের index+1 ; receive: componentwise max তারপর নিজের index+1
- attach (send হলে)দুই timestamp-ই message-payload-এ সেঁটে যায়
- পরের compareLamport: শুধু < ; Vector: <=componentwise+!= চার-রায়ের একটা
উদাহরণ
হাতে-হিসাব — একটা send-receive-send-receive chain
দুই process X, Y: X message m পাঠাল, Y জবাবে m2
পাঠাল। মাঝে দুজনেরই একটা করে independent local event।
| event | process | কাজ | L | V (X, Y) |
|---|---|---|---|---|
| x1 | X | local | 1 | (1, 0) |
| x2 | X | send m | 2 | (2, 0) |
| y1 | Y | local | 1 | (0, 1) |
| y2 | Y | receive m | 3 | (2, 2) |
| y3 | Y | send m2 | 4 | (2, 3) |
| x3 | X | receive m2 | 5 | (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 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নিজে চালিয়ে দেখুন
সম্পূর্ণ independent দুই process — তবু Lamport 'ক্রম' দেখায়
# 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 থেকে না।
নিজে বানান
LamportClock আর VectorClock — 3-process simulation-এ পাশাপাশি
- LamportClock: tick/send/receive — তিন rule-ই তিনটা method
- VectorClock: tick/send/receive — একই তিন rule, vector-এ; compare() static method যা BEFORE/AFTER/CONCURRENT/EQUAL ফেরত দেয়
- ৩-process (P1,P2,P3) message-exchange design করুন — কিছু event সত্যিকার causal chain-এ, কিছু genuinely concurrent
- প্রতিটা event-এ দুই timestamp পাশাপাশি print করুন
- শেষে টেবিল বানান: কোন 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-এ ভুল ক্রম হিসেবে বেরিয়ে আসত।
নিজে বাড়ান:
VectorClock.compare-এEQUALcase-টা কখনো ট্রিগার হয় কি এই scenario-তে? না হলে, কী পরিবর্তন করলে হতো — চিন্তা করুন- Node-সংখ্যা
3থেকে6-এ বাড়িয়ে চালান — misleading-pair তালিকা কীভাবে বাড়ে বা কমে দেখুন (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-ই সেই কাঁটার আংশিক সমাধান।
বুঝেছেন কি না দেখুন
1Lamport clock-এর তিনটা rule কী কী, আর receive rule-এ কেন শুধু
max না নিয়ে max(...) + 1 নেওয়া হয়?
স্মরণ
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 সবসময় কঠোরভাবে
পরে বসে।
2Experiment-এ দেখানো হলো দুই independent process-এ Lamport সংখ্যা
প্রায়ই strict order দেখায়, যদিও কোনো causal সম্পর্ক নেই। এটা
one-way guarantee-র সাথে কীভাবে সামঞ্জস্যপূর্ণ?
যুক্তি
Guarantee বলে causal হলে timestamp অবশ্যই ordered হবে — কিন্তু বিপরীত impliation দেয় না। Timestamp ordered হওয়া causal-হওয়ার প্রমাণ না; দুই independent process নিজ নিজ tick-গতিতে সংখ্যা বাড়ায়, কাকতালীয়ভাবে ordered দেখাতেই পারে।
3Build-এর simulation-এ c2-এর vector {P1:2, P2:3, P3:2} কীভাবে
পাওয়া গেল — merge rule হাতে দেখান।
প্রয়োগ
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}।
4Leader election-এ candidate-দের মধ্যে tie ভাঙতে (L, node_id)
trick যথেষ্ট, vector clock দরকার নেই কেন?
ডিজাইন
(L, node_id)
trick যথেষ্ট, vector clock দরকার নেই কেন?Leader election-এ শুধু একটা deterministic total order দরকার — কে
জিতবে — causal semantics লাগে না। (L, node_id) প্রতিটা candidate-এর
একটামাত্র তুলনাযোগ্য key দেয়, যথেষ্ট। Vector clock এখানে
O(nodes) অতিরিক্ত খরচ যোগ করবে এমন প্রশ্নের জন্য যেটা আদৌ জিজ্ঞাসা
করাই হচ্ছে না।
5Vector clock-এর খরচ O(nodes) কেন, আর node-churn কেন এটাকে আরও
জটিল করে?
যুক্তি
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