Engineering Roadmap: Why 'Exactly Once' Delivery Is Mostly a Lie

On this page
শুরুতেই একটি বাস্তব production incident কল্পনা করুন। ধরুন আপনি একটি ই-কমার্স প্ল্যাটফর্মের Payment API তৈরি করেছেন। ইউজার যখন চেকআউট বাটনে ক্লিক করে, আপনার Payment Service সেই রিকোয়েস্টটি প্রসেস করে Stripe বা PayPal-এর মতো পেমেন্ট গেটওয়েতে পাঠায়। সিস্টেম ডিজাইন রিভিউয়ের সময় কোড ও ফ্লো একদম নিখুঁত মনে হচ্ছিল।
একদিন রাত ৩টায় আপনার PagerDuty-তে অ্যালার্ট বাজল। কাস্টমার সাপোর্ট থেকে জানানো হলো, শত শত ইউজারের ব্যাংক কার্ড থেকে একই অর্ডারের জন্য দুইবার করে টাকা কেটে নেওয়া হয়েছে।
আপনি প্রোডাকশন লগ ওপেন করে ইনভেস্টিগেট করে দেখলেন পর্দার আড়ালে ঠিক কী ঘটেছিল:
- আপনার Payment Service পেমেন্ট গেটওয়েতে HTTP POST রিকোয়েস্ট পাঠাল:
Charge $100। - পেমেন্ট গেটওয়ে ইউজারের কার্ড থেকে
$100চার্জ করে সফলভাবে ট্রানজেকশন সম্পন্ন করল। - গেটওয়ে থেকে আপনার সার্ভারে
200 OKরেসপন্স প্যাকেট ফেরত আসার ঠিক এক মিলি-সেকেন্ড আগে মাঝপথের নেটওয়ার্ক কানেকশন ড্রপ করল এবং আপনার সার্ভার পেল একটিTCP Socket Timeout। - আপনার Payment Service-এর দৃষ্টিকোণ থেকে সে কোনোভাবেই জানে না টাকা আসলে কেটেছে নাকি রিকোয়েস্টটি গেটওয়ে পর্যন্ত পৌঁছায়ইনি।
- আপনার অ্যাপ্লিকেশনকে ফল্ট-টল্যারেন্ট বানানোর জন্য একটি সাধারণ
Retry Mechanismযুক্ত করা ছিল। নেটওয়ার্ক টাইমআউট দেখে সার্ভিসটি সাথে সাথে আরেকটি Retry রিকোয়েস্ট পাঠাল। - পেমেন্ট গেটওয়ে দ্বিতীয় রিকোয়েস্টটি পেয়ে ইউজারের কার্ড থেকে আবার
$100চার্জ করে নিল।
একজন ইউজারের একাউন্ট থেকে $100-এর বদলে $200 কেটে নেওয়া হলো। এটাই হলো Distributed System-এ Exactly-Once Delivery-র আসল সংকট।
ডিস্ট্রিবিউটেড আর্কিটেকচারে "Exactly-Once" গ্যারান্টি শুনতে খুব আকর্ষণীয় ও সরল মনে হলেও, বাস্তব নেটওয়ার্ক ফিজিক্সে এটি অর্জন করা গাণিতিকভাবে অসম্ভব। মনোলিথ থেকে মাইক্রোসার্ভিসে শিফট করার পর ইঞ্জিনিয়াররা সবচেয়ে বড় ভুল করেন নেটওয়ার্কের রিলায়েবিলিটিকে শতভাগ নিশ্চিত ধরে নিয়ে।
আজকের এই ডিপ ডাইভে আমরা ডিস্ট্রিবিউটেড সিস্টেমের লো-লেভেল মেকানিক্সে ঢুকব। আমরা দেখব কেন নেটওয়ার্ক লেয়ারে Exactly-Once নিশ্চিত করা অসম্ভব, এবং কীভাবে সিনিয়র ইঞ্জিনিয়াররা ডাটাবেজ ও অ্যাপ্লিকেশন লেয়ারে Idempotency, Transactional Outbox, এবং Inbox Pattern ব্যবহার করে প্রোডাকশনে Effectively-Once আর্কিটেকচার তৈরি করেন।
Network Uncertainty এবং The Lost ACK Problem
ডিস্ট্রিবিউটেড সিস্টেমের মৌলিক অনিশ্চয়তা বুঝতে হলে প্রথমে নেটওয়ার্ক লেয়ারের লিমিটেশন বুঝতে হবে। যখন দুটি স্বাধীন সার্ভার বা নোড নেটওয়ার্কের মাধ্যমে যোগাযোগ করে, তখন মাঝখানের নেটওয়ার্ক ক্যাবল, সুইচ ও রাউটার হলো পুরো সিস্টেমের সবচেয়ে আনরিলায়েবল কম্পোনেন্ট।
কম্পিউটার সায়েন্সের একটি ক্লাসিক থিওরেম হলো Two Generals' Problem। এটি গাণিতিকভাবে প্রমাণ করে যে, একটি অনির্ভরযোগ্য কমিউনিকেশন চ্যানেলের ওপর দাঁড়িয়ে দুটি আলাদা নোড কখনোই কোনো বিষয়ে ১০০% নিশ্চয়তায় একমত হতে পারে না।
Sender যখন কোনো Message বা Request পাঠায়, তখন তার দৃষ্টিকোণ থেকে তিনটি সম্ভাবনার যেকোনো একটি ঘটতে পারে:
- Request Lost: রিকোয়েস্ট মাঝপথে হারিয়ে গেছে (প্যাকেট লস বা রাউটার ড্রপ)। সার্ভার কিছুই পায়নি।
- Server Crashed Pre-Execution: সার্ভার রিকোয়েস্ট পেয়েছে কিন্তু প্রসেস করার আগেই মেমোরি ক্র্যাশ বা পাওয়ার অফ হয়ে গেছে।
- Execution Succeeded but ACK Lost: সার্ভার রিকোয়েস্ট পেয়েছে, ডাটাবেজে সেভ করেছে, কিন্তু তার পাঠানো কনফার্মেশন বা ACK (Acknowledgement) প্যাকেট মাঝপথে নেটওয়ার্ক ড্রপের কারণে সেন্ডারের কাছে পৌঁছায়নি।
সবচেয়ে বড় সমস্যা হলো, Sender যখন একটি Timeout Error পায়, সে কোনোভাবেই জানতে পারে না ১, ২, এবং ৩ নম্বর ঘটনার মধ্যে ঠিক কোনটি ঘটেছে।
সেন্ডার যদি ধরে নেয় "কাজ হয়নি" এবং আবার রিকোয়েস্ট পাঠায়, তবে ৩ নম্বর ক্ষেত্রে ডুপ্লিকেট অপারেশন ঘটে যায়। আবার সেন্ডার যদি ধরে নেয় "কাজ হয়ে গেছে", তবে ১ ও ২ নম্বর ক্ষেত্রে চিরতরে ডাটা লস হয়।
ডাকঘরের চিঠির অ্যানালজি
আপনি যদি ডাকযোগে কাউকে একটি গুরুত্বপূর্ণ চিঠি পাঠান এবং কোনো ফিরতি উত্তর না পান, তবে আপনি নিশ্চিত হতে পারবেন না যে চিঠিটি হারিয়ে গেছে, নাকি প্রাপক চিঠিটি ঠিকই পেয়েছেন কিন্তু রিপ্লাই দিতে ভুলে গেছেন। ডিস্ট্রিবিউটেড সিস্টেমের ACK ড্রপ ঠিক এইভাবেই কাজ করে। সেন্ডার বাধ্য হয়ে দ্বিতীয় চিঠি পাঠায়।
Three Delivery Semantics: At-Most-Once, At-Least-Once, Exactly-Once
নেটওয়ার্কের এই অনিশ্চয়তা সামাল দিতে ইন্ডাস্ট্রিতে মেসেজ ডেলিভারিকে তিনটি ক্যাটাগরিতে ভাগ করা হয়:
১. At-Most-Once (০ বা ১ বার ডেলিভারি)
এই মডেলে কোনো ফেইলর হলে কোনোভাবেই Retry করা হয় না (Fire and Forget)।
- সুবিধা: সিস্টেমে কখনোই ডুপ্লিকেট ডাটা তৈরি হবে না।
- ঝুঁকি: নেটওয়ার্কে যেকোনো গ্লিচ হলে মেসেজ চিরতরে হারিয়ে যাবে (High Data Loss)।
- ব্যবহার: লগিং, রিয়েল-টাইম মেট্রিক্স বা IoT সেন্সর ডেটা, যেখানে প্রতি সেকেন্ডে লাখ লাখ ডাটা আসে এবং দু-একটা ডাটা হারালে ব্যবসার কোনো ক্ষতি নেই।
২. At-Least-Once (১ বা একাধিক বার ডেলিভারি)
এই মডেলে মেসেজ পাঠানোর পর যদি নির্দিষ্ট সময়ে ACK না আসে, তবে সেন্ডার বারবার Retry করতে থাকে যতক্ষণ না কনফার্মেশন পায়।
- সুবিধা: মেসেজ হারানোর সম্ভাবনা শূন্য (Zero Data Loss)।
- ঝুঁকি: নেটওয়ার্কে ACK ড্রপ হলে কনজিউমার একই মেসেজ একাধিকবার রিসিভ করবে (Guaranteed Duplicates)।
- ব্যবহার: ফিন্যান্সিয়াল লেজার, অর্ডার প্রসেসিং এবং এন্টারপ্রাইজ মেসেজ কিউ (RabbitMQ, SQS, Kafka default)।
৩. Exactly-Once (ঠিক ১ বার ডেলিভারি)
এটি হলো ইঞ্জিনিয়ারদের বহু আকাঙ্খিত স্বপ্ন, যেখানে মেসেজ ঠিক একবারই ট্রাভেল করবে এবং একবারই প্রসেস হবে। কিন্তু নেটওয়ার্ক ফিজিক্সের বাস্তবতায় পিওর নেটওয়ার্ক লেভেলে এটি অসম্ভব। তবে অ্যাপ্লিকেশন ও ডাটাবেজ লেয়ারের সমন্বয়ে আমরা এটিকে Effectively-Once আকারে সিমুলেট করতে পারি।
The Core Misconception: Delivery বনাম Processing
সফটওয়্যার ইঞ্জিনিয়ারদের একটি বড় ভুল ধারণা হলো তারা মনে করেন "Message Delivery" এবং "Message Processing" একই বিষয়।
- Delivery (Network Layer): মেসেজটি ব্রোকার বা সেন্ডার থেকে রিসিভারের নেটওয়ার্ক সকেটে পৌঁছানো।
- Processing (Application Layer): মেসেজের ভেতরের পেলোড পড়ে বিজনেস কোড এক্সিকিউট করা।
- Side Effect (Storage / Third-party Layer): ডাটাবেজে ব্যালেন্স আপডেট করা, ফাইল রাইট করা, কিংবা Stripe বা Twilio-তে API কল পাঠানো।
নেটওয়ার্ক লেভেলে ডুপ্লিকেট মেসেজ ডেলিভারি হওয়া আপনি কখনোই শতভাগ আটকাতে পারবেন না। কিন্তু আপনার অ্যাপ্লিকেশন লজিক যদি এমন হয় যে একই মেসেজ দশবার আসলেও বিজনেস সাইড-ইফেক্ট ঠিক একবারই ঘটবে, তবে ইউজারের দৃষ্টিকোণ থেকে পুরো সিস্টেমটি Effectively-Once হিসেবে কাজ করবে।
আসল শত্রু Duplicate Message নয়, Duplicate Side Effect
একটি ডিস্ট্রিবিউটেড সিস্টেমে ডুপ্লিকেট মেসেজ আসা কোনো সমস্যা নয়। আসল সমস্যা তৈরি হয় যখন ডুপ্লিকেট মেসেজের কারণে সিস্টেমে Duplicate Side Effect ঘটে।
যদি একটি পেমেন্ট মেসেজ দুইবার প্রসেস হওয়ার কারণে:
- ইউজারের ক্রেডিট কার্ডে দুইবার চার্জ হয় (
Charge Card x2), - ইনভেন্টরি থেকে দুইবার স্টক মাইনাস হয় (
Reduce Stock x2), - ডাটাবেজে দুটি আলাদা অর্ডার তৈরি হয় (
Create Order x2), - ইউজারের কাছে দুটি কনফার্মেশন ইমেইল চলে যায়,
তখন পুরো সিস্টেমের স্টেট করাপ্ট হয়ে যায়। আমাদের আর্কিটেকচারাল লক্ষ্য হলো ডুপ্লিকেট মেসেজ আসবেই, কিন্তু প্রতিটি সাইড-ইফেক্টকে সেফগার্ড করতে হবে।
Idempotency: The Practical Escape Hatch
ডুপ্লিকেট সাইড-ইফেক্ট থেকে বাঁচার সবচেয়ে শক্তিশালী হাতিয়ার হলো Idempotency (আইডেমপোটেন্সি)।
গণিত ও কম্পিউটার সায়েন্সে একটি অপারেশনকে Idempotent বলা হয় যদি সেই অপারেশনটি একবার চালানোর ফলাফল আর একশোবার চালানোর ফলাফল সম্পূর্ণ একই থাকে ()।
Idempotency Key কীভাবে কাজ করে:
- ক্লায়েন্ট যখন কোনো গুরুত্বপূর্ণ রিকোয়েস্ট তৈরি করে, সে একটি গ্লোবালি ইউনিক আইডি তৈরি করে হেডারে পাঠিয়ে দেয় (যেমন:
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d)। - সার্ভার কোনো বিজনেস লজিক চালানোর আগে এই Key দিয়ে তার সেন্ট্রাল স্টোরে (Redis বা PostgreSQL) চেক করে।
- যদি Key আগে থেকেই পাওয়া যায় এবং স্ট্যাটাস
COMPLETEDথাকে, তবে সার্ভার নতুন করে কোনো পেমেন্ট বা প্রসেসিং না করে ক্যাশড রেসপন্সটি সাথে সাথে রিটার্ন করে দেয়। - যদি Key না থাকে, সার্ভার স্ট্যাটাসটিকে
IN_PROGRESSহিসেবে লক করে, মূল বিজনেস কাজ সম্পন্ন করে, রেসপন্স সেভ করে এবং স্ট্যাটাসCOMPLETEDকরে দেয়।
HTTP Methods ও Idempotency
RESTful API ডিজাইনে GET, PUT, এবং DELETE মেথডগুলো নিয়ম অনুযায়ী naturalmente idempotent। একটি রিসোর্স আপনি ১০ বার ডিলিট করার রিকোয়েস্ট পাঠালেও ডাটাবেজের ফাইনাল স্টেট একই থাকবে (রিসোর্সটি মুছে গেছে)। কিন্তু POST মেথড স্বভাবতই idempotent নয়। তাই Payment, Transfer, বা Order তৈরির মতো ক্রিটিক্যাল POST এন্ডপয়েন্টে Idempotency-Key হেডার বাধ্যতামূলক করা উচিত।
Database Unique Constraint ও Check-Then-Insert Trap
অনেকে অ্যাপ্লিকেশনের মেমোরিতে সাধারণ if (!exists) চেক দিয়ে আইডেমপোটেন্সি বানানোর চেষ্টা করেন:
// মারাত্মক Anti-Pattern: Check-Then-Insert Race Conditionconst existing = await db.query('SELECT * FROM payments WHERE idempotency_key = $1', [key]);if (!existing.rows.length) { // দুটি রিকোয়েস্ট একই মিলিসেকেন্ডে এলে দুজনেই এই লাইনে ঢুকে যাবে! await chargeCard(amount); await db.query('INSERT INTO payments (idempotency_key, status) VALUES ($1, $2)', [key, 'DONE']);}যদি দুটি ডুপ্লিকেট রিকোয়েস্ট একদম একই মিলিসেকেন্ডে দুটি আলাদা সার্ভার থ্রেডে পৌঁছায়, তবে Thread A এবং Thread B দুজনেই ডাটাবেজে চেক করে দেখবে existing শূন্য। এরপর দুজনেই কার্ড চার্জ করবে। একে বলা হয় Check-Then-Insert Race Condition।
অ্যাপ্লিকেশন মেমোরির চেকের ওপর কখনোই কনকারেন্সি নির্ভর করা যায় না। ডাটাবেজের Unique Constraint হলো ডিস্ট্রিবিউটেড সিস্টেমের একমাত্র অথরিটেটিভ গার্ড।
CREATE TABLE idempotency_keys ( key VARCHAR(255) PRIMARY KEY, status VARCHAR(50) NOT NULL, -- IN_PROGRESS, COMPLETED, FAILED response_body JSONB, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());ডাটাবেজের B-Tree ইনডেক্স গ্যারান্টি দেয় যে, দশটি থ্রেড একই সঙ্গে ইনসার্ট করার চেষ্টা করলেও ঠিক একটি থ্রেড সফল হবে এবং বাকি নয়টি থ্রেডে ডাটাবেজ লেভেলে Unique Violation Error (Postgres Code 23505) থ্রো হবে।
Consumer Crash ও Inbox Pattern
মেসেজ ব্রোকার (Kafka, RabbitMQ, SQS) ব্যবহার করার সময় আরেকটি বড় ক্র্যাশ সিনারিও তৈরি হয়:
- Consumer সার্ভিস ব্রোকার থেকে একটি মেসেজ রিসিভ করল।
- Consumer ইউজারের একাউন্টে
$100যোগ করল। - ব্রোকারকে
ACKপাঠানোর ঠিক এক মিলি-সেকেন্ড আগে কনজিউমার সার্ভিসটি OOM (Out of Memory) হয়ে ক্র্যাশ করল।
যেহেতু ব্রোকার কোনো ACK পায়নি, সে মনে করবে মেসেজটি প্রসেস হয়নি। কনজিউমার পড যখন রিস্টার্ট নেবে, ব্রোকার একই মেসেজ আবার ডেলিভার করবে। কনজিউমার আবার $100 যোগ করবে!
এই ক্র্যাশ সিনারিও আটকানোর জন্য ব্যবহার করা হয় Inbox Pattern।
Inbox Pattern-এর মূল নিয়ম হলো: মেসেজ আইডি সেভ করা এবং বিজনেস ডাটা আপডেট করাকে আলাদা আলাদা অপারেশনে না চালিয়ে একটি সিঙ্গেল ACID Database Transaction-এর মধ্যে চালাতে হবে।
BEGIN;
-- ১. মেসেজ আইডি ইনবক্স টেবিলে ইনসার্ট করার চেষ্টা করুন (message_id কলামে UNIQUE constraint আছে)-- ডুপ্লিকেট মেসেজ হলে ডাটাবেজ সাথে সাথে Unique Constraint Error দিয়ে পুরো ট্রানজেকশন আটকে দেবেINSERT INTO message_inbox (message_id, received_at)VALUES ('msg-ord-98765', NOW());
-- ২. বিজনেস ডাটা আপডেট করুন (শুধুমাত্র যদি উপরের ইনসার্ট সফল হয়)UPDATE user_walletsSET balance = balance + 100WHERE user_id = 'usr-001';
-- ৩. অডিট লগ লিখুনINSERT INTO wallet_transactions (user_id, amount, type)VALUES ('usr-001', 100, 'CREDIT');
-- ৪. পারমাণবিকভাবে ট্রানজেকশন কমিট করুনCOMMIT;যদি সার্ভার COMMIT-এর আগে যেকোনো মুহূর্তে ক্র্যাশ করে, ডাটাবেজ পুরো অপারেশন রোলব্যাক করবে। মেসেজ আইডিও সেভ হবে না, ব্যালেন্সও বাড়বে না। আবার যদি COMMIT-এর পর ক্র্যাশ করে, তবে মেসেজ আইডি অলরেডি সেভ আছে, তাই রিট্রাই এলে ডাটাবেজ ইউনিক এরর দিয়ে ডুপ্লিকেট ব্যালেন্স আপডেট আটকে দেবে।
Dual-Write Problem ও Transactional Outbox Pattern
Inbox দিয়ে আমরা মেসেজ কনজিউম করার সমস্যা সমাধান করলাম। কিন্তু ডাটা প্রডিউস করার সময় কী হবে?
ধরা যাক, Order Service ডাটাবেজে অর্ডার সেভ করার পর Kafka-তে একটি OrderCreated ইভেন্ট পাঠাবে যাতে Inventory Service স্টক কমাতে পারে।
// মারাত্মক Anti-Pattern: The Dual-Write Problemawait db.query('INSERT INTO orders ...'); // ধাপ ১: সফলawait kafkaProducer.send('OrderCreated', ...); // ধাপ ২: নেটওয়ার্ক ফেইলরে ক্র্যাশ!ধাপ ১ সফল হওয়ার পর যদি ব্রোকারে ইভেন্ট পাঠানো ফেইল করে, তবে ডাটাবেজে অর্ডার থেকে যাবে কিন্তু ইনভেন্টরি সার্ভিস জানতেই পারবে না অর্ডার হয়েছে। একে বলা হয় Dual-Write Problem। দুটি ভিন্ন স্টোরেজ সিস্টেমে একই সাথে পারমাণবিকভাবে রাইট করা যায় না।
এই সমস্যার সমাধান হলো Transactional Outbox Pattern:
- আমরা সরাসরি মেসেজ ব্রোকারে ইভেন্ট পাঠাব না।
- মূল বিজনেস টেবিল (
orders) এবং একটি বিশেষoutbox_eventsটেবিলে একই লোকাল ডাটাবেজ ট্রানজেকশনের মধ্যে ইভেন্টটি সেভ করব। - যেহেতু দুটি টেবিল একই ডাটাবেজের ভেতরে আছে, ডাটাবেজের ACID প্রোপার্টি গ্যারান্টি দেয় যে দুটি টেবিলেই ডাটা সেভ হবে, নতুবা কোনোটিতেই হবে না।
- একটি আলাদা ব্যাকগ্রাউন্ড রিলে ওয়ার্কার (বা Debezium CDC)
outbox_eventsটেবিল থেকে ডাটা রিড করে ব্রোকারে পাবলিশ করবে।
Outbox-এর ডেলিভারি গ্যারান্টি
মনে রাখবেন, Transactional Outbox প্যাটার্ন কিন্তু At-Least-Once ডেলিভারি নিশ্চিত করে, Exactly-Once নয়। রিলে ওয়ার্কার ব্রোকারে মেসেজ পাঠিয়ে ACK পাওয়ার আগে ক্র্যাশ করলে একই ইভেন্ট আবার ব্রোকারে যেতে পারে। তাই কনজিউমার সাইডে আমরা যে Inbox Pattern শিখেছি, তার উপস্থিতি বাধ্যতামূলক।
Retry Storm, Exponential Backoff এবং Jitter
ডিস্ট্রিবিউটেড সিস্টেমে যখন কোনো ডাউনস্ট্রিম সার্ভিস সাময়িকভাবে স্লো বা আনঅ্যাভেইলেবল হয়ে যায়, তখন At-Least-Once মডেলে ক্লায়েন্টরা ঘন ঘন রিট্রাই পাঠাতে শুরু করে।
ডাউন থাকা সার্ভিসটি যখনই রিকভার করার চেষ্টা করে, হাজার হাজার ক্লায়েন্টের জমে থাকা রিট্রাই রিকোয়েস্ট একযোগে সার্ভারে আঘাত হানে। ফলে সার্ভারটি সাথে সাথেই আবার ক্র্যাশ করে। এই ধ্বংসাত্মক চক্রকে বলা হয় Retry Storm বা Thundering Herd Problem।
রিট্রাই স্টর্ম থেকে বাঁচতে দুটি মেকানিজম একসাথে ব্যবহার করতে হয়:
- Exponential Backoff: প্রতিবার ফেইলরের পর রিট্রাইয়ের ব্যবধান দ্বিগুণ করা ( সেকেন্ড)।
- Jitter (র্যান্ডম ভ্যারিয়েশন): ব্যাকঅফ টাইমের সাথে একটি র্যান্ডম মিলিসেকেন্ড যোগ করা যাতে সব ক্লায়েন্ট একদম একই মুহূর্তে একসাথে রিট্রাই না পাঠিয়ে ট্রাফিককে স্মুথভাবে ছড়িয়ে দেয়।
// প্রোডাকশন গ্রেড Exponential Backoff + Full Jitter অ্যালগরিদমasync function fetchWithResilience(url, options, maxRetries = 5) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url, options); if (response.ok) return response;
// 4xx ক্লায়েন্ট এররে রিট্রাই করবেন না (429 Rate Limit ছাড়া) if (response.status !== 429 && response.status < 500) { throw new Error(`Client Error: ${response.status}`); } } catch (error) { if (attempt === maxRetries - 1) throw error;
// ১. Exponential Base Delay: 1s, 2s, 4s, 8s, 16s... const baseDelay = Math.pow(2, attempt) * 1000;
// ২. Full Jitter: 0 থেকে baseDelay-র মধ্যে র্যান্ডম সময় নির্বাচন const delay = Math.random() * baseDelay;
console.warn(`Attempt ${attempt + 1} failed. Retrying in ${Math.round(delay)}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } }}Apache Kafka-র 'Exactly-Once' মিথ বনাম বাস্তবতা
ইঞ্জিনিয়ারদের মাঝে একটি বহুল প্রচলিত ভুল ধারণা আছে যে, Apache Kafka-তে processing.guarantee="exactly_once" অন করে দিলেই পুরো এন্ড-টু-এন্ড সিস্টেম ম্যাজিক্যালি Exactly-Once হয়ে যায়।
বাস্তবতা হলো, Kafka EOS (Exactly-Once Semantics)-এর একটি নির্দিষ্ট আর্কিটেকচারাল বাউন্ডারি আছে:
- Kafka EOS যেখানে কাজ করে: আপনি Kafka-র একটি Topic A থেকে ডাটা পড়লেন, Kafka Streams দিয়ে ট্রান্সফর্ম করলেন, এবং Kafka-র Topic B-তে রাইট করলেন (
Consume-Transform-Produce Cycle)। এই ক্লোজড লুপের ভেতরে Kafka Transaction Coordinator গ্যারান্টি দেয় যে ডাটা ঠিক একবারই প্রসেস হবে। - Kafka EOS যেখানে ব্যর্থ হয়: আপনার কনজিউমার যখন মেসেজ পড়ে কোনো এক্সটার্নাল সিস্টেম যেমন Stripe API, SendGrid ইমেইল গেটওয়ে, বা থার্ড-পার্টি ডাটাবেজে কল করে, তখন Kafka EOS কোনো সাহায্য করতে পারে না। এক্সটার্নাল নেটওয়ার্ক কলের মুহূর্তে ক্র্যাশ হলে রিস্টার্টের পর Kafka মেসেজটি আবার দেবে এবং আপনি এক্সটার্নাল গেটওয়েতে ডুপ্লিকেট কল করবেন।
End-to-End Limitations: ডাটাবেজ ট্রানজেকশন বনাম এক্সটার্নাল API
ডিস্ট্রিবিউটেড সিস্টেমের সবচেয়ে বড় হার্ড রিয়ালিটি হলো: আপনি কখনোই একটি রিলেশনাল ডাটাবেজ ট্রানজেকশনের ভেতরে এক্সটার্নাল HTTP API কলকে পারমাণবিকভাবে বান্ডেল করতে পারবেন না।
// চরম বিপজ্জনক Anti-Pattern: DB Transaction-এর ভেতর External API কলawait db.query('BEGIN');await db.query('UPDATE accounts SET balance = balance - 100 WHERE id = $1', [userId]);
// এই API কল যদি নেটওয়ার্কের কারণে ১৫ সেকেন্ড আটকে থাকে, তবে ডাটাবেজ রো ১৫ সেকেন্ড লক থাকবে!// আর যদি API সফল হওয়ার পর ডাটাবেজ COMMIT ফেইল করে, তবে টাকা কাটা হবে না কিন্তু পেমেন্ট হয়ে যাবে!const paymentRes = await stripe.charges.create({ amount: 10000 });
await db.query('COMMIT');Golden Rule of Database Transactions
ডাটাবেজ ট্রানজেকশন ব্লক (BEGIN ... COMMIT)-এর ভেতরে কখনোই কোনো নেটওয়ার্ক কল, HTTP রিকোয়েস্ট বা থার্ড-পার্টি সার্ভিস কল রাখবেন না। নেটওয়ার্ক ল্যাটেন্সি ডাটাবেজের কানেকশন পুল নষ্ট করে দেবে এবং ডিসট্রিবিউটেড স্টেটকে আনরিকভারেবল করে তুলবে।
Real-World Architecture: Payment Processing Pipeline
একটি ইন্ডাস্ট্রি-স্ট্যান্ডার্ড, হাই-স্কেল পেমেন্ট প্রসেসিং পাইপলাইন কীভাবে মাল্টিপল লেয়ারে ফল্ট-টল্যারেন্স হ্যান্ডেল করে তা লক্ষ্য করুন:
এন্ড-টু-এন্ড রিকোয়েস্ট ফ্লো:
- Client Layer: ব্রাউজার বা মোবাইল অ্যাপ একটি
client_request_idসহ পেমেন্ট সার্ভিসে কল করে। - Ingress & DB Guard: পেমেন্ট সার্ভিস ডাটাবেজে Unique Constraint দিয়ে চেক করে যে এই
client_request_idআগে প্রসেস হয়েছে কি না। ডাটাবেজে ইভেন্টটি Outbox টেবিলে সেভ করে ক্লায়েন্টকে202 Acceptedরিটার্ন করে। - Async Worker Layer: ব্যাকগ্রাউন্ড ওয়ার্কার আউটবক্স থেকে ইভেন্ট তুলে নিয়ে পেমেন্ট গেটওয়ের জন্য একটি নিজস্ব
provider_idempotency_keyজেনারেট করে। - Gateway Call: পেমেন্ট গেটওয়েতে (Stripe) সেই কি সহ কল পাঠানো হয়। নেটওয়ার্ক ড্রপ হলে ওয়ার্কার নিরাপদে একই কি দিয়ে রিট্রাই করে। Stripe তার নিজস্ব লেয়ারে ডুপ্লিকেট চার্জ আটকে দেয়।
- Webhook Deduplication: পেমেন্ট সম্পন্ন হলে গেটওয়ে থেকে Webhook আসে। পেমেন্ট সার্ভিস Webhook-এর
event_idইনবক্স টেবিলে যাচাই করে ডুপ্লিকেট ওয়েবহুক ফিল্টার করে এবং ইউজারের অর্ডার ফাইনাল কনফার্ম করে।
Effectively-Once System Architecture
একটি পূর্ণাঙ্গ প্রোডাকশন সিস্টেমে রিলায়েবিলিটি কোনো সিঙ্গেল ম্যাজিক টুলের ওপর দাঁড়িয়ে থাকে না, বরং এটি একাধিক আর্কিটেকচারাল লেয়ারের সমন্বয়ে গড়ে ওঠে:
- Delivery Layer (At-Least-Once): নেটওয়ার্ক ড্রপে ডাটা লস রোধ করতে রিট্রাই মেকানিজম নিশ্চিত করা।
- Traffic Smoothing (Backoff + Jitter): সিস্টেম রিকভারির সময় রিট্রাই স্টর্ম প্রতিহত করা।
- Consumer Guard (Inbox + DB Unique Constraints): ডুপ্লিকেট মেসেজ ডেলিভারি হলেও ডাটাবেজ লেভেলে রেস কন্ডিশন ও ডুপ্লিকেট স্টেট আপডেট ব্লক করা।
- Publisher Guard (Transactional Outbox): ডাটাবেজ রাইট ও মেসেজ পাবলিশিংয়ের মধ্যে Dual-Write সমস্যা দূর করা।
- Distributed Workflow (Saga & Compensation): মাল্টি-সার্ভিস অপারেশনে কোনো ধাপ ফেইল করলে কমপেনসেটিং ট্রানজেকশন চালিয়ে পূর্বের স্টেট নিরাপদে রিভার্স করা।
এই পাঁচটি লেয়ার একসাথে কাজ করলে এন্ড-ইউজারের কাছে পুরো সিস্টেমটি একটি ত্রুটিহীন Effectively-Once Business Outcome প্রদান করে।
Recap ও Production Readiness Checklist
ডিস্ট্রিবিউটেড সিস্টেম ফেইলর ম্যাট্রিক্সটি মনে রাখুন:
আপনার Production Readiness Checklist
- Define Boundaries: সিস্টেমে "Exactly-Once" শব্দ ব্যবহার বন্ধ করুন। Delivery Semantics (At-Least-Once) এবং Processing Semantics (Idempotency)-কে টিমের সাথে আলাদাভাবে ডিফাইন করুন।
- Enforce DB Uniqueness: মেমোরিতে
if (!exists)চেক বাদ দিয়ে ডাটাবেজ লেভেলেরUNIQUEকনস্ট্রেইন্ট ব্যবহার করুন। - Implement Idempotency Keys: সমস্ত স্টেট-মডিফাইং
POSTAPI-তে বাধ্যতামূলকIdempotency-Keyহেডার যুক্ত করুন। - Adopt Inbox Pattern: মেসেজ কনজিউমার সার্ভিসে মেসেজ আইডি এবং বিজনেস স্টেট আপডেটকে একই ডাটাবেজ ট্রানজেকশনের ভেতরে কমিট করুন।
- Adopt Outbox Pattern: ডাটাবেজ সেভ এবং মেসেজ পাবলিশিংয়ের ক্ষেত্রে ডিরেক্ট নেটওয়ার্ক কল বাদ দিয়ে Transactional Outbox প্যাটার্ন ব্যবহার করুন।
- Isolate DB Transactions: ডাটাবেজ ট্রানজেকশন ব্লকের ভেতর যেকোনো ধরনের External HTTP API বা নেটওয়ার্ক কল পুরোপুরি নিষিদ্ধ করুন।
- Resilient Retries: সমস্ত রিট্রাই লজিকে Exponential Backoff-এর সাথে Full Jitter যুক্ত করুন।
- Deduplicate Webhooks: থার্ড-পার্টি পেমেন্ট গেটওয়ের প্রতিটি Webhook ইভেন্টকে
event_idদিয়ে ইনবক্সে ডিডুপ্লিকেট করুন। - Observability & Tracing: প্রতিটি রিকোয়েস্টে
trace_id,idempotency_key, এবংmessage_idস্ট্রাকচার্ড লগে রাখুন।
ডিস্ট্রিবিউটেড সিস্টেমে নেটওয়ার্ক ফেইলর কোনো ব্যতিক্রমী ঘটনা নয়, এটিই প্রতিদিনের রূঢ় বাস্তবতা। সিস্টেমকে এমনভাবে ডিজাইন করুন যেন নেটওয়ার্ক যেকোনো মুহূর্তে ড্রপ করলেও আপনার ডাটাবেজ ও বিজনেস স্টেট সর্বদা নিরাপদ ও অভ্রান্ত থাকে।