Saga ও Distributed Transactions — 2PC-র বিকল্প পথ
Sagas and Distributed Transactions
2PC-র blocking আর coordinator-SPOF scale-এ অসহনীয় — saga লম্বা transaction-কে local transaction-সিরিজ আর compensating action-এ ভাঙে; দাম হলো intermediate state বাইরের চোখে দেখা যাওয়া।
আগে এটা বুঝি
গত লেসনে 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-এ অচল
| সমস্যা | কখন ঘটে | আসল খরচ |
|---|---|---|
| Blocking | Participant vote yes দিয়ে decision-এর অপেক্ষায় | Locks ধরে — অন্য কেউ ওই row ছুঁতে পারে না |
| Coordinator SPOF | Decision-এর ঠিক আগে/পরে coordinator crash | Participant timeout ছাড়া জানবেই না commit না abort |
| Lock stretch | প্রতি extra participant = বাড়তি round-trip | Lock-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। FailureTk-তে হলে compensationC(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 lock | Status field — PENDING/CONFIRMED; reader PENDING-কে ‘নেই’ ধরে | Dirty read |
| Commutative update | SET balance=X নয়, balance += −amount-জাতীয় delta | Lost update |
| Version check | লেখার সময় expected_version verify | Lost update |
| Retry + idempotency | Compensation বারবার-চালানো-নিরাপদ | Duplicate effect |
Semantic lock database-এর Two-Phase Locking নয় — application-এর চুক্তি: PENDING মানে অসম্পূর্ণ saga, সব consumer সম্মান করবে। Enforce করে database না, discipline।
Orchestration vs choreography
ধাপগুলো কে চালায় — দুই উত্তর:
| দিক | Orchestration | Choreography |
|---|---|---|
| Structure | Central conductor command পাঠায় | Service event ছড়ায়, পরেরটা subscribe করে |
| Flow knowledge | এক জায়গায় পুরো workflow visible | ছড়ানো — code-এ সরাসরি দেখা যায় না |
| Coupling | Orchestrator সবাইকে চেনে | Services শুধু events চেনে |
| Failure handling | Conductor 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 sentRelay-র দুই স্বাদ:
- 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-এর হাতেOrderCreated ──► [Flight] ──FlightHeld──► [Hotel] ──HotelFull!──┐
│
◄──OrderCancelled── [Order] ◄──FlightReleased── [Flight] ◄─────┘
(উল্টো-দিকের event-chain = compensation cascade)
সফল পথ: OrderCreated ▶ FlightHeld ▶ HotelReserved ▶ CardCharged ▶ Confirmed
প্রতিটা ▶ published event — flow-র কোনো মালিক নেই; এটাই শক্তি ও বিপদনিজে চালিয়ে দেখুন
Booking-flow simulator — failure inject, cascade দেখো, duplicate-delivery মাপো
"""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 দ্বিগুণ হয় — চোখে হাতে।
নিজে বানান
SagaOrchestrator — step registry, retry policy, journal, crash-resume
- Step registry — প্রতিটা ধাপে action ও compensation callable
- Retry policy — per-step attempts, শেষ হলে saga-fail
- State journal — প্রতিটা effect-এর intent ও done durable দাগ
- Failure injection — যে-কোনো ধাপে crash simulate
- 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 শুধু
donereplay করে; half-done state বলে কিছু নেই। Temporal/Cadence এই journal-কেই event-history বলে disk+replication-এ রাখে — মূলনীতি এক।
নিজে বাড়ান:
- Compensate-phase retry-exhaustion handle করুন — human- escalation flag জমা দিন (poison-saga queue)
- দুই concurrent saga এক world-এ চালিয়ে lost update দেখুন; version-check যোগ করে আটকান
- 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-এর অংশ।
বুঝেছেন কি না দেখুন
1Saga-র সংজ্ঞা — T-সিরিজ, C-সিরিজ, আর কোন compensation কবে চলে?
স্মরণ
Local transaction-সিরিজ T1..Tn, প্রতিটা নিজের DB-তে পূর্ণ ACID।
Failure Tk-তে হলে compensation C(k−1)..C1 উল্টো ক্রমে চলে; সব
T সফল হলে saga complete — global commit বলে কিছু নেই।
2Compensation-এর তিনটা বাধ্যতামূলক ধর্ম, আর 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) ছোঁয়ার জায়গাই নেই।
3T2 snapshot পড়ে SET stock = snapshot - 1 লেখে; parallel saga
একই row একইভাবে লেখে। Anomaly, কারণ, দুই fix?
প্রয়োগ
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।
4Payment-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 নয়।
5Service 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 মিথ।
আরও পড়ুন
- Sagas — Hector Garcia-Molina, Kenneth Salem · 1987-র মূল SIGMOD paper — long-lived transaction ভেঙে saga আর compensating transaction-এর জন্ম
- Microservices Patterns — Managing transactions with sagas — Chris Richardson · Orchestration/choreography, countermeasure আর outbox-এর শিল্প-মান আলোচনা
- Designing Data-Intensive Applications, Chapter 11 — Stream Processing — Martin Kleppmann · Event-driven coordination, CDC আর stream-side exactly-once processing-এর প্রেক্ষাপট