.NET-এ কাজ করার সময় এই কনসেপ্টটা ক্লিয়ার থাকা খুব জরুরি।
আগে চলো উচ্চারণটা দেখে নিই: Socket Exhaustion-এর বাংলা উচ্চারণ হলো: সকেট এগজশন (বা এক্সজশন)। Exhaustion মানে হলো কোনো কিছু নিঃশেষ হয়ে যাওয়া বা ফুরিয়ে যাওয়া।
এবার আসি মূল কথায়।
সকেট এগজশন (Socket Exhaustion) কী?
যখন তোমার অ্যাপ্লিকেশন অন্য কোনো সার্ভারে রিকোয়েস্ট পাঠায় (যেমন কোনো থার্ড-পার্টি API কল করা), তখন অপারেটিং সিস্টেম (Fedora হোক বা Windows) তাকে ডেটা আদান-প্রদানের জন্য একটা ভার্চুয়াল রাস্তা বা পোর্ট তৈরি করে দেয়। এই কানেকশন পয়েন্টকেই বলা হয় সকেট (Socket)।
অপারেটিং সিস্টেমের কাছে সকেটের সংখ্যা লিমিটেড (প্রায় ৬৫,৫৩৫টি)। তুমি যখন new HttpClient() তৈরি করে কাজ শেষে সেটা Dispose() করে দাও, তখন OS সাথে সাথে সেই সকেটটা পুরোপুরি বন্ধ করে না। সে কানেকশনটাকে TIME_WAIT স্টেটে (প্রায় ৪ মিনিটের জন্য) হোল্ড করে রাখে, যাতে নেটওয়ার্কে আটকে থাকা কোনো বিলম্বিত ডাটা প্যাকেট ঠিকমতো পৌঁছাতে পারে।
এখন, যদি তোমার অ্যাপ্লিকেশনে প্রচুর ট্রাফিক আসে এবং তুমি প্রতিটা রিকোয়েস্টের জন্য বারবার new HttpClient() বানাও আর ডিসপোজ করো, তাহলে একসময় OS-এর হাজার হাজার সকেট TIME_WAIT স্টেটে গিয়ে জ্যাম হয়ে থাকবে। একপর্যায়ে নতুন রিকোয়েস্ট করার জন্য OS আর কোনো ফাঁকা সকেট দিতে পারবে না এবং তোমার অ্যাপ SocketException এরর দিয়ে ক্র্যাশ করবে। একেই বলে সকেট এগজশন।
মেমোরি লিক (Memory Leak) কেন হবে?
১. আনম্যানেজড রিসোর্স: HttpClient হুড-এর নিচে (under the hood) অনেকগুলো কানেকশন এবং আনম্যানেজড রিসোর্স (Unmanaged resources) ধরে রাখে। তুমি যদি বারবার নতুন ইন্সট্যান্স বানাও কিন্তু ঠিকমতো Dispose না করো, তবে .NET-এর গারবেজ কালেক্টর (Garbage Collector) এগুলো সহজে মেমোরি থেকে মুছতে পারে না। ফলে র্যামে অযথাই জায়গা দখল হতে থাকে।
২. সকেটের চাপ: ওই যে সকেটগুলো TIME_WAIT স্টেটে আটকে থাকছে, সেগুলো শুধু পোর্টই দখল করছে না, সাথে OS-এর মেমোরি এবং ব্যাকগ্রাউন্ড রিসোর্সও ব্লক করে রাখছে।
IHttpClientFactory কীভাবে বাঁচায়?
IHttpClientFactory বারবার নতুন কানেকশন বা সকেট তৈরি করার বদলে, ভেতরের HttpMessageHandler-গুলোকে একটা পুলে (Pool) ম্যানেজ করে। অর্থাৎ, সে পুরনো খোলা সকেটগুলোকেই নতুন রিকোয়েস্টের জন্য রিইউজ (Reuse) করে। কাজ শেষ হলে সে নিজেই সুন্দরভাবে কানেকশন ম্যানেজ করে। ফলে বারবার নতুন সকেট খুলতে হয় না, সকেট শেষও হয় না আর মেমোরি লিকের ঝামেলাও থাকে না।
ইন্টারনাল মেকানিজম
এখানে একটা খুব ইন্টারেস্টিং মেকানিজম কাজ করে। সত্যি বলতে, IHttpClientFactory কিন্তু HttpClient-কে সরাসরি রিইউজ করে না! শুনতে অবাক লাগলেও এটাই সত্যি।
তুমি যখন factory.CreateClient() কল করো, সে প্রতিবারই তোমাকে একদম নতুন একটা HttpClient অবজেক্ট দেয়। তাহলে রিইউজটা হচ্ছে কোথায়?
রিইউজ হয় HttpClient-এর ভেতরের ইঞ্জিনে। চলো বিষয়টা ধাপে ধাপে দেখি:
**১. আসল কারিগর: HttpMessageHandler**
HttpClient মূলত একটা বাইরের খোলস (Wrapper) ছাড়া আর কিছুই না। সার্ভারে রিকোয়েস্ট পাঠানো, কানেকশন ধরে রাখা, সকেট ম্যানেজমেন্ট—এই আসল কাজগুলো করে এর ভেতরে থাকা HttpMessageHandler (যেমন .NET-এ SocketsHttpHandler)।
২. হ্যান্ডলার পুল (Handler Pool)
IHttpClientFactory নিজের কাছে এই HttpMessageHandler-গুলোর একটা পুল বা সংগ্রহশালা (Pool) মেইনটেইন করে।
৩. রিইউজ হওয়ার পুরো সাইকেলটা:
- যখন তুমি রিকোয়েস্ট করার জন্য
CreateClient()কল করো, ফ্যাক্টরি একটা নতুনHttpClientবানায়। - এরপর সে তার পুল থেকে একটা রানিং
HttpMessageHandlerনিয়ে সেই নতুনHttpClient-এর ভেতরে ফিট করে দেয়। - তুমি যখন কাজ শেষে
HttpClient-টাকেDispose()করে দাও, তখন শুধু বাইরের খোলসটা (HttpClientঅবজেক্ট) মেমোরি থেকে মুছে যায় বা ডিসপোজ হয়। - কিন্তু ভেতরের
HttpMessageHandler-টা ডিসপোজ হয় না! ফ্যাক্টরি ওই হ্যান্ডলারটাকে আবার নিজের পুলে রেখে দেয়। - এরপর তোমার অ্যাপে নতুন কোনো রিকোয়েস্ট আসলে, ফ্যাক্টরি আবার ওই একই হ্যান্ডলারকে নতুন আরেকটা
HttpClient-এর ভেতরে দিয়ে দেয়।
যেহেতু HttpMessageHandler-টাই আসল সকেট কানেকশনগুলো ধরে রাখে, তাই ওটা রিইউজ হওয়া মানেই তোমার আগের খোলা সকেটগুলোই নতুন রিকোয়েস্টের জন্য রিইউজ হওয়া। বারবার নতুন করে OS-এর কাছে সকেট চাইতে হয় না।
তাহলে static HttpClient বানালেই তো হতো?
তোমার মনে প্রশ্ন আসতে পারে— “আমি নিজেই তো একটা static HttpClient বানিয়ে পুরো অ্যাপ্লিকেশনে ব্যবহার করতে পারি, তাহলে আর এত ফ্যাক্টরি, পুল—এসবের দরকার কী?”
স্ট্যাটিক ব্যবহার করলে সকেট এগজশন থেকে বাঁচা যায় ঠিকই, কিন্তু সেখানে DNS Staleness নামের আরেকটা বড় সমস্যা তৈরি হয়। ধরো, তুমি যে থার্ড-পার্টি API-তে কল করছ, তাদের সার্ভারের IP অ্যাড্রেস চেঞ্জ হয়ে গেল (যেমন লোড ব্যালেন্সিং বা রাউটিংয়ের কারণে)। তোমার অ্যাপের স্ট্যাটিক HttpClient সেটা ধরতে পারে না, সে তার আগের চেনা পুরনো আইপিতেই রিকোয়েস্ট পাঠাতে থাকে এবং রিকোয়েস্ট ফেইল করে।
IHttpClientFactory এই সমস্যারও সমাধান করে। সে পুলে থাকা হ্যান্ডলারগুলোকে নির্দিষ্ট সময় (যেমন ২ মিনিট) পর পর রিফ্রেশ বা রিনিউ করে। ফলে DNS আপডেট হলেও কানেকশনে কোনো সমস্যা হয় না।
HttpMessageHandler নাকি SocketsHttpHandler?
আসলে দুটোই সত্যি, তবে এদের লেয়ার আলাদা।
HttpMessageHandlerহলো একটা অ্যাবস্ট্রাক্ট ক্লাস (Base class)। ডটনেটে রিকোয়েস্ট পাঠানোর মূল ব্লু-প্রিন্ট হলো এটা।SocketsHttpHandlerহলো সেই অ্যাবস্ট্রাক্ট ক্লাসের কনক্রিট ইমপ্লিমেন্টেশন (Concrete implementation)। আধুনিক .NET-এ (Core 2.1 এর পর থেকে) হুড-এর নিচে বাই-ডিফল্ট এইSocketsHttpHandler-ই আসল কাজটা করে।
সহজ কথায়, IHttpClientFactory থিওরিটিক্যালি HttpMessageHandler-এর একটা পুল ম্যানেজ করে ঠিকই, কিন্তু বাস্তবে সেই হ্যান্ডলারগুলো আসলে SocketsHttpHandler-এরই ইন্সট্যান্স।
পুলে কয়টা হ্যান্ডলার থাকে?
এখানেই সবচেয়ে বড় ভুল ধারণাটা কাজ করে! অনেকেই ভাবে পুলে হয়তো একসাথে ৫০-১০০টা হ্যান্ডলার রেডি করা থাকে। কিন্তু বাস্তবে, একটি নির্দিষ্ট Named বা Typed ক্লায়েন্টের (ধরো, তুমি ChatrabashApi নামে একটা ক্লায়েন্ট কনফিগার করেছ) জন্য সাধারণত একটিই অ্যাকটিভ (Active) হ্যান্ডলার থাকে!
তাহলে ১টা হ্যান্ডলার দিয়ে হাজার হাজার ইউজারের রিকোয়েস্ট কীভাবে হ্যান্ডেল হয়?
কারণ, SocketsHttpHandler হলো থ্রেড-সেইফ (Thread-safe) এবং হাইলি কনকারেন্ট। যখন ১০০০ ইউজার একসাথে হিট করে, ফ্যাক্টরি ১০০০টা নতুন হালকা খোলস (HttpClient) বানায়, কিন্তু সেই ১০০০টা অবজেক্টই ওই একই অ্যাকটিভ হ্যান্ডলারকে পয়েন্ট করে থাকে।
হুট করে ইউজার বাড়লে বা কমলে কীভাবে মেইনটেইন করা হয়?
এখানে দুইটা আলাদা মেকানিজম একসাথে কাজ করে:
১. ট্রাফিক বাড়লে (High Load):
ইউজার যখন হুট করে বেড়ে যায়, তখন IHttpClientFactory নতুন করে হ্যান্ডলার বানায় না। বরং, ভেতরের যে SocketsHttpHandler-টি আছে, সে নিজের ভেতরে একটা TCP Connection Pool মেইনটেইন করে।
যখন একই সাথে অনেক রিকোয়েস্ট আসে, SocketsHttpHandler দরকার অনুযায়ী সার্ভারের সাথে নতুন নতুন ফিজিক্যাল TCP কানেকশন বা সকেট ওপেন করতে থাকে (এর একটা ডিফল্ট লিমিট থাকে, যেমন int.MaxValue, যা তুমি চাইলে MaxConnectionsPerServer দিয়ে কাস্টমাইজ করতে পারো)।
২. ট্রাফিক কমলে এবং DNS ম্যানেজমেন্ট (Lifecycle & Cleanup): ফ্যাক্টরি মূলত তৈরি হয়েছে DNS Staleness (সার্ভারের আইপি চেঞ্জ হয়ে যাওয়া) প্রবলেম সলভ করার জন্য। এটার লাইফসাইকেল এভাবে কাজ করে:
- Expiration (মেয়াদ শেষ): ডিফল্টভাবে প্রতিটা হ্যান্ডলারের লাইফটাইম থাকে ২ মিনিট (
HandlerLifetime)। ২ মিনিট পর ফ্যাক্টরি ওই অ্যাকটিভ হ্যান্ডলারটাকে “Expired” মার্ক করে দেয়। - নতুন হ্যান্ডলার তৈরি: ২ মিনিট পর যখনই নতুন কোনো ইউজার রিকোয়েস্ট করে (অর্থাৎ
CreateClient()কল হয়), ফ্যাক্টরি তখন ভবিষ্যতের রিকোয়েস্টগুলোর জন্য একদম নতুন একটা অ্যাকটিভ হ্যান্ডলার বানায়। - ব্যাকগ্রাউন্ড ক্লিনআপ: পুরনো “Expired” হ্যান্ডলারটাকে ফ্যাক্টরি সাথে সাথে ডিলিট করে দেয় না। কারণ, হয়তো আগের কোনো রিকোয়েস্ট এখনো ওই হ্যান্ডলার দিয়ে ডেটা প্রসেস করছে। ফ্যাক্টরির ভেতরে একটা ব্যাকগ্রাউন্ড ক্লিনার থাকে। যখন সে দেখে পুরনো হ্যান্ডলারটা আর কোনো
HttpClientব্যবহার করছে না (অর্থাৎ গারবেজ কালেক্টর খোলসগুলোকে মেমোরি থেকে মুছে দিয়েছে), তখন সে পুরনো হ্যান্ডলারটাকেDispose()করে দেয়। এতে করে অব্যবহৃত কানেকশনগুলো ক্লোজ হয়ে যায় এবং মেমোরি ফ্রি হয়।
সারসংক্ষেপ:
IHttpClientFactoryমেইনটেইন করে Handler Pool (DNS আপডেট রাখার জন্য, প্রতি ২ মিনিট পর পর রিফ্রেশ হয়)।- আর ভেতরের
SocketsHttpHandlerমেইনটেইন করে Connection Pool (হুট করে ইউজার বাড়লে বা কমলে সকেট কানেকশন ব্যালেন্স করার জন্য)।