Foundationপ্রথম নীতি থেকে
LEVEL 9লেসন ১৬/১৯অ্যাডভান্সড৫০ মিনিট

Distributed Lock ও তার বিপদ — Redlock বিতর্কসহ

Distributed Locks and Their Perils — the Redlock Debate

Shared memory না থাকায় আর pause-মৃত্যু আলাদা করা যায় না বলে distributed mutual exclusion mutex-এর চেয়ে মৌলিকভাবে কঠিন; lease TTL crash সামলায় কিন্তু pause-এ ভাঙে — fencing token-ই ঢাল।

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

  • Single-process mutex আর distributed lock-এর মৌলিক ফারাক ব্যাখ্যা করতে পারবেন — shared memory নেই, আর কোনো failure detector-ই pause আর মৃত্যু আলাদা করতে পারে না
  • Lease-based lock-এর দুই মুখ trace করতে পারবেন — TTL expiry holder-crash সামলায়, কিন্তু GC-pause-এ client expired হয়েও নিজেকে holder ভাবে
  • Fencing token-এর mechanism ব্যাখ্যা করে দেখাতে পারবেন monotonic token resource-side reject কীভাবে stale write আটকায়
  • Redlock algorithm-এর ধাপগুলো চালিয়ে Kleppmann বনাম antirez বিতর্কের সারকথা timing-assumption আর clock-jump প্রশ্নটা চিহ্নিত করে ন্যায্যভাবে সারাংশ করতে পারবেন
  • কোন অবস্থায় ZooKeeper-style recipe, কোন অবস্থায় database-এর pg_advisory_lock — fate-sharing যুক্তি দিয়ে lock-technology সিদ্ধান্ত নিতে পারবেন

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

আগে এটা বুঝি

আপনি operating-systems module-এ mutex লিখেছেন — আচ্ছাদিত আরামের একটা জগৎ। pthread_mutex_lock()-এর নিচে kernel একটা atomic instruction চালায় shared memory word-এ; নিরাপত্তা দাঁড়িয়ে একটা ধর্মে: সব participant একই memory দেখে।

Production-এর এক রাত: billing-cron-এর দুই instance ঘুম থেকে উঠেছে (deploy-এ পুরনোটা মরতে দেরি হয়েছিল), দুজনেই একই customer- এর invoice তৈরি করেছে, দুটো charge গেছে। Team বলল — lock ব্যবহার করব; Redis-এ SET key val NX PX 30000 লিখে দিল। সমস্যা শেষ? না — আর এখান থেকেই আজকের লেসন। Network-জুড়ে ছড়ানো lock mutex-এর মতো শোনায়, কিন্তু physics সম্পূর্ণ আলাদা: shared memory নেই — সব খবর message হয়ে যায় (message-queues-লেসন); আর কেউ নিশ্চিত জানে না অন্য party মরেছে নাকি শুধু দেরি করেছে — failure-models-লেসনের crash-recovery model মনে করুন: বাইরে থেকে pause আর মৃত্যু অপারেবল।

দুইটা প্রশ্ন আজকের spine: (১) lock service কাকে lock দিল, holder মরলে সেটা ফেরত আসে কীভাবে? (২) lease মরার পরেও পুরনো holder জেগে উঠে লেখা চালিয়ে গেলে (GC pause!) শেষ রক্ষা কে? উত্তরটার নাম fencing token — এই লেসনের হৃদয়।

মূল ধারণা

Mutex-এর ধর্ম কোথায় ভাঙে

অনুমানSingle-process mutexDistributed lock
Shared stateএকটা memory wordকোথাও নেই — network message
Atomic primitiveCPU test-and-setserver-side request handling
Participant মরলেOS জানে, cleanup করেজানার উপায় নেই — timeout-ই ইঙ্গিত
সময়ের ভূমিকানগণ্যTTL, timeout, clock — ঝুঁকির উৎস

Lease — মেয়াদসহ ভাড়া

এই মেয়াদ-চুক্তির নাম lease: lock মানে মালিকানা না, সময়সীমাসহ ভাড়া — acquire-এ server টোকেন দেয় TTL মেয়াদে, কাজ বড় হলে renewal, শেষে release। চিরতরে-অপেক্ষা- নীতির বদলে মেয়াদ — কারণ participant মরলে server-এর কাছে মৃত্যু আর ধীরগতি একরকম: উত্তরই আসে না। আর জাদুটা release-ধাপে: release না-ও করলেও চলে — holder crash করলে TTL ফুরোতেই lease নিজে নিজে মরে, zombie-holder-চিরদখলের সমাধান। এই একটা বৈশিষ্ট্যের জন্যই lease-based lock শিল্পের default।

টাইমার-বোমাটা — process pause

Crash solved। আসল দানব থেমে-যাওয়া:

t=0    Alice acquire (TTL 30 s), DB read: stock=42
t=1    Alice-এর JVM full GC — stop-the-world... ৩০ সেকেন্ড,
       এক log-লাইন নেই, কারণ সব থেমে
t=31   Lease মরেছে। Bob acquire করে, stock=41 লেখে, শেষ
t=52   GC শেষ। Alice জাগল — তার হিসেবে ২ সেকেন্ড!
       সে ভাবে তার lock বেঁচে; stock=40 লিখে দিল...

Alice কোনো ভুল করেনি — retry করেনি, lock ভাঙেনি। কিন্তু তার ঘড়ির সত্য আর lock-server-এর সত্য আলাদা। শিক্ষণীয় asymmetry: client কখনো জানতে পারে না lock বেঁচে আছে কি না — জানে শুধু resource, যখন প্রথম write reject হয়।

Fencing token — resource-এর হাতে ঢাল

সমাধান: lock দিলে server fencing token-ও দেয় — প্রতিবার acquire-এ monotonically বাড়তি সংখ্যা (33, 34, 35…)। আর resource (storage/API — যা-ই রক্ষা হচ্ছে) মানে নিয়ম: সর্বোচ্চ seen-token-এর চেয়ে ছোট token এলে reject।

Alice   acquire → token 33
Bob     acquire → token 34      (Alice-এর lease মরার পরে)
Bob     storage.write(token=34) → accepted, last_seen=34
Alice   (GC থেকে জেগে) write(token=33) → REJECTED: 33 ≤ 34

কেন কাজ করে? এটা timing-assumption-কে ordering-assumption-এ বদলায় — Alice-এর ঘড়ি, pause-দৈর্ঘ্য, TTL-হিসাব কিছুই আর গুরুত্বপূর্ণ না; একমাত্র সংখ্যার ক্রম, যা server একমুখীভাবে দেয়।

Redlock — পাঁচ Redis-এ ভোট

এক Redis node-এর failover-ফাঁকে lock হারায় (async replication — primary মরলে replica lock-এর খবর না-ও পায়)। antirez-এর Redlock উত্তর দেয় ভোটে:

১. নিজের ঘড়িতে T1 নাও
২. পাঁচটা স্বাধীন master-এ SET lock NX PX=ttl
৩. T2 নাও; validity = ttl − (T2−T1) − clock-drift margin
৪. ≥৩ node-এ set হলো আর validity > 0 → acquired
৫. unlock = সব node-এ Lua-script check-and-delete

বিতর্কটা — Kleppmann বনাম antirez, ন্যায্য সারাংশ

2016-এ Kleppmann-এর “How to do distributed locking” আক্রমণ, antirez- এর জবাব — দুজনেই ঠিক, ভিন্ন ধর্মে:

প্রশ্নKleppmann-পক্ষantirez-পক্ষ
System-modelAsynchronous — pause/delay-র bound নেইSemi-synchronous — drift ছোট, measurable
GC-pauseValidity client-পাশে হিসাব — pause-এ ভাঙে; fencing ছাড়া unsafeEdge-case; fencing-সহ Redlock practical
Clock jumpNTP-step/VM-migration-এ দুই holder সম্ভববিরল; drift-margin + monitoring
দর্শনSafety চাইলে consensus-store + fencingProgress ও সরলতার সৎ trade-off

সারকথা: Redlock-এর safety দাঁড়িয়ে আছে সময়ের আচরণে; fencing token-ই সেই নির্ভরতা কাটে — আর fencing লাগলে multi-node ভোটের complexity অনেক ক্ষেত্রেই অপ্রয়োজনীয়। Decision, dogma নয়।

ভেতরে কী ঘটছে

Failure detector — মৃত্যু কেন নিশ্চিত জানা যায় না

Lock-server আসলে একটা failure detector: timeout ফুরোলে ঘোষণা holder সম্ভবত মৃত। দুই গুণ — completeness (মৃতকে শেষে ধরা) আর accuracy (জীবিতকে ভুল ঘোষণা না করা) — asynchronous network-এ দুটোই একসাথে perfect অসম্ভব: GC-pause accuracy ভাঙে, infinite delay completeness। Lease-এর নীতি এই অসম্ভবতার সমঝোতা: ভুল ঘোষণা হতে দাও, তবে ঘোষণার ফল ordering দিয়ে সারাও।

ZooKeeper recipes — ephemeral sequential znodes

/locks/order-processor/
    lock-0000000041   ← node A (ephemeral)
    lock-0000000042   ← node B (ephemeral)
    lock-0000000043   ← node C (ephemeral)
নিয়ম: সবচেয়ে ছোট sequence-ই holder;
বাকিরা ঠিক আগের sibling-এ watch — সে মরলেই জানায়।

তিন ভাষ্য। Ephemeral = znode owner-session-এর জীবনে বাঁধা; session মরলে (heartbeat বন্ধ) znode মুছে যায় — ZooKeeper-এর lease। Sequential monotonically বাড়তি নম্বর দেয় — fencing-token-এর raw material (enforce করে ZK না, client-কেই resource-এ পৌঁছে দিতে হয়)। Watch-predecessor herd effect ঠেকায় — release-এ একজনই জাগে। আর session-timeout-এর পেছনে সেই পুরনো সত্য: ZK-ও GC-pause- এ client-কে ভুলে মৃত ঘোষণা করতে পারে — তাই ZK-lock-ও fencing ছাড়া correctness-দাবি করে না।

Lock-এর স্তূপ — কোন স্তর কী গ্যারান্টি দেয়
  1. Lock serviceGrant/renew/revoke + monotonic token issue
  2. Lease (TTL)Holder-crash থেকে পুনরুদ্ধার — liveness-এর চুক্তি
  3. Fencing tokenSafety-র চুক্তি — stale writer ordering দিয়ে হারানো
  4. Resource guardশেষ প্রাচীর — token check, reject stale write

Database advisory lock — lock আর data এক ঘরে

PostgreSQL-এর pg_advisory_lock(key) প্রায়ই সবচেয়ে সঠিক উত্তর: lock-অবস্থা connection-এর জীবনে বাঁধা — connection মরলে (crash, network drop, timeout) server নিজেই ছেড়ে দেয়; আলাদা TTL, renewal, fencing-যন্ত্র কিছুই লাগে না। Fate-sharing যুক্তি: যে data রক্ষা হচ্ছে সেটা যদি এক database-এই থাকে, lock-ও সেখানেই — lock-service আর storage একসাথে পড়লে partial-failure-দুঃস্বপ্ন নেই। Batch-job dedup থেকে worker claim — এক-database জগতে advisory lock সস্তা-সঠিক; multi-storage জগতেই আলাদা coordination service লাগে।

উদাহরণ

Payment-worker-এর এক রাত — token ছাড়া আর token-সহ

দুই worker, এক lock service, এক ledger-table; task: invoice process — balance read, fee যোগ, write; TTL ৩০ s। Fencing ছাড়া:

সময়Worker AWorker BLedger
0 sacquire, read bal=500500
1 sGC pause শুরু500
32 s(ঘুমন্ত)acquire, read bal=500500
33 swrite bal=550, release550
54 sজেগে (নিজের হিসেবে ৩ s) write bal=560560 ✕

B-র fee হারাল — lost update: A পুরনো snapshot-এ গণনা করে overwrite করেছে। Fencing-সহ: একই ঘটনায় A-র token 17, B-র 18; t=54-এ A-র write guard-এ আটকায় (17 \< 18) — ledger থাকে 550, সঠিক। A-র দুই বৈধ পথ: কাজ বাদ (lock ছিল efficiency-র জন্য), বা fresh acquire + recompute (correctness দরকারে)। আর লক্ষ্য করুন — A-কে lease-মৃত্যুর খবর কেউ বলেনি; খবরটা পৌঁছায় rejected-write-এ। এই asymmetry-ই distributed lock-এর core mental model।

ছবিতে

 LOCK SERVICE              WORKER A                    STORAGE (guard)
 ┌─────────────┐   acquire ┌──────────┐                ┌──────────────┐
 │             │◄──────────│          │                │  last_seen=? │
 │  token=17   │           │  read    │───read────────►│              │
 │  ttl=30s    │           │  bal=500 │                │              │
 │  [LEASE A]  │           │  ▒▒▒▒▒▒▒ │                │              │
 └─────┬───────┘           │  ▒GC▒▒▒▒ │   ← ৫৪ সেকেন্ড  │              │
       │ ttl expires       │  ▒pause▒ │     এক ঝলকে    │              │
       ▼                   │  ▒▒▒▒▒▒▒ │                │              │
 ┌─────────────┐   acquire │  ▒▒▒▒▒▒▒ │                │              │
 │  token=18   │◄──────────────────────│ B ─────────────│──write(18)──►│
 │  [LEASE B]  │           │          │                │ accept, 18   │
 └─────────────┘           │          │                ├──────────────┤
                           │  জেগে উঠে │──write(17)───►│ 17 ≤ 18      │
                           │  bal=560 │                │ ★ REJECTED ★ │
                           └──────────┘                └──────────────┘
GC-pause-এ দুই holder-এর জন্ম — আর fencing token stale writer-কে storage-এর দরজাতেই ফেরত পাঠায়।

A-র সব সিদ্ধান্ত তার নিজের বিশ্বাস থেকে (লক আমার); শেষ সত্য- পরীক্ষা storage-এর এক line-এ — last_seen-এর সাথে তুলনা।

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

EXPERIMENT

pg_advisory_lock-এর session-fate, আর thread-based GC-pause simulation-এ fencing-এর পার্থক্য

PostgreSQL (brew install postgresql / apt) + Python 3.10+· ২০ মিনিট

ধাপ ১ — দুই session, এক advisory lock:

# Terminal 1 — holder:
psql -d postgres -c 'SELECT pg_advisory_lock(424242);'   # ধরে রাখে
# Terminal 2 — চেষ্টাকারী (প্রথমবার):
psql -d postgres -c 'SELECT pg_try_advisory_lock(424242);'   # → f (deny)
# Terminal 1-এর session খুন করুন:
psql -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity \
                     WHERE datname='postgres' AND pid <> pg_backend_pid();"
# Terminal 2 — আবার:
psql -d postgres -c 'SELECT pg_try_advisory_lock(424242);'   # → t (মুক্ত!)

কোনো TTL সেট করেননি, unlock চালাননি — connection-মৃত্যুই release- event। এটাই lease-semantics-এর পরিচ্ছন্নতম বাস্তবায়ন; DB-advisory lock-এ GC-pause-দুশ্চিন্তাও ছোট (জীবিত connection ধরে রাখে, মরলেই সত্যিই মুক্ত)।

ধাপ ২ — GC-pause simulation, fencing on/off:

"""gc_pause_fencing.py — python3 gc_pause_fencing.py fenced|unfenced"""
import sys, threading, time

TTL, PAUSE = 0.30, 0.60            # pause ইচ্ছা করে TTL ছাড়িয়ে যায়
FENCED = (sys.argv[1] if len(sys.argv) > 1 else 'unfenced') == 'fenced'

tok, expires, cv = [0], [0.0], threading.Condition()

def acquire():
    with cv:
        now = time.monotonic()
        if expires[0] > now:
            return None                    # live lease আছে
        tok[0] += 1                        # fencing token: একমুখী বৃদ্ধি
        expires[0] = now + TTL
        return tok[0]

balance = {'v': 500}                       # shared ledger
last_seen, glock, trail = [0], threading.Lock(), []

def guarded_write(writer, new_value, my_tok):
    if FENCED:
        with glock:
            if my_tok <= last_seen[0]:
                return False               # ★ stale token — reject
            last_seen[0] = my_tok
    balance['v'] = new_value
    trail.append((writer, new_value))
    return True

def worker_a():
    my_tok = acquire()                     # token 1, তারপর read —
    snap = balance['v']                    # B শুরুর আগেই হয়ে যায়
    print(f'A: tok={my_tok}, read={snap}, pause {PAUSE}s...')
    time.sleep(PAUSE)                      # ← THE PAUSE (lease মরে যায়)
    planned = snap - 60                    # পুরনো snapshot-এ হিসাব!
    ok = guarded_write('A', planned, my_tok)
    print(f'A: woke, write {planned} tok={my_tok} → {"ACCEPTED" if ok else "REJECTED"}')

def worker_b():
    while True:
        my_tok = acquire()
        if my_tok: break
        time.sleep(0.05)                   # lease ফুরোলেই পাবে
    ok = guarded_write('B', balance['v'] - 50, my_tok)
    print(f'B: tok={my_tok} → {"ACCEPTED" if ok else "REJECTED"}')

a = threading.Thread(target=worker_a); a.start()
b = threading.Thread(target=worker_b); b.start()
a.join(); b.join()
print(f'final = {balance["v"]}  (expected: 450)')
assert balance['v'] == 450, 'LOST UPDATE — stale write overwrote fresh!'
print(f'PASS [{sys.argv[1] if len(sys.argv)>1 else "unfenced"}] — {trail}')
$ python3 gc_pause_fencing.py unfenced
A: tok=1, read=500, pause 0.6s...
B: tok=2 → ACCEPTED
A: woke, write 440 tok=1 → ACCEPTED
final = 440  (expected: 450)
AssertionError: LOST UPDATE

$ python3 gc_pause_fencing.py fenced
B: tok=2 → ACCEPTED
A: woke, write 440 tok=1 → REJECTED
final = 450  (expected: 450)
PASS [fenced]

তিন পাঠ: unfenced run-এ A কোনো exception পায়নি — silent corruption-ই এই বিপদের স্বভাব; reject এলো storage-guard থেকে, lock-service থেকে না — theory-র হুবহু প্রতিধ্বনি; আর বাস্তব PAUSE হতো ৫০ ms GC, TTL ৩০ s — ratio আলাদা, গঠন একই।

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

Session মরলে pg_advisory_lock নিজে নিজে মুক্ত হয় — lock-জীবন connection-জীবনে বাঁধা; আর GC-pause simulation-এ fencing ছাড়া stale holder সত্যিই lost-update লেখে, fencing চালু হলে সেই write সত্যিই reject হয়।

নিজে বানান

BUILD IT

LeaseLock — lease-granting server, injectable pause, fencing guard, chaos test

Python · ●●●●○
  1. SimClock — virtual clock যা tick(ms) দিয়ে এগোয়, chaos test deterministic
  2. LockServer.acquire — live lease থাকলে deny, নাহলে monotonic token সহ TTL lease
  3. LockServer.release — শুধু নিজের token দিয়েই release করা যায়
  4. GuardedResource.write — fencing চালু হলে stale token reject, বন্ধ থাকলে সব accept
  5. chaos_run — holder-এর মাঝপথে TTL-ছাড়িয়ে pause inject করে property যাচাই
"""leaselock.py — simulated-clock chaos lab for lease locks"""

class SimClock:
    def __init__(self):
        self.now = 0                       # ms

    def tick(self, ms):
        self.now += ms

class LockServer:
    def __init__(self, clock, ttl_ms=100):
        self.clock, self.ttl = clock, ttl_ms
        self.leases = {}                   # res -> [holder, token, expiry]
        self.last_token = {}               # res -> monotonic counter

    def acquire(self, res, client):
        h = self.leases.get(res)
        if h and h[2] > self.clock.now:    # live lease — deny
            return None
        tok = self.last_token.get(res, 0) + 1
        self.last_token[res] = tok         # একমুখী, কখনো reset না
        self.leases[res] = [client, tok, self.clock.now + self.ttl]
        return tok                         # ← fencing token

    def release(self, res, client, tok):
        h = self.leases.get(res)
        if h and h[:2] == [client, tok]:
            del self.leases[res]

class GuardedResource:
    """storage — fencing চালু হলে stale token ফেরত"""

    def __init__(self, fenced):
        self.fenced, self.seen, self.writes = fenced, 0, []

    def write(self, holder, token):
        if self.fenced and token <= self.seen:
            return False                   # ★ stale writer blocked
        self.seen = max(self.seen, token)
        self.writes.append(holder)
        return True

def chaos_run(fenced):
    clock = SimClock()
    srv = LockServer(clock, ttl_ms=100)
    res = GuardedResource(fenced)

    tok_a = srv.acquire('stock', 'A')      # A-র lease: token 1
    clock.tick(40)                         # A কাজ করছে...
    clock.tick(90)                         # ...GC pause! (130 > TTL 100)

    tok_b = srv.acquire('stock', 'B')      # lease মরা — B পায় token 2
    ok_b = res.write('B', tok_b)
    ok_a = res.write('A', tok_a)           # A জেগে লেখার চেষ্টা

    srv.release('stock', 'B', tok_b)       # B ছাড়ল — crash-safe handoff
    assert srv.acquire('stock', 'C') == 3
    return ok_b, ok_a, res.writes

if __name__ == '__main__':
    b1, a1, w1 = chaos_run(fenced=False)
    print(f'unfenced: B={b1}, stale-A={a1}, trail={w1}')
    assert (b1, a1) == (True, True), 'দুই writer-ই ঢুকেছে!'

    b2, a2, w2 = chaos_run(fenced=True)
    print(f'fenced  : B={b2}, stale-A={a2}, trail={w2}')
    assert (b2, a2) == (True, False), 'fencing stale-A আটকাতেই হবে!'

    print('PASS — no-two-successful-writers টিকেছে শুধু fencing-এ')

নমুনা output:

unfenced: B=True, stale-A=True, trail=['B', 'A']
fenced  : B=True, stale-A=False, trail=['B']
PASS — no-two-successful-writers টিকেছে শুধু fencing-এ

তিন পাঠ: SimClock-ই সস্তা chaos-engineering — wall-clock-race নয়, সময় হাতে ধরে exact দুর্ঘটনা replay; last_token কখনো reset হয় না (reset করলে fencing-ক্রম ভাঙে); আর property-টা ছোট — successful writers ≤ 1, যা টেকে শুধু guard-সহ অবস্থায়।

নিজে বাড়ান:

  1. Background renewal-thread যোগ করুন — renewal miss-এর পরিণতি chaos-test-এ দেখান
  2. clock.jump_back(-50) injection — fencing কেন clock-jump-এও অটুট, প্রমাণ করুন
  3. Guard-বিহীন resource simulate করুন — lock-shudhu-efficiency দর্শন সেখানেই সৎ

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

বাস্তবে distributed lock

Redis SET NX PX + Redlock। সবচেয়ে বহুল ব্যবহৃত stack — single-instance lock efficiency-র কাজে দুর্দান্ত (cache-fill dedup, cron-leader pick); correctness-critical লেখায় fencing-ছাড়া ব্যবহার না-করার দিকেই শিল্প-consensus ঝুঁকেছে।

Kubernetes Lease objects। controller-manager, scheduler — K8s- এর নিজের component-রা leader election করে Lease API-তে: holder identity + renewTime; মেয়াদ ফুরোলে অন্যরা প্রতিদ্বন্দ্বী — cloud-native জগতের ZooKeeper-session-উত্তরাধিকার, operator-লেখকদের জন্যও একই pattern।

etcd ও ZooKeeper। Consensus-backed coordination stores — etcd- র concurrency package সরাসরি session+lock recipe দেয়, revision number ready-made fencing source; heavy-duty correctness এদের ডাকে।

Postgres advisory locks। Oban (Elixir), GoodJob (Ruby), pg-boss (Node) — আধুনিক job-queue framework-এর পুরো ধারা advisory-lock-এই: job dedup, cron-leader, worker claim — fate-sharing যুক্তির শিল্প- বিজয়। আর Google-এর Chubby (2006) lock+membership service GFS/Bigtable- কে ভিত দিয়েছিল; ZooKeeper তার খোলা-উৎস প্রতিধ্বনি — lock সস্তা, তার প্রতিশ্রুতির সীমা জানাই engineering।

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

“Redis-এ SET NX PX করলেই mutual exclusion নিশ্চিত — lock তো পেয়েছি।”

Acquire-confirmation মানে ভবিষ্যতের একচেটিয়া দখল নয় — TTL ফুরোলে server আপনার অজান্তেই অন্যকে দিতে পারে, আর pause থেকে জেগে আপনি একই ভাবে চলতে থাকেন। Lock পাওয়া momentary fact, continuing guarantee না; guarantee আসে resource-side fencing check থেকে।

“TTL বড় দিলেই সমস্যা মিটে যায় — ১০ মিনিট দাও, pause ঢাকা পড়বে।”

TTL বাড়া মানে crash-recovery দেরি বাড়া — মৃত holder-এর lock সবার কাজ TTL পর্যন্ত বন্ধ রাখে। আর pause-এর upper-bound নেই: swap, checkpoint, VM-stun যে-সংখ্যাই ছাড়িয়ে যেতে পারে। TTL liveness-tuning knob, safety-mechanism না; safety-র ঢাল আলাদা — fencing।

“Fencing token ব্যবহার করলে lock আর দরকার নেই — token-ই সব করে।”

Token নিজে কিছুই করে না — মূল্য তখনই, যখন resource check করে। Check-না-করা storage-এ (S3 PUT, third-party API) token সাজানো সংখ্যা মাত্র। উল্টোটাও সত্য: token-checking storage থাকলে lock-এর কাজ মূলত token-issue — তখন সস্তা single-node lock-ই যথেষ্ট, Redlock- বাহুল্য লাগে না।

“ZooKeeper/etcd-consensus আছে বলে ওদের lock সবসময় সঠিক — fencing-দুশ্চিন্তা সেখানে নেই।”

Consensus নিশ্চিত করে lock-service-এর নিজের অবস্থা consistent — client-GC-pause-এর বিরুদ্ধে নয়। Session-timeout ফুরোলে ephemeral znode মুছে অন্য client lock পেতে পারে, অথচ পুরনো client জেগে কাজ চালিয়ে যেতেই পারে — হুবহু Alice-দৃশ্য। ZK-recipe-ও sequence- number-as-fencing-token পর্যন্তই পূর্ণ; consensus store বেসলাইন শক্ত, জাদুর বাঁশ নয়।

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

1

Mutex-এর কোন দুটো ভিত্তি-অনুমান distributed lock-এ ভাঙে, আর প্রতিটা ভাঙন কোন concrete ব্যর্থতা জন্ম দেয়?

যুক্তি

এক: shared memory নেই — lock-state পৌঁছায় message-এ, আর message দেরিতে/হারায়, তাই client-এর জ্ঞান সম্ভাব্য-পুরনো (stale belief); থেকে যায় ghost-write। দুই: মৃত্যু সরাসরি শনাক্ত অসম্ভব — pause আর crash বাইরে থেকে একরকম; থেকে যায় মৃত holder-এর lock-চিরদখল বা জীবিতকে ভুল বহিষ্কার। প্রথমের ঔষধ fencing, দ্বিতীয়টার ঔষধ lease।

2

Service token 41 A-কে, তারপর 42 B-কে দিয়েছে; storage last_seen=42। এখন আসলো A(token 41), B(token 42 আবার), C(token 39 — পুরনো cache)। কে ঢুকবে, কেন?

প্রয়োগ

A: 41 ≤ 42 → reject। C: 39 ≤ 42 → reject — সবচেয়ে পুরনো। B: strict-rule-এ 42 ≤ 42 → reject — duplicate delivery idempotency- guard-এ ধরা পড়ে; চাইলে equal-token আলাদা handle, কিন্তু stale-নয়। মূল নিয়ম এক: accept শুধু strictly-greater token; একমুখী ক্রম কখনো ভাঙে না।

3

Kleppmann-এর Redlock-আপত্তি এক বাক্যে কী, antirez-এর শক্ততম জবাব কোনটা? মতভেদটা ঠিক কোন অনুমানের উপর?

যুক্তি

Kleppmann: Redlock-এর safety timing-assumption-এ দাঁড়িয়ে — bound ভাঙলে দুই holder সম্ভব, তাই fencing অমিটযোগ্য, আর fencing লাগলে Redlock-complexity-র দরকারই নেই। antirez: বাস্তব semi-synchronous — drift মাপা যায়, margin দেওয়া যায়; fencing-সহ Redlock progress-সহ বৈধ choice। কেন্দ্র: system-model — asynchronous না semi-synchronous।

4

Redlock N=5, majority=3। Node-3 স্থায়ীভাবে মরেছে — acquire এখনো সম্ভব? Validity থেকে drift-margin কেন বিয়োগ হয়; margin=0 রাখলে ঠিক কোন দুর্ঘটনা?

প্রয়োগ

সম্ভব — বাকি ৪ node-এ চেষ্টা, ≥3 পেলেই acquired; এক-মৃত্যু tolerated (দুই-মৃত্যুতে majority নেই)। Margin কাটা হয় কারণ acquire-এর সময়খরচ (RTT×5) TTL খায় আর node-ঘড়ি আলাদাভাবে এগোয়: safe validity = ttl − elapsed − drift। Margin=0-তে slow-acquire/এগিয়ে-ঘড়িতে validity কাগজে বেঁচে, বাস্তবে ফুরোয় — client বিশ্বাস করে lock বেঁচে, অথচ অন্যের কাছে সব lease শেষ।

5

Nightly-report job দিনে একবার, দুই replica deploy (দুটোই cron পায়), লেখে শুধু report-file — shared DB নেই। Lock-strategy কী, fencing লাগবে? উল্টো: একই job billing-ledger-এ লেখত?

ডিজাইন

Report-file: lock এখানে efficiency — duplicate হলে double compute, correctness নয় (deterministic overwrite)। সস্তা Redis lock বা leader- lease যথেষ্ট, fencing-অপ্রয়োজনীয় — শর্ত: idempotent/atomic-rename write। Billing-ledger: correctness — double charge; তখন ledger-এ fencing-column সহ conditional write (WHERE token > last) বা transaction-ভেতরে dedup-unique-constraint; lock-কে কখনো একাই safety- র ভরসা করা যাবে না।

এরপর কী

এরপর কী

Lock ছিল coordination-এর সবচেয়ে সরাসরি হাতিয়ার — একজন একসময়ে, বাকিরা অপেক্ষায়। পরের লেসনে দিক উল্টে যাবে: coordination-কেই বাদ দিয়ে convergence-এর গণিত। Gossip protocol — coordinator ছাড়া খবর O(log N) round-এ cluster ছড়ায় কীভাবে — আর CRDT: commutative+associative+idempotent merge-এ প্রতিটা replica শেষে হুবহু এক state-এ মেলে। Redlock-বিতর্ক বলল safety-র জন্য coordination লাগে — CRDT সেই প্রশ্নের AP-পাশের উত্তর: coordination বাদ, গণিত সামনে।

আরও পড়ুন