Idempotency ও Exactly-once-এর মিথ
Idempotency and the Exactly-once Myth
Retry-যুক্ত যেকোনো channel-এ duplicate অনিবার্য — exactly-once বলতে আসলে at-least-once delivery-র সাথে idempotent effect; Idempotency-Key pattern-ই payment double-charge ঠেকানোর শিল্প-উত্তর।
আগে এটা বুঝি
২০১৯-এ একটা পরিচিত fintech-startup-এর ঘটনা: gateway-র load-balancer flap করল, mobile app-এর HTTP client timeout দেখে স্বভাবতই retry করল — আর payment-service প্রতিটা request-কে নতুন চার্জ ভেবে কাটল। এক user, এক purchase, কার্ডে গেল তিনবার। Refund-run-এ রাত গেল, support-ticket উড়ল। Bug কোথায় ছিল? Retry-তে না — retry তো ঠিকই করেছিল। ভুল ছিল server-side: প্রতিটা request-কে first-time ভেবে effect চালানো।
Network-এ একটা কঠিন সত্য আছে: response হারালে client জানতেই পারে না request পৌঁছেছিল কি না। তাই সে retry করবেই — আর retry মানেই server হয়তো একই কাজ দ্বিতীয়বার করবে। এই লেসনের মূল প্রশ্ন দুটো: delivery-guarantee-র সিঁড়িতে ঠিক কী কী promise দেওয়া সম্ভব, আর ‘exactly-once’ নামের প্রচারিত জিনিসটা আসলে কী? উত্তর- দুটোই এক শব্দের চারপাশে ঘোরে — idempotency।
মূল ধারণা
Delivery-guarantee-র সিঁড়ি
Message/request পাঠানোর তিনটা সম্ভাব্য চুক্তি:
| Guarantee | যন্ত্র | বৈশিষ্ট্য |
|---|---|---|
| At-most-once | Fire-and-forget, retry নেই | Duplicate নেই, কিন্তু lossy — যা হারাল তা হারাই |
| At-least-once | Retry until ack | Loss নেই, কিন্তু duplicate অনিবার্য |
| Exactly-once | ??? | নিচে দেখুন — সোজা পথে অসম্ভব |
At-most-once sensor-heartbeat-এর মতো জায়গায় চলে (পরেরটা আসবেই)। কিন্তু payment, order, inventory — যেখানে প্রতিটা message-এর দাম — সেখানে at-least-once-ই একমাত্র বাস্তব ভিত্তি: হারানো যাবে না। আর তখনই duplicate ঢোকে, কারণ ack নিজেই একটা message, যা-ও হারাতে পারে।
Exactly-once-এর সৎ সংজ্ঞা
সোজা প্রশ্ন: receiver-এর কাছে পৌঁছানো-প্রক্রিয়াকে exactly-once করা যায়? End-to-end argument বলে — না। কারণ:
- Sender দুইবার পাঠাতেই পারে (ack হারালে);
- Receiver-এর মৃত্যু-পুনর্জন্মের মাঝে ‘আমি এটা করে ফেলেছি’ স্মৃতি না থাকলে আবার করবে;
- ‘ঠিক একবার পাঠানো’-র কোনো physical mechanism নেই — wire-এ যা যায় সেটা packet, packet হারে, packet duplicate হয়।
তাহলে Kafka-র মতো system-র ‘exactly-once’ দাবি কী? সত্যটা হলো:
Exactly-once EFFECT = at-least-once DELIVERY + idempotent PROCESSING।
Duplicate আসবেই — কিন্তু প্রতিটা duplicate প্রক্রিয়া করার সময় আগে-করা-কাজের স্মৃতি দেখে নিজেকে বিরত রাখবে। Delivery-layer নয়, effect-layer-এ exactly-once কেনা যায়। এটাই end-to-end argument- এর practical রূপ: সমস্যার স্থায়ী সমাধান বসে knowledge-সর্বশেষ- প্রান্তে (application), transport-এর মাঝে নয়।
Idempotency-Key pattern — শিল্প-standard walkthrough
Stripe থেকে Shopify — প্রায় সব payment API একই ছাঁচে চলে:
১. Client প্রতিটা logical operation-এর জন্য নতুন UUID বানায়
(retry-তে একই UUID, নতুন operation-এ নতুন — নিয়মটা কঠোর!)
২. Request-header-এ যায়: Idempotency-Key: 7f3c...
৩. Server দেখে — key store-এ আছে?
না → effect চালাও, response store-এ জমাও, উত্তর দাও
হ্যাঁ → stored response হুবহু ফেরত দাও, effect ছোঁয় না
৪. Timeout-ambiguous retry-ও একই উত্তর পায় — double-charge অসম্ভবDesign-এর তিনটা সূক্ষ্ম জায়গা:
- Key বানায় client — server নয়। Server-generated key হলে প্রতিটা retry-ই নতুন key পেয়ে যায়, dedup অর্থহীন।
- Store করা হয় pura response, শুধু success/fail flag নয় — যাতে replay-এ client হুবহু সেই-এক উত্তর পায় (idempotent-response contract)।
- Key-র সাথে scope জড়ে: একই key ভিন্ন endpoint/ভিন্ন account- এ collision হলে ভুল response ফেরত যেতে পারে — তাই store-এর primary key = (scope, key)।
Natural idempotency আর CAS
Key-store সব জায়গা লাগে না — মাঝে মাঝে operation নিজেই idempotent:
-- NOT idempotent: retry-তে দ্বিগুণ কাটবে
UPDATE accounts SET balance = balance - 100 WHERE id = 7;
-- idempotent by construction: কতবার চালালেও state এক
UPDATE accounts SET balance = 400 WHERE id = 7;
-- conditional (CAS-style): version মিললেই একবারই apply হবে
UPDATE accounts SET status='PAID', version = version + 1
WHERE id = 7 AND version = 3;SET absolute-value আর conditional-write দুটোই natural idempotency —
duplicate apply-এর ফল শূন্য। কিন্তু absolute-set নিজেই lost-update-
ঝুঁকি বহন করে (আগের লেসন দেখুন) — তাই বাস্তবে CAS-version ছাড়া
natural path নিরাপদ নয়।
ভেতরে কী ঘটছে
Key-store ভেতরে — race window-ই আসল কাজ
CREATE TABLE idempotency_keys (
scope text NOT NULL, -- endpoint + account
key uuid NOT NULL,
response jsonb NOT NULL,
created_at timestamptz DEFAULT now(),
PRIMARY KEY (scope, key)
);Naive handler-এ ফাঁক থেকে যায়: check (SELECT) আর insert
দুই statement-এর মাঝে দুইটা concurrent duplicate ঢুকে পড়লে
দুটোই ‘নেই’ দেখে effect চালায়। সমাধান check-then-act নয় —
atomic claim:
INSERT INTO idempotency_keys(scope, key, response)
VALUES ($1, $2, '{"state": "processing"}')
ON CONFLICT DO NOTHING;
-- inserted row-count == 0 ? আগের কেউ দাবি করেছে → wait/replayUnique-constraint-ই distributed lock — database এক হলে। Database
ভাঙা থাকলে Redis SET NX PX-জাতীয় put-if-absent বা আগের লেসনের
semantic-lock discipline। আর effect চলাকালীন দ্বিতীয় duplicate
এলে? In-flight lock: দাবি-করা row এখনো processing — দ্বিতীয়
request হয় অপেক্ষা করবে, নয়তো 409 Conflict/425 Too Early
ফেরত দেবে; কখনোই parallel-এ effect চালাবে না।
TTL — স্মৃতির মেয়াদ
Key forever রাখা যায় না — store ফুলবে। Standard: ২৪ ঘণ্টার TTL। দামটা জেনে নিন: TTL-expire-এর পরে একই key-তে retry এলে সেটা নতুন operation — double-effect আবার সম্ভব। তাই client-নীতি: expire-এর বেশি পুরনো retry retry-ই না, নতুন decision।
Kafka EOS-এর scope-সততা
Kafka-র ‘exactly-once’ দুই স্তরে, দুই সীমায়:
- Idempotent producer:
enable.idempotence=true— broker producer-id + sequence-number দেখে দুইবার-লেখা আটকায়। Scope: এক producer, এক session, এক partition। - Transactional producer +
read_committed: ক’টা partition- এ ক’টা write এক atomically; consumer transaction-marker না-দেখা record পড়ে না।
যেখানে এসব শেষ হয় না: consumer-এর effect বাইরে — DB-write, email, card-charge। Kafka সেই external effect-এর duplicate আটকায় না, আটকাতে পারে না। ওখানে ফের আপনার হাতেই idempotent consumer / dedup-store। প্রচারের ‘exactly-once’ শুনে transport-magic ভেবো না — pipeline-এর ভেতরের segment-scope, বাইরে নয়।
উদাহরণ
এক payment-এর পূর্ণ trace
# প্রথম চেষ্টা
→ POST /v1/charges
→ Idempotency-Key: 7f3c9a2e-4b1d-4c8f-9e2a-51f0b3d7a811
→ {"order": 88231, "amount_cents": 450000}
✕ network glitch — response হারাল, কিন্তু server কাজ করেছে:
effect: charge#1041 created, key stored (scope=charges/acc_42)
# client timeout → retry, একই key!
→ POST /v1/charges
→ Idempotency-Key: 7f3c9a2e-4b1d-4c8f-9e2a-51f0b3d7a811
→ {"order": 88231, "amount_cents": 450000}
← server: key found → NO new effect
← 200 OK {"id": "ch_1041", "status": "succeeded",
"amount_cents": 450000} ← হুবহু cached response
← Idempotency-Replayed: true ← কিছু API এই header দেয়
# ledger: charge#1041 — ঠিক একবার। user: happy। auditor: happy।উল্টোদিকটাও দেখুন — একই scenario, key ছাড়া: প্রতিটা retry
নতুন POST — charge#1041, #1042, #1043… তিনটাই real। Gateway
আর client দুজনেই ‘স্বাভাবিকভাবে’ ব্যর্থ হয়েছিল; double-charge-
এর দোষী শুধু missing-dedup-design।
ছবিতে
Retry-এর দুই মুখ
CLIENT NETWORK SERVER (no dedup)
│── POST charge ──────►│ │
│ │─────────────────────► │ effect! charge#A
│ ✕ response lost ◄─────────────✕ │
│── POST charge ──────►│ (client ভাবলেই পৌঁছায়নি)│
│ │─────────────────────► │ effect! charge#B
│◄── 200 ──────────────┼───────────────────────│
▼
ledger: দুটো real charge — এক purchaseCLIENT NETWORK SERVER (dedup store)
│── POST + Key:k ─────►│ │
│ │─────────────────────► │ claim(k)=miss
│ │ │ effect! charge#A
│ ✕ lost ◄────────────────────✕ │ store(k)=resp(A)
│── POST + Key:k ─────►│ │
│ │─────────────────────► │ claim(k)=HIT
│◄── 200 resp(A) ──────┼───────────────────────│ (effect ছোঁয়নি)
▼
ledger: charge#A — ঠিক একবারনিজে চালিয়ে দেখুন
Unreliable-channel lab — retry-storm বনাম idempotency-key, আর dedup-lookup-এর দাম
"""flaky_charge.py — response-drop + retry বনাম Idempotency-Key"""
import random, time
class Server:
def __init__(self, use_keys):
self.use_keys = use_keys
self.charges, self.store = [], {}
def handle(self, req):
k = req.get('key')
if self.use_keys and k in self.store: # dedup-hit!
return {'replayed': True, 'charge_id': self.store[k]}
self.charges.append(req['amount']) # ★ real-money effect ★
cid = len(self.charges)
if self.use_keys:
self.store[k] = cid # key → result store
return {'replayed': False, 'charge_id': cid}
class Channel:
"""Server প্রতিবারই কাজ শেষ করে — কিন্তু response মাঝে মাঝে wire-এ হারায়"""
def __init__(self, server, drop_rate, seed):
self.server, self.drop_rate = server, drop_rate
self.rng = random.Random(seed)
def send(self, req):
resp = self.server.handle(req)
if self.rng.random() > self.drop_rate:
return resp
return None # timeout-ambiguity
TRIALS, DROP_RATE = 200, 0.35
for mode in (False, True):
srv = Server(use_keys=mode)
ch = Channel(srv, drop_rate=DROP_RATE, seed=42)
for i in range(TRIALS):
req = {'amount': 4500}
if mode:
req['key'] = f'pay-{i}' # client-generated UUID
for _ in range(6): # retry until answered
if ch.send(req):
break
print(f'use_keys={mode}: {TRIALS} logical payments -> '
f'{len(srv.charges)} real charges '
f'(duplicates={len(srv.charges) - TRIALS})')
store = {f'k{i}': i for i in range(100000)}
start = time.perf_counter()
for i in range(1000000):
store.get(f'k{i % 100000}') # hash lookup probe
elapsed = time.perf_counter() - start
print(f'1M dedup lookups: {elapsed:.3f}s (~{elapsed * 1000:.0f}ns each)')নমুনা output (seed-fixed):
use_keys=False: 200 logical payments -> 321 real charges (duplicates=121)
use_keys=True : 200 logical payments -> 200 real charges (duplicates=0)
1M dedup lookups: 0.488s (~488ns each)তিনটা পাঠ:
- Key-ছাড়া পথে ২০০ payment-এ ১২১টা ghost-charge — retry-policy যত generous, ghost তত বেশি।
- Key-সহ পথে real charges == logical payments, চিরকাল — channel যত খারাপই হোক।
- Dedup-lookup sub-microsecond; payment-এর ms-scale latency-তে rounding-error — ‘performance-excuse’ dedup-বাদ দেওয়ার কারণ নয়।
Response-loss + retry-তে key-ছাড়া channel বারবার double-charge করে, key-সহ channel প্রতিটা logical payment-এ ঠিক একবার কাটে; আর dedup-store lookup-এর latency দাম নগণ্য — nanosecond-scale।
নিজে বানান
IdempotentEndpoint — key→response cache, in-flight lock, TTL, fuzz-test
- Dedup store — key → completed-response mapping
- Atomic claim — একই key-র দুই concurrent caller একজনের হাতে
- In-flight lock — effect চলাকালীন দ্বিতীয়জন অপেক্ষায়
- TTL eviction — expire হলে স্মৃতি মুছে যায়
- Fuzz test — random retry/timeout-এ effect-count==1 প্রমাণ
"""idempotent_endpoint.py — exactly-once EFFECT wrapper (~80 line)"""
import random, threading, time
from collections import defaultdict
class IdempotentEndpoint:
def __init__(self, ttl_s=0.05):
self.ttl = ttl_s
self.mu = threading.Lock()
self.results = {} # key -> (expires_at, value)
self.claims = set() # claimed-but-running keys
self.waiters = defaultdict(list)
def _expired(self, key, now):
rec = self.results.get(key)
return rec is not None and now >= rec[0]
def handle(self, key, effect):
"""returns (verdict, value) — effect ঠিক একবারই চলে"""
while True:
now = time.monotonic()
with self.mu:
if self._expired(key, now):
del self.results[key] # TTL eviction
if key in self.results:
return ('replayed', self.results[key][1])
if key in self.claims:
evt = threading.Event()
self.waiters[key].append(evt) # in-flight lock
else:
self.claims.add(key) # atomic claim!
break # we execute
evt.wait(2) # outside lock
try:
value = effect() # ★ single effect ★
now = time.monotonic()
with self.mu:
self.results[key] = (now + self.ttl, value)
self.claims.discard(key)
for w in self.waiters.pop(key, []):
w.set()
return ('executed', value)
except Exception:
with self.mu:
self.claims.discard(key) # retry allowed
for w in self.waiters.pop(key, []):
w.set()
raise
# ── fuzz test: random clients, flaky effect, duplicate storms ──
def fuzz(trials=300, seed=99):
import concurrent.futures as cf
rng = random.Random(seed)
ep = IdempotentEndpoint(ttl_s=60) # TTL > test-window
committed = defaultdict(int) # key -> real effects
def effect(key):
if rng.random() > 0.8: # 20% provider-fail:
raise RuntimeError('payment hiccup') # কিছুই commit হয়নি
committed[key] += 1 # ★ committed effect ★
return f'result-{key}'
for i in range(trials): # retry-storm phase
key = f'op-{rng.randint(0, 24)}'
deadline = time.monotonic() + 0.5
while True:
try:
ep.handle(key, lambda k=key: effect(k))
break # success/replay
except RuntimeError:
if time.monotonic() >= deadline:
break # gave up
with cf.ThreadPoolExecutor(8) as ex: # duplicate storm!
futs = [ex.submit(ep.handle, 'op-storm',
lambda: effect('op-storm'))
for _ in range(16)]
verdicts = [f.result()[0] for f in futs]
bad = {k: c for k, c in committed.items() if c != 1}
assert not bad, f'exactly-once ভাঙল: {bad}'
assert committed['op-storm'] == 1
executed = sum(1 for v in verdicts if v == 'executed')
print(f'{trials} trials + 16-thread storm on 1 key')
print(f'storm verdicts: {executed} executed, '
f'{16 - executed} replayed/in-flight-wait')
print('PASS — প্রতিটা key-র effect ঠিক একবার')
fuzz()নমুনা output:
300 trials + 16-thread storm on 1 key
storm verdicts: 1 executed, 15 replayed/in-flight-wait
PASS — প্রতিটা key-র effect ঠিক একবারতিনটা পাঠ:
claims.addআরresults-check একই lock-এর ভেতরে — atomicity- টা এখানেই; আলাদা হলে race window ফিরে আসে।- Effect-fail হলে claim ছেড়ে দেওয়া হয় — নইলে poison-request key-টা চিরতরে বন্দি করে ফেলত।
- Fuzz-এ
effects[key] != 1কখনো ধরা না পড়লেও confidence আসে thread-interleaving থেকে — production-এ এই assertion-টাই dedup-monitoring-এর ভিত্তি।
নিজে বাড়ান:
handle-কে multi-node বানান — claims-কে RedisSET NX PX-এ সরিয়ে নিয়ে দুই process-এর duplicate-execution আটকান- Response-body বড় হলে store-এ pointer (S3-key) রাখার pattern যোগ করুন — Stripe-এর কাছাকাছি
- TTL-expire-পরবর্তী retry-এ ‘stale-key’ warning metric বাড়ান
বাস্তব সিস্টেমে
বাস্তবে idempotency
Payment API-রা। Stripe Idempotency-Key header (২৪ ঘণ্টা TTL,
cached full-response replay), PayPal, Adyen — সবাই এই pattern-ই;
Stripe docs-এর retry-guide প্রায় এই লেসনের summary।
Mobile-first traffic। Cellular network-এ ১-২% request-ই timeout-ambiguous — elevator-এ subway-তে tap করা প্রতিটা user আসলে retry-generator; client-SDK-রা এখন built-in retry দেয়, তাই key ছাড়া POST-API মানেই double-effect।
Kafka ecosystem। enable.idempotence=true producer, EOS
transactions, read_committed consumer — সব transport-segment-scope;
downstream DB/email-effect-এ আবার আপনার dedup। CDC pipeline-ও
at-least-once — consumer-table-এ (source, offset) unique-index-ই
সেখানকার idempotency-key।
Banking ledger। Transfer-instruction-এ client-supplied reference-number
- unique-constraint — SWIFT-যুগের idempotency-key; duplicate-file-এর বিরুদ্ধে শিল্প-পুরনো defense।
যে ভুলগুলো সবাই করে
“Kafka transactions মানে end-to-end exactly-once — আর duplicate নিয়ে ভাবতে হবে না।”
Scope-টা পড়ুন: transactional producer + read_committed শুধু Kafka-pipeline-এর ভেতরের read-process-write লুপ atomic করে। Consumer-এর বাইরের effect — DB-row, email, refund — Kafka দেখেই না। সেখানে dedup/idempotent-consumer আপনারই দায়; প্রচার-শব্দ contract নয়।
“GET/PUT/DELETE idempotent, তাই ওদিকে dedup লাগে না — শুধু POST-এ লাগবে।”
Method-idempotency concurrency-safe নয়: দুই parallel PUT একই resource-এ ভিন্ন body নিয়ে last-write-wins করে; DELETE-retry নিরীহ কিন্তু DELETE-এর response হারালে client জানে না resource আগেই ছিল কি না। Method-classification retry-নিরাপত্তা বোঝায়, business-level dedup বোঝায় না — দুই স্তর আলাদা প্রশ্ন।
“Idempotency-key একবার বানালেই চিরকাল নিরাপদ।”
Store-এর TTL-ই চুক্তির সীমা — ২৪ ঘণ্টা পরে একই key-তে নতুন request নতুন operation, double-effect আবার সম্ভব। আর key নতুন logical operation-এ reuse করলে (client-bug) উল্টো problem: সত্যিকারের দ্বিতীয় payment সাইলেন্টলি replayed-response খেয়ে হারিয়ে যাবে। Key-discipline দুই দিক থেকেই কঠোর।
“Retry বিরল ব্যর্থতা-হ্যান্ডলিং — normal path-এ ভাবার দরকার নেই।”
Production-measurement বলে উল্টো: p99-latency spike, deploy-restart, LB-drain — প্রতিটাতেই in-flight request retry হয়। বড় scale-এ ‘duplicate-per-million’ মানেই প্রতিদিন ক’টা double-charge। তাই design-principle: retry নিশ্চিত, duplicate প্রশ্নই জন্মায় না — এমন structure বানাও।
বুঝেছেন কি না দেখুন
1Delivery-ladder-এর তিন ধাপ আর প্রত্যেকটার মূল trade-off এক line-এ।
স্মরণ
At-most-once — lossy, duplicate নেই; at-least-once — loss নেই, duplicate অনিবার্য; exactly-once — সোজা পথে অসম্ভব, বাস্তবে at-least-once delivery + idempotent effect।
2Ack/response হারার জন্যই duplicate ঢোকে — কেন sender শুধু ‘অন্যরকম
ack’ পাঠিয়ে সমস্যাটা মেটাতে পারে না?
যুক্তি
Ack নিজেও message — সেটাও হারায়, duplicate হয়; infinite regress। Wire-এ যা-ই থাকুক, lossy-channel-এ ‘ঠিক একবার’ signal করার physical উপায় নেই। তাই সমাধান receiver-side স্মৃতিতে — effect-layer, transport-layer নয় (end-to-end argument)।
3UPDATE stock = stock − 1 আর SET status='PAID' WHERE version=3
— retry-তে কোনটা বিপজ্জনক, কেন, আর safe alternative?
প্রয়োগ
UPDATE stock = stock − 1 আর SET status='PAID' WHERE version=3
— retry-তে কোনটা বিপজ্জনক, কেন, আর safe alternative?Delta-update non-idempotent — প্রতি retry-তে বাড়তি decrement। CAS-style conditional natural-idempotent — version mismatch হলে zero rows, duplicate effect অসম্ভব। Alternative: delta-ই রাখতে হলে request-scoped idempotency-key + applied-keys table, অথবা absolute-value set (কিন্তু lost-update-ঝুঁকি আলাদাভাবে cover করে)।
4Check-then-insert (SELECT তারপর INSERT) কেন ভাঙে, আর ON CONFLICT DO NOTHING + rowcount কেন ভাঙে না?
যুক্তি
ON CONFLICT DO NOTHING + rowcount কেন ভাঙে না?Check-then-insert দুই statement — মাঝে অন্য duplicate ঢুকে দুটোই ‘miss’ দেখে effect চালায় (race window)। ON CONFLICT এক statement- এ atomic claim: unique/PK constraint-ই lock; rowcount 0 মানে অন্যজন আগেই দাবি করেছে → wait/replay-path। Claim আর effect মাঝে in-flight-lock-ও জরুরি — নইলে running-effect-এর সমান্তরাল duplicate।
5Order-service প্রতি night bulk-refund job চালায়; queue at-least-once।
Refund-effect-এ কোন dedup-key, store-এ কী রাখবেন, TTL কত?
ডিজাইন
Key = (refund-batch-id, order-id) — batch-retry আর order-retry দুটোই cover। Store: refund-ledger-এ unique-constraint — effect আর dedup-record একই local txn-এ (outbox-ধাঁচ), নইলে একটা হারাবে। TTL চিরস্থায়ী-প্রায় (ledger-ই তো) — refund এমন domain যেখানে ‘s্মৃতি মোছা’ option-ই নেই; মানে exactly-once effect, সস্তায়।
এরপর কী
এরপর কী
Effect-layer-এ exactly-once কেনা গেল — এবার সেই effect-গুলো service-এর মাঝে কীভাবে বহন হয় সেটা দেখা যাক। পরের লেসনে message queues: RabbitMQ-র exchange-routing-push-ack জগৎ বনাম Kafka-র append-only partitioned log-offset-replay জগৎ — smart-broker বনাম smart-consumer, retention বনাম ack-delete, আর consumer-group- এর rebalance-storm; দুই দর্শনই আগের দুই লেসনের guarantee-র উপর দাঁড়িয়ে।
আরও পড়ুন
- Designing Data-Intensive Applications, Chapter 11 — Stream Processing — Martin Kleppmann · Exactly-once semantics-এর সৎ বিশ্লেষণ আর end-to-end argument-এর ভিত্তি
- Apache Kafka Documentation — Delivery Semantics / Idempotent Producer — Apache Software Foundation · Transactional producer, idempotence, read_committed-এর official specification