Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ১৪/১৯রিসার্চ-লেভেল১ ঘণ্টা ৫ মিনিট

WAL ও ARIES Recovery — crash-এর পরে হিসাব

Write-Ahead Log and ARIES Recovery

Crash যেকোনো মুহূর্তে আসতে পারে, আর buffer pool disk-এ যা-ই লিখে ফেলতে পারে — WAL আর ARIES-এর তিন-pass recovery কীভাবে সব committed কাজ ফিরিয়ে আনে আর সব uncommitted কাজ নীরবে মুছে দেয়।

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

  • Steal আর no-force policy দুটোই পারফরম্যান্সের জন্য কেন অপরিহার্য, আর এই দুটো মিলে crash-এর পরে disk state-কে ঠিক কোন দুই দিক থেকে ভাঙা রেখে যায় — ব্যাখ্যা করতে পারবেন
  • WAL rule-টা — log record ডেটা page-এর আগে durable — একটা fsync ordering contract হিসেবে বলতে পারবেন, আর commit point আসলে ঠিক কোন ঘটনা তা চিনতে পারবেন
  • WAL record-এর anatomy — LSN, redo info, undo info — আর page header-এ জমা page LSN-এর সম্পর্ক বুঝিয়ে idempotent redo-র apply/skip শর্তটা ব্যাখ্যা করতে পারবেন
  • ARIES-এর তিনটা pass — analysis, redo (repeating history), undo (backward + CLR) — প্রতিটার ইনপুট, আউটপুট আর 'কেন এভাবেই করতে হয়' যুক্তিসহ trace করতে পারবেন
  • pg_waldump দিয়ে বাস্তব PostgreSQL WAL segment খুলে record type, transaction ID আর LSN পড়ে চিনতে পারবেন

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

আগে এটা বুঝি

Buffer pool লেসনে দেখেছি — database-এর সব read/write আসলে memory-তে ঘটে; dirty page background-এ নিজস্ব সময়সূচিতে disk-এ যায়। দারুণ দ্রুত। কিন্তু অশুভ প্রশ্নটা ঝুলে আছে: ঠিক যেই মুহূর্তে disk-এর হাফ লেখা শেষ, তখনই power chale jabe?

সেই মুহূর্তে disk-এ পড়বে এক অদ্ভুত মিশ্রণ: কিছু transaction-এর লেখা পৌঁছেছে, কিছুর পৌঁছায়নি; কোনো page-এ এমন transaction-এর অর্ধেক কাজও থাকতে পারে যেটা কখনো commit-ই হয়নি। ACID-এর promise — committed-দের প্রভাব টিকবে, uncommitted-দের চিহ্ন মুছে যাবে। এই দুই দিকের মেরামতই crash recovery, এই লেসনের বিষয়।

আর recovery-র পুরো নকশা ঠিক হয়ে যায় normal operation-এর দুই সিদ্ধান্তে — buffer manager কি dirty page ইচ্ছেমতো disk-এ লিখতে পারবে (steal), commit-এ কি সব page flush করতে হবে (force)। দুটো উত্তরই “না” — আর সেখান থেকেই WAL-এর নিয়ম আর ARIES-এর তিন pass-এর জন্ম।

মূল ধারণা

Steal আর no-force — দুটো “না” যেগুলো সব বদলে দেয়

ধরুন একটা transaction ১০০০টা page বদলেছে, তারপর commit করল। Force policy মানে commit-এর মুহূর্তে সেই ১০০০টা page disk-এ flush — হবুল-ফবুল খরচ; hot page প্রতি commit-এ আবার লেখা। বাস্তবে কেউ করে না। No-force: commit মানেই শুধু commit log record durable — data page যাবে buffer manager-এর সুবিধামতো সময়ে।

উল্টো দিকে: pool ভরে গেছে, victim হয়েছে dirty page যেটা একটা চলমান transaction লিখেছে। Steal-no policy বলত ওটা ফেলা যাবে না — তাহলে uncommitted কাজের page memory আটকে রাখবে, pool ভরে সিস্টেম থেমে যাবে। তাই steal: victim যা-ই হোক, dirty হলেও disk-এ লেখা চলে।

Policyমানেনা মানলে যা হতো
No-forcecommit-এ data page flush বাধ্যতামূলক নয়প্রতিটা commit-এ write burst
Stealuncommitted txn-এর dirty page-ও disk-এ যায়pool ভরে deadlock

এই দুটো স্বাধীনতার দাম — crash-পরবর্তী disk-অবস্থা দুই দিকেই “ভুল”: committed কাজ হারাতে পারে (no-force: page পৌঁছায়নি), আর uncommitted কাজ disk-এ জমা থাকতে পারে (steal: লেখা already গেছে)।

তাই recovery-রও দুই কাজ: redo (committed ফিরিয়ে আনা) আর undo (uncommitted মুছে ফেলা)। উপকরণ — log।

WAL rule — একটা মাত্র ordering contract

পুরো ব্যবস্থার ভিত্তি একটা নিয়ম — Write-Ahead Logging:

data page-এর log record নিজে stable storage-এ পৌঁছানোর আগে সেই page কখনো stable storage-এ লেখা হবে না।

এটা একটা fsync ordering contract — “কী লিখব” নয়, “কোনটা আগে লিখব”। Dirty page flush-এর আগে সেই page-এর নতুনতম log record পর্যন্ত log fsync বাধ্যতামূলক; আর client-কে “committed” উত্তরের আগেই commit record fsync হতে হয়। Commit point আসলে এই মুহূর্তটাই — data page disk-এ গেছে কি না তাতে কিছু যায় আসে না; commit record durable হওয়াটাই সংজ্ঞা।

Log record-এর anatomy — LSN, redo আর undo

প্রতিটা log record-এর একটা LSN (Log Sequence Number) — log-এ byte-position, কেবল বাড়ন্ত। Record-এর ভেতরটা:

Fieldকাজ
LSNএই record-এর অনন্য, বাড়ন্ত অবস্থান
typeBEGIN / UPDATE / COMMIT / ABORT / CLR / checkpoint
txidকোন transaction-এর কাজ
page IDকোন page বদলাল
redo infoআবার করলে একই ফল হয় — সাধারণত physical: page, offset, new value
undo infoফেরত নিতে যা লাগে — সাধারণত old value
prev LSNএকই transaction-এর আগের record — undo-chain-এর সূতা

আর সবচেয়ে সূক্ষ্ম অংশ: প্রতিটা page-এর header-এও একটা LSN জমা থাকে — page LSN = “এই page-এ সর্বশেষ কোন log record প্রয়োগ হয়েছে”। Page আর log আলাদা schedule-এ disk-এ গেলেও এই সংখ্যা বলে দেয় page-টা log-এর কোথা পর্যন্ত আপডেট।

Idempotent redo — page_LSN \< rec.LSN শর্তটা

Redo pass একটা update record apply করবে কি না — সিদ্ধান্ত এক তুলনায়: disk-copy-র page LSN যদি record-এর LSN-এর আগের হয়, তবেই apply, নাহলে skip। এতে redo idempotent: একবার apply হয়ে গেলে শর্তটা আর সত্যি হয় না, তাই একই record recovery যতবার চলুক ঠিক একবারই কার্যকর। Recovery-র মাঝেই crash হলেও রক্ষা — পরের recovery নিঃশঙ্কচিত্তে সব আবার করতে পারে।

apply(R→P)  ⟺  page_LSN(P)<R.LSN\text{apply}(R \to P) \iff \text{page\_LSN}(P) \lt R.\text{LSN}

ভেতরে কী ঘটছে

ARIES — তিনটা pass-এর নাটক

IBM Almaden-এর ARIES (Mohan ও সহযোগীদের 1992 paper) এই জগতের canonical নকশা — PostgreSQL-সহ বেশিরভাগ database এই চিন্তায় চলে। তিন pass, ঠিক এই ক্রমে:

Recovery-র তিন pass
  1. Analysiscrash-এর মুহূর্তে কোন txns চলছিল, কোন pages dirty — তালিকা বানানো
  2. Redoforward scan — history repeat, uncommitted-ও
  3. Undobackward scan — losers ফেরত, CLR লেখা

Pass 1 — Analysis: অবস্থা জানা

Checkpoint থেকে দুই তালিকা: active txns আর dirty pages। Log scan-এ হিসাব টাটকা: commit/abort পেলে বাদ, BEGIN পেলে যোগ। শেষে losers — crash-মুহূর্তে commit/abort-হীন — আর dirty pages-এর পুরনোতম recLSN, redo-র শুরুর-রেখা।

Pass 2 — Redo: repeating history

ARIES-এর সবচেয়ে counter-intuitive সিদ্ধান্ত: redo pass সব update প্রয়োগ করে — uncommitted-দেরটাও। steal-policy-তে disk- এ কী পৌঁছেছে জানার উপায় নেই; নির্ভরযোগ্য পথ একটাই — history পুরোটা disk-এ আবার চালিয়ে দেওয়া, যাতে disk crash-মুহূর্তের memory-অবস্থায় ফেরে। সুবিধা: undo deterministic অবস্থা থেকে শুরু করে; page_LSN শর্ত up-to-date page-এর খরচ বাঁচায়।

Pass 3 — Undo: backward + CLR

Losers ফেরত: scan উল্টো দিকে — পরের লেখা আগেরটা overwrite করেছে, শেষ-লেখা-আগে-ফেরত না করলে ভুল old value বসে। প্রতিটা loser-update-এর undo info প্রয়োগ করে CLR (Compensation Log Record) লেখা হয়, ভেতরে undo_next_lsn — undo-chain-এর পরের stop। CLR দুই সমস্যা একসাথে মেটায়:

  • Recovery-র মাঝে আরেকবার crash: CLR durable হওয়ার পরে দ্বিতীয় recovery-র redo pass CLR replay করে — অর্ধেক-done rollback টিকে থাকে; CLR কখনো undo হয় না (redo-only) — এটাই idempotence।
  • Progress tracking: undo_next_lsn already-compensated update-এর ঠিকানা — একই কাজ দুবার নয়।

Fuzzy checkpoint — teaser

থামা-flush checkpoint production-এ অসম্ভব। ARIES-এর উত্তর: fuzzy checkpoint — চলন্ত অবস্থাতেই active-txn আর dirty-page তালিকা log-এ লেখা, flush নিজের মতো; Analysis সেই তালিকা থেকেই শুরু। কম্পাঙ্ক-খরচ-recovery-time হিসাবটাই পরের লেসন।

উদাহরণ

একটা crash, ধাপে ধাপে

তিনটা transaction, একটা page P (শুরুতে A), আর অমার্জন্ময় crash:

T1: BEGIN; P:=B; COMMIT            <- committed; P disk-এ যায়নি (no-force)
T2: BEGIN; P:=C                    <- in-flight; steal করে P flush হয়েছে!
T3: BEGIN; P:=D                    <- in-flight, শুধু log-এ
                                        *** CRASH ***

Log: 1:T1-BEGIN 2:T1-P:=B 3:T1-COMMIT 4:T2-BEGIN 5:T2-P:=C 6:T3-BEGIN 7:T3-P:=D। Disk-এ P: steal-এ LSN 5 পর্যন্ত আপডেট — uncommitted C বসে আছে, committed B-ও পৌঁছায়নি। Analysis: losers = {T2, T3}।

Redo (forward) — disk crash-মুহূর্তের হুবহু অবস্থায় (D, uncommitted হলেও):

rec 2: page_LSN(-1) \< 2  -> P:=B, page_LSN=2
rec 5: page_LSN(2)  \< 5  -> P:=C, page_LSN=5
rec 7: page_LSN(5)  \< 7  -> P:=D, page_LSN=7

Undo (backward) — T3-এর D ফেরত, তারপর T2-এর C → CLR-সহ:

rec 7 (T3): P:=old-of-T3, CLR(undo_next=nil)
rec 5 (T2): P:=B (T2-এর old value), CLR(undo_next=...)

শেষ ফল: P = B — T1-এর committed মান। Committed কাজ ফিরে পেল, uncommitted-দের চিহ্ন নেই — durability আর atomicity দুটোই রক্ষা।

ছবিতে

পুরো pipeline এক ছবিতে

     NORMAL OPERATION                  CRASH RECOVERY
┌─────────────────────┐      ┌───────────────────────────┐
│ txn write           │      │ ANALYSIS (last ckpt থেকে) │
│   ▼                 │      │   losers + dirty pages    │
│ [1] UPDATE record   │      │        ▼                  │
│   ▼                 │      │ REDO (forward scan)       │
│ [2] log fsync       │      │   history repeat —        │
│   │    ┌────────┐   │      │   uncommitted-ও; শর্ত:    │
│   └───►│        │   │      │   page_LSN \< rec.LSN     │
│        ▼        ▼   │      │        ▼                  │
│ [3] data page flush │      │ UNDO (backward scan)      │
│     (শুধু [2]-এর    │      │   losers + CLR (redo-only, │
│      পরে!)          │      │   idempotent)             │
│ COMMIT = commit-    │      └───────────────────────────┘
│          record fsync│
└─────────────────────┘
Normal operation বাঁয়ে, recovery ডানে — WAL rule মাঝখানের সেতু: page disk-এ যাওয়ার আগে তার log অবশ্যই durable।

আর undo-chain এক লাইনে: U1 ── U2 ── U3 (এক txn-এর updates, prev-LSN-সূতায় বাঁধা) → backward undo প্রতিটার জন্য old ফেরত + CLR(undo_next→পরেরটা) লেখে, শেষ CLR-এর undo_next=nil। দ্বিতীয় crash হলে redo pass CLR replay করে অর্ধেক-done rollback ধরে রাখে; undo pass বাকিটা শেষ করে — কোনো কাজ দুবার নয়, বাদও নয়।

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

EXPERIMENT

বাস্তব WAL খুলে পড়ুন — pg_waldump আর pg_current_wal_lsn

macOS / Linux (PostgreSQL installed)· ২০ মিনিট

ধাপ ১ — ছোট scratch cluster, checksum-সহ:

initdb -D /tmp/waldemo --data-checksums
pg_ctl -D /tmp/waldemo -o "-p 5544" -l /tmp/waldemo.log start
export PGPORT=5544
psql -c "CREATE TABLE t(id int, note text);"

ধাপ ২ — LSN দেখা আর WAL ঘোরানো:

psql -c "SELECT pg_current_wal_lsn();"        # যেমন: 0/181D2A0
psql -c "INSERT INTO t VALUES (1,'wal rules');"
psql -c "SELECT pg_current_wal_lsn();"        # বেড়েছে — প্রতিটা লেখার দাম
psql -c "SELECT pg_switch_wal();"

pg_current_wal_lsn হুবহু আমাদের LSN ধারণা — log-এ বর্তমান অবস্থান। pg_switch_wal segment (16 MB ফাইল) ঘুরিয়ে দেয়।

ধাপ ৩ — record decode:

pg_waldump -p /tmp/waldemo/pg_wal -s 0/01000000 -n 25

Annotated নমুনা:

rmgr: Heap        len (rec/tot):     58/  1178, tx:        742,
desc: INSERT off 1 flags 0x00, blkref #0: rel 1663/5/16384 blk 1 FPW
rmgr: Transaction len (rec/tot):     72/    72, tx:        742,
desc: COMMIT @ 2026-08-24 12:00:00
rmgr: XLOG        desc: SWITCH
  • Heap ... rel .../16384 blk 1 — t-table-এর update record; FPW = checkpoint-পরব প্রথম ছোঁয়ায় page-image (পরের লেসনের নায়ক)
  • Transaction ... COMMIT — commit point; SWITCH — pg_switch_wal()-এর record-এর record

ধাপ ৪ — torn-page আলোচনার read-only প্রদর্শনী:

oid=$(psql -Atc "SELECT relfilenode FROM pg_class WHERE relname='t'")
dd if=/tmp/waldemo/base/5/$oid bs=8192 count=1 status=none | od -A d -t x1 | head -3
pg_controldata /tmp/waldemo | grep -i checksum

প্রতিটা 8 KB page disk-এ ১৬টা 512-byte sector-এ ছড়িয়ে — sector- লেখার মাঝে power গেলে page torn: অর্ধেক পুরনো, অর্ধেক নতুন। Data page checksum version: 1 — এই checksum-ই বিকৃত page ধরে; চিকিৎসা একটাই: log থেকে page আবার গড়া। এই লেসনের যন্ত্রটা এই মুহূর্তের জন্যই।

pg_ctl -D /tmp/waldemo stop && rm -rf /tmp/waldemo
এটা কী প্রমাণ করে

WAL কোনো abstract ধারণা নয় — আসল byte-sequence, যাতে প্রতিটা INSERT/COMMIT-এর নাম-ধারী record আছে; LSN সত্যিই বাড়ন্ত অবস্থান, আর torn-page ঝুঁকিটা checksum দিয়ে শনাক্তযোগ্য।

নিজে বানান

BUILD IT

ARIES-lite — log, steal, crash, recover, আবার crash

Python · ●●●○○
  1. Append-only log — list append-ই stable-storage অভিনয়
  2. Buffer pool + steal eviction; flush-এর আগে WAL rule
  3. crash()-এ pool-এর volatile state ফেলে দেওয়া
  4. recover(): analysis → redo (page_LSN শর্তে) → undo (backward + CLR)
  5. Random crash-point যাচাই: final == committed-prefix, recover() দুবার idempotent

লক্ষ্য: example section-এর হাতে-trace-টা স্বয়ংক্রিয় — crash যেকোনো মুহূর্তে হলেও ফল এক থাকে তা প্রমাণসহ।

import random
from dataclasses import dataclass

NIL = -1

@dataclass
class Rec:
    lsn: int
    kind: str                    # 'begin'|'update'|'commit'|'clr'
    txid: int
    page: str = ''
    old: object = None
    new: object = None
    undo_next: int = NIL         # শুধু CLR ব্যবহার করে
class Node:
    """WAL + buffer pool + disk — steal/no-force, ARIES-lite recovery"""
    def __init__(self):
        self.log = []                         # stable storage — crash-proof
        self.pool = {}                        # volatile buffer pool
        self.pool_lsn = {}                    # volatile
        self.disk = {}                        # survives
        self.disk_lsn = {}                    # page header-এর page_LSN
        self.holds = {}                       # strict-2PL অভিনয়: page -> txid

    def append(self, **kw) -> int:
        rec = Rec(len(self.log), **kw)        # list append == instant durable
        self.log.append(rec)
        return rec.lsn

    def current(self, page):
        return self.pool.get(page, self.disk.get(page))

    def begin(self, tx):
        self.append(kind='begin', txid=tx)

    def write(self, tx, page, new):
        if self.holds.get(page, tx) != tx:
            raise RuntimeError("strict-2PL: page অন্য active txn-এর হাতে")
        self.holds[page] = tx
        old = self.current(page)
        lsn = self.append(kind='update', txid=tx, page=page, old=old, new=new)
        self.pool[page] = new                 # WAL-first: log-এর *পরে* dirty
        self.pool_lsn[page] = lsn

    def commit(self, tx):
        self.append(kind='commit', txid=tx)   # এই LSN durable == commit point
        for p in [p for p, o in self.holds.items() if o == tx]:
            del self.holds[p]

    def steal_somewhere(self):
        """buffer manager-এর স্বাধীনতা: যেকোনো dirty page disk-এ"""
        if not self.pool:
            return
        page = random.choice(list(self.pool))
        self.disk[page] = self.pool.pop(page)     # WAL rule: log already durable
        self.disk_lsn[page] = self.pool_lsn.pop(page)

    def crash(self):
        self.pool.clear()                     # সব volatile state শেষ
        self.pool_lsn.clear()

    def recover(self):
        # PASS 1: analysis — losers বের করা
        losers = set()
        for r in self.log:
            if r.kind == 'begin':
                losers.add(r.txid)
            elif r.kind == 'commit':
                losers.discard(r.txid)
        # PASS 2: redo — history repeat (uncommitted-সহ), idempotent শর্তে
        for r in self.log:
            if r.kind not in ('update', 'clr'):
                continue
            if self.disk_lsn.get(r.page, NIL) < r.lsn:
                self.disk[r.page] = r.new
                self.disk_lsn[r.page] = r.lsn
        # PASS 3: undo — backward, losers-এর জন্য CLR emission
        pending = {}
        for i, r in enumerate(self.log):      # প্রতিটা loser-এর শেষ update
            if r.kind == 'update' and r.txid in losers:
                pending[r.txid] = i
        for i in range(len(self.log) - 1, -1, -1):
            r = self.log[i]
            if r.kind == 'update' and pending.get(r.txid) == i:
                self.disk[r.page] = r.old
                self.disk_lsn[r.page] = self.append(
                    kind='clr', txid=r.txid, page=r.page,
                    new=r.old, undo_next=pending[r.txid])
                prev = NIL
                for j in range(i - 1, -1, -1):
                    if self.log[j].kind == 'update' and \
                            self.log[j].txid == r.txid:
                        prev = j
                        break
                pending[r.txid] = prev


def committed_prefix(work):
    """স্বাধীন যাচাই: committed-prefix অনুযায়ী প্রত্যাশিত অবস্থা"""
    expect, staged, holds = {}, {}, {}
    for op in work:
        if op[0] == 'w':
            _, tx, page, val = op
            if holds.get(page, tx) == tx:
                holds[page] = tx
                staged.setdefault(tx, {})[page] = val
        elif op[0] == 'c':
            expect.update(staged.get(op[1], {}))
            holds = {p: o for p, o in holds.items() if o != op[1]}
    return expect


def trial(seed):
    rng = random.Random(seed)
    node, work, active, tx = Node(), [], [], 0
    for step in range(rng.randint(15, 45)):
        act = rng.random()
        if act < 0.25 or not active:
            tx += 1
            node.begin(tx)
            active.append(tx)
            work.append(('b', tx))
        elif act < 0.8:
            t = rng.choice(active)
            page, val = f"P{rng.randint(1, 4)}", rng.randint(0, 99)
            try:
                node.write(t, page, val)
                work.append(('w', t, page, val))
            except RuntimeError:
                pass                          # lock-conflict — বাদ
        else:
            t = rng.choice(active)
            active.remove(t)
            node.commit(t)
            work.append(('c', t))        if rng.random() < 0.3:
            node.steal_somewhere()
    node.crash()
    node.recover()
    once = dict(node.disk)
    node.recover()                            # idempotence: দ্বিতীয়বার
    assert dict(node.disk) == once, "recovery idempotent নয়!"
    # শুধু loser-রা ছোঁয়া pages None-এ ফেরে — prefix-এ ওরা নেই
    final = {p: v for p, v in once.items() if v is not None}
    assert final == committed_prefix(work), f"{final} != প্রত্যাশিত"
    return True


for s in range(300):
    trial(s)
print("300 crash-point সবই committed-prefix-এ; recovery idempotent।")

নিজে বাড়ান:

  1. Checkpoint — প্রতি N record-এ dirty-list + active-set snapshot; redo সেখান থেকে
  2. steal_somewhere adversarial — recent-est page flush; ফল একই দেখান

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

বাস্তব জগতে WAL

PostgreSQL। pg_waldump-এর record-গুলোই এর log; wal_level, full_page_writes, synchronous_commit — এই লেসনের knob। PG-ও history repeat করে; aborted txn-দের টুপল MVCC-visibility-তে নিষ্ক্রিয় থাকে (vacuum সাফ করে) — ARIES flavor, মূলনীতি এক।

Oracle আর MySQL InnoDB। Redo log আর redo+undo segment — ARIES-পরিবারের সরাসরি উত্তরাধিকার; InnoDB rollback segment-এ CLR-সমতুল্য undo record-ও আছে।

SQLite আর Kafka। SQLite-এর rollback journal (undo-ঘেঁষা) বনাম WAL mode (redo-ঘেঁষা) — journal_mode switch-ই strategy বাছাই। Kafka-র পুরো broker এক commit log; ext4-journal, APFS copy-on-write — crash-consistency-র একই ordering-contract-চিন্তা সর্বত্র।

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

“Commit হলে ডেটা disk-এ চলে যায়।”

না — commit মানে শুধু commit log record durable; data page তখনো memory-তে থাকতে পারে (no-force)। Durability আসে WAL rule থেকে: পরিবর্তনের পূর্ণ বিবরণ log-এ আছে, page পৌঁছানো-না-পৌঁছানো সমস্যাই নয়। “saved” indicator-এর পেছনের সত্যটা এটাই।

“Redo pass শুধু committed transaction-গুলো replay করে।”

ARIES-এর সবচেয়ে বিখ্যাত বিভ্রান্তি! Redo সব update replay করে — uncommitted-ও — steal-policy-তে কোনটা disk-এ ছিল জানার অন্য উপায় নেই। বাছাইটা undo pass-এ, পরে।

“Crash-এর আগে কোনো uncommitted লেখা disk-এ যায়নি, তাই undo দরকারই নেই।”

Steal-policy-তে uncommitted লেখা নিয়মিত-ই disk-এ যায় — pool-এ জায়গা দরকার হলেই। ধারণাটা মানলে uncommitted page memory-তে আটকে থাকবে, আর একটাই লম্বা transaction পুরো pool দখল করে সিস্টেম আটকে দিতে পারে।

“WAL বন্ধ করলে (`synchronous_commit=off` বা fsync off) বিনামূল্যে গতি পাওয়া যায়।”

গতি পাবেন — দাম crash-window: synchronous_commit=off-এ crash-এ সদ্য-”confirmed” transaction হারাতে পারে (durability delay, corruption নয়); fsync=off পুরো contract ভাঙে — corruption পর্যন্ত। প্রথমটা মাঝে মাঝে গ্রহণযোগ্য, দ্বিতীয়টা কখনোই না।

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

1

Page LSN কেন page-এর header-এ রাখা হয়, log-এ আলাদা কোনো table-এ নয়?

যুক্তি

প্রশ্নটা ওঠে specific page-এর সাথে: “এই page-এর disk-copy কোন পরিবর্তন পর্যন্ত ধারণ করে?” উত্তরটা তাই page-এর সাথেই travel করে — header-এ। আলাদা table crash-এ volatile হতো বা sync-সমস্যা হতো। Header-এ থাকলে redo এক তুলনায় (page_LSN \< rec.LSN) decide করে — আর সেই তুলনাই idempotence।

2

Undo pass backward scan করে কেন — forward scan করলে ঠিক কোন ভুল হতো? একটা দুই-update transaction দিয়ে দেখান।

যুক্তি

txn লিখেছে P: A→B, তারপর P: B→C। Forward undo: A→B-এর undo (P=B), তারপর B→C-এর undo (old=B) — শেষে P=B, ফেরতই হয়নি! Backward: শেষ-লেখা ফেরত (P=B), তারপর আগেরটা (P=A) — LIFO-ই একমাত্র সঠিক ক্রম।

3

Recovery-র undo pass-এর মাঝখানে আবার crash। তৃতীয় startup-এ সিস্টেম কীভাবে বুঝবে কোন rollback half-done? CLR-এর undo_next_lsn ছাড়া কী ভুল হতে পারত?

প্রয়োগ

CLR-রা নিজেই log-এ durable — redo pass replay করে half-done rollback ফিরিয়ে আনে, undo pass বাকিটা পায়। undo_next_lsn chain- অবস্থান বলে — না থাকলে already-compensated update আবার compensate হতো (double-undo), old value-গুলো ভুল অবস্থা লিখে দিত।

4

আপনার team-এর analytics database-এ প্রতি সকালে ২০ মিনিট ধরে recovery লাগে। তিনটা আলাদা দিক থেকে diagnosis আর fix-এর প্রস্তাব দিন — সবগুলোই এই লেসনের ধারণায় দাঁড়িয়ে।

ডিজাইন

(১) Recovery window বড় — শেষ checkpoint থেকে crash দূরে ছিল: checkpoint frequency বাড়ান (checkpoint_timeout, max_wal_size) — পরের লেসন। (২) Redo খরচ — FPW-heavy workload মানে replay-ও heavy; WAL-volume দেখুন। (৩) Hardware সীমা — recovery fundamentally log-replay; log device-এর fsync latency আর sequential- read throughput-ই ceiling। প্রতিটা fix-ই trade-off বহন করে।

এরপর কী

এরপর কী

Recovery-র খরচ নির্ধারণ করে একটাই সংখ্যা: শেষ checkpoint থেকে crash পর্যন্ত কতটা log। Checkpoint ঘন হলে recovery দ্রুত কিন্তু normal-operation-এ IO spike — PG এই দো-টানায় ভারসাম্য খোঁজে (checkpoint_completion_target)। আর পাশে MVCC-জগতের housekeeping — dead tuple সাফানো VACUUM, xid wraparound-রক্ষা। পরের লেসনে এই দুই “জমাট বাঁধা” কাজের অন্দরমহল।

আরও পড়ুন

  • ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging — C. Mohan, D. Haderle, B. Lindsay, H. Pirahesh, P. Schwarz — ACM TODS 17(1), 1992 · মূল ARIES paper — এই লেসনের তিন-pass design-এর প্রামাণ্য উৎস
  • Database System Concepts — Recovery System chapter — A. Silberschatz, H. Korth, S. Sudarshan · Log-based recovery, steal/no-force, checkpoint-এর পাঠ্যপুস্তক আলোচনা
  • Designing Data-Intensive Applications, Chapter 7 — Transactions: Durability and the Write-Ahead Log — Martin Kleppmann · WAL আর crash recovery-র practitioner-দৃষ্টিভঙ্গি