Distributed Sagas এবং Compensating Transactions আসলে Under-the-Hood কীভাবে কাজ করে?

On this page
মনোলিথিক আর্কিটেকচার ছেড়ে মাইক্রোসার্ভিসে আসার পর আমরা যে সবচেয়ে কঠিন বাস্তবতার মুখোমুখি হই, তা হলো ডেটাবেজ ট্রানজেকশনের বিলুপ্তি। মনোলিথে একটি সিঙ্গেল ডেটাবেজ থাকে, যেখানে আমরা খুব সহজেই ACID (Atomicity, Consistency, Isolation, Durability) প্রোপার্টি ব্যবহার করে ডেটা ব্যালেন্সড রাখতে পারি। কিন্তু যখন আমাদের সিস্টেম ডিস্ট্রিবিউটেড হয়ে যায়, যেখানে প্রতিটি মাইক্রোসার্ভিসের নিজস্ব ডেটাবেজ থাকে, তখন গ্লোবাল ট্রানজেকশন মেইনটেইন করা একটি দুঃস্বপ্নে পরিণত হয়।
অনেকেই মনে করেন প্রথাগত Two-Phase Commit বা 2PC প্রটোকল ব্যবহার করে এই সমস্যার সমাধান করা সম্ভব। কিন্তু হাই-স্কেল প্রোডাকশন রিয়েলিটিতে 2PC একটি মৃত আর্কিটেকচার। আজ আমরা একদম হোয়াইটবোর্ড সেশনের মতো লো-লেভেল থেকে বিশ্লেষণ করব কেন ডিস্ট্রিবিউটেড লকিং সিস্টেমে ফেইল করে, এবং কীভাবে Distributed Saga ও Compensating Transactions-এর মেকানিজম ব্যবহার করে আমরা একটি রেজিলিয়েন্ট, প্রোডাকশন-গ্রেড স্টেট মেশিন তৈরি করতে পারি।
১. The Core Distributed Nightmare এবং Why It Matters
Two-Phase Commit (2PC) আর্কিটেকচারের সবচেয়ে বড় দুর্বলতা হলো এর Synchronous Lock Trap। 2PC প্রটোকলে একটি সেন্ট্রাল কোঅর্ডিনেটর সমস্ত ডেটাবেজকে প্রথমে ট্রানজেকশনের জন্য প্রস্তুত হতে বলে (Prepare Phase)। এই সময় প্রতিটি ডেটাবেজ তার নিজের রেকর্ডের উপর Distributed Lock বসিয়ে রাখে। যতক্ষণ না কোঅর্ডিনেটর ফাইনাল কমিট নির্দেশ (Commit Phase) দিচ্ছে, ততক্ষণ এই লকগুলো রিলিজ হয় না। এখন চিন্তা করুন, যদি আপনার সিস্টেমে পাঁচটি মাইক্রোসার্ভিস থাকে এবং তাদের মধ্যে একটি সার্ভিসের নেটওয়ার্ক রেসপন্স দিতে মাত্র ২০০ মিলিসেকেন্ড দেরি করে, তবে পুরো সিস্টেমের সমস্ত ডেটাবেজ থ্রেড সেই ২০০ মিলিসেকেন্ড ব্লক হয়ে বসে থাকবে। হাই-থ্রুপুট সিস্টেমে এই ব্লকিং বিহেভিয়ার আপনার ডেটাবেজ কানেকশন পুল মুহূর্তের মধ্যে শেষ করে দেয় এবং সিস্টেমের Latency বাড়িয়ে থ্রুপুট শূন্যে নামিয়ে আনে।
এই কারণেই আধুনিক ক্লাউড-নেটিভ আর্কিটেকচারে আমরা ACID-এর Strict Isolation (I) প্রোপার্টি পুরোপুরি বাদ দিয়ে BASE (Basically Available, Soft state, Eventual consistency) মডেল আপন করে নিই। ডিস্ট্রিবিউটেড সিস্টেমে পারফেক্ট আইসোলেশন ধরে রাখা ম্যাথমেটিক্যালি এবং প্র্যাক্টিক্যালি অসম্ভব। আমাদের মেনে নিতে হয় যে সিস্টেমের স্টেট কিছু সময়ের জন্য আন-সিঙ্ক বা Soft state-এ থাকবে, কিন্তু একটি নির্দিষ্ট সময় পর সমস্ত ডেটা ইভেনচুয়ালি কন্সিস্টেন্ট বা সামঞ্জস্যপূর্ণ হয়ে যাবে।
কিন্তু এখানে একটি মারাত্মক Linear Execution Problem তৈরি হয়। ধরুন, আপনার একটি ই-কমার্স অর্ডার চেইন আছে: Order Service থেকে ডেটা যায় Payment Service-এ, এবং সেখান থেকে যায় Inventory Service-এ। এখন পেমেন্ট সফল হওয়ার পর যদি ইনভেন্টরি সার্ভিস কোনো কারণে ক্র্যাশ করে বা আউট অফ স্টক এরর দেয়, তবে কী হবে? মনোলিথে আমরা একটি ROLLBACK কমান্ড দিলেই সব আগের মতো হয়ে যেত। কিন্তু এখানে পেমেন্ট সার্ভিসের ডেটাবেজ ট্রানজেকশন ইতোমধ্যে কমিট হয়ে গেছে এবং মেমোরি থেকে তার আগের স্টেট মুছে গেছে। এই ডাউনস্ট্রিম ফেইলরের কারণে আপনার ইউজারের টাকা কেটে নেওয়া হলো অথচ সে প্রোডাক্ট পেল না। এই সাইলেন্ট ডেটা করাপশন ঠেকানোর জন্যই আমাদের Distributed Saga আর্কিটেকচারের প্রয়োজন।
২. The Foundational Mental Model এবং Analogy
Distributed Saga-র কোর কনসেপ্ট অত্যন্ত শক্তিশালী এবং লজিক্যাল। এখানে আমরা পুরো প্রসেসটিকে একটি গ্লোবাল ডেটাবেজ লকের ভেতরে না রেখে, একাধিক স্বাধীন Local Transaction-এর একটি সিকোয়েন্সে বা চেইনে ভাগ করে ফেলি। প্রতিটি সার্ভিস তার নিজের ডেটাবেজে কাজ সম্পন্ন করে কমিট করে দেয়, এবং একটি Asynchronous Message বা Event পাবলিশ করে পরবর্তী সার্ভিসকে কাজ শুরু করার নির্দেশ দেয়।
বিষয়টি পরিষ্কার করার জন্য আমরা একটি রিয়াল-ওয়ার্ল্ড Analogy (অ্যানালজি) বিবেচনা করতে পারি। ধরুন, আপনি একটি ট্রাভেল এজেন্সির মাধ্যমে ঢাকা থেকে প্যারিস যাওয়ার একটি মাল্টি-সিটি ট্যুর প্যাকেজ বুক করছেন। এই প্যাকেজে তিনটি কাজ আছে: ফ্লাইট বুকিং, হোটেল বুকিং এবং কার রেন্টাল। ট্রাভেল এজেন্ট কিন্তু তিনটি কোম্পানির সার্ভার একসাথে লক করে বসে থাকে না। সে প্রথমে এয়ারলাইন্সের সিস্টেমে ঢুকে ফ্লাইট বুক করে এবং একটি কনফার্মেশন টোকেন নেয়। এরপর সে হোটেল বুক করে। এখন ধরুন, কার রেন্টাল বুক করতে গিয়ে দেখা গেল কোনো গাড়ি ফাঁকা নেই। এজেন্ট কি এখন এয়ারলাইন্সের ডেটাবেজে ঢুকে আগের বুকিং ডিলিট করতে পারবে? অবশ্যই না, কারণ সেই ট্রানজেকশন ইতোমধ্যে শেষ। এজেন্ট এখন যা করবে তা হলো: সে এয়ারলাইন্স এবং হোটেলে একটি নতুন ক্যানসেলেশন রিকোয়েস্ট পাঠাবে।
ডিস্ট্রিবিউটেড সিস্টেমে এই মানসিক মডেলটিকে আমরা বলি Eventual Consistency বাউন্ডারি। আমরা জানি যে ট্রানজেকশন চলাকালীন কিছু মিলিসেকেন্ড বা সেকেন্ডের জন্য আমাদের সিস্টেম একটি ইন-ফ্লাইট স্টেটে থাকবে। কিন্তু আমাদের আর্কিটেকচার এমনভাবে ডিজাইন করা থাকে যে শেষ পর্যন্ত হয় পুরো প্যাকেজ বুক হবে, অথবা সমস্ত লোকাল স্টেপ একে একে রিভার্স হয়ে সিস্টেম একদম প্রাথমিক ব্যালেন্সড স্টেটে ফিরে আসবে।
৩. Structural Typology: Orchestration vs Choreography (Under-the-Hood)
সাগা প্যাটার্ন ইমপ্লিমেন্ট করার জন্য ইন্ডাস্ট্রিতে দুটি প্রধান আর্কিটেকচারাল প্যাটার্ন ব্যবহার করা হয়: Orchestration এবং Choreography। আসুন দেখি লো-লেভেল মেমোরি এবং নেটওয়ার্ক লেভেলে এরা কীভাবে কাজ করে।
Orchestration মডেলে একটি সেন্ট্রাল ব্রেন বা কোঅর্ডিনেটর থাকে, যাকে আমরা বলি Orchestrator Engine (যেমন: Temporal বা Camunda)। এই ইঞ্জিনটি পুরো সাগা ওয়ার্কফ্লো-এর একটি স্টেট মেশিন নিজের মেমোরিতে এবং ডেটাবেজে মেইনটেইন করে। যখন কোনো অর্ডার আসে, অর্কেস্ট্রেটর প্রথমে পেমেন্ট সার্ভিসকে কমান্ড পাঠায়। পেমেন্ট সফল হলে সেই উত্তর অর্কেস্ট্রেটরের কাছে ফেরে, এবং অর্কেস্ট্রেটর তখন ইনভেন্টরি সার্ভিসকে কমান্ড পাঠায়।
এখানে একটি চমৎকার Under-the-hood মেকানিজম কাজ করে, যাকে বলা হয় Event Sourcing Mechanism। ধরুন, পেমেন্ট সফল হওয়ার পর অর্কেস্ট্রেটর ইঞ্জিন নিজেই ক্র্যাশ করল। রিস্টার্ট হওয়ার পর সে কীভাবে জানবে ট্রানজেকশন কোন স্টেপ পর্যন্ত এসেছিল? অর্কেস্ট্রেটর তার কারেন্ট স্টেট কখনোই সরাসরি আপডেট করে না। এর বদলে সে একটি Write-Ahead Log (WAL) বা ইভেন্ট স্টোরে প্রতিটি স্টেপের ইভেন্ট সিকোয়েনশিয়ালি লিখে রাখে (OrderStarted, PaymentCommandSent, PaymentSucceeded)। সার্ভার রিস্টার্ট হলে সে শূন্য থেকে এই ইভেন্ট লগগুলো রিপ্লে (Replay) করে নিজের ইন-মেমোরি স্টেট মেশিনকে ঠিক আগের মুহূর্তে রিকনস্ট্রাক্ট করে নেয়।
অন্যদিকে, Choreography মডেলে কোনো সেন্ট্রাল ব্রেন থাকে না। এটি সম্পূর্ণ ডিসেন্ট্রালাইজড এবং Event-Driven Architecture। এখানে প্রতিটি সার্ভিস মেসেজ ব্রোকারের (যেমন: Kafka বা RabbitMQ) নির্দিষ্ট টপিক থেকে ইভেন্ট শোনে, নিজের লোকাল ডেটাবেজে কাজ করে, এবং আরেকটি নতুন ইভেন্ট ব্রোকারে ছেড়ে দেয়। যেমন: Order Service একটি OrderCreated ইভেন্ট ছাড়ল, সেটি শুনে Payment Service টাকা কেটে PaymentBilled ইভেন্ট ছাড়ল, যা শুনে Inventory Service স্টক আপডেট করল।
কিন্তু কোরিওগ্রাফি মডেলে একটি মারাত্মক Cyclic Dependency Loop Dilemma তৈরি হওয়ার ঝুঁকি থাকে। ধরুন, সার্ভিস A ইভেন্ট পাঠায় B-কে, B পাঠায় C-কে, এবং কোনো রাউটিং এরর বা বিজনেস লজিকের কারণে C আবার একটি ইভেন্ট পাঠিয়ে দেয় A-কে। সিস্টেমে ট্রাফিক বাড়লে এই ডিসেন্ট্রালাইজড ইভেন্ট চেইন একটি ইনফিনিট লুপ বা ডেডলক তৈরি করে ব্রোকারের মেমোরি বাফার সম্পূর্ণ ক্র্যাশ করিয়ে দেয়। এই কারণে পাঁচটি সার্ভিসের বেশি জটিল ওয়ার্কফ্লো হলে আমরা সবসময় Orchestration মডেল বেছে নিই।
৪. Anatomy of a Saga Transaction: The Tri-Partition Rule
একটি ডিস্ট্রিবিউটেড সাগার প্রতিটি লোকাল ট্রানজেকশনকে সমান চোখে দেখা একটি বড় আর্কিটেকচারাল ভুল। সিস্টেম ডিজাইন করার সময় প্রতিটি সাগা চেইনকে তিনটি সুনির্দিষ্ট ফেইলর বাউন্ডারিতে ভাগ করতে হয়, যাকে আমরা বলি Tri-Partition Rule:
১. Compensable Transactions: সাগা সিকোয়েন্সের একদম শুরুর দিকের ট্রানজেকশনগুলো, যেগুলোকে ফেইলর হলে রিভার্স করা সম্ভব। যেমন: ইউজারের অ্যাকাউন্ট থেকে সাময়িক ব্যালেন্স হোল্ড করা বা ইনভেন্টরি রিভার্ভ করা। এই স্টেপগুলোতে সমস্যা হলে আমরা বিপরীত কমান্ড পাঠিয়ে ডেটা আগের অবস্থায় ফিরিয়ে আনতে পারি।
২. The Pivot Transaction: এটি পুরো সাগা ওয়ার্কফ্লো-এর সবচেয়ে ক্রিটিক্যাল পয়েন্ট বা পয়েন্ট অফ নো রিটার্ন। যদি এই ট্রানজেকশনটি সফল হয়ে যায়, তবে সাগা আর কোনোভাবেই পেছনে ফিরবে না। এর পরে আর কোনো Backward Recovery বা রোলব্যাক করার সুযোগ থাকে না। যেমন: থার্ড-পার্টি পেমেন্ট গেটওয়ে (Stripe বা PayPal) থেকে ফাইনাল পেমেন্ট ক্যাপচার হওয়া। টাকা একবার ব্যাংক থেকে কেটে বের হয়ে গেলে সেটিকে আর ডেটাবেজ রোলব্যাক দিয়ে ফেরানো যায় না।
৩. Retriable Transactions: পিভট ট্রানজেকশন সফল হওয়ার পর যে ডাউনস্ট্রিম স্টেপগুলো আসে, সেগুলো হলো Retriable Transactions। আর্কিটেকচারালি এই স্টেপগুলো কখনই স্থায়ীভাবে ফেইল করতে পারবে না। যেমন: ইউজারকে ইমেইল বা এসএমএস নোটিফিকেশন পাঠানো, অথবা ইনভেন্টরির স্ট্যাটাস 'Reserved' থেকে 'Sold'-এ পরিবর্তন করা। যদি নোটিফিকেশন সার্ভিসের নেটওয়ার্ক ড্রপ করে, তবে সিস্টেম ফেইল না করে Exponential Backoff দিয়ে ইনফিনিট রিট্রাই চালাতে থাকবে যতক্ষণ না কাজ সম্পন্ন হয় (Forward Recovery)।
৫. The Reverse Engine: Compensating Transactions এবং Recovery Patterns
মনোলিথিক ডেটাবেজে ROLLBACK কমান্ড দিলে স্টোরেজ ইঞ্জিন ডিস্কের আন-কমিটেড পেজগুলো মুছে ফেলে আগের স্ন্যাপশটে ফিরে যায়। কিন্তু মাইক্রোসার্ভিসে এটি অসম্ভব, কারণ আপনার ট্রানজেকশন ইতোমধ্যে ডেটাবেজে কমিট হয়ে গেছে এবং অন্য কোনো থ্রেড হয়তো সেই ডেটা রিডও করে ফেলেছে। তাই এখানে আমাদের Semantic Rollback মেকানিজম ব্যবহার করতে হয়।
Semantic Rollback হলো এমন একটি প্রক্রিয়া যেখানে আমরা আগের ডেটা ডিলিট না করে, একটি নতুন বিপরীত এন্ট্রি বা Compensating Transaction ডেটাবেজে ইনসার্ট করি যা আগের ট্রানজেকশনের প্রভাবকে গাণিতিকভাবে শূন্য করে দেয়। উদাহরণস্বরূপ, যদি আপনার লোকাল ট্রানজেকশন হয় UPDATE accounts SET balance = balance - 500 WHERE id = 1, তবে এর কম্পেনসেটিং ট্রানজেকশন হবে UPDATE accounts SET balance = balance + 500 WHERE id = 1। আমরা ডেটাবেজে একটি ডেবিট এন্ট্রির বিপরীতে একটি ক্রেডিট এন্ট্রি তৈরি করে লেজার ব্যালেন্সড করি।
সাগা প্যাটার্নে দুটি প্রধান রিকভারি ওয়ার্কফ্লো কাজ করে:
- Backward Recovery Workflow: যদি পিভট ট্রানজেকশনে পৌঁছানোর আগেই কোনো স্টেপ ফেইল করে, তবে সিস্টেম পেছনের দিকে হাঁটা শুরু করে। ধরুন আপনার সিকোয়েন্স ছিল । যদি স্টেপে এসে এরর হয়, তবে অর্কেস্ট্রেটর রিভার্স অর্ডারে কম্পেনসেটিং কমান্ড ফায়ার করবে: । মনে রাখবেন, যে স্টেপ ফেইল করেছে (), তার কোনো কম্পেনসেটিং কমান্ড ফায়ার হবে না, কারণ সে তো সফলই হয়নি।
- Forward Recovery Workflow: যদি পিভট ট্রানজেকশন পার হওয়ার পর কোনো স্টেপে এরর দেখা দেয়, তবে সিস্টেম আর পেছনে ফেরে না। সে রোলব্যাক না করে সামনের দিকে এগোতে থাকে। সে ফেইলড স্টেপটিকে একটি বাফারে রেখে বারবার রিট্রাই চালিয়ে যায় যাতে পুরো প্রসেসটি এন্ড-টু-এন্ড কমপ্লিট হতে পারে।
উপরে দেখানো টাইমলাইন ডায়াগ্রামটিতে আপনি দেখতে পাচ্ছেন কীভাবে বিভিন্ন মাইক্রোসার্ভিসের মধ্যে ইভেন্টগুলো প্যারালালি পাস হচ্ছে, এবং একটি ডাউনস্ট্রিম ফেইলর ঘটার সাথে সাথে কীভাবে ক্যাসকেডিং রিভার্স ইভেন্ট বা কম্পেনসেটিং ট্রানজেকশন ট্রিগার হয়ে সিস্টেমকে সামঞ্জস্যপূর্ণ অবস্থায় ফিরিয়ে আনছে।
৬. Step-by-Step Execution Trace এবং Code Mechanics
একটি প্রোডাকশন-গ্রেড সাগা অর্কেস্ট্রেটর কীভাবে কোড লেভেলে কাজ করে, তা বোঝার জন্য আমরা Node.js এবং TypeScript ব্যবহার করে একটি মেমোরি-সেফ কোঅর্ডিনেটর ইঞ্জিনের ব্লুপ্রিন্ট তৈরি করব। এই কোডটি প্রতিটি স্টেপ এক্সিকিউট করবে এবং ফেইলর হলে স্বয়ংক্রিয়ভাবে রিভার্স অর্ডারে কম্পেনসেটিং ফাংশনগুলো কল করবে।
ধাপ ১: The Orchestrator Coordinator Code Structure
আমরা এমন একটি ক্লাস তৈরি করব যা ট্রানজেকশন স্টেপ এবং তাদের রেস্পেক্টিভ কম্পেনসেটিং ফাংশনগুলোর ম্যাপ হোল্ড করবে।
type SagaStep = { name: string; action: () => Promise<any>; compensate: () => Promise<any>;};
export class SagaOrchestrator { private steps: SagaStep[] = []; private executedSteps: SagaStep[] = [];
// সাগাতে নতুন স্টেপ এবং তার রিভার্স লজিক রেজিস্টার করা public addStep(name: string, action: () => Promise<any>, compensate: () => Promise<any>): void { this.steps.push({ name, action, compensate }); }
// সাগা এক্সিকিউট করা এবং ফেইলর হলে ব্যাকওয়ার্ড রিকভারি চালানো public async execute(sagaId: string): Promise<boolean> { console.log(`[Saga Engine] Starting Saga Execution: ${sagaId}`);
for (const step of this.steps) { try { console.log(`[Saga Engine] Executing Step: ${step.name}`); await step.action(); // সফল হলে স্টেপটি এক্সিকিউটেড লিস্টে জমা করা হবে this.executedSteps.push(step); } catch (error) { console.error(`[Saga Engine] Step Failed: ${step.name}. Triggering Backward Recovery!`, error); await this.rollback(sagaId); return false; } }
console.log(`[Saga Engine] Saga Execution Completed Successfully: ${sagaId}`); return true; }
// রিভার্স অর্ডারে কম্পেনসেটিং ট্রানজেকশন চালানো private async rollback(sagaId: string): Promise<void> { console.log(`[Saga Engine] Starting Rollback for Saga: ${sagaId}`);
// এক্সিকিউটেড লিস্ট রিভার্স করে শেষের স্টেপ থেকে শুরুতে আসা const reversedSteps = [...this.executedSteps].reverse();
for (const step of reversedSteps) { try { console.log(`[Saga Engine] Compensating Step: ${step.name}`); await step.compensate(); } catch (compensateError) { // কম্পেনসেটিং স্টেপ ফেইল করলে এটি Dead Letter Queue (DLQ)-তে পাঠাতে হবে console.error(`[CRITICAL] Compensation Failed for Step: ${step.name}. Manual intervention required!`, compensateError); } } }}ধাপ ২: Idempotent Compensations এবং Composite Key Design
ডিস্ট্রিবিউটেড সিস্টেমে নেটওয়ার্ক রিট্রাইয়ের কারণে আপনার কম্পেনসেটিং কমান্ড (যেমন: cancelInventory) একই সার্ভিসে একাধিকবার পৌঁছাতে পারে। যদি আপনার লজিক ইডেমপোটent না হয়, তবে ডেটাবেজ ভুল করে দুইবার স্টক বাড়িয়ে দেবে। এটি ঠেকানোর জন্য আমাদের ইউনিক সাগা আইডি (saga_id) এবং কম্পোজিট কী ব্যবহার করে ডেটাবেজ গার্ড তৈরি করতে হবে।
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function compensateInventory(sagaId: string, productId: string, quantity: number) { // একটি একক ট্রানজেকশনে ইডেমপোটেন্সি চেক এবং স্টক রিস্টোর করা return await prisma.$transaction(async (tx) => { // ১. চেক করা এই সাগা আইডির জন্য রোলব্যাক ইতোমধ্যে হয়েছে কিনা const existingCompensation = await tx.compensation_logs.findUnique({ where: { saga_id_step_name: { saga_id: sagaId, step_name: 'INVENTORY_DEDUCT_COMPENSATION', }, }, });
if (existingCompensation) { console.log(`[Idempotency Guard] Compensation already processed for Saga: ${sagaId}. Skipping.`); return; }
// ২. স্টক রিস্টোর করা await tx.products.update({ where: { id: productId }, data: { stock: { increment: quantity } }, });
// ৩. কম্পেনসেটিং লগ এন্ট্রি করা যাতে পরবর্তীতে ডুপ্লিকেট রিকোয়েস্ট ব্লক হয় await tx.compensation_logs.create({ data: { saga_id: sagaId, step_name: 'INVENTORY_DEDUCT_COMPENSATION', processed_at: new Date(), }, });
console.log(`[Saga Engine] Inventory successfully restored for Product: ${productId}`); });}৭. Distributed Isolation Anomalies: The Hidden Traps
যেহেতু সাগা প্যাটার্নে কোনো গ্লোবাল ডেটাবেজ লক থাকে না, তাই ইন্টারমিডিয়েট বা আংশিক স্টেটগুলো অন্য কোনো সমসাময়িক ট্রানজেকশন দেখে ফেলতে পারে। এই আইসোলেশন না থাকার কারণে সিস্টেমে কিছু মারাত্মক Concurrency Anomaly বা ফাঁদ তৈরি হয়:
- Lost Updates: ধরুন, সাগা A কোনো ইউজারের ক্রেডিট লিমিট চেক করে সাময়িকভাবে ১০০ টাকা কমালো। ঠিক একই সময়ে সাগা B এসে ওই ইউজারের ক্রেডিট লিমিট আপডেট করে দিল। এখন যদি সাগা A ফেইল করে এবং রোলব্যাক চালাতে যায়, তবে সে সাগা B-এর আপডেট করা ডেটা ওভাররাইট করে আগের ডেটা বসিয়ে দেবে। ফলে সাগা B-এর আপডেটটি চিরতরে হারিয়ে যাবে।
- Dirty Reads: সাগা মাঝপথে থাকা অবস্থায় (যেমন: পেমেন্ট সফল হয়েছে কিন্তু ইনভেন্টরি চেক পেন্ডিং) অন্য কোনো সাধারণ ইউজার বা ট্রানজেকশন সেই আন-কমিটেড প্রোডাক্ট স্টকে আছে দেখে অর্ডার করে ফেলল। পরবর্তীতে সাগাটি ফেইল করে রোলব্যাক হয়ে গেল। ফলে দ্বিতীয় ইউজার এমন একটি প্রোডাক্ট অর্ডার করে বসল যা বাস্তবে স্টকেই ছিল না।
এই আইসোলেশন সমস্যাগুলো দূর করার জন্য আমাদের প্রোডাকশন সিস্টেমে কিছু বিশেষ Architectural Countermeasure বা প্রতিরোধমূলক ব্যবস্থা নিতে হয়:
১. Semantic Lock: আমরা ডেটাবেজ ইঞ্জিনের রো-লেভেল লকের ওপর নির্ভর না করে বিজনেস লজিক লেভেলে একটি লক তৈরি করি। যেমন: অর্ডারের স্ট্যাটাস সরাসরি ACTIVE না করে আমরা কলামে PENDING_CHECKOUT, RESERVED, বা LOCK_ACQUIRED স্টেট সেট করি। অন্য কোনো ট্রানজেকশন যখন এই রেকর্ড রিড করতে আসবে, সে স্ট্যাটাস দেখেই বুঝতে পারবে যে একটি সাগা ট্রানজেকশন চলছে এবং সে ওই ডেটায় হাত দেবে না।
২. Pessimistic View: আমরা আমাদের রিড এপিআইগুলোকে এমনভাবে ডিজাইন করি যেন তারা সবসময় একটি পেসিমিস্টিক বা কনজারভেটিভ ভিউ প্রদান করে। যেমন: ইউজারের ব্যালেন্স দেখানোর সময় আমরা Actual Balance থেকে Reserved Balance বাদ দিয়ে শুধুমাত্র Available Balance প্রদর্শন করি। এতে সাগা ফেইল বা রোলব্যাক হলেও ইউজারের কনকারেন্ট এক্সপেরিয়েন্সে কোনো সমস্যা হয় না।
৮. প্রোডাকশন সতর্কতা ও সুরক্ষাকবচ
The Infinite Rollback Loop
আপনার কম্পেনসেটিং ট্রানজেকশন (Rollback Function) যেন কোনোভাবেই নিজে কোনো আনহ্যান্ডেলড এক্সেপশন থ্রো না করে। যদি রোলব্যাক ফাংশন নিজেই ডেটাবেজ এরর বা বিজনেস লজিক এক্সেপশনের কারণে ক্র্যাশ করে, তবে সিস্টেম একটি ইনফিনিট রিট্রাই লুপে আটকে যাবে এবং ডেটা পার্মানেন্টলি আনব্যালেন্সড হয়ে পড়বে। কম্পেনসেটিং লজিক সবসময় ফল্ট-টল্যারেন্ট হতে হবে এবং প্রয়োজনে ফলব্যাক হিসেবে Dead Letter Queue (DLQ) ব্যবহার করতে হবে।
Asynchronous Deadlocks in Choreography
কোরিওগ্রাফি সাগাতে কখনোই সাইক্লিক ডিপেন্ডেন্সি তৈরি করবেন না (যেমন: Service A ইভেন্ট পাঠায় B-কে, B পাঠায় C-কে, এবং C আবার পাঠায় A-কে)। নেটওয়ার্ক স্পাইক বা হাই-থ্রুপুট লোডের সময় এটি ডিস্ট্রিবিউটেড ডেডলক তৈরি করে ব্রোকার বাফার ক্র্যাশ করাবে। ইভেন্ট ফ্লো সবসময় একমুখী (Unidirectional) বা ট্রি-স্ট্রাকচারড হতে হবে।
৯. Summary & Production Decision Rules
পুরো Deep Dive থেকে আমরা দেখতে পেলাম যে ডিস্ট্রিবিউটেড সাগা কোনো সাধারণ কোডিং প্যাটার্ন নয়, এটি একটি সম্পূর্ণ আর্কিটেকচারাল প্যারাডাইম শিফট। আপনার প্রোডাকশন সিস্টেমে এই প্যাটার্নটি সঠিক উপায়ে প্রয়োগ করার জন্য নিচের সিদ্ধান্ত রুলসগুলো মেনে চলুন:
- আর্কিটেকচারাল সিলেকশন ফ্রেমওয়ার্ক: আপনার সিস্টেমে যদি ২ থেকে ৪টি মাইক্রোসার্ভিস থাকে এবং তাদের ওয়ার্কফ্লো খুব সহজ হয়, তবে Choreography মডেল ব্যবহার করুন। কিন্তু সার্ভিসের সংখ্যা যদি ৫-এর বেশি হয়, অথবা ওয়ার্কফ্লোতে জটিল কন্ডিশনাল লজিক ও টাইমআউট থাকে, তবে কোনো চিন্তা ছাড়াই Orchestration (যেমন: Temporal) বেছে নিন। সেন্ট্রাল ভিজিবিলিটি ছাড়া লার্জ-স্কেল সাগা ডিবাগ করা অসম্ভব।
- রিলিজ অ্যান্ড ট্র্যাকিং মেকানিজম: ডিস্ট্রিবিউটেড সাগা আর্কিটেকচার ইমপ্লিমেন্ট করার পর প্রোডাকশন এনভায়রনমেন্টে Observability নিশ্চিত করা মাস্ট। প্রতিটি রিকোয়েস্টের শুরুতে একটি ইউনিক
trace_idবাsaga_idজেনারেট করুন এবং OpenTelemetry ব্যবহার করে প্রতিটি মাইক্রোসার্ভিসের লগ ও ইভেন্টে সেই আইডি প্রোপাগেট করুন। এতে কোনো ট্রানজেকশন ফেইল করলে ডিস্ট্রিবিউটেড ট্রেসিং ড্যাশবোর্ড থেকে এক সেকেন্ডেই বের করা সম্ভব হবে ঠিক কোন সার্ভিসে এবং কেন রোলব্যাক ট্রিগার হয়েছিল।
ডিস্ট্রিবিউটেড সিস্টেমের এই Low-Level মেকানিজমগুলো সঠিকভাবে বুঝতে পারলে এবং আপনার প্রোডাকশন আর্কিটেকচারে ইমপ্লিমেন্ট করতে পারলে, আপনার সিস্টেম যেকোনো নেটওয়ার্ক ফেইলর বা কনকারেন্সি লোড অত্যন্ত সাবলীলভাবে হ্যান্ডেল করতে সক্ষম হবে।