এই পর্বে যা শিখবেন
- হারানো বার্তা, হারানো ack আর “কাজ আর ack-এর মাঝে crash” — তিন ব্যর্থতা থেকে কেন duplicate বা ক্ষতি আসে
- তিনটি delivery নিশ্চয়তার সঠিক সংজ্ঞা, আর exactly-once delivery কেন অসম্ভব তার ছোট প্রমাণ
- Idempotent operation, idempotency key, dedup store-এর window আর মেমরির হিসাব
- Inbox pattern, transactional consumer আর Kafka-ধাঁচের idempotent producer ও transaction — simulator আর লাইভ SQL-এ
- ক্রমের নিশ্চয়তা, বাইরের side effect আর কখন at-most-once-ই ঠিক পছন্দ
১ · একটি টাকা-যোগের বার্তা, তিন জায়গায় ভাঙন — কেন delivery কঠিন
একটি কাল্পনিক mobile wallet সেবার কথা ভাবুন। রিনা দোকানে ৫০০ টাকা ফেরত পাবেন। পেমেন্ট সার্ভিস একটি বার্তা পাঠায়: “wallet ৭-এ ৫০০ টাকা যোগ করুন”. Wallet সার্ভিস বার্তাটি পেয়ে টাকা যোগ করে, তারপর ফেরত জানায় “পেয়েছি” — একে বলে acknowledgement, ছোট করে ack. এই সাদামাটা আদানপ্রদানই পুরো পর্বের বিষয়।
ডাকঘরের রেজিস্ট্রি চিঠির কথা ভাবুন। আপনি চিঠি পাঠালেন, প্রাপ্তিস্বীকারের কার্ড ফেরত আসার কথা। দুই সপ্তাহেও কার্ড এলো না। এখন দুটি সম্ভাবনা: চিঠিটাই পথে হারিয়েছে, নয়তো চিঠি পৌঁছেছে কিন্তু কার্ডটি হারিয়েছে। আপনার টেবিলে বসে দুটির পার্থক্য বোঝার কোনো উপায় নেই। আবার পাঠালে প্রাপক হয়তো দুটি চিঠি পাবেন। না পাঠালে হয়তো একটিও পাবেন না। চিঠিতে যদি লেখা থাকে “৫০০ টাকা দিন”, প্রশ্নটি আর নিরীহ থাকে না।
বাস্তবে ভাঙন তিন জায়গায় হতে পারে:
- বার্তা হারায়: network-এ packet হারায়, broker বা queue-এর সার্ভার পড়ে যায়। Wallet কিছুই জানে না।
- Ack হারায়: টাকা যোগ হয়ে গেছে, কিন্তু “পেয়েছি” ফেরার পথে হারাল। প্রেরকের কাছে এটি প্রথম ঘটনার হুবহু মতো দেখায় — শুধু একটি timeout.
- কাজ আর ack-এর মাঝে crash: Wallet সার্ভিস টাকা যোগ করল, ack পাঠানোর ঠিক আগে process মরে গেল। Broker ভাবে বার্তাটি প্রক্রিয়া হয়নি, তাই অন্য কাউকে আবার দেয়।
দ্বিতীয় আর তৃতীয় ঘটনায় কাজ হয়ে গেছে, অথচ প্রেরক বা broker তা জানে না। এর গভীরে আছে Two Generals’ Problem: হারাতে পারে এমন বার্তাপথে দুই পক্ষ কখনো নিশ্চিত হতে পারে না যে শেষ বার্তাটি পৌঁছেছে। গল্প আর ইতিহাস পর্ব ০৮-এর ভাবনার প্রশ্নে আছে; partial failure-এর পুরো ছবি আসবে পর্ব ৩৮-এ। এখানে আমরা দেখব ফলটা — নিচের stepper-এ “পরে” চাপুন:
- পেমেন্ট সার্ভিস
প্রেরক - Network
অনির্ভরযোগ্য - Wallet সার্ভিস
গ্রাহক - Wallet DB
wallet ৭
| পাঠানো কপি | |
| প্রয়োগ হলো | |
| সঠিক balance |
- পাঠানো। পেমেন্ট সার্ভিস m-42 পাঠাল — “wallet ৭-এ ৫০০ যোগ” — আর ৩০ সেকেন্ডের timer চালু করল। রিনার balance এখন ১০০০.
- পৌঁছাল, প্রয়োগ হলো। Wallet সার্ভিস ৫০০ যোগ করল: balance ১৫০০. তারপর ack পাঠাল।
- Ack হারাল। ফেরার পথে ack হারিয়ে গেল। পেমেন্ট সার্ভিসের চোখে এটি “বার্তাই পৌঁছায়নি”-র হুবহু মতো।
- Timeout, তারপর retry. নিয়ম মেনে পেমেন্ট সার্ভিস একই m-42 আবার পাঠাল। এটি ভুল নয় — না পাঠালে প্রথম ধাপের বার্তা হারালে টাকাই যেত না।
- দ্বিতীয় প্রয়োগ। Wallet সার্ভিস জানে না যে m-42 আগেই এসেছিল, তাই আবার ৫০০ যোগ করল: balance ২০০০, অথচ হওয়ার কথা ১৫০০. দোষ network-এর নয়, গ্রাহকের নকশার — সে duplicate চিনতে পারে না।
Retry নিজে খারাপ নয়; retry ছাড়া হারানো বার্তা আর ফেরে না। সমস্যা হলো এমন কাজ, যা দুবার ঘটলে ফল বদলে যায়। তাই পুরো পর্বের প্রশ্ন একটাই: বার্তা দুবার এলেও ফল যেন একবারের মতো হয় — এটা কীভাবে নিশ্চিত করবেন?
২ · তিনটি নিশ্চয়তার সঠিক সংজ্ঞা — at-most-once, at-least-once, exactly-once
“কতবার পৌঁছাল” আর “কতবার কাজ হলো” — দুটি আলাদা প্রশ্ন। প্রতিটি বার্তা m-এর জন্য ধরুন k(m) = তার effect কতবার প্রয়োগ হলো (টাকা যোগ, ইমেইল, সারি লেখা)। তাহলে delivery semantics-এর তিনটি নিশ্চয়তা হলো:
| নিশ্চয়তা | সংজ্ঞা | কীভাবে পাওয়া যায় | ঝুঁকি |
|---|---|---|---|
| At-most-once | k(m) ∈ {০, ১} | একবার পাঠানো, retry নেই; বা কাজের আগে ack / offset commit | বার্তা হারায় |
| At-least-once | k(m) ≥ ১ (প্রেরক বেঁচে থাকলে আর network শেষে কাজ করলে) | ack না আসা পর্যন্ত retry; কাজের পরে ack / offset commit | duplicate |
| Exactly-once | k(m) = ১ | at-least-once + duplicate চিনে বাদ দেওয়া | জটিলতা, মেমরি, latency |
খেয়াল করুন, প্রতিটি সংজ্ঞা effect নিয়ে, তার আসল শর্ত গ্রাহকের দিকে। At-least-once-এর শর্তটিও গুরুত্বপূর্ণ: প্রেরক নিজেই চিরতরে মরে গেলে বা retry-এর সীমা ফুরালে বার্তা হারাতে পারে — তাই বাস্তবে retry শেষ হলে বার্তা যায় একটি dead letter queue-তে (পর্ব ৩১)।
নিচের simulator-এ ১০০০টি wallet credit, প্রতিটি ১০০ টাকা। ধরা যাক network ১০% বার্তা আর ১০% ack হারায়। তিনটি কৌশলের ওপর একই seeded ক্ষতি চাপানো হয়েছে, যাতে তুলনা ন্যায্য হয়:
// Wallet credits over a lossy network: the SAME seeded faults for every strategy.
function mulberry32(a) { return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
const N = 1000, AMOUNT = 100, MAX_TRY = 8; // assumed: 1000 credits of 100 taka, up to 8 attempts
const P_MSG = 0.10, P_ACK = 0.10; // assumed: 10% of messages and 10% of acks are lost
const rnd = mulberry32(34), msgLost = [], ackLost = [];
for (let i = 0; i < N; i++) { msgLost.push([]); ackLost.push([]);
for (let a = 0; a < MAX_TRY; a++) { msgLost[i].push(rnd() < P_MSG); ackLost[i].push(rnd() < P_ACK); } }
function run(name, retry, dedup) {
let balance = 0, sends = 0, applied = 0, lost = 0, dups = 0;
const seen = new Set(); // the consumer's dedup store (message ids)
for (let i = 0; i < N; i++) {
let acked = false, count = 0;
for (let a = 0; a < (retry ? MAX_TRY : 1) && !acked; a++) {
sends++;
if (msgLost[i][a]) continue; // message never arrived; sender times out
if (!(dedup && seen.has(i))) { balance += AMOUNT; count++; }
seen.add(i); // a duplicate is still acked again
if (!ackLost[i][a]) acked = true; // ack lost -> sender cannot tell, so it retries
}
if (count === 0) lost++; else { applied++; dups += count - 1; }
}
const ok = balance === N * AMOUNT ? "CORRECT" : "WRONG";
console.log(name.padEnd(22) + String(sends).padStart(6) + String(applied).padStart(8) +
String(lost).padStart(6) + String(dups).padStart(6) + String(balance).padStart(9) + " " + ok);
}
console.log("strategy sends applied lost dups balance (expected " + N * AMOUNT + ")");
run("at-most-once", false, false);
run("at-least-once", true, false);
run("at-least-once + dedup", true, true);আউটপুট:
strategy sends applied lost dups balance (expected 100000)
at-most-once 1000 905 95 0 90500 WRONG
at-least-once 1231 1000 0 119 111900 WRONG
at-least-once + dedup 1231 1000 0 0 100000 CORRECT
At-most-once-এ ১০০০টি পাঠানো, ৯৫টি হারাল: balance ৯০৫০০, ৯,৫০০ টাকা কম। At-least-once-এ কিছুই হারায়নি, কিন্তু ১১৯টি duplicate — রিনাদের wallet-এ ১১,৯০০ টাকা বেশি। তৃতীয় সারিতে পাঠানো কপি হুবহু একই (১২৩১), duplicate-ও network-এ এসেছে, কিন্তু গ্রাহকের dedup store সেগুলো চিনে বাদ দিয়েছে: balance ঠিক ১০০০০০. খেয়াল করুন, duplicate-কেও আবার ack করা হয় — না করলে প্রেরক retry থামাত না।
সংখ্যাগুলো কি কাকতালীয়? ছোট একটি হিসাব
ধরুন বার্তা হারানোর সম্ভাবনা pm, ack হারানোর pa, আর retry-এর সীমা নেই। একটি চেষ্টা সফলভাবে শেষ হয় (বার্তা পৌঁছাল এবং ack ফিরল) q = (১ − pm)(১ − pa) সম্ভাবনায়। চেষ্টার সংখ্যা geometric, তাই গড়ে ১/qটি কপি পাঠানো হয়। যে চেষ্টা ব্যর্থ, তার মধ্যে বার্তা পৌঁছেছে কিন্তু ack হারিয়েছে এমনগুলোর ভাগ (১ − pm)pa / (১ − q). তাহলে প্রতি বার্তায় গড় duplicate:
মজার ফল: duplicate-এর হার বার্তা হারানোর ওপর নির্ভর করে না, শুধু ack হারানোর ওপর। pa = ০.১ হলে প্রতি বার্তায় গড়ে ০.১১১টি duplicate — ১০০০টিতে প্রায় ১১১, simulator-এ ১১৯. পাঠানো কপি ১/০.৮১ ≈ ১.২৩৫ গুণ, অর্থাৎ প্রায় ১২৩৫, simulator-এ ১২৩১. At-most-once-এ প্রত্যাশিত ক্ষতি ১০০০ × ০.১ = ১০০, পাওয়া গেল ৯৫. বাস্তবে pa অনেক ছোট, যেমন ০.০০১; কিন্তু সেকেন্ডে ১০,০০০ বার্তায় সেটিও প্রায় ১০টি duplicate প্রতি সেকেন্ডে — দিনে আট লাখের বেশি। বিরল ঘটনা বড় মাপে প্রতিদিনের ঘটনা।
৩ · “Exactly-once delivery” কেন অসম্ভব, আর effectively-once কেন সম্ভব
প্রশ্নটি ইন্টারভিউতে প্রায়ই আসে, আর উত্তর দুই স্তরে দিতে হয়। প্রথমে একটি ছোট প্রমাণ।
ধরুন কোনো প্রোটোকল দাবি করে, হারাতে পারে এমন network-এ প্রতিটি বার্তা গ্রাহকের কাছে ঠিক একবার পৌঁছে দেবে, আর গ্রাহক কোনো স্মৃতি রাখবে না। দুটি সম্ভাব্য ঘটনা নিন। E১: প্রথম কপি পথে হারাল। E২: প্রথম কপি পৌঁছাল, কিন্তু ack হারাল। প্রেরকের কাছে দুটি ঘটনা হুবহু এক: সে একটি বার্তা পাঠিয়েছে, কিছুই ফেরেনি। তাই প্রেরক দুই ঘটনায় একই কাজ করবে। আবার পাঠালে E২-তে গ্রাহক দুবার পায়। না পাঠালে E১-এ একবারও পায় না। কোনো timeout, কোনো বাড়তি ack-এর ack এই অভিন্নতা ভাঙে না, কারণ শেষ বার্তাটিও হারাতে পারে। অতএব শুধু delivery-র স্তরে exactly-once অসম্ভব।
প্রমাণের ফাঁকটি লক্ষ করুন: “গ্রাহক কোনো স্মৃতি রাখবে না”. গ্রাহক যদি মনে রাখে “m-42 আগেই প্রয়োগ করেছি”, তাহলে প্রেরক নিশ্চিন্তে আবার পাঠাতে পারে। দ্বিতীয় কপি পৌঁছাবে, কিন্তু effect হবে না। একেই বলে effectively-once বা exactly-once processing:
(effect আর “দেখা হয়েছে” চিহ্ন একসাথে, অবিভাজ্যভাবে লেখা)
শেষ শর্তটি বাদ দিলে পুরো কাঠামো ভেঙে পড়ে। চিহ্ন আগে লিখে effect-এর আগে crash হলে, retry-তে চিহ্ন দেখে কাজটি বাদ যাবে — effect হারাল। Effect আগে লিখে চিহ্নের আগে crash হলে, retry-তে চিহ্ন নেই — effect দুবার। তাই দুটি লেখা একই transaction-এ, যেমন একই ডেটাবেসে। এর বাস্তব রূপ পরের তিন অংশে।
নামের গোলমাল: বিভিন্ন পণ্য আর লেখক একই জিনিসকে ভিন্ন নামে ডাকেন — “exactly-once semantics”, “exactly-once processing”, “effectively-once”. কোনো broker “exactly-once” দেখালে প্রশ্ন করুন: কোন সীমার ভেতরে? কোন পথের শুরু থেকে কোন শেষ পর্যন্ত? কতক্ষণের duplicate পর্যন্ত? যেমন, লেখার সময়ে AWS-এর ডকুমেন্টেশন অনুযায়ী SQS FIFO queue একটি deduplication ID দেখে ৫ মিনিটের মধ্যে আসা duplicate বাদ দেয় — ৫ মিনিট পরের retry আর গ্রাহকের নিজের crash এর বাইরে।
৪ · Idempotent operation — SET বনাম INCR, version check আর শর্তসাপেক্ষ লেখা
সবচেয়ে সস্তা dedup হলো যেটি লাগেই না। কিছু কাজ নিজের স্বভাবেই idempotent: f(f(x)) = f(x). দুবার করলে একবারের মতোই ফল। একে বলে natural idempotency.
| কাজ | দুবার চললে | Idempotent? |
|---|---|---|
SET status = 'PAID' | এখনো PAID | হ্যাঁ |
balance = balance + 500 (INCR) | ১০০০ বাড়ল | না |
DELETE … WHERE id = 9 | দ্বিতীয়বার ০টি সারি | হ্যাঁ |
| তালিকায় একটি item append | দুটি একই item | না |
| Set-এ একটি সদস্য যোগ (SADD) | সদস্য একটিই | হ্যাঁ |
| Email বা SMS পাঠানো | দুটি email | না |
HTTP PUT পুরো resource | একই অবস্থা | হ্যাঁ (পর্ব ০৮) |
তাহলে কি সব INCR-কে SET বানিয়ে ফেলবেন — “balance = 1500”? সাবধান। Absolute মান নতুন একটি বিপদ আনে: পুরোনো বার্তা দেরিতে এলে নতুন মানকে মুছে দেয়। ধরুন “balance = 1500” আটকে ছিল, এর মধ্যে “balance = 1700” প্রয়োগ হয়েছে; তারপর পুরোনো বার্তার retry এলে balance ফিরে যায় ১৫০০-এ। একে লেখার ভাষায় বলে lost update.
প্রতিকার: প্রতিটি বার্তায় একটি version, আর লেখা হবে শর্তসাপেক্ষ — compare-and-set. “এই version আমার জানা version-এর চেয়ে নতুন হলে তবেই লিখুন।” এটি optimistic concurrency-এর বার্তা-সংস্করণ:
-- message: {order: 1001, status: 'SHIPPED', version: 3}
UPDATE orders SET status = 'SHIPPED', version = 3
WHERE id = 1001 AND version < 3;
-- 1 row changed -> applied
-- 0 rows changed -> already have v3 or newer: a duplicate or a late message; ack and skipএই নিয়মে duplicate আর দেরিতে আসা বার্তা দুটোই নিরাপদে বাদ যায়, কোনো আলাদা dedup টেবিল ছাড়াই। দাম: প্রেরককে প্রতিটি entity-র version জানতে হয়, যা সাধারণত সত্যের উৎস ডেটাবেস থেকেই আসে। লাইভ SQL-এ এই নিয়ম অংশ ৮-এ চালিয়ে দেখবেন।
“ফলাফল এক”-এর মানে নিয়ে সতর্ক থাকুন। Idempotent মানে ডেটার অবস্থা এক, সব কিছু এক নয়। দ্বিতীয় DELETE হয়তো 404 ফেরায়, log-এ দুটি লাইন পড়ে, metric-এ দুটি গণনা হয়। নকশায় ঠিক করে নিন কোন effect-গুলো একবার হতেই হবে — সাধারণত টাকা, স্টক আর বাইরের জগতে যাওয়া বার্তা।
৫ · Idempotency key আর dedup store — window, TTL আর মেমরির হিসাব
যে কাজ স্বভাবে idempotent নয়, সেখানে প্রতিটি বার্তাকে একটি অনন্য পরিচয় দিতে হয় — message id বা idempotency key. গ্রাহক একটি dedup store-এ মনে রাখে কোন id আগেই প্রক্রিয়া হয়েছে। ব্যাংকের কাউন্টারের টোকেনের মতো: একই টোকেন নম্বর দ্বিতীয়বার ডাকা হলে ক্যাশিয়ার খাতা দেখে বলেন, “এটার টাকা আগেই দিয়েছি।”
Key কে বানাবে, কী দিয়ে?
- অভিপ্রায়ের জন্মস্থানে: key বানাতে হয় যেখানে কাজটির জন্ম — রিনার ফেরতের অনুরোধ তৈরির মুহূর্তে, একটি UUID বা ব্যবসায়িক পরিচয় দিয়ে, যেমন “refund-for-order-1001”. প্রতিটি retry-তে সেই একই key. HTTP-র দিক পর্ব ০৮-এ।
- Broker-এর offset বা delivery tag key নয়: প্রেরক নিজে আবার publish করলে নতুন offset পায়, তাই একই কাজ দুটি ভিন্ন “id” পায়।
- Payload-এর hash সাবধানে: দুটি বৈধ, আলাদা ৫০০ টাকার ফেরত একই payload পেতে পারে। Hash তখনই চলে, যখন payload-এ আগে থেকেই অনন্য কিছু থাকে।
এবার নিচের stepper-এ সেই একই হারানো ack, কিন্তু wallet সার্ভিসের হাতে এখন একটি dedup store:
- প্রেরক
key = m-42 - Wallet সার্ভিস
গ্রাহক - Dedup store
TTL ২৪ ঘণ্টা - Balance
wallet ৭
- প্রথম কপি এলো। Wallet সার্ভিস প্রথমে dedup store-এ m-42 খোঁজে — নেই। Balance এখনো ১০০০.
- Effect আর চিহ্ন একসাথে। একই transaction-এ balance ১৫০০ আর dedup store-এ m-42. দুটির একটিও একা commit হয় না।
- Ack হারাল। আগের মতোই ack পথে হারাল; প্রেরকের timeout হলো।
- দ্বিতীয় কপি এলো। Dedup store-এ m-42 পাওয়া গেল। Balance ছোঁয়া হলো না — এখনো ১৫০০.
- তবু ack. Duplicate হলেও “পেয়েছি” পাঠাতে হয়, নইলে প্রেরক চিরকাল retry করবে। এবার ack পৌঁছাল, প্রেরক থামল। Network দুবার দিয়েছে, effect একবার।
কতদিন মনে রাখবেন — window আর মেমরি
Dedup store চিরকাল বড় হতে পারে না, তাই প্রতিটি entry একটি TTL পায়। এই সময়কে বলে dedup window. নিয়ম একটাই: window অবশ্যই সবচেয়ে পুরোনো সম্ভাব্য duplicate-এর বয়সের চেয়ে বড় হতে হবে। শুধু retry-এর সময়সূচি নয়, গ্রাহকের দীর্ঘ বিভ্রাট, dead letter queue থেকে আবার পাঠানো (redrive) আর পুরোনো offset থেকে replay — সব গুনতে হয়। আর মেমরির সূত্র:
// Dedup store sizing: how long must message ids be remembered, and what does it cost?
const RATE = 2000; // assumed: 2,000 messages/s at peak, every one with a unique id
const BYTES = 100; // assumed: ~100 B per entry (16 B id + expiry + store overhead)
const REPLICAS = 2; // assumed: the store keeps 2 copies
// Ages at which a duplicate of an already-processed message can still arrive (assumed):
let retry = 0; for (let k = 0; k < 10; k++) retry += 2 ** k; // backoff 1,2,4..512 s
const sources = [
["retry", retry], // producer and broker retries: 1023 s, about 17 min
["lag", 6 * 3600], // consumer down for 6 h, then catches up
["redrive", 2 * 86400], // dead-letter messages re-sent 2 days later
["replay", 3 * 86400], // offsets reset to 3 days ago
];
const human = (s) => s < 3600 ? (s / 60).toFixed(0) + " min" : s < 86400 ? (s / 3600).toFixed(0) + " h" : (s / 86400).toFixed(0) + " d";
const size = (b) => b >= 1e9 ? (b / 1e9).toFixed(2) + " GB" : (b / 1e6).toFixed(0) + " MB";
console.log("retry schedule ends after " + retry + " s (" + human(retry) + ")\n");
console.log("window keys stored memory x" + REPLICAS + " duplicates still caught");
for (const w of [5 * 60, 3600, 86400, 7 * 86400]) {
const keys = RATE * w, mem = keys * BYTES * REPLICAS;
const caught = sources.filter(([, age]) => age <= w).map(([n]) => n);
console.log(human(w).padEnd(8) + String(keys.toLocaleString("en-US")).padStart(13) +
size(mem).padStart(14) + " " + (caught.join(", ") || "none"));
}
// Alternative: remember only the highest sequence number per (producer, partition).
const PRODUCERS = 400, PARTITIONS = 48, SEQ_BYTES = 32; // assumed
console.log("\nper-producer sequence table: " + (PRODUCERS * PARTITIONS).toLocaleString("en-US") +
" rows, about " + (PRODUCERS * PARTITIONS * SEQ_BYTES / 1e3).toFixed(0) + " KB, independent of the window");আউটপুট:
retry schedule ends after 1023 s (17 min)
window keys stored memory x2 duplicates still caught
5 min 600,000 120 MB none
1 h 7,200,000 1.44 GB retry
1 d 172,800,000 34.56 GB retry, lag
7 d 1,209,600,000 241.92 GB retry, lag, redrive, replay
per-producer sequence table: 19,200 rows, about 614 KB, independent of the window
Backoff-এর ১০টি retry মিলে ১০২৩ সেকেন্ড, প্রায় ১৭ মিনিট — তাই ৫ মিনিটের window কোনো duplicate-ই পুরোপুরি ধরে না। ১ ঘণ্টায় retry ধরা পড়ে (৭২ লাখ key, ১.৪৪ GB). ১ দিনে ৬ ঘণ্টার বিভ্রাটও ধরা পড়ে, কিন্তু মেমরি ৩৪.৫৬ GB. ৩ দিনের replay ধরতে ৭ দিন লাগে: ১২০ কোটির বেশি key, ২৪১.৯২ GB — RAM-এ রাখা তখন ব্যয়বহুল, তাই সাধারণত ডেটাবেসের টেবিল বা disk-ভিত্তিক store. শেষ লাইনটি বিকল্প: প্রতিটি (producer, partition) জোড়ার শুধু সর্বোচ্চ sequence number মনে রাখলে ১৯,২০০টি সারি, প্রায় ৬১৪ KB — window যত বড়ই হোক। শর্ত: প্রেরক প্রতিটি partition-এ ফাঁকহীন, বাড়তে থাকা নম্বর দেবে, যেটি অংশ ৭-এর idempotent producer-এর কৌশল।
- Window ছোট রাখার উপায়: replay-এর সময় ব্যবসায়িক key-এর version check (অংশ ৪) চালু রাখা, যাতে dedup store-এর বাইরের পুরোনো বার্তাও নিরাপদ থাকে।
- Bloom filter দিয়ে মেমরি বাঁচানো? এতে false positive মানে একটি নতুন বার্তাকে duplicate ভেবে ফেলে দেওয়া — অর্থাৎ ক্ষতি। শুধু দ্রুত প্রাথমিক যাচাইয়ে চলে, চূড়ান্ত সিদ্ধান্তে নয় (পর্ব ৪৫, ভাবনার প্রশ্ন ৩)।
- Dedup store আর ডেটাবেস আলাদা হলে: Redis-এ চিহ্ন আর PostgreSQL-এ টাকা — দুটি আলাদা লেখা, আবার সেই অংশ ৩-এর crash-এর ফাঁক। তাই টাকার মতো effect-এ চিহ্নটি একই ডেটাবেসে রাখাই নিরাপদ। পরের অংশ ঠিক এটাই।
৬ · Inbox pattern আর transactional consumer — effect, চিহ্ন আর offset এক transaction-এ
Inbox pattern-এ dedup store হলো গ্রাহকের নিজের ডেটাবেসের একটি টেবিল, যার message_id-তে UNIQUE constraint. প্রতিটি বার্তায় একটি local transaction: inbox-এ সারি, তারপর effect, তারপর COMMIT. Duplicate এলে insert-এ ০টি সারি — effect বাদ। পর্ব ২৫-এ outbox-এর অপর প্রান্তে এই idempotent consumer দেখেছেন; এখানে আমরা তার ভেতরটা খুলব।
Producer → broker → consumer, আর একটি local transaction
Inbox (dedup), effect and consumer offset commit together
লাইভ SQL-এ handler-টি চালিয়ে দেখুন। wallets-এ রিনার wallet ৭, balance ১০০০; inbox-এ message_id অনন্য। SQLite-এর changes() আগের বাক্যে কয়টি সারি বদলেছে তা দেয়: ১ মানে নতুন বার্তা, ০ মানে duplicate:
-- Credit handler for one message: the inbox row and the credit commit TOGETHER.
-- Delivery 1 of m-42 (credit 500 taka to wallet 7):
BEGIN;
INSERT OR IGNORE INTO inbox VALUES ('m-42', 7, 500, '10:00:01');
-- changes() = rows the INSERT just above added: 1 = new message, 0 = duplicate.
UPDATE wallets SET balance = balance + 500 WHERE id = 7 AND changes() = 1;
COMMIT;
SELECT balance FROM wallets WHERE id = 7;
-- The ack was lost, so the broker delivers m-42 AGAIN. Same handler, same statements:
BEGIN;
INSERT OR IGNORE INTO inbox VALUES ('m-42', 7, 500, '10:00:31');
UPDATE wallets SET balance = balance + 500 WHERE id = 7 AND changes() = 1;
COMMIT;
SELECT balance FROM wallets WHERE id = 7;
-- A new message m-43 (credit 200) is applied normally:
BEGIN;
INSERT OR IGNORE INTO inbox VALUES ('m-43', 7, 200, '10:00:40');
UPDATE wallets SET balance = balance + 200 WHERE id = 7 AND changes() = 1;
COMMIT;
SELECT w.balance, (SELECT count(*) FROM inbox) AS inbox_rows,
(SELECT first_seen FROM inbox WHERE message_id = 'm-42') AS m42_first_seen
FROM wallets w WHERE w.id = 7;প্রথম delivery-তে INSERT আর UPDATE দুটিতেই ১টি সারি বদলেছে, balance 1500. দ্বিতীয় delivery-তে একই বাক্য, কিন্তু UNIQUE-এর কারণে INSERT-এ ০টি সারি বদলেছে, ফলে changes() = 1 শর্ত মিথ্যা আর UPDATE-এও ০টি সারি বদলেছে — balance এখনো 1500. নতুন m-43 স্বাভাবিকভাবে প্রয়োগ হলো। শেষ সারি 1700 2 10:00:01: balance ১০০০ + ৫০০ + ২০০, inbox-এ দুটি বার্তা, আর m-42-এর সময় প্রথম delivery-রই — দ্বিতীয়টি কোনো চিহ্ন রাখেনি। আসল অ্যাপে changes()-এর বদলে driver-এর affected-row সংখ্যা দেখে সিদ্ধান্ত নেওয়া হয়; PostgreSQL-এ INSERT … ON CONFLICT DO NOTHING একই কাজ করে। SQLite এখানে শুধু একটি ডেটাবেসের ভেতরের যুক্তি দেখায় — broker, network বা ack নয়।
Offset-ও একই transaction-এ — transactional consumer
Log-ভিত্তিক broker-এ (Kafka-ধাঁচের) গ্রাহক নিজে মনে রাখে কোথা পর্যন্ত পড়েছে — partition-এর offset; consumer group আর offset-এর বিস্তারিত পর্ব ৩৩-এ। প্রশ্ন হলো, offset কখন commit করবেন? নিচের stepper-এ crash-এর ফাঁকটি দেখুন (Kafka-র রীতিতে committed offset মানে “এরপর এটি পড়ব”):
- Partition 3
log - Consumer
group: wallet - Wallet DB
wallet ৭
| Committed offset | |
| m-42 প্রয়োগ (inbox সহ) |
- Poll. Consumer A-র committed offset 41, তাই সে offset 41-এর বার্তা m-42 পড়ল। Balance ১০০০.
- প্রক্রিয়া আর DB commit. Balance ১৫০০ আর inbox-এ m-42 — এক transaction-এ commit হলো। Broker-কে এখনো জানানো হয়নি।
- Crash-এর ফাঁক। Offset 42 commit করার ঠিক আগে A মরে গেল। Broker-এর খাতায় এখনো 41 — অর্থাৎ “m-42 পড়া হয়নি”.
- Rebalance আর redelivery. Group-এর আরেক সদস্য B partition 3 পেল, committed offset 41 থেকে শুরু করল — m-42 আবার এলো। এটি bug নয়, at-least-once-এর স্বাভাবিক চেহারা।
- Inbox-এর পরীক্ষা। Inbox না থাকলে balance ২০০০ — ভুল। Inbox থাকলে insert-এ ০টি সারি, effect বাদ, balance ১৫০০.
- এবার offset 42. B offset 42 commit করল। উল্টো ক্রমে — আগে offset, পরে কাজ — একই crash-এ m-42 চিরতরে হারাত: সেটিই at-most-once.
আরও এক ধাপ এগোনো যায়। Offset-টিকেই যদি ডেটাবেসের একটি টেবিলে (যেমন consumer_offsets) effect-এর সাথে একই transaction-এ লেখেন, আর চালু হওয়ার সময় broker-এর বদলে সেখান থেকে offset পড়েন, তাহলে সেই partition-এর জন্য আলাদা inbox-ও লাগে না: একটি partition-এ offset নিজেই একটি বাড়তে থাকা sequence number (মাঝে ফাঁক থাকলেও ক্ষতি নেই, কারণ রাখা হয় “পরের offset”). একে বলে transactional consumer — consume, effect আর offset commit একসাথে, অবিভাজ্যভাবে। শর্ত: effect আর offset একই ডেটাবেসে; আর অন্য পথে আসা duplicate (যেমন প্রেরক নিজে দুবার publish করলে, দুটি ভিন্ন offset-এ) ধরতে message_id-এর inbox তবুও লাগে।
৭ · Kafka-ধাঁচের idempotent producer আর transaction — আর end-to-end-এর শিকল
এতক্ষণ duplicate ঠেকিয়েছি গ্রাহকের দিকে। প্রেরক থেকে broker-এর পথেও একই সমস্যা: producer batch পাঠাল, broker লিখল, ack হারাল, producer আবার পাঠাল — log-এ একই বার্তা দুটি offset-এ। Kafka ২০১৭ সালে (সংস্করণ 0.11, KIP-98 প্রস্তাব) এর জন্য দুটি ব্যবস্থা আনে। খুঁটিনাটি সংস্করণভেদে আলাদা, তাই নিচের বর্ণনা ধারণার স্তরে:
- Idempotent producer: broker প্রতিটি producer-কে একটি producer ID (PID) দেয়। প্রতিটি partition-এ producer-এর প্রতিটি batch একটি বাড়তে থাকা sequence number বহন করে। Broker প্রতিটি (PID, partition)-এর শেষ sequence মনে রাখে; একই বা পুরোনো নম্বর এলে আবার লেখে না, শুধু সফল উত্তর দেয়; ফাঁক থাকলে প্রত্যাখ্যান করে। এটি অংশ ৫-এর “per-producer sequence” কৌশল — মেমরি সামান্য।
- Transaction: একটি স্থির
transactional.idদিয়ে producer কয়েকটি partition-এ লেখা আর consumer offset একসাথে commit বা abort করতে পারে। নতুন instance চালু হলে একটি epoch বাড়ে, আর পুরোনো epoch-এর “zombie” producer-এর লেখা প্রত্যাখ্যাত হয় — fencing token-এর একই ধারণা (পর্ব ৪৩)। - read_committed: consumer-এর
isolation.level=read_committedহলে সে শুধু commit হওয়া transaction-এর বার্তা দেখে; abort হওয়াগুলো বাদ যায়। লেখার সময়ে ডিফল্টread_uncommitted. এটি ডেটাবেসের READ COMMITTED isolation level থেকে আলাদা জিনিস, নাম মিললেও।
- Producer
enable.idempotence - Broker · partition 2
শেষ seq মনে রাখে
| শেষ sequence | |
| Log-এ কপি |
- Batch seq 5. PID 7-এর producer partition 2-তে seq 5 পাঠাল। Broker-এর খাতায় এই জোড়ার শেষ নম্বর 4, তাই 5 প্রত্যাশিত।
- লেখা হলো, ack হারাল। Offset 900-এ লেখা হলো, শেষ নম্বর এখন 5. Ack পথে হারাল।
- Retry. Producer একই batch, একই seq 5 আবার পাঠাল — নতুন নম্বর নয়, এটাই কৌশলের মূল।
- Duplicate চেনা। 5 ≤ শেষ নম্বর 5, তাই broker আবার লেখে না, কিন্তু সফল উত্তর দেয়। Log-এ কপি একটিই।
- ফাঁক ধরা। seq 6 আটকে থাকতে seq 7 এলে broker প্রত্যাখ্যান করে, কারণ 6 প্রত্যাশিত। ফলে retry-তে partition-এর ভেতরের ক্রমও রক্ষা হয় (অংশ ৮)।
# producer
enable.idempotence=true # PID + per-partition sequence numbers
acks=all # required for idempotence
transactional.id=wallet-proc-3 # stable across restarts; enables fencing
# consumer
isolation.level=read_committed # skip messages of aborted transactions
enable.auto.commit=false # offsets go inside the transaction instead
# loop: poll -> beginTransaction -> send results -> sendOffsetsToTransaction -> commitTransactionসীমা: শিকলের প্রতিটি কড়া
এই ব্যবস্থার নিশ্চয়তা Kafka থেকে পড়া, প্রক্রিয়া, Kafka-তে লেখা পর্যন্ত। আপনার consumer যদি PostgreSQL-এ টাকা লেখে বা email পাঠায়, সেই লেখা Kafka-র transaction-এর অংশ নয়। সেখানে আবার অংশ ৬-এর inbox বা transactional consumer লাগে। পুরো পথটি একটি শিকল:
একটি কড়া দুর্বল হলে পুরো শিকল at-least-once
Stream processing framework-গুলোও (যেমন Flink বা Kafka Streams) “exactly-once” দেয় নিজেদের state আর সমর্থিত sink-এর ভেতরে — checkpoint আর transactional লেখা দিয়ে; তার বিবরণ পর্ব ৩৬-এ।
আরও গভীরে: Kafka-র নকশা KIP-98 (২০১৭)-এ বিস্তারিত — transaction coordinator, partition-এ commit/abort marker আর “last stable offset”, যা ভেতরে একরকম two-phase commit (পর্ব ২৫)। Google-এর MillWheel গবেষণাপত্র (VLDB ২০১৩) প্রকাশিত বর্ণনা অনুযায়ী প্রতিটি record-এর অনন্য id দিয়ে duplicate বাদ দিত, আর দ্রুত যাচাইয়ে Bloom filter ব্যবহার করত। Flink-এর checkpoint-এর ভিত্তি Carbone ও সহকর্মীদের “Lightweight Asynchronous Snapshots for Distributed Dataflows” (২০১৫)। আর Pat Helland-এর ছোট প্রবন্ধ “Idempotence Is Not a Medical Condition” (ACM Queue, ২০১২) এই পর্বের পুরো দর্শনটি সুন্দরভাবে বলে।
৮ · ক্রমের নিশ্চয়তা — key আর partition, আগে-পরে আসা বার্তা আর sequence number
“কতবার” প্রশ্নের পাশে আরেকটি প্রশ্ন: “কোন ক্রমে?” ফেরিঘাটের সিরিয়ালের মতো: একটি লাইনে গাড়ি ক্রমেই ওঠে, কিন্তু দুটি ঘাটের দুই লাইনের মধ্যে কে আগে পার হবে, তার কোনো নিশ্চয়তা নেই। Broker-এও তাই:
- Per-key ordering: Kafka-ধাঁচের log ক্রম দেয় শুধু একটি partition-এর ভেতরে। একই key (যেমন wallet_id) সবসময় একই partition-এ যায়, তাই একই wallet-এর বার্তা ক্রমে আসে; দুই wallet-এর মধ্যে ক্রমের প্রতিশ্রুতি নেই (পর্ব ৩৩)। Partition key বাছাইয়ের যুক্তি পর্ব ২৩-এর মতোই।
- Retry ক্রম উল্টাতে পারে: batch ১ ব্যর্থ, batch ২ সফল, তারপর batch ১-এর retry — log-এ ২, ১. Idempotent producer-এর sequence check এটি ঠেকায় (stepper-এর শেষ ধাপ)।
- কয়েকজন গ্রাহক বা thread: সাধারণ queue-তে (পর্ব ৩১) কয়েকজন গ্রাহক সমান্তরালে নিলে, বা একজন গ্রাহক partition-এর বার্তা কয়েকটি thread-এ ছড়ালে, ক্রম যায়।
- সবকিছুর এক ক্রম (total order) দামি: সব বার্তা একটি partition-এ মানে একজন গ্রাহক, তাই throughput একটি যন্ত্রের সমান। বহু যন্ত্রে total order লাগলে consensus লাগে (পর্ব ৪২)।
তাই গ্রাহককে নিজে আগে-পরে আসা বার্তা সামলাতে হয়, প্রতিটি entity-র জন্য একটি version বা sequence number দিয়ে। বার্তার ধরন অনুযায়ী নিয়ম দুটি: absolute অবস্থা (“অর্ডার এখন SHIPPED, version 3”) হলে নতুনতর এলে লিখুন, পুরোনো বা সমান বাদ দিন। পরিবর্তন (“৫০ পয়েন্ট যোগ, seq 1”) হলে ঠিক পরের নম্বরটিই লিখুন; আগাম এলে অপেক্ষার তালিকায় রাখুন, পুরোনো এলে বাদ দিন। লাইভ SQL-এ দুটিই:
-- (a) Absolute state ("status is X as of version v"): apply only if NEWER than what we have.
UPDATE orders SET status = 'SHIPPED', version = 3 WHERE id = 1001 AND version < 3;
UPDATE orders SET status = 'PAID', version = 2 WHERE id = 1001 AND version < 2; -- late v2
UPDATE orders SET status = 'SHIPPED', version = 3 WHERE id = 1001 AND version < 3; -- duplicate v3
SELECT id, status, version FROM orders;
-- (b) Deltas ("add 50 points", seq per customer): apply only the NEXT seq, in order.
UPDATE points SET total = total + 50, last_seq = 1 WHERE customer = 55 AND last_seq = 0;
UPDATE points SET total = total + 30, last_seq = 3 WHERE customer = 55 AND last_seq = 2; -- early seq 3
UPDATE points SET total = total + 20, last_seq = 2 WHERE customer = 55 AND last_seq = 1;
UPDATE points SET total = total + 30, last_seq = 3 WHERE customer = 55 AND last_seq = 2; -- seq 3 again
UPDATE points SET total = total + 20, last_seq = 2 WHERE customer = 55 AND last_seq = 1; -- duplicate seq 2
SELECT customer, total, last_seq FROM points;(a) v3 এলো আগে: ১টি সারি বদলেছে. দেরিতে আসা v2 আর duplicate v3 — দুটিতেই ০টি সারি বদলেছে, তাই শেষে 1001 SHIPPED 3: অর্ডার পেছনে PAID-এ ফেরেনি। Absolute অবস্থায় মাঝের version বাদ পড়লেও ক্ষতি নেই, শেষ অবস্থাই সত্য। (b) seq 1 প্রয়োগ হলো; আগাম seq 3-এ ০টি সারি বদলেছে — অ্যাপ এটি অপেক্ষার তালিকায় রাখবে। seq 2 এলে প্রয়োগ, তারপর অপেক্ষায় থাকা seq 3 আবার চেষ্টা করলে প্রয়োগ, আর duplicate seq 2-তে ০. শেষে 55 100 3: ৫০ + ২০ + ৩০ = ১০০, প্রতিটি একবার, ক্রমে। পরিবর্তনের বার্তায় ফাঁক রেখে এগোলে যোগফল ভুল হতো, তাই এখানে নিয়ম “ঠিক পরেরটি”, “নতুনতর যেকোনোটি” নয়।
আগাম বার্তা আটকে রাখার দামও আছে: পরের নম্বরটি না এলে পেছনের সব আটকে থাকে — head-of-line blocking. তাই অপেক্ষার একটি সময়সীমা আর তারপর সতর্কবার্তা বা হাতে মীমাংসা রাখুন। ক্রম আর সময়ের তাত্ত্বিক দিক (Lamport clock, vector clock) আসবে পর্ব ৩৯-এ, chat-এর বার্তার ক্রম পর্ব ৫৫-এ।
৯ · নিয়ন্ত্রণের বাইরের side effect — email, SMS, payment, আর কখন at-most-once ভালো
Inbox কাজ করে, কারণ চিহ্ন আর effect একই ডেটাবেসে। কিন্তু কিছু side effect আপনার ডেটাবেসে ঘটে না: SMS gateway-তে বার্তা, email, ব্যাংক বা payment provider-এ টাকা কাটা। সেগুলো আপনার transaction-এ বাঁধা যায় না। SMS পাঠানোর পর আর inbox-এর commit-এর আগে crash হলে retry-তে আরেকটি SMS যাবে।
| পরিস্থিতি | উপায় |
|---|---|
| Provider idempotency key নেয় (অনেক payment API-র প্রকাশিত ডকুমেন্টেশনে আছে) | message_id থেকে key বানিয়ে প্রতিটি retry-তে একই key; provider দ্বিতীয়বার টাকা কাটে না, আগের ফলই ফেরায়। Key-এর মেয়াদ provider-এর নীতিতে — dedup window-এর মতোই মিলিয়ে দেখুন |
| Key নেয় না, কিন্তু অবস্থা জানা যায় | retry-এর আগে অবস্থা জিজ্ঞেস করা (“অর্ডার ১০০১-এর চার্জ হয়েছে?”); নিজের দিকে “পাঠানো হচ্ছে” অবস্থা লিখে রাখা, তারপর পাঠানো, তারপর “পাঠানো হয়েছে” |
| কিছুই জানা যায় না (সাধারণ email, SMS) | ঠিক করুন কোনটি সহনীয়: মাঝে মাঝে দুটি, নাকি মাঝে মাঝে একটিও না; outbox দিয়ে পাঠান (পর্ব ২৫) আর সিদ্ধান্ত নকশার নথিতে লিখুন। বিস্তারিত পর্ব ৫৭-এ |
| টাকা, ledger, টিকিট | key ছাড়া gateway নয়; দিনশেষে provider-এর হিসাবের সাথে নিজের ledger মিলিয়ে দেখা (reconciliation); পুরো নকশা পর্ব ৬০-এ |
যখন duplicate হারানোর চেয়েও খারাপ
সব জায়গায় at-least-once ভালো নয়। কয়েকটি ক্ষেত্রে at-most-once-ই সৎ পছন্দ:
- শিগগিরই পুরোনো হয়ে যাওয়া তথ্য: ride-sharing অ্যাপে চালকের অবস্থান প্রতি কয়েক সেকেন্ডে আসে। একটি হারালে পরেরটি আসছে; পুরোনো অবস্থানের দেরিতে retry বরং মানচিত্রে গাড়িকে পেছনে নিয়ে যায়।
- উল্টানো যায় না এমন ভৌত কাজ: ATM-এর টাকা বের করা, দরজার তালা খোলা। দুবার টাকা বের হওয়ার চেয়ে একবারও না হওয়া, তারপর হিসাব মিলিয়ে ফেরত দেওয়া, অনেক নিরাপদ।
- বিরক্তিকর বা ক্ষতিকর বার্তা: “আপনার রাইড এসে গেছে” দেরিতে দ্বিতীয়বার আসা অর্থহীন; ভুল সময়ের একটি push notification বিশ্বাস নষ্ট করে।
- হিসাবি telemetry: click বা metric-এর নমুনায় সামান্য ক্ষতি চলে, কিন্তু duplicate-এর জন্য dedup store-এর খরচ বেশি।
At-most-once বেছে নিলেও হারানো ধরা আর মেটানোর পথ রাখুন: ATM-এর ক্ষেত্রে মুদি দোকানের খাতার মতো দিনশেষের মিলানো, অবস্থানের ক্ষেত্রে পরের আপডেট।
পরীক্ষা ও ইন্টারভিউতে:
- Exactly-once delivery কি সম্ভব? — না, প্রেরক হারানো বার্তা আর হারানো ack আলাদা করতে পারে না; সম্ভব effectively-once = at-least-once + idempotent প্রক্রিয়াকরণ।
- Offset কাজের আগে commit করলে কী পাবেন? — at-most-once: crash-এ বার্তা হারায়; পরে করলে at-least-once.
- Dedup window কীভাবে ঠিক করবেন? — সবচেয়ে পুরোনো সম্ভাব্য duplicate-এর বয়স (retry, বিভ্রাট, redrive, replay) থেকে; মেমরি = হার × window × byte.
- Kafka-তে ক্রম কোথায় নিশ্চিত? — একটি partition-এর ভেতরে; তাই একই entity-র key একই partition-এ।
- Payment gateway-তে duplicate চার্জ কীভাবে ঠেকাবেন? — provider-এর idempotency key, retry-তে একই key, আর দিনশেষে হিসাব মিলানো।
১০ · AI-এর যুগে delivery semantics যাচাই
“বার্তা যেন দুবার প্রক্রিয়া না হয়, কী করব?” — AI প্রায়ই উত্তর দেয়, “Kafka-র exactly-once চালু করুন।” কথাটি আংশিক সত্য, কিন্তু আপনার ডেটাবেস আর SMS gateway সেই নিশ্চয়তার বাইরে। চেনা ভুলগুলো:
| AI-এর ভুল | কেমন দেখায় | কীভাবে যাচাই করবেন |
|---|---|---|
| সাধারণ, ভারী উত্তর | “Kafka, Redis আর microservices যোগ করুন”, অথচ একটি টেবিলের UNIQUE constraint-ই যথেষ্ট | প্রশ্ন করুন: effect আর চিহ্ন কি এক ডেটাবেসে রাখা যায়? |
| Broker-এর নিশ্চয়তা পুরো পথে টানা | “exactly-once চালু, তাই email দুবার যাবে না” | শিকলের প্রতিটি কড়া আলাদা করে লিখুন (অংশ ৭) |
| Ack বা offset-এর ভুল জায়গা | auto-commit চালু, বা কাজের আগে ack() | কাজ আর commit-এর মাঝে process মেরে দেখুন (অংশ ৬-এর stepper) |
| Dedup চিহ্ন আলাদা store-এ | Redis-এ SETNX, তারপর আলাদা DB লেখা | দুই লেখার মাঝে crash ভাবুন: effect হারায়, না দুবার? |
| হিসাবের ভুল | dedup মেমরি দিনে-সেকেন্ডে গুলিয়ে, replica বাদ | নিজে হিসাব: হার × window × byte × replica |
| পণ্যের সীমা বানানো | dedup interval, ডিফল্ট সেটিং বা “সব queue ক্রম রাখে” | অফিশিয়াল ডকুমেন্টেশন আর সংস্করণ মিলিয়ে দেখুন |
| ক্রম উপেক্ষা | absolute মানের retry পুরোনো মান ফিরিয়ে আনে | প্রতিটি entity-তে version check আছে কি না |
| অতি-নকশা | GPS অবস্থানের জন্য exactly-once পাইপলাইন | জিজ্ঞেস করুন: হারালে বা দুবার এলে আসল ক্ষতি কী? |
AI-কে নকশাকার নয়, কড়া reviewer বানান। আগে নিজে বার্তার পথ আর প্রতিটি effect লিখুন, তারপর এভাবে যাচাই করান:
AI-এর উত্তর মানার আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১২০১৭ সালে Kafka “exactly-once semantics” ঘোষণা করলে অনেকে আপত্তি তুলেছিলেন। আসলে কী পাওয়া গেল, আর কী নয়?
আপত্তির কারণ ছিল নামটি। বহু বছর ধরে distributed systems-এর লেখায় বলা হয়েছে “exactly-once delivery অসম্ভব”, আর তার পক্ষে অংশ ৩-এর মতো যুক্তি সবার জানা। তাই ঘোষণা দেখে অনেকে ভেবেছিলেন, হয় কোনো গণিত ভাঙা হয়েছে, নয়তো বিজ্ঞাপনের ভাষা। Kafka-র দলের প্রকাশিত ব্যাখ্যা ছিল সূক্ষ্ম: তাঁরা delivery নয়, processing-এর কথা বলছেন, আর তা একটি নির্দিষ্ট সীমার ভেতরে।
যা পাওয়া গেল: producer থেকে broker-এর পথে retry-র duplicate PID আর sequence number দিয়ে বাদ যায়। Kafka থেকে পড়ে, প্রক্রিয়া করে, Kafka-তে লেখার একটি চক্রে output-এর বার্তা আর input-এর offset একটি transaction-এ commit হয়। read_committed গ্রাহক abort হওয়া লেখা দেখে না। আর পুরোনো instance-এর zombie লেখা epoch দিয়ে আটকায়। এর ফলে Kafka Streams-এর মতো কাঠামোয় state আর output দুবার গোনা হয় না।
যা পাওয়া গেল না: আপনার consumer যদি বাইরের ডেটাবেসে লেখে, REST API ডাকে বা email পাঠায়, সেই কাজ Kafka-র transaction-এর অংশ নয়। Processing-এর মাঝে crash হলে transaction abort হয়, আবার চলে — কিন্তু ততক্ষণে বাইরের email হয়তো চলে গেছে। কোডের ভেতরের side effect, যেমন log লেখা বা metric বাড়ানো, দুবার ঘটতে পারে। Transaction-এর দামও আছে: commit marker, coordinator-এর সাথে বাড়তি round trip আর read_committed গ্রাহকের কিছুটা দেরি।
বিতর্কটি শেষে একটি উপকার করেছে: শিল্পে এখন “exactly-once” শুনলে অনেকে জিজ্ঞেস করেন, “কোন পথের, কোন সীমার?” এটাই সঠিক প্রশ্ন। অংশ ৭-এর শিকলের যেকোনো কড়ায় key বা inbox না থাকলে পুরো পথ at-least-once থেকে যায়।
মূল উপলব্ধি: “exactly-once” কোনো সুইচ নয়, একটি দাবি — আর প্রতিটি দাবির সাথে তার সীমা লেখা থাকতে হয়। সীমার বাইরে প্রতিটি effect-এর দায়িত্ব আপনার।
প্র ০২TCP নিজেই sequence number দিয়ে duplicate packet বাদ দেয়। তাহলে TCP-র ওপর চলা অ্যাপকে আবার duplicate নিয়ে ভাবতে হয় কেন?
কারণ TCP-র নিশ্চয়তা একটি connection-এর ভেতরে, আর আপনার কাজের পরিধি তার চেয়ে অনেক বড়। TCP প্রতিটি byte-এ sequence number বসায়, হারানো segment আবার পাঠায়, duplicate segment ফেলে দেয় আর ক্রম ঠিক রাখে (বিস্তারিত Computer Networks-এর TCP reliability পর্বে)। কিন্তু এই কাজ চলে দুই প্রান্তের kernel-এর মধ্যে, আর তাদের স্মৃতি connection-এর সাথেই মরে।
তিনটি পরিস্থিতি ভাবুন। এক, wallet সার্ভিসের kernel বার্তা পেয়ে TCP ack দিল, কিন্তু অ্যাপ প্রক্রিয়া করার আগেই crash করল — TCP-র চোখে সব ঠিক, অথচ কাজ হয়নি। দুই, অ্যাপ কাজ করল, উত্তর লেখার আগে connection কেটে গেল; client নতুন connection খুলে আবার পাঠাল — নতুন connection, নতুন sequence number, TCP একে duplicate বলে চেনেই না। তিন, মাঝে একটি load balancer বা broker আছে; প্রতিটি hop আলাদা TCP connection, কারও কাছে পুরো পথের হিসাব নেই।
এখানে TCP-র নিজের নকশাতেও একটি শিক্ষা আছে। পুরোনো connection-এর দেরিতে পৌঁছানো packet যাতে নতুন connection-এ ঢুকে না পড়ে, সেজন্য TCP বন্ধ connection-কে কিছুক্ষণ TIME_WAIT অবস্থায় রাখে — segment-এর সর্বোচ্চ আয়ুর দ্বিগুণ। এটি আসলে একটি dedup window: “এই সময়ের পরে পুরোনো duplicate আর আসবে না” — এই অনুমানের ওপর দাঁড়ানো। দ্রুত network-এ sequence number ঘুরে আসার সমস্যায় পরে timestamp-ভিত্তিক সুরক্ষাও (PAWS) যোগ হয়েছে। অর্থাৎ TCP-ও window আর অনুমান দিয়েই কাজ চালায়, ঠিক অংশ ৫-এর মতো।
তাই নির্ভরযোগ্যতার শেষ যাচাই হয় অ্যাপের দুই প্রান্তে — অভিপ্রায়ের জন্মস্থানে একটি key, আর effect-এর পাশে একটি চিহ্ন। নিচের স্তর এই কাজ সস্তা করে, কিন্তু প্রতিস্থাপন করে না।
মূল উপলব্ধি: প্রতিটি স্তরের নিশ্চয়তা তার নিজের পরিধি পর্যন্ত। আপনার effect যে পরিধিতে, duplicate ঠেকানোর দায়িত্বও সেই পরিধিতে।
প্র ০৩Dedup store-এর মেমরি বাঁচাতে Bloom filter ব্যবহার করা কি বুদ্ধিমানের কাজ?
প্রলোভনটি বোঝা যায়। Bloom filter প্রতি key-তে মাত্র কয়েকটি bit খরচ করে: ১% false positive-এর জন্য মোটামুটি ৯.৬ bit, অর্থাৎ ১০০ byte-এর entry-র তুলনায় প্রায় ৮০ গুণ কম জায়গা। অংশ ৫-এর ৭ দিনের window-এ ২৪১ GB থেকে নেমে আসে কয়েক GB-তে। তাহলে সমস্যা কোথায়?
সমস্যা হলো ভুলের দিক। Bloom filter কখনো false negative দেয় না — যা ঢুকেছে, সেটি সবসময় “আছে”. কিন্তু false positive দেয়: কখনো একটি নতুন key-কেও বলে “আছে”. Dedup-এর ভাষায় এর মানে, একটি সত্যিকারের নতুন বার্তাকে duplicate ভেবে ফেলে দেওয়া। দিনে ১৭ কোটি বার্তায় ১% false positive মানে দিনে ১৭ লাখ বার্তা নিঃশব্দে হারানো। আপনি at-least-once-কে exactly-once বানাতে গিয়ে চুপিচুপি at-most-once বানিয়ে ফেললেন — আর হারানোটা duplicate-এর চেয়েও খুঁজে পাওয়া কঠিন।
তাহলে কীভাবে ব্যবহার হয়? দ্রুত প্রাথমিক ছাঁকনি হিসেবে। Filter “নেই” বললে সেটি নিশ্চিত, তাই সরাসরি প্রক্রিয়া করুন — এটাই বেশিরভাগ ক্ষেত্রে, কারণ বেশিরভাগ বার্তা নতুন। “আছে” বললে আসল store-এ (ডেটাবেস বা disk) যাচাই করুন। এতে বেশিরভাগ খোঁজা disk-এ যায় না, অথচ সিদ্ধান্ত নির্ভুল থাকে। MillWheel-এর প্রকাশিত নকশায় এই ধাঁচের কৌশলের বর্ণনা আছে। আরেকটি সতর্কতা: সাধারণ Bloom filter থেকে কিছু মোছা যায় না, তাই TTL চালাতে সময়ের ভাগে কয়েকটি filter ঘোরাতে হয়।
আর সবচেয়ে ভালো সমাধান প্রায়ই অন্য দিকে: প্রেরক যদি প্রতিটি partition-এ ফাঁকহীন sequence number দেয়, তাহলে key-এর তালিকাই লাগে না — শুধু শেষ নম্বর। Bloom filter-এর গণিত আর error rate-এর সূত্র পর্ব ৪৫-এ।
মূল উপলব্ধি: আনুমানিক কাঠামো ব্যবহারের আগে জিজ্ঞেস করুন, ভুলটি কোন দিকে হবে, আর সেই দিকের ক্ষতি কে বহন করবে। Dedup-এ false positive মানে ক্ষতি।
প্র ০৪সব বার্তার একটি বিশ্বব্যাপী ক্রম পেলে তো সব সমস্যা মিটে যেত। বড় সিস্টেম কেন per-key ordering-এই সন্তুষ্ট থাকে?
কারণ total order-এর দাম throughput আর availability দিয়ে মেটাতে হয়। একটি ক্রম মানে কোথাও একজন সিদ্ধান্ত নেয় “এটি আগে, ওটি পরে”. একটি partition বা একটি leader সেই কাজ করলে তার গতিই পুরো সিস্টেমের সীমা, আর সে বিকল হলে নতুন leader বাছাই না হওয়া পর্যন্ত সব থামে। বহু যন্ত্র মিলে একটি ক্রমে একমত হতে হলে লাগে consensus — Paxos বা Raft-এর মতো প্রোটোকল (পর্ব ৪২), প্রতিটি সিদ্ধান্তে বেশিরভাগ replica-র সাথে round trip.
অথচ বেশিরভাগ ব্যবসায়িক নিয়ম আসলে একটি entity-র ভেতরে। রিনার wallet-এর credit আর debit ক্রমে আসা জরুরি; রিনা আর রফিকের wallet-এর মধ্যে কোনটি আগে, তাতে কারও কিছু আসে-যায় না। তাই wallet_id-কে partition key করলে প্রতিটি wallet-এর ক্রম রক্ষা পায়, আর হাজার wallet সমান্তরালে হাজার partition-এ চলে। ঢাকার যানজটে প্রতিটি লেনে গাড়ি ক্রমে চলে; সব রাস্তার সব গাড়িকে একটি সারিতে দাঁড় করালে শহর অচল হতো।
এর দামও আছে। এক, একটি key খুব ব্যস্ত হলে তার partition hot হয়ে যায় — পর্ব ২৩-এর hot spot. দুই, partition-এর সংখ্যা বদলালে key-এর ঠিকানা বদলায়, আর সেই মুহূর্তে একই key-এর পুরোনো আর নতুন বার্তা দুই partition-এ থাকতে পারে — ক্রম ক্ষণিকের জন্য ভাঙে। তিন, যে নিয়ম দুটি entity জুড়ে (যেমন রিনা থেকে রফিকে টাকা), সেখানে per-key ordering যথেষ্ট নয়; তখন একটি entity-কে মালিক করে তার দিক থেকে ধাপে ধাপে চালাতে হয়, saga-র মতো (পর্ব ২৫)।
গ্রাহকের দিকেও ক্রম রক্ষা করতে হয়: partition-এর বার্তা কয়েকটি thread-এ ছড়ালে, key ধরে thread বাছতে হয়, নইলে broker-এর ক্রম গ্রাহকের ভেতরে হারায়।
মূল উপলব্ধি: যতটুকু ক্রম ব্যবসার নিয়ম সত্যিই চায়, ঠিক ততটুকু কিনুন। Per-key ordering সমান্তরালতা রেখে বেশিরভাগ নিয়ম রক্ষা করে।
প্র ০৫ATM বা কার্ড পেমেন্টের মতো ব্যবস্থা কেন অনেক সময় at-most-once বেছে নিয়ে পরে হিসাব মেলায়? এটি কি দুর্বল নকশা?
না, এটি দুই ধরনের ক্ষতির সৎ তুলনা। ATM টাকা বের করতে গিয়ে ব্যাংকের সার্ভার থেকে উত্তর পেল না। দুটি পথ: আবার চেষ্টা করে টাকা বের করা (at-least-once), অথবা থেমে যাওয়া, অর্থাৎ at-most-once. প্রথম পথে গ্রাহক হয়তো দুবার টাকা পান, আর হাতে চলে যাওয়া নগদ ফেরত আনার কোনো idempotency key নেই। দ্বিতীয় পথে গ্রাহক এখনই টাকা পান না, কিন্তু কোনো স্থায়ী ক্ষতি নেই — পরে হিসাব মিলিয়ে ঠিক করা যায়।
এখানে মূল কৌশলটি হলো “হারানো” ঘটনাকে খুঁজে পাওয়ার উপায় রাখা। বাস্তবের লেনদেন ব্যবস্থায় প্রায়ই অনিশ্চিত লেনদেনের জন্য একটি উল্টো বার্তা (reversal) পাঠানো হয়, আর দিনশেষে দুই পক্ষের খাতা মেলানো হয় — reconciliation. পাড়ার মুদি দোকানের খাতার কথা ভাবুন: মাসের শেষে দোকানি আর ক্রেতা খাতা মিলিয়ে নেন, কোনো লেনদেন বাদ পড়লে যোগ করেন। এখানে at-most-once-এর ক্ষতিকে নিরাপদে ধরে রাখে এই মিলানো, যা বার্তার পথের বাইরে আলাদা একটি পরীক্ষা।
একই চিন্তা সফটওয়্যারে: payment provider-এর সাথে চার্জের অনিশ্চয়তা থাকলে অন্ধ retry নয়, অবস্থা জিজ্ঞেস করা; উত্তর না মিললে লেনদেনকে “অনিশ্চিত” চিহ্ন দিয়ে রাখা আর মিলানোর কাজে পাঠানো। ব্যবহারকারীকে সৎ বার্তা: “লেনদেনটি যাচাই হচ্ছে, টাকা কাটা হলে ফেরত বা নিশ্চিত হবে।” এই নকশা ধীর মনে হতে পারে, কিন্তু ভুল দিকে যায় না।
অবশ্য এটি সব জায়গায় খাটে না। যেখানে হারানো বার্তা খুঁজে পাওয়ার উপায় নেই, যেমন একটি সাধারণ event-এর পাইপলাইনে, সেখানে at-most-once মানে স্থায়ী ক্ষতি। তাই সিদ্ধান্তটি সবসময় তিন প্রশ্নে: দুবার হলে ক্ষতি কত? না হলে ক্ষতি কত? আর পরে ধরে মেটানোর পথ আছে কি না?
মূল উপলব্ধি: delivery semantics বার্তার পথের একার সিদ্ধান্ত নয়; reconciliation-এর মতো পাশের ব্যবস্থাও তার অংশ। ঠিক কোন ক্ষতি সহনীয়, সেটি ব্যবসা ঠিক করে, প্রযুক্তি নয়।
অনুশীলন
খাতা-কলম নিয়ে বসুন — হিসাবগুলো নিজে করে তারপর উত্তর মেলান।
-
হিসাব — duplicate আর dedup মেমরি: একটি event pipeline-এ সেকেন্ডে ৫,০০০টি বার্তা। Ack হারানোর সম্ভাবনা ০.০২, retry-এর সীমা নেই। (ক) সেকেন্ডে গড়ে কয়টি duplicate আসবে? (খ) ৪৮ ঘণ্টার dedup window, প্রতি entry ১২০ B আর ৩টি replica হলে মেমরি কত? (গ) window ২ ঘণ্টা করলে?
(ক) প্রতি বার্তায় গড় duplicate = pa/(১ − pa) = ০.০২/০.৯৮ ≈ ০.০২০৪. সেকেন্ডে ৫০০০ × ০.০২০৪ ≈ ১০২টি duplicate — দিনে প্রায় ৮৮ লাখ।
(খ) ৪৮ ঘণ্টা = ১,৭২,৮০০ সেকেন্ড। Key = ৫০০০ × ১,৭২,৮০০ = ৮৬,৪০,০০,০০০ (৮৬.৪ কোটি)। মেমরি = ৮৬.৪ কোটি × ১২০ B × ৩ = ৩১১.০৪ GB.
(গ) ২ ঘণ্টা = ৭২০০ সেকেন্ড; key = ৩.৬ কোটি; মেমরি = ৩.৬ কোটি × ১২০ × ৩ = ১২.৯৬ GB, ২৪ গুণ কম। কিন্তু তখন ২ ঘণ্টার বেশি পুরোনো duplicate (বিভ্রাটের পরে, replay) ধরা পড়বে না — তাদের জন্য business key-এর version check বা per-producer sequence রাখতে হবে।
-
Critique — এই consumer-এ কী ভুল? একজন সহকর্মী লিখেছেন:
for msg in poll(): ack(msg); db.execute("UPDATE wallets SET balance = balance + ? WHERE id = ?"); sms.send(msg.phone). কী কী ব্যর্থতা সম্ভব, আর কীভাবে ঠিক করবেন?- কাজের আগে ack: ack-এর পরে, UPDATE-এর আগে crash হলে broker বার্তাটি আর দেবে না — টাকা হারাল, অর্থাৎ at-most-once. Ack যাবে effect commit-এর পরে।
- Dedup নেই: ack ঠিক জায়গায় আনলে redelivery আসবে, আর
balance + ?idempotent নয় — দুবার টাকা। Inbox টেবিলে message_id (UNIQUE), একই transaction-এ। - SMS transaction-এর বাইরে: SMS-এর পরে crash হলে retry-তে আরেকটি SMS. SMS-কে outbox-এ একটি সারি হিসেবে একই transaction-এ লিখুন, আলাদা relay পাঠাবে; gateway key নিলে message_id দিন, না নিলে মাঝে মাঝে duplicate SMS মেনে নেওয়ার সিদ্ধান্ত লিখে রাখুন।
- ঠিক করা ক্রম: poll → BEGIN → inbox INSERT (০টি সারি হলে effect বাদ) → balance UPDATE → outbox INSERT → COMMIT → ack / offset commit.
-
Output অনুমান করুন: Wallet-এর শুরুর balance ১০০০. Handler inbox ব্যবহার করে (অংশ ৬-এর মতো)। বার্তা এই ক্রমে আসে: m1 (+১০০), m2 (+৫০), m1 আবার, m3 (−৩০); m3-এর DB commit-এর পরে, offset commit-এর আগে crash; restart-এর পর m3 আবার, তারপর m2 আবার। শেষে balance আর inbox-এর সারি কত? Inbox না থাকলে balance কত হতো?
Inbox সহ: m1 → ১১০০; m2 → ১১৫০; m1 আবার → বাদ; m3 → ১১২০ (commit হয়ে গেছে, crash তার পরে); m3 আবার → বাদ; m2 আবার → বাদ। Balance ১১২০, inbox-এ ৩টি সারি: m1, m2 আর m3.
Inbox ছাড়া: প্রতিটি কপি প্রয়োগ — ১০০০ + ১০০ + ৫০ + ১০০ − ৩০ − ৩০ + ৫০ = ১২৪০. ঠিক ১২০ টাকা বেশি, অর্থাৎ m1, m2 আর m3 প্রত্যেকে একবার করে বাড়তি।
-
নকশা — কোন নিশ্চয়তা কোথায়? নিচের প্রতিটির জন্য at-most-once, at-least-once না effectively-once বেছে নিন, এক লাইনে কারণসহ: (ক) চালকের GPS অবস্থান প্রতি ৪ সেকেন্ডে (খ) wallet-এ ফেরতের টাকা (গ) লগইনের OTP SMS (ঘ) বিজ্ঞাপনের click গণনা, যা দিয়ে বিল হয় (ঙ) ATM থেকে নগদ বের করা।
- (ক) At-most-once: পরেরটি ৪ সেকেন্ডেই আসছে; পুরোনো অবস্থানের retry ক্ষতিকর। Timestamp দেখে পুরোনো বাদ।
- (খ) Effectively-once: হারানো বা দুবার — দুটোই টাকার ক্ষতি; inbox আর একই transaction.
- (গ) At-least-once: একটি OTP দুবার এলে ক্ষতি সামান্য, না এলে ব্যবহারকারী আটকে যান; তবে অল্প কয়েকবারেই retry থামান।
- (ঘ) Effectively-once: বিলের ভিত্তি, তাই click id দিয়ে dedup; শুধু দর্শকসংখ্যার আনুমানিক ড্যাশবোর্ড হলে at-least-once চলত।
- (ঙ) At-most-once + reconciliation: দুবার নগদ বের হওয়া ফেরানো যায় না; অনিশ্চয়তায় থেমে, পরে খাতা মিলিয়ে ঠিক করা।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমInbox-সহ একটি consumer-এর এক বার্তার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📥 Broker থেকে বার্তা poll করা
- 🔒 ডেটাবেসে BEGIN
- 🗂️ Inbox-এ message_id INSERT — ০টি সারি হলে effect বাদ
- 💰 Effect লেখা: balance আর outbox-এর সারি
- ✅ COMMIT
- 📨 Broker-এ ack বা offset commit
একটি consumer বার্তা পড়েই offset commit করে, তারপর ডেটাবেসে লেখে। লেখার আগে crash হলে কী হবে?
- বার্তাটি আবার আসবে আর দুবার প্রক্রিয়া হবে
- বার্তাটি হারাবে — এটি at-most-once আচরণ
- Broker ক্ষতি টের পেয়ে নিজেই আবার পাঠাবে
- কিছু হবে না — এটি exactly-once
নিচের কোন কাজটি স্বভাবেই idempotent, অর্থাৎ দুবার চললেও ফল একই?
- UPDATE wallets SET balance = balance + 500 WHERE id = 7
- অর্ডারের ইতিহাসের তালিকায় “PAID” লেখা একটি নতুন সারি যোগ করা
- UPDATE orders SET status = 'PAID' WHERE id = 1001
- গ্রাহককে “পেমেন্ট সফল” SMS পাঠানো
সেকেন্ডে ১,০০০টি বার্তা, dedup window ১ দিন, প্রতি entry ১০০ byte, কোনো replica নেই। Dedup store-এর মেমরি মোটামুটি কত?
- ৮৬৪ MB
- ৮.৬৪ GB
- ৮৬.৪ GB
- ১০০ MB
Kafka-ধাঁচের idempotent producer-এ broker কীভাবে retry-এর duplicate চেনে?
- Producer ID আর প্রতিটি partition-এ batch-এর sequence number দেখে
- প্রতিটি বার্তার payload-এর hash মিলিয়ে
- বার্তার offset মিলিয়ে
- একই timestamp-এর বার্তা বাদ দিয়ে
একটি log-ভিত্তিক broker-এ wallet_id-কে message key করা হয়েছে। কোন ক্রমের নিশ্চয়তা পাওয়া যায়?
- সব wallet-এর সব বার্তা একটি বিশ্বব্যাপী ক্রমে
- প্রতিটি বার্তা তার timestamp অনুযায়ী
- কোনো ক্রমের নিশ্চয়তা নেই
- একই wallet-এর বার্তাগুলো লেখার ক্রমে, ভিন্ন wallet-এর মধ্যে নয়
“Effectively-once” প্রক্রিয়াকরণের জন্য কোন সমন্বয়টি দরকার?
- At-most-once delivery আর দ্রুত network
- At-least-once delivery আর একটি আলাদা Redis-এ চিহ্ন, effect-এর পরে লেখা
- At-least-once delivery আর idempotent প্রক্রিয়াকরণ, যেখানে effect আর dedup চিহ্ন একই transaction-এ
- Broker-এর “exactly-once” সেটিং চালু করা — আর কিছু লাগে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Timeout মানে “জানি না”: হারানো বার্তা আর হারানো ack প্রেরকের চোখে এক; তাই হয় retry (duplicate), নয় retry নেই (ক্ষতি)।
- At-most-once ০ বা ১, at-least-once ১ বা বেশি, exactly-once ঠিক ১; ack আর offset commit effect-এর আগে না পরে — সেটাই ঠিক করে। গড় duplicate = pa/(১ − pa).
- Effectively-once = at-least-once + idempotent প্রক্রিয়াকরণ, যেখানে effect আর চিহ্ন (inbox বা offset) একই transaction-এ; duplicate-কেও ack.
- Dedup-এর দাম: মেমরি ≈ হার × window × byte × replica; window সবচেয়ে পুরোনো সম্ভাব্য duplicate-এর চেয়ে বড়; per-producer sequence আর version check সস্তা বিকল্প।
- শিকল আর ক্রম: Kafka-র idempotent producer আর transaction broker-এর সীমায়; ডেটাবেস আর বাইরের provider-এ নিজের key; ক্রম শুধু একই key বা partition-এ; আর যেখানে duplicate বেশি খারাপ, সেখানে at-most-once + reconciliation.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৫ · Event sourcing আর CQRSপরবর্তী পর্বEvent থেকে state, replay আর আলাদা read model.
- পর্ব ৩৩ · Log-ভিত্তিক streamingফিরে দেখুনPartition, offset, consumer group আর retention.
- পর্ব ২৫ · Distributed transactionফিরে দেখুনOutbox, relay আর inbox — duplicate কোথা থেকে আসে।
- পর্ব ০৮ · REST API নকশা — idempotency keyফিরে দেখুনHTTP-র পথে একটি POST-এর retry নিরাপদ করা।
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Computer Networks · TCP reliabilityগভীরে যানSequence number, ack আর retransmission — transport স্তরে একই সমস্যা।
- DBMS ও SQL · Transaction আর ACIDপাশের ট্র্যাকEffect আর চিহ্ন একসাথে commit-এর ভিত্তি — atomicity.