জাভাস্ক্রিপ্টের অ্যাসিঙ্ক কন্টেক্সট চ্যালেঞ্জ ও Node.js AsyncLocalStorage সহ থ্রেড সেফটি আয়ত্ত করুন। শক্তিশালী, কনকারেন্ট অ্যাপ্লিকেশনের জন্য কন্টেক্সট আইসোলেশনের একটি পূর্ণাঙ্গ নির্দেশিকা।
জাভাস্ক্রিপ্ট অ্যাসিঙ্ক কন্টেক্সট ও থ্রেড সেফটি: কন্টেক্সট আইসোলেশন ম্যানেজমেন্টে একটি গভীর অনুসন্ধান
আধুনিক সফটওয়্যার ডেভেলপমেন্টের জগতে, বিশেষ করে সার্ভার-সাইড অ্যাপ্লিকেশনগুলিতে, স্টেট ম্যানেজ করা একটি মৌলিক চ্যালেঞ্জ। মাল্টি-থ্রেডেড রিকোয়েস্ট মডেলের ভাষাগুলির জন্য, থ্রেড-লোকাল স্টোরেজ প্রতি-থ্রেড, প্রতি-রিকোয়েস্ট ভিত্তিতে ডেটা আইসোলেট করার একটি সাধারণ সমাধান সরবরাহ করে। কিন্তু Node.js-এর মতো একটি সিঙ্গেল-থ্রেডেড, ইভেন্ট-ড্রাইভেন পরিবেশে কী ঘটে? আমরা কীভাবে একটি লেনদেনের আইডি, ব্যবহারকারী সেশন বা স্থানীয়করণ সেটিংসের মতো রিকোয়েস্ট-নির্দিষ্ট কন্টেক্সটকে অ্যাসিঙ্ক্রোনাস অপারেশনের একটি জটিল চেইন জুড়ে নিরাপদে পরিচালনা করব যাতে এটি অন্যান্য কনকারেন্ট রিকোয়েস্টে লিক না করে?
এটি অ্যাসিঙ্ক্রোনাস কন্টেক্সট ম্যানেজমেন্টের মূল সমস্যা। এটি সমাধান করতে ব্যর্থ হলে কোড অগোছালো হয়, টাইট কাপলিং সৃষ্টি হয় এবং সবচেয়ে খারাপ ক্ষেত্রে, বিপর্যয়কর বাগ তৈরি হয় যেখানে একজন ব্যবহারকারীর রিকোয়েস্ট থেকে প্রাপ্ত ডেটা অন্যজনের ডেটাকে দূষিত করে। এটি প্রচলিত থ্রেডবিহীন বিশ্বে 'থ্রেড সেফটি' অর্জনের একটি প্রশ্ন।
এই ব্যাপক নির্দেশিকাটি জাভাস্ক্রিপ্ট ইকোসিস্টেমে এই সমস্যার বিবর্তন অন্বেষণ করবে, বেদনাদায়ক ম্যানুয়াল ওয়ার্কঅ্যারাউন্ড থেকে শুরু করে Node.js-এর `AsyncLocalStorage` API দ্বারা সরবরাহকৃত আধুনিক, শক্তিশালী সমাধান পর্যন্ত। এটি কীভাবে কাজ করে, কেন এটি স্কেলেবল এবং অবজারভেবল সিস্টেম তৈরির জন্য অপরিহার্য এবং আপনার নিজের অ্যাপ্লিকেশনগুলিতে এটি কীভাবে কার্যকরভাবে প্রয়োগ করা যায় তা আমরা বিশ্লেষণ করব।
চ্যালেঞ্জ: অ্যাসিঙ্ক্রোনাস জাভাস্ক্রিপ্টে অদৃশ্য কন্টেক্সট
সমাধানটির প্রকৃত মূল্যায়ন করার জন্য, আমাদের প্রথমে সমস্যাটি গভীরভাবে বুঝতে হবে। জাভাস্ক্রিপ্টের এক্সিকিউশন মডেল একটি একক থ্রেড এবং একটি ইভেন্ট লুপের উপর ভিত্তি করে তৈরি। যখন একটি অ্যাসিঙ্ক্রোনাস অপারেশন (যেমন একটি ডেটাবেস ক্যোয়ারী, একটি HTTP কল, বা একটি `setTimeout`) শুরু হয়, তখন এটি একটি পৃথক সিস্টেমে (যেমন OS কার্নেল বা একটি থ্রেড পুল) অফলোড করা হয়। জাভাস্ক্রিপ্ট থ্রেড তখন অন্য কোড এক্সিকিউট করতে স্বাধীন থাকে। যখন অ্যাসিঙ্ক অপারেশনটি সম্পূর্ণ হয়, একটি কলব্যাক ফাংশন একটি কিউতে স্থাপন করা হয়, এবং কল স্ট্যাক খালি হলে ইভেন্ট লুপ এটি এক্সিকিউট করবে।
এই মডেলটি I/O-বাউন্ড ওয়ার্কলোডগুলির জন্য অবিশ্বাস্যভাবে কার্যকর, তবে এটি একটি উল্লেখযোগ্য চ্যালেঞ্জ তৈরি করে: একটি অ্যাসিঙ্ক অপারেশনের সূচনা এবং এর কলব্যাকের এক্সিকিউশনের মধ্যে এক্সিকিউশন কন্টেক্সট হারিয়ে যায়। কলব্যাকটি ইভেন্ট লুপের একটি নতুন টার্ন হিসাবে চলে, যা এটিকে শুরু করা কল স্ট্যাক থেকে বিচ্ছিন্ন থাকে।
আসুন একটি সাধারণ ওয়েব সার্ভার পরিস্থিতি দিয়ে এটি চিত্রিত করি। কল্পনা করুন যে আমরা একটি রিকোয়েস্টের জীবনচক্রের প্রতিটি সম্পাদিত কর্মের সাথে একটি অনন্য `requestID` লগ করতে চাই।
সরল পদ্ধতি (এবং কেন এটি ব্যর্থ হয়)
Node.js-এ নতুন একজন ডেভেলপার একটি গ্লোবাল ভ্যারিয়েবল ব্যবহার করার চেষ্টা করতে পারেন:
let globalRequestID = null;
// A simulated database call
function getUserFromDB(userId) {
console.log(`[${globalRequestID}] Fetching user ${userId}`);
return new Promise(resolve => setTimeout(() => resolve({ id: userId, name: 'Jane Doe' }), 100));
}
// A simulated external service call
async function getPermissions(user) {
console.log(`[${globalRequestID}] Getting permissions for ${user.name}`);
await new Promise(resolve => setTimeout(resolve, 150));
console.log(`[${globalRequestID}] Permissions retrieved`);
return { canEdit: true };
}
// Our main request handler logic
async function handleRequest(requestID) {
globalRequestID = requestID;
console.log(`[${globalRequestID}] Starting request processing`);
const user = await getUserFromDB(123);
const permissions = await getPermissions(user);
console.log(`[${globalRequestID}] Request finished successfully`);
}
// Simulate two concurrent requests arriving at nearly the same time
console.log("Simulating concurrent requests...");
handleRequest('req-A');
handleRequest('req-B');
আপনি যদি এই কোডটি চালান, আউটপুটটি একটি দূষিত বিশৃঙ্খল অবস্থা হবে:
Simulating concurrent requests...
[req-A] Starting request processing
[req-A] Fetching user 123
[req-B] Starting request processing
[req-B] Fetching user 123
[req-B] Getting permissions for Jane Doe
[req-B] Getting permissions for Jane Doe
[req-B] Permissions retrieved
[req-B] Request finished successfully
[req-B] Permissions retrieved
[req-B] Request finished successfully
লক্ষ্য করুন কীভাবে `req-B` অবিলম্বে `globalRequestID` কে ওভাররাইট করে। `req-A` এর অ্যাসিঙ্ক অপারেশনগুলি পুনরায় শুরু হওয়ার সময়, গ্লোবাল ভ্যারিয়েবলটি পরিবর্তিত হয়ে যায়, এবং পরবর্তী সমস্ত লগগুলি ভুলভাবে `req-B` দিয়ে ট্যাগ করা হয়। এটি একটি ক্লাসিক রেস কন্ডিশন এবং একটি কনকারেন্ট পরিবেশে কেন গ্লোবাল স্টেট বিপর্যয়কর তার একটি নিখুঁত উদাহরণ।
বেদনাদায়ক ওয়ার্কঅ্যারাউন্ড: প্রপ ড্রিলিং
সবচেয়ে সরাসরি, এবং তর্কসাপেক্ষে সবচেয়ে কষ্টকর, সমাধান হল কল চেইনের প্রতিটি ফাংশনের মাধ্যমে কন্টেক্সট অবজেক্টটি পাস করা। এটিকে প্রায়শই "প্রপ ড্রিলিং" বলা হয়।
// context is now an explicit parameter
function getUserFromDB(userId, context) {
console.log(`[${context.requestID}] Fetching user ${userId}`);
// ...
}
async function getPermissions(user, context) {
console.log(`[${context.requestID}] Getting permissions for ${user.name}`);
// ...
}
async function handleRequest(requestID) {
const context = { requestID };
console.log(`[${context.requestID}] Starting request processing`);
const user = await getUserFromDB(123, context);
const permissions = await getPermissions(user, context);
console.log(`[${context.requestID}] Request finished successfully`);
}
এটি কাজ করে। এটি নিরাপদ এবং অনুমানযোগ্য। তবে, এর প্রধান অসুবিধাগুলি হল:
- বয়েলারপ্লেট: প্রতিটি ফাংশন সিগনেচার, টপ-লেভেল কন্ট্রোলার থেকে লোয়েস্ট-লেভেল ইউটিলিটি পর্যন্ত, `context` অবজেক্ট গ্রহণ ও পাস করার জন্য অবশ্যই পরিবর্তন করতে হবে।
- টাইট কাপলিং: যে ফাংশনগুলির নিজেদের কন্টেক্সটের প্রয়োজন নেই কিন্তু কল চেইনের অংশ, সেগুলিকে এটি সম্পর্কে জানতে বাধ্য করা হয়। এটি ক্লিন আর্কিটেকচার এবং সেপারেশন অফ কনসার্নস এর নীতি লঙ্ঘন করে।
- ত্রুটিপ্রবণ: একজন ডেভেলপারের জন্য এক স্তর নিচে কন্টেক্সট পাস করতে ভুলে যাওয়া সহজ, যা পরবর্তী সমস্ত কলের জন্য চেইন ভেঙে দেয়।
বহু বছর ধরে, Node.js সম্প্রদায় এই সমস্যা নিয়ে সংগ্রাম করেছে, যার ফলে বিভিন্ন লাইব্রেরি-ভিত্তিক সমাধান তৈরি হয়েছিল।
পূর্বসূরী এবং প্রাথমিক প্রচেষ্টা: আধুনিক কন্টেক্সট ম্যানেজমেন্টের পথ
অপসারণকৃত `domain` মডিউল
Node.js-এর প্রাথমিক সংস্করণগুলি `domain` মডিউলকে ত্রুটিগুলি পরিচালনা করতে এবং I/O অপারেশনগুলিকে গ্রুপ করতে একটি উপায় হিসাবে প্রবর্তন করেছিল। এটি অ্যাসিঙ্ক্রোনাস কলব্যাকগুলিকে একটি সক্রিয় "ডোমেন"-এর সাথে ইমপ্লিসিটলি আবদ্ধ করত, যা কন্টেক্সট ডেটা ধারণ করতে পারত। যদিও এটি আশাব্যঞ্জক মনে হয়েছিল, তবে এতে উল্লেখযোগ্য পারফরম্যান্স ওভারহেড ছিল এবং এটি কুখ্যাতভাবে অবিশ্বস্ত ছিল, যেখানে কন্টেক্সট হারিয়ে যেতে পারত এমন সূক্ষ্ম প্রান্তিক ক্ষেত্র ছিল। এটি অবশেষে অপ্রচলিত করা হয়েছে এবং আধুনিক অ্যাপ্লিকেশনগুলিতে ব্যবহার করা উচিত নয়।
কন্টিনিউশন-লোকাল স্টোরেজ (CLS) লাইব্রেরি
সম্প্রদায় "কন্টিনিউশন-লোকাল স্টোরেজ" (Continuation-Local Storage) নামক একটি ধারণা নিয়ে এগিয়ে আসে। `cls-hooked` এর মতো লাইব্রেরিগুলি খুব জনপ্রিয় হয়ে ওঠে। তারা Node-এর অভ্যন্তরীণ `async_hooks` API ব্যবহার করে কাজ করত, যা অ্যাসিঙ্ক্রোনাস রিসোর্সগুলির জীবনচক্রে দৃশ্যমানতা প্রদান করে।
এই লাইব্রেরিগুলি মূলত Node.js-এর অ্যাসিঙ্ক প্রিমিটিভগুলিকে প্যাচ বা "মাঙ্কি-প্যাচ" করত বর্তমান কন্টেক্সট ট্র্যাক করার জন্য। যখন একটি অ্যাসিঙ্ক অপারেশন শুরু হত, তখন লাইব্রেরিটি বর্তমান কন্টেক্সট সংরক্ষণ করত। যখন এর কলব্যাক চালানোর জন্য নির্ধারিত হত, তখন লাইব্রেরিটি কলব্যাক এক্সিকিউট করার আগে সেই কন্টেক্সটটি পুনরুদ্ধার করত।
যদিও `cls-hooked` এবং অনুরূপ লাইব্রেরিগুলি অপরিহার্য ছিল, তবুও সেগুলি একটি ওয়ার্কঅ্যারাউন্ড ছিল। তারা অভ্যন্তরীণ API-এর উপর নির্ভর করত যা পরিবর্তিত হতে পারত, তাদের নিজস্ব পারফরম্যান্স প্রভাব থাকতে পারত এবং যদি নিখুঁতভাবে কনফিগার না করা হয় তবে `async/await` এর মতো নতুন জাভাস্ক্রিপ্ট ভাষার বৈশিষ্ট্যগুলির সাথে সঠিকভাবে কন্টেক্সট ট্র্যাক করতে কখনও কখনও সংগ্রাম করত।
আধুনিক সমাধান: `AsyncLocalStorage` এর পরিচিতি
একটি স্থিতিশীল, মূল সমাধানের গুরুতর প্রয়োজনীয়তা উপলব্ধি করে, Node.js দল `AsyncLocalStorage` API চালু করে। এটি Node.js v14-এ স্থিতিশীল হয় এবং আজ অ্যাসিঙ্ক্রোনাস কন্টেক্সট পরিচালনার জন্য এটিই স্ট্যান্ডার্ড, প্রস্তাবিত উপায়। এটি একই শক্তিশালী `async_hooks` মেকানিজম ব্যবহার করে তবে একটি পরিষ্কার, নির্ভরযোগ্য এবং কার্যকর পাবলিক API সরবরাহ করে।
`AsyncLocalStorage` আপনাকে একটি বিচ্ছিন্ন স্টোরেজ কন্টেক্সট তৈরি করার অনুমতি দেয় যা অ্যাসিঙ্ক্রোনাস অপারেশনগুলির পুরো চেইন জুড়ে টিকে থাকে, কার্যকরভাবে প্রপ ড্রিলিং ছাড়াই একটি "রিকোয়েস্ট-লোকাল" স্টোরেজ তৈরি করে।
মূল ধারণা এবং পদ্ধতি
`AsyncLocalStorage` ব্যবহার কয়েকটি মূল পদ্ধতির উপর নির্ভর করে:
new AsyncLocalStorage(): আপনি ক্লাসটির একটি ইনস্ট্যান্স তৈরি করে শুরু করেন। সাধারণত, আপনি নির্দিষ্ট ধরণের কন্টেক্সটের জন্য একটি একক ইনস্ট্যান্স তৈরি করেন (যেমন, সমস্ত HTTP রিকোয়েস্টের জন্য একটি) এবং এটিকে একটি শেয়ার্ড মডিউল থেকে এক্সপোর্ট করেন।.run(store, callback): এটি এন্ট্রি পয়েন্ট। এটি দুটি আর্গুমেন্ট নেয়: একটি `store` (আপনি যে ডেটা উপলব্ধ করতে চান) এবং একটি `callback` ফাংশন। এটি অবিলম্বে কলব্যাক চালায়, এবং সেই কলব্যাকের এক্সিকিউশনের পুরো সিঙ্ক্রোনাস এবং অ্যাসিঙ্ক্রোনাস সময়কালের জন্য, প্রদত্ত `store` অ্যাক্সেসযোগ্য থাকে।.getStore(): এটি কীভাবে আপনি ডেটা পুনরুদ্ধার করেন। যখন `.run()` দ্বারা শুরু করা অ্যাসিঙ্ক্রোনাস ফ্লো-এর অংশ এমন একটি ফাংশনের মধ্যে থেকে কল করা হয়, তখন এটি সেই কন্টেক্সটের সাথে সম্পর্কিত `store` অবজেক্টটি প্রদান করে। যদি এমন একটি কন্টেক্সটের বাইরে থেকে কল করা হয়, তবে এটি `undefined` প্রদান করে।
আসুন `AsyncLocalStorage` ব্যবহার করে আমাদের পূর্ববর্তী উদাহরণটি রিফ্যাক্টর করি।
const { AsyncLocalStorage } = require('async_hooks');
// 1. Create a single, shared instance
const asyncLocalStorage = new AsyncLocalStorage();
// 2. Our functions no longer need a 'context' parameter
function getUserFromDB(userId) {
const store = asyncLocalStorage.getStore();
console.log(`[${store.requestID}] Fetching user ${userId}`);
return new Promise(resolve => setTimeout(() => resolve({ id: userId, name: 'Jane Doe' }), 100));
}
async function getPermissions(user) {
const store = asyncLocalStorage.getStore();
console.log(`[${store.requestID}] Getting permissions for ${user.name}`);
await new Promise(resolve => setTimeout(resolve, 150));
console.log(`[${store.requestID}] Permissions retrieved`);
return { canEdit: true };
}
async function businessLogic() {
const store = asyncLocalStorage.getStore();
console.log(`[${store.requestID}] Starting request processing`);
const user = await getUserFromDB(123);
const permissions = await getPermissions(user);
console.log(`[${store.requestID}] Request finished successfully`);
}
// 3. The main request handler uses .run() to establish the context
function handleRequest(requestID) {
const context = { requestID };
asyncLocalStorage.run(context, () => {
// Everything called from here, sync or async, has access to the context
businessLogic();
});
}
console.log("Simulating concurrent requests with AsyncLocalStorage...");
handleRequest('req-A');
handleRequest('req-B');
আউটপুট এখন সম্পূর্ণরূপে সঠিক এবং বিচ্ছিন্ন:
Simulating concurrent requests with AsyncLocalStorage...
[req-A] Starting request processing
[req-A] Fetching user 123
[req-B] Starting request processing
[req-B] Fetching user 123
[req-A] Getting permissions for Jane Doe
[req-B] Getting permissions for Jane Doe
[req-A] Permissions retrieved
[req-A] Request finished successfully
[req-B] Permissions retrieved
[req-B] Request finished successfully
পরিষ্কার বিচ্ছিন্নতা লক্ষ্য করুন। `getUserFromDB` এবং `getPermissions` ফাংশনগুলি পরিষ্কার; তাদের `context` প্যারামিটার নেই। যখন তাদের প্রয়োজন হয় তখন তারা `getStore()` এর মাধ্যমে কন্টেক্সট অনুরোধ করতে পারে। রিকোয়েস্টের এন্ট্রি পয়েন্টে (`handleRequest`) একবার কন্টেক্সট স্থাপিত হয় এবং পুরো অ্যাসিঙ্ক্রোনাস চেইন জুড়ে এটি ইমপ্লিসিটলি বহন করা হয়।
ব্যবহারিক বাস্তবায়ন: Express.js সহ একটি বাস্তব-বিশ্বের উদাহরণ
`AsyncLocalStorage` এর সবচেয়ে শক্তিশালী ব্যবহারগুলির মধ্যে একটি হল Express.js-এর মতো ওয়েব সার্ভার ফ্রেমওয়ার্কগুলিতে রিকোয়েস্ট-স্কোপড কন্টেক্সট পরিচালনা করা। আসুন একটি ব্যবহারিক উদাহরণ তৈরি করি।
দৃশ্যকল্প
আমাদের একটি ওয়েব অ্যাপ্লিকেশন রয়েছে যা নিম্নলিখিতগুলি করতে হবে:
- ট্রেসেবিলিটির জন্য প্রতিটি ইনকামিং রিকোয়েস্টে একটি অনন্য `requestID` অ্যাসাইন করা।
- একটি কেন্দ্রীভূত লগিং পরিষেবা থাকা যা স্বয়ংক্রিয়ভাবে এই `requestID` কে প্রতিটি লগ মেসেজে অন্তর্ভুক্ত করে, ম্যানুয়ালি পাস না করে।
- অথেন্টিকেশন এর পরে ব্যবহারকারীর তথ্য ডাউনস্ট্রিম পরিষেবাগুলিতে উপলব্ধ করা।
ধাপ 1: একটি কেন্দ্রীয় কন্টেক্সট পরিষেবা তৈরি করুন
`AsyncLocalStorage` ইনস্ট্যান্স পরিচালনা করে এমন একটি একক মডিউল তৈরি করা একটি সেরা অনুশীলন।
ফাইল: `context.js`
const { AsyncLocalStorage } = require('async_hooks');
// This instance is shared across the entire application
const requestContext = new AsyncLocalStorage();
module.exports = { requestContext };
ধাপ 2: কন্টেক্সট স্থাপনের জন্য একটি মিডলওয়্যার তৈরি করুন
এক্সপ্রেসে, `.run()` ব্যবহার করে পুরো রিকোয়েস্ট লাইফসাইকেল র্যাপ করার জন্য মিডলওয়্যার হল উপযুক্ত স্থান।
ফাইল: `app.js` (বা আপনার প্রধান সার্ভার ফাইল)
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { requestContext } = require('./context');
const logger = require('./logger');
const userService = require('./userService');
const app = express();
// Middleware to establish the async context for each request
app.use((req, res, next) => {
const store = {
requestID: uuidv4(),
user: null // Will be populated after authentication
};
// .run() wraps the rest of the request handling (next())
requestContext.run(store, () => {
logger.info(`Request started: ${req.method} ${req.url}`);
next();
});
});
// A simulated authentication middleware
app.use((req, res, next) => {
// In a real app, you'd verify a token here
const store = requestContext.getStore();
if (store) {
store.user = { id: 'user-123', name: 'Alice' };
}
next();
});
// Your application routes
app.get('/user', async (req, res) => {
logger.info('Handling /user request');
try {
const userProfile = await userService.getProfile();
res.json(userProfile);
} catch (error) {
logger.error('Failed to get user profile', { error: error.message });
res.status(500).send('Internal Server Error');
}
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
ধাপ 3: একটি লগার যা স্বয়ংক্রিয়ভাবে কন্টেক্সট ব্যবহার করে
এখানেই জাদু ঘটে। আমাদের লগার এক্সপ্রেস, রিকোয়েস্ট বা ব্যবহারকারী সম্পর্কে সম্পূর্ণভাবে অজ্ঞ থাকতে পারে। এটি কেবল আমাদের কেন্দ্রীয় কন্টেক্সট পরিষেবা সম্পর্কে জানে।
ফাইল: `logger.js`
const { requestContext } = require('./context');
function log(level, message, details = {}) {
const store = requestContext.getStore();
const requestID = store ? store.requestID : 'N/A';
const logObject = {
timestamp: new Date().toISOString(),
level: level.toUpperCase(),
requestID,
message,
...details
};
console.log(JSON.stringify(logObject));
}
const logger = {
info: (message, details) => log('info', message, details),
error: (message, details) => log('error', message, details),
warn: (message, details) => log('warn', message, details),
};
module.exports = logger;
ধাপ 4: একটি গভীরভাবে নেস্টেড পরিষেবা যা কন্টেক্সট অ্যাক্সেস করে
আমাদের `userService` এখন কন্ট্রোলার থেকে কোনো প্যারামিটার পাস না করেই রিকোয়েস্ট-নির্দিষ্ট তথ্য আত্মবিশ্বাসের সাথে অ্যাক্সেস করতে পারে।
ফাইল: `userService.js`
const { requestContext } = require('./context');
const logger = require('./logger');
// A simulated database call
async function fetchUserDetailsFromDB(userId) {
logger.info(`Fetching details for user ${userId} from database.`);
await new Promise(resolve => setTimeout(resolve, 50));
return { company: 'Global Tech Inc.', country: 'Worldwide' };
}
async function getProfile() {
const store = requestContext.getStore();
if (!store || !store.user) {
throw new Error('User not authenticated');
}
logger.info(`Building profile for user: ${store.user.name}`);
// Even deeper async calls will maintain context
const details = await fetchUserDetailsFromDB(store.user.id);
return {
id: store.user.id,
name: store.user.name,
...details
};
}
module.exports = { getProfile };
আপনি যখন এই সার্ভারটি চালান এবং `http://localhost:3000/user` এ একটি রিকোয়েস্ট করেন, তখন আপনার কনসোল লগগুলি স্পষ্টভাবে দেখাবে যে একই `requestID` প্রতিটি লগ মেসেজে উপস্থিত রয়েছে, প্রাথমিক মিডলওয়্যার থেকে গভীরতম ডেটাবেস ফাংশন পর্যন্ত, যা নিখুঁত কন্টেক্সট আইসোলেশন প্রদর্শন করে।
থ্রেড সেফটি এবং কন্টেক্সট আইসোলেশন ব্যাখ্যা করা হয়েছে
এখন আমরা "থ্রেড সেফটি" পদটিতে ফিরে যেতে পারি। Node.js-এ, আসল সমান্তরাল ফ্যাশনে একই সময়ে একাধিক থ্রেডের একই মেমরি অ্যাক্সেস করার বিষয়ে উদ্বেগ নেই। পরিবর্তে, এটি ইভেন্ট লুপের মাধ্যমে একক প্রধান থ্রেডে একাধিক কনকারেন্ট অপারেশন (রিকোয়েস্ট) তাদের এক্সিকিউশনকে ইন্টারলিভ করা নিয়ে। "সেফটি" সমস্যাটি হল একটি অপারেশনের কন্টেক্সট অন্যটিতে লিক না হওয়া নিশ্চিত করা।
`AsyncLocalStorage` অ্যাসিঙ্ক্রোনাস রিসোর্সগুলির সাথে কন্টেক্সট লিঙ্ক করার মাধ্যমে এটি অর্জন করে।
এখানে কী ঘটে তার একটি সরলীকৃত মানসিক মডেল দেওয়া হলো:
- যখন `asyncLocalStorage.run(store, ...)` কল করা হয়, Node.js অভ্যন্তরীণভাবে বলে: "আমি এখন একটি বিশেষ কন্টেক্সটে প্রবেশ করছি। এই কন্টেক্সটের ডেটা হল `store`।" এটি এই এক্সিকিউশন কন্টেক্সটকে একটি অনন্য অভ্যন্তরীণ আইডি অ্যাসাইন করে।
- এই কন্টেক্সট সক্রিয় থাকা অবস্থায় নির্ধারিত যেকোনো অ্যাসিঙ্ক্রোনাস অপারেশন (যেমন, একটি `new Promise`, `setTimeout`, `fs.readFile`) এই অনন্য কন্টেক্সট আইডি দিয়ে ট্যাগ করা হয়।
- পরে, যখন ইভেন্ট লুপ এই ট্যাগ করা অপারেশনগুলির একটির জন্য একটি কলব্যাক তুলে নেয়, Node.js ট্যাগটি পরীক্ষা করে। এটি বলে, "আহ, এই কলব্যাকটি কন্টেক্সট আইডি X এর অন্তর্গত। আমি এখন কলব্যাকটি এক্সিকিউট করার আগে সেই কন্টেক্সটটি পুনরুদ্ধার করব।"
- এই পুনরুদ্ধার কলব্যাকের মধ্যে `getStore()` এর জন্য সঠিক `store` উপলব্ধ করে।
- যখন আরেকটি রিকোয়েস্ট আসে, তখন তার `.run()` কলটি একটি ভিন্ন অভ্যন্তরীণ আইডি সহ একটি সম্পূর্ণ নতুন কন্টেক্সট তৈরি করে এবং এর অ্যাসিঙ্ক অপারেশনগুলি এই নতুন আইডি দিয়ে ট্যাগ করা হয়, যা জিরো ওভারল্যাপ নিশ্চিত করে।
এই শক্তিশালী, নিম্ন-স্তরের মেকানিজম নিশ্চিত করে যে ইভেন্ট লুপ বিভিন্ন রিকোয়েস্ট থেকে কলব্যাকগুলির এক্সিকিউশনকে কীভাবে ইন্টারলিভ করুক না কেন, `getStore()` সর্বদা সেই কন্টেক্সটের জন্য ডেটা প্রদান করবে যেখানে সেই কলব্যাকের অ্যাসিঙ্ক অপারেশনটি মূলত নির্ধারিত হয়েছিল।
পারফরম্যান্স বিবেচনা এবং সেরা অনুশীলন
যদিও `AsyncLocalStorage` অত্যন্ত অপ্টিমাইজ করা হয়েছে, এটি বিনামূল্যে নয়। অন্তর্নিহিত `async_hooks` প্রতিটি অ্যাসিঙ্ক্রোনাস রিসোর্সের তৈরি এবং সম্পূর্ণ হওয়ার সাথে কিছুটা ওভারহেড যোগ করে। তবে, বেশিরভাগ অ্যাপ্লিকেশনের জন্য, বিশেষত I/O-বাউন্ডগুলির জন্য, কোড পরিষ্কারতা, রক্ষণাবেক্ষণযোগ্যতা এবং পর্যবেক্ষণযোগ্যতার সুবিধার তুলনায় এই ওভারহেড নগণ্য।
- একবার ইনস্ট্যান্স তৈরি করুন: আপনার অ্যাপ্লিকেশনের টপ লেভেলে `AsyncLocalStorage` ইনস্ট্যান্স তৈরি করুন এবং সেগুলিকে পুনরায় ব্যবহার করুন। প্রতি রিকোয়েস্টে নতুন ইনস্ট্যান্স তৈরি করবেন না।
- স্টোরকে স্লিম রাখুন: কন্টেক্সট স্টোর ক্যাশে নয়। আইডি, টোকেন বা লাইটওয়েট ব্যবহারকারী অবজেক্টের মতো ছোট, অপরিহার্য ডেটার জন্য এটি ব্যবহার করুন। বড় পেলোড সংরক্ষণ করা এড়িয়ে চলুন।
- স্পষ্ট এন্ট্রি পয়েন্টে কন্টেক্সট স্থাপন করুন: `.run()` কল করার সেরা জায়গা হল একটি স্বাধীন অ্যাসিঙ্ক্রোনাস ফ্লো-এর নির্দিষ্ট শুরুতে। এর মধ্যে সার্ভার রিকোয়েস্ট মিডলওয়্যার, মেসেজ কিউ কনজিউমার বা জব শিডিউলার অন্তর্ভুক্ত।
- ফায়ার-অ্যান্ড-ফরগেট অপারেশন সম্পর্কে সচেতন থাকুন: যদি আপনি একটি `run` কন্টেক্সটের মধ্যে একটি অ্যাসিঙ্ক অপারেশন শুরু করেন কিন্তু এটিকে `await` না করেন (যেমন, `doSomething().catch(...)`), তবে এটি এখনও সঠিকভাবে কন্টেক্সট উত্তরাধিকারসূত্রে পাবে। এটি ব্যাকগ্রাউন্ড কাজগুলির জন্য একটি শক্তিশালী বৈশিষ্ট্য যা তাদের উৎস পর্যন্ত ট্রেস করা প্রয়োজন।
- নেস্টিং বুঝুন: আপনি `.run()` কলগুলিকে নেস্ট করতে পারেন। একটি বিদ্যমান কন্টেক্সটের মধ্যে থেকে `.run()` কল করলে একটি নতুন, নেস্টেড কন্টেক্সট তৈরি হবে। `getStore()` তখন সবচেয়ে ভেতরের স্টোরটি প্রদান করবে। এটি একটি নির্দিষ্ট সাব-অপারেশনের জন্য কন্টেক্সটকে সাময়িকভাবে ওভাররাইড বা যোগ করার জন্য কার্যকর হতে পারে।
Node.js এর বাইরে: `AsyncContext` এর সাথে ভবিষ্যৎ
অ্যাসিঙ্ক্রোনাস কন্টেক্সট ম্যানেজমেন্টের প্রয়োজনীয়তা শুধুমাত্র Node.js-এর জন্য অনন্য নয়। পুরো জাভাস্ক্রিপ্ট ইকোসিস্টেমের জন্য এর গুরুত্ব উপলব্ধি করে, `AsyncContext` নামক একটি আনুষ্ঠানিক প্রস্তাব TC39 কমিটির মাধ্যমে তার পথ তৈরি করছে, যা জাভাস্ক্রিপ্ট (ECMAScript) কে মানসম্মত করে।
`AsyncContext` প্রস্তাবটি Node.js-এর `AsyncLocalStorage` দ্বারা গভীরভাবে অনুপ্রাণিত এবং এর লক্ষ্য হল প্রায় অভিন্ন একটি API সরবরাহ করা যা ওয়েব ব্রাউজার সহ সমস্ত আধুনিক জাভাস্ক্রিপ্ট পরিবেশে উপলব্ধ হবে। এটি ফ্রন্ট-এন্ড ডেভেলপমেন্টের জন্য শক্তিশালী ক্ষমতা উন্মোচন করতে পারে, যেমন কনকারেন্ট রেন্ডারিংয়ের সময় React-এর মতো জটিল ফ্রেমওয়ার্কগুলিতে কন্টেক্সট পরিচালনা করা বা জটিল কম্পোনেন্ট ট্রি জুড়ে ব্যবহারকারী ইন্টারঅ্যাকশন ফ্লো ট্র্যাক করা।
উপসংহার: ডিক্লারেটিভ এবং শক্তিশালী অ্যাসিঙ্ক্রোনাস কোড গ্রহণ করা
অ্যাসিঙ্ক্রোনাস অপারেশন জুড়ে স্টেট পরিচালনা করা একটি প্রতারণামূলকভাবে জটিল সমস্যা যা বছরের পর বছর ধরে জাভাস্ক্রিপ্ট ডেভেলপারদের চ্যালেঞ্জ করেছে। ম্যানুয়াল প্রপ ড্রিলিং এবং ভঙ্গুর কমিউনিটি লাইব্রেরি থেকে `AsyncLocalStorage` এর আকারে একটি মূল, স্থিতিশীল API-এর দিকে যাত্রা Node.js প্ল্যাটফর্মের একটি উল্লেখযোগ্য পরিপক্কতা চিহ্নিত করে।
নিরাপদ, বিচ্ছিন্ন এবং ইমপ্লিসিটলি প্রচারিত কন্টেক্সটের জন্য একটি মেকানিজম সরবরাহ করে, `AsyncLocalStorage` আমাদের পরিষ্কার, আরও ডিকাপলড এবং আরও রক্ষণাবেক্ষণযোগ্য কোড লিখতে সক্ষম করে। এটি আধুনিক, অবজারভেবল সিস্টেম তৈরির জন্য একটি ভিত্তিপ্রস্তর যেখানে ট্রেসিং, মনিটরিং এবং লগিং পরবর্তীতে যোগ করা বিষয় নয়, বরং অ্যাপ্লিকেশনটির ফ্যাব্রিকে বোনা থাকে।
আপনি যদি এমন কোনো অ-তুচ্ছ Node.js অ্যাপ্লিকেশন তৈরি করেন যা কনকারেন্ট অপারেশন পরিচালনা করে, তবে `AsyncLocalStorage` গ্রহণ করা এখন শুধু একটি সেরা অনুশীলন নয় – এটি একটি অ্যাসিঙ্ক্রোনাস বিশ্বে দৃঢ়তা এবং স্কেলেবিলিটি অর্জনের জন্য একটি মৌলিক কৌশল।