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

Saga ও Distributed Transactions — 2PC-র বিকল্প পথ

Sagas and Distributed Transactions

2PC-র blocking আর coordinator-SPOF scale-এ অসহনীয় — saga লম্বা transaction-কে local transaction-সিরিজ আর compensating action-এ ভাঙে; দাম হলো intermediate state বাইরের চোখে দেখা যাওয়া।

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

  • 2PC কেন scale-এ ভাঙে — voting window-এর blocking, coordinator single point of failure, network round-trip জুড়ে lock stretch — তিনটা ব্যর্থতা আলাদা করে ব্যাখ্যা করতে পারবেন
  • Saga-র যন্ত্রটা trace করতে পারবেন — long transaction-কে local transaction-সিরিজ T1..Tn-এ ভাঙা, failure-এ C(k−1)..C1 compensation cascade উল্টো ক্রমে চালানো
  • Orchestration (central conductor) বনাম choreography (event-triggered chain)-এর trade-off মিলিয়ে দেখে কোন workflow-এ কোনটা মানাবে, যুক্তি দিয়ে বলতে পারবেন
  • Intermediate state visibility-ই saga-র মূল দাম — dirty read of uncommitted saga state আর lost update চিনে semantic locking-জাতীয় countermeasure প্রয়োগ করতে পারবেন
  • Compensation-কে business-level, idempotent, retryable unit হিসেবে ডিজাইন করবেন, আর outbox pattern দিয়ে DB commit আর message publish atomic করতে পারবেন

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

আগে এটা বুঝি

গত লেসনে two-phase commit দেখেছি — distributed ACID-এর ক্লাসিক প্রতিশ্রুতি: সব participant একসাথে commit, নয়তো কেউই না। ছোট cluster-এ protocol-টা কাজও করে। কিন্তু এবার বাস্তব e-commerce flow: Order service order রাখছে, Flight service আসন ধরছে, Hotel service কক্ষ বুক করছে, Payment service টাকা কাটছে — চারটা service, চারটা database, হয়তো চার datacenter। 2PC চালালে prepare থেকে decide phase পর্যন্ত চার database-এর row-lock জমে থাকবে। ঠিক তখন coordinator মরল? চার service অনির্দিষ্টকালের অপেক্ষায় বন্দি।

আজকের প্রশ্ন: transaction ছড়াতে না পারলে, ভাঙলে কী হয়? উত্তরটা ১৯৮৭-র — Garcia-Molina আর Salem-এর Saga: লম্বা transaction-কে local transaction-সিরিজে ভাঙো, প্রতিটা নিজেই commit করুক, মাঝপথে ব্যর্থ হলে আগের ধাপগুলোর business-level undo চালাও। শোনায় সহজ; এই ভাঙনের আসল দাম আর সেই দাম মেটার যন্ত্র (semantic locking, outbox, idempotent compensation) — আজকের পাঠ।

মূল ধারণা

2PC কেন scale-এ অচল

সমস্যাকখন ঘটেআসল খরচ
BlockingParticipant vote yes দিয়ে decision-এর অপেক্ষায়Locks ধরে — অন্য কেউ ওই row ছুঁতে পারে না
Coordinator SPOFDecision-এর ঠিক আগে/পরে coordinator crashParticipant timeout ছাড়া জানবেই না commit না abort
Lock stretchপ্রতি extra participant = বাড়তি round-tripLock-holding time latency-র সাথে গুণ — throughput ভাঙে

গণিত নির্মম: n participant, RTT r — prepare+decide-এ অন্তত 2r সময়ের জন্য সবার সব lock জমে; microservice-এ এক checkout-এই ৫-৭ service, partition হলে blocking window অসীম। তাই scale-এ প্রশ্ন 2PC optimize নয় — 2PC ছাড়া পথ।

Saga — ভাঙা transaction-এর সেলাই

Long-lived transaction = local transaction-সিরিজ T1 → ... → Tn, প্রতিটা Ti নিজের database-এ পূর্ণ ACID। Failure Tk-তে হলে compensation C(k−1) → ... → C1 উল্টো ক্রমে চলবে।

তিনটা অপরিহার্য সূক্ষ্মতা:

  • Compensation transactional rollback নয় — সামনে-গামী নতুন local transaction যা business-অর্থে বাতিল করে: seat release, refund, cancel। Undo-log দিয়ে refund পাঠানো যায় না; charge একবার ঘটেই গেছে।
  • Compensation-ও fail করে — তাই retryable হতে হয়; retry মানেই duplicate delivery (পরের লেসন), তাই idempotent লিখতেই হয়।
  • সব T শেষ = saga complete — কোনো global commit লাগে না।

Intermediate state — THE cost

Flight-seat saga-তে T2 (seat hold) commit, T3 (payment) চলছে — এই জানালায়:

  • Dirty read of uncommitted saga state: অন্য user-এর query দেখে seat held — অথচ saga-level-এ booking uncommitted। Payment fail করলে seat ছাড়বে — ঠিক তখনই ওই user ওই seat চেয়ে বসেছিল।
  • Lost update: দুই concurrent saga একই inventory-row এক-এক ধাপে read-modify-write করল, মাঝে কেউ lock ধরে না — প্রথমটার write দ্বিতীয়টার লেখায় চাপা পড়ল।

Countermeasure-পরিবার (Garcia-Molina ও Richardson দুজনেরই):

Countermeasureযন্ত্রকী আটকায়
Semantic lockStatus field — PENDING/CONFIRMED; reader PENDING-কে ‘নেই’ ধরেDirty read
Commutative updateSET balance=X নয়, balance += −amount-জাতীয় deltaLost update
Version checkলেখার সময় expected_version verifyLost update
Retry + idempotencyCompensation বারবার-চালানো-নিরাপদDuplicate effect

Semantic lock database-এর Two-Phase Locking নয় — application-এর চুক্তি: PENDING মানে অসম্পূর্ণ saga, সব consumer সম্মান করবে। Enforce করে database না, discipline।

Orchestration vs choreography

ধাপগুলো কে চালায় — দুই উত্তর:

দিকOrchestrationChoreography
StructureCentral conductor command পাঠায়Service event ছড়ায়, পরেরটা subscribe করে
Flow knowledgeএক জায়গায় পুরো workflow visibleছড়ানো — code-এ সরাসরি দেখা যায় না
CouplingOrchestrator সবাইকে চেনেServices শুধু events চেনে
Failure handlingConductor cascade চালায় — explicitপ্রতিটা service নিজের compensation-event ছড়ায়
বিপদConductor SPOF-ঝুঁকি (stateless-replayable করা যায়)Cycle, dead-end, distributed monolith
মানায়Complex, audit-heavy flow (payment, travel)ছোট chain, দ্রুত-evolving pipeline

Universal winner নেই — বড় team orchestration দিয়ে শুরু করে, সীমানা-পরিষ্কার ছোট chain choreography-তে ফেলে।

ভেতরে কী ঘটছে

Semantic lock ভেতরে কেমন

CREATE TABLE bookings (
  id      bigint PRIMARY KEY,
  status  text NOT NULL DEFAULT 'PENDING',   -- semantic lock
  version int NOT NULL DEFAULT 0
);

-- saga step শেষে — optimistic confirm:
UPDATE bookings SET status='CONFIRMED', version=version+1
WHERE id=$1 AND version=$2;

কোনো SELECT FOR UPDATE-দীর্ঘঅপেক্ষা নেই — lock-এর বদলে **state-machine

  • optimistic version**। Conflict হলে zero rows; reader-দের চুক্তি উল্টো: status='CONFIRMED'-কেই exists ধরা।

Outbox pattern — dual-write-এর ফাঁদ

Choreography-র প্রথম মৃত্যুফাঁদ: DB-commit হলো, broker-publish-এ গিয়ে process মরল। DB-তে ঘটেছে, বিশ্ব জানে না; উল্টোটাও সমান জঘন্য। দুই system-এ atomic dual-write-এর সরাসরি উপায় নেই — তাই দুটোকে এক local txn-এর ভেতরে টানতে হয়:

ভাঙা dual-write:                 Outbox pattern:
BEGIN                            BEGIN
  UPDATE orders ...                UPDATE orders ...
  send_to_broker(event)  ←─✕       INSERT INTO outbox(payload)
COMMIT                           COMMIT     ←─ একই local txn!
                                 ── পরে, আলাদা relay process ──
                                 poll outbox → publish → mark sent

Relay-র দুই স্বাদ:

  • Poll publisher: loop — SELECT ... FROM outbox WHERE published=false ORDER BY id LIMIT n, publish, mark। Simple; polling-latency আর DB-chap দাম।
  • CDC: Debezium-জাতীয় tool WAL tail করে — outbox-insert log-এ পৌঁছাতেই broker-এ event। Latency milliseconds; দাম WAL-decoding যন্ত্রের complexity। দুই path-ই at-least-once — relay ফিরে এসে একই event দু’বার পাঠাতে পারে; consumer-side dedup অমিটযোগ্য (পরের লেসনের কেন্দ্রীয় বিষয়)। গ্যারান্টি-স্তর মনে রাখুন: local ACID (একমাত্র free lunch) → saga-level atomicity/isolation = compensation + semantic lock (discipline-কেনা) → cross-service delivery = at-least-once; বাকিটা consumer-এর দায়।

উদাহরণ

Order → Flight → Hotel — দুই গন্তব্য

সফল পথ:

t0  orchestrator: saga#501 শুরু (order_id=88)
t1  T1 create_order        orders.status=PENDING      COMMIT ✓
t2  T2 hold_flight 23A     flights.seat=HELD          COMMIT ✓
t3  T3 reserve_hotel 410   hotels.room=RESERVED       COMMIT ✓
t4  T4 charge_card 4500tk  payments.state=CAPTURED    COMMIT ✓
t5  সব T শেষ → saga COMPLETE, কোনো C-ই লাগেনি

এবার T3-এ বিস্ফোরণ — hotel full:

t0-t2  T1 ✓ T2 ✓                    ← effects already committed!
t3     T3 reserve_hotel ✗ FAIL (no rooms)
       ── compensation cascade, উল্টো ক্রমে ──
t4     C2 release_flight 23A        flights.seat=FREE        ✓
t5     C1 cancel_order 88           orders.status=CANCELLED  ✓
       saga ROLLED-BACK (business-অর্থে)
দাগের নিচের বাস্তবতা: t2-t4 জানালায় user দেখেছে seat HELD — অথচ
saga-level ব্যর্থতা তখনো ঘোষিতই হয়নি। intermediate state।

মনে রাখার সূত্র: failure-এর পরে সময় উল্টে যায় — যত দূরে গেছ, তত বেশি C; validation-first মানে ব্যর্থতা যত আগে ধরা পড়বে, compensation cascade তত ছোট।

ছবিতে

দুই স্থাপত্যের মানচিত্র

                  ┌──────────────────────┐
      command ▼   │     ORCHESTRATOR     │   ▲ result/reply
 ┌────────────────┤  (saga state machine) ├──────────────┐
 │                └──────────────────────┘              │
 ▼                    ▼              ▼                   ▼
┌────────┐       ┌─────────┐   ┌────────┐         ┌──────────┐
│ Order  │       │ Flight  │   │ Hotel  │         │ Payment  │
│ svc+db │       │ svc+db  │   │ svc+db │         │ svc+db   │
│ T1 / C1│       │ T2 / C2 │   │ T3 / C3│         │ T4 / C4  │
└────────┘       └─────────┘   └────────┘         └──────────┘
flow: T1→T2→T3→T4 ; fail@Tk → C(k−1)..C1 — সব conductor-এর হাতে
Orchestration — conductor প্রতি ধাপে command পাঠায়, উত্তর দেখে পরের সিদ্ধান্ত নেয়; compensation-ও তারই হাতে।
OrderCreated ──► [Flight] ──FlightHeld──► [Hotel] ──HotelFull!──┐
                                                                │
 ◄──OrderCancelled── [Order] ◄──FlightReleased── [Flight] ◄─────┘
          (উল্টো-দিকের event-chain = compensation cascade)

সফল পথ: OrderCreated ▶ FlightHeld ▶ HotelReserved ▶ CardCharged ▶ Confirmed
প্রতিটা ▶ published event — flow-র কোনো মালিক নেই; এটাই শক্তি ও বিপদ
Choreography — conductor নেই; প্রতিটা service event ছড়ায়, পরেরটা শুনে কাজ করে; failure-এ উল্টো-event chain-ই compensation।

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

EXPERIMENT

Booking-flow simulator — failure inject, cascade দেখো, duplicate-delivery মাপো

macOS / Linux (Python 3.10+)· ২০ মিনিট
"""booking_saga.py — forward-run + compensation cascade + dup-injection"""
from dataclasses import dataclass


@dataclass
class Step:
    name: str
    action: str
    compensate: str


def run_saga(steps, fail_at=None, dup_delivery=False):
    log, done_forward, effects = [], [], {}
    verdict = "COMPLETE"
    for i, s in enumerate(steps, start=1):
        if i == fail_at:
            log.append(f"T{i} {s.name:<13} FAIL (injected)")
            verdict = "ROLLED-BACK"
            break
        log.append(f"T{i} {s.name:<13} OK  [{s.action}]")
        done_forward.append(i)
        effects[f"T{i}"] = effects.get(f"T{i}", 0) + 1

    if dup_delivery:                       # broker redelivery simulation
        j = len(done_forward)
        effects[f"C{j}"] = effects.get(f"C{j}", 0) + 1   # guard ছাড়া!

    for j in reversed(done_forward[1:] if fail_at else []):
        c = steps[j - 1]
        effects[f"C{j}"] = effects.get(f"C{j}", 0) + 1
        log.append(f"C{j} {c.name:<13} undo [{c.compensate}]")
    return log, effects, verdict


steps = [
    Step("create_order",  "orders=PENDING",   "orders=CANCELLED"),
    Step("hold_flight",   "seat23A=HELD",     "seat23A=FREE"),
    Step("reserve_hotel", "room410=RESERVED", "room410=RELEASED"),
    Step("charge_card",   "CAPTURED 4500tk",  "refund 4500tk"),
]

log, eff, v = run_saga(steps)
print(f"  সফল পথ: {len(eff)} effect, verdict={v}\n")

print("=== case 2: T3-তে failure ===")
log, eff, v = run_saga(steps, fail_at=3)
print("\n".join("  " + line for line in log), f"\n  verdict={v}\n")

print("=== case 3: একই, কিন্তু C2 duplicate-delivery (guard ছাড়া) ===")
log, eff, v = run_saga(steps, fail_at=3, dup_delivery=True)
for k in sorted(eff):
    print(f"  {k}: effect x{eff[k]}")
print("  C2 duplicate চলেছে — non-idempotent effect-এ এটা দ্বিগুণ টাকা")

নমুনা output:

=== case 2: T3-তে failure ===
  T1 create_order  OK  [orders=PENDING]
  T2 hold_flight   OK  [seat23A=HELD]
  T3 reserve_hotel FAIL (injected)
  C2 hold_flight   undo [seat23A=FREE]
  C1 create_order  undo [orders=CANCELLED]
  verdict=ROLLED-BACK

=== case 3: একই, কিন্তু C2 duplicate-delivery (guard ছাড়া) ===
  T1: effect x1
  T2: effect x1
  C2: effect x2   ← guard ছাড়া দ্বিগুণ!

তিনটা পর্যবেক্ষণ:

  • Cascade length = failure-index − 1 — তাই payment-জাতীয় ঝুঁকি শেষে রাখা validation-first design-এর অর্থনীতি।
  • Duplicate-টা seat-release-এ নিরীহ (already FREE) — কিন্তু non-idempotent effect-এ দ্বিগুণ টাকা; guard মানে compensation-key journal: already-done হলে skip (build-section-এই বানাব)।
এটা কী প্রমাণ করে

Failure যত দেরিতে, compensation cascade তত লম্বা; আর duplicate delivery compensation-এ ঢুকলে idempotency-guard ছাড়া effect দ্বিগুণ হয় — চোখে হাতে।

নিজে বানান

BUILD IT

SagaOrchestrator — step registry, retry policy, journal, crash-resume

Python · ●●●●○
  1. Step registry — প্রতিটা ধাপে action ও compensation callable
  2. Retry policy — per-step attempts, শেষ হলে saga-fail
  3. State journal — প্রতিটা effect-এর intent ও done durable দাগ
  4. Failure injection — যে-কোনো ধাপে crash simulate
  5. Resume-after-crash — journal থেকে completed keys পুনর্নির্মাণ

লক্ষ্য: crash mid-compensation হলেও restart-এ saga শুদ্ধ শেষ হবে — প্রতিটা effect ঠিক একবার।

"""saga_orchestrator.py — journal-based resumable saga engine"""
import json, os, tempfile
from dataclasses import dataclass
from typing import Callable

@dataclass
class Step:
    name: str
    action: Callable[[dict], str]
    compensate: Callable[[dict], str]
    retries: int = 2

class SagaCrashed(Exception):
    pass

class Orchestrator:
    def __init__(self, steps, world, journal_path, fail_at=None):
        self.steps, self.world, self.journal_path = steps, world, journal_path
        self.fail_at, self.effects = fail_at, {}   # effect-key -> count

    def _journal(self, entry):
        with open(self.journal_path, 'a') as fh:
            fh.write(json.dumps(entry) + '\n')

    def _recover(self):
        """restart-এ journal পড়ে completed-effect map পুনর্নির্মাণ"""
        try:
            with open(self.journal_path) as fh:
                for line in fh:
                    e = json.loads(line)
                    if e.get('s') == 'done':
                        self.effects[e['k']] = 1
        except FileNotFoundError:
            pass

    def _maybe_crash(self, phase, idx):
        if self.fail_at == (phase, idx):
            raise SagaCrashed(f'crash @ {phase}{idx}')

    def _run_effect(self, key, fn, attempts):
        if self.effects.get(key):            # exactly-once guard (dedup)
            return
        self._journal({'k': key, 's': 'intent'})
        last = None
        for _ in range(attempts):            # retry policy
            try:
                result = fn(self.world)
                self.effects[key] = 1
                self._journal({'k': key, 's': 'done', 'r': result})
                return result
            except Exception as err:
                last = err
        raise RuntimeError(f'{key} failed: {last}')

    def run(self, i=1, phase='forward'):
        self._recover()
        rng = range(i, len(self.steps) + 1) if phase == 'forward' \
            else range(i, 0, -1)
        for n in rng:
            st = self.steps[n - 1]
            if phase == 'forward':
                try:
                    self._run_effect(f'T{n}', st.action, st.retries)
                except RuntimeError as err:      # business-fail → rollback
                    print(f'  >> {err}; cascade শুরু C{n-1}')
                    return self.run(n - 1, 'compensate')
                self._maybe_crash(phase, n)      # crash = process-death
            else:
                self._maybe_crash(phase, n)      # mid-compensation crash!
                self._run_effect(f'C{n}', st.compensate, st.retries)
        rolled = any(k.startswith('C') for k in self.effects)
        return 'ROLLED-BACK' if rolled else 'COMPLETE'

# ── demo domain: world = shared fake-state dict ──
def mk(tag, ok, undo):
    return Step(tag,
                lambda w: (w.setdefault(tag, []).append(ok), ok)[1],
                lambda w: (w.setdefault(tag, []).append(undo), undo)[1])

steps = [
    mk('order',  'PENDING',  'CANCELLED'),
    mk('flight', 'HELD',     'RELEASED'),
    mk('hotel',  'RESERVED', 'ROOMLESS'),
]
boom = lambda w: (_ for _ in ()).throw(RuntimeError('no rooms'))
steps[2] = Step('hotel', boom, lambda w: 'room released')

tmp = os.path.join(tempfile.mkdtemp(), 'journal.ndjson')
print('=== run 1: crash mid-compensation (C1-এর ঠিক আগে process-death) ===')
orc = Orchestrator(steps, {}, tmp, fail_at=('compensate', 1))
try:
    orc.run()
except SagaCrashed as err:
    print(f'  died: {err}; journaled-done: {sorted(orc.effects)}')

print('=== run 2: restart — একই journal ফাইল, নতুন process ===')
orc2 = Orchestrator(steps, {}, tmp)
verdict = orc2.run()
print(f'  verdict={verdict}')
print(f'  effects={orc2.effects}')
assert all(v == 1 for v in orc2.effects.values()), 'exactly-once ভাঙল!'
assert orc2.effects.get('C2') == 1   # C2 আবার চলেনি — dedup-guard কাজ করেছে
print('  PASS — crash-পরেও প্রতিটা effect ঠিক একবার')

Journal-এর নমুনা content:

{"k": "T1", "s": "done", "r": "PENDING"}
{"k": "T2", "s": "done", "r": "HELD"}
{"k": "C2", "s": "intent"}
{"k": "C2", "s": "done", "r": "RELEASED"}   ← এখানেই process মরেছিল

তিনটা পাঠ:

  • Exactly-once এখানেও illusion — _run_effect বাস্তবে at-least-once চালায়, effects-guard duplicate-effect আটকায়; end-to-end সত্য।
  • Intent-এর পরে done-entry — restart শুধু done replay করে; half-done state বলে কিছু নেই। Temporal/Cadence এই journal-কেই event-history বলে disk+replication-এ রাখে — মূলনীতি এক।

নিজে বাড়ান:

  1. Compensate-phase retry-exhaustion handle করুন — human- escalation flag জমা দিন (poison-saga queue)
  2. দুই concurrent saga এক world-এ চালিয়ে lost update দেখুন; version-check যোগ করে আটকান
  3. Journal-টাই append-only event-log বানান — event sourcing- এর প্রিভিউ!

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

Saga বাস্তবে

Temporal আর Uber Cadence। Production orchestrator — আপনি workflow-code লেখেন, ওরা event-history (journal), retry, resume চালায়; আমাদের Orchestrator-এরই শিল্প-রূপ। Payment-flow-রা এতেই চলে।

Eventuate Tram আর Axon। Java-জগতের saga framework — outbox + relay built-in, দুই style-ই API; Richardson-এর বই এই pattern-গুলোর catalog।

Debezium + Kafka CDC। Outbox-relay-র আধুনিক স্বাদ — WAL tail করে insert-event Kafka-topic-এ; downstream saga-step topic consume করে। Poll-publisher-এর চেয়ে কম latency, বেশি moving-parts।

Travel আর ride booking। Flight+hotel bundle, dispatch+payment — সবই আংশিক-success মেনে চলা saga; airline-দের ‘refund 5-7 days’ বার্তাটা compensation-এর customer-facing মুখ।

Distributed-monolith সতর্কতা। Choreography-overuse-এর কবর: event-web এত জটিল যে flow-change-এর impact কেউ বলতে পারে না — তাই বড় team saga-definition আলাদা artifact রাখে, code-এ লুকিয়ে নয়।

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

“Saga মানে distributed ACID transaction — শুধু নাম আলাদা।”

Saga atomicity-র business-সমতুল্য রূপ দেয়; ACID-এর isolation সরাসরি ছাড়ে। মাঝপথে সবাই partial state দেখে — implementation-bug নয়, model-এর সংজ্ঞাগত দাম। যেখানে exact momentary consistency বাধ্যতামূলক (bank-core ledger move), সেখানে saga উত্তর না — single-writer বা 2PC-জাতীয় design-ই থাকে।

“Compensation মানে database rollback — আগের txn undo করলেই হলো।”

Rollback শুধু commit-হয়নি txn ফেরায়। Saga-র ধাপ committed — side-effect বিশ্বে ছড়ানো: email গেছে, card charged, events published। Compensation তাই সামনে-গামী business-action: refund, cancel-notification, seat-release। Undo-log বনাম compensation-এর এই পার্থক্যই লেসনের প্রথম পরীক্ষা।

“Choreography-তে central point নেই, তাই superior architecture।”

Conductor-এর SPOF-ঝুঁকি সত্যি; বিনিময়ে flow-visibility, explicit failure-handling, versioned workflow। Choreography visibility-টা subscriber-web-এ ছড়িয়ে দেয় — cycle, dead-letter আর ‘কেন এই event গেল’ debugging রাতজেগে-খাওয়া বিষয়। প্রশ্ন ‘কোনটা ভালো’ নয় — ‘এই flow-র জন্য কোন trade-off টিকবে’।

“Intermediate state সাময়িক — শেষে সব ঠিক হয়ে যায়, ব্যবস্থাপনা লাগে না।”

‘শেষে ঠিক’ হয় saga-র ভেতরের হিসেবে; বাইরের actor তখনো সিদ্ধান্ত নিচ্ছে — user seat available দেখে click করল, saga compensate করে seat ছেড়ে দিল। Lost update ‘শেষে-ঠিক’ জানেই না — চাপা-পড়া write চিরতরে হারায়। Semantic lock, commutative update, version-check তাই decoration না — correctness-এর অংশ।

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

1

Saga-র সংজ্ঞা — T-সিরিজ, C-সিরিজ, আর কোন compensation কবে চলে?

স্মরণ

Local transaction-সিরিজ T1..Tn, প্রতিটা নিজের DB-তে পূর্ণ ACID। Failure Tk-তে হলে compensation C(k−1)..C1 উল্টো ক্রমে চলে; সব T সফল হলে saga complete — global commit বলে কিছু নেই।

2

Compensation-এর তিনটা বাধ্যতামূলক ধর্ম, আর database-roll-back কেন বিকল্প নয়?

যুক্তি

Business-level (refund/release — বিশ্বের side-effect ঘোরায়), retryable (transient fail-এ আবার চলবেই), idempotent (duplicate delivery-তে effect একবারই)। Rollback শুধু uncommitted local txn-এর undo — committed ধাপের ছড়ানো side-effect (email, charge, event) ছোঁয়ার জায়গাই নেই।

3

T2 snapshot পড়ে SET stock = snapshot - 1 লেখে; parallel saga একই row একইভাবে লেখে। Anomaly, কারণ, দুই fix?

প্রয়োগ

Lost update — দুই read-modify-write interleave, একটার decrement হারায়। Fix-১: commutative delta — `SET stock = stock - 1 WHERE stock

0। Fix-২: optimistic version — WHERE id=? AND version=?`; zero rows এলে conflict-path/retry।

4

Payment-flow (audit-critical, ৬ ধাপ) আর notification-fanout (৩ ধাপ) — কোনটায় কোন style, কেন?

যুক্তি

Payment: orchestration — flow এক জায়গায় auditable, compensation-policy explicit, workflow versioned। Notification: choreography — chain ছোট, coupling কমলেই লাভ, cycle-ঝুঁকি নগণ্য। Choice করে audit-need আর chain-complexity, ideology নয়।

5

Service DB-commit করে, তারপর broker-publish-এ গিয়ে crash। কী ভাঙল, outbox কীভাবে বাঁচায়, relay মরলে কী হয়?

ডিজাইন

Dual-write hole: DB-তে ঘটেছে, বিশ্ব অন্ধ। Outbox: event একই local txn-এ outbox-table-এ insert — commit আর ঘোষণা-ইচ্ছা এক atomically; publish পরে relay দিয়ে। Relay মরলে at-least-once: ফিরে এসে un-published rows আবার পাঠায়, হয়তো duplicate-সহ — consumer-side idempotent dedup বাধ্যতামূলক; outbox নিজে exactly-once দেয় না।

এরপর কী

এরপর কী

পুরো লেসন জুড়ে এক ভূত — retry মানেই duplicate, শেষ বুলওয়ার্ক idempotency। কিন্তু ‘exactly-once delivery’ নামের জিনিসটা আদৌ আছে? পরের লেসনে delivery-guarantee-ladder ভাঙব — at-most-once, at-least-once, আর exactly-once-এর আসল রূপ (at-least-once + idempotent effect); Idempotency-Key header-pattern হাতে বানাব, payment double-charge ঠেকাব, আর দেখব কেন end-to-end argument বলে exactly-once delivery মিথ।

আরও পড়ুন