WebSocket ডেমো ওয়েব ডেভেলপমেন্টের সবচেয়ে তৃপ্তিদায়ক পাঁচ মিনিটের একটা। একটা connection খোলেন, একটা মেসেজ push করেন, দেখেন সেটা তাৎক্ষণিকভাবে আরেকটা স্ক্রিনে দেখা দিচ্ছে। জাদুর মতো লাগে ও রিয়েল-টাইমকে সহজ দেখায়।
তারপর এটা বাস্তব ব্যবহারকারীদের কাছে শিপ করেন ও আবিষ্কার করেন ডেমো আপনাকে মিথ্যা বলছিল। happy path নিয়ে নয় — ওই অংশটা সত্যি — কিন্তু তার চারপাশের সবকিছু নিয়ে। আমি এখন কয়েকটা event-চালিত রিয়েল-টাইম সিস্টেম তৈরি করেছি, যার মধ্যে একটা ফিজিক্যাল ডিভাইসকে অভ্যন্তরীণ সার্ভিসের সঙ্গে sync করে ও প্রায় সব আসল ইঞ্জিনিয়ারিং ছিল সেই অংশগুলোতে যা ডেমো এড়িয়ে যায়।
প্রথমত, আপনার কি আদৌ socket দরকার ছিল?
দামের কথার আগে সৎ প্রশ্নটা: অনেক "রিয়েল-টাইম" ফিচারের persistent connection আদৌ দরকার নেই। যদি আপডেট বেশিরভাগ এক দিকে যায় — সার্ভার থেকে ক্লায়েন্ট — Server-Sent Events সহজ, সাধারণ HTTP-তে চলে ও নিজে থেকেই reconnect করে। যদি ডেটা প্রতি ত্রিশ সেকেন্ডে বদলায় আর ছোট দেরি কেউ খেয়াল করবে না, polling ঠিক আছে আর আপনি ভালো ঘুমাবেন।
WebSocket-এর দিকে হাত বাড়াই যখন ট্রাফিক সত্যিই দ্বিমুখী আর latency সত্যিই গুরুত্বপূর্ণ — লাইভ সহযোগিতা, তাৎক্ষণিক status পরিবর্তন, একটা ডিভাইস উপরে event push করছে আর সার্ভার নিচে command push করছে। সেই দরকার দেখাতে না পারলে এই লেখার বাকিটা আপনি স্বেচ্ছায় নিচ্ছেন এমন দামের একটা তালিকা।
Reconnection আপনার সমস্যা, ব্রাউজারের নয়
ডেমো যা লুকায় তার প্রথমটা: connection ড্রপ করে। ল্যাপটপ ঘুমায়, ফোন নেটওয়ার্ক বদলায়, WiFi হেঁচকি দেয়, load balancer রিসাইকেল করে। ব্রাউজার এটা চুপচাপ আপনার জন্য ঠিক করবে না। ড্রপ হওয়া socket ড্রপ হয়েই থাকে যতক্ষণ না আপনার কোড খেয়াল করে ও কিছু করে।
তাই আপনি reconnection লজিক লেখেন ও তখন শেখেন কেন এটা একটা retry লুপের চেয়ে কঠিন। প্রতিটি ক্লায়েন্ট ড্রপ হওয়ার মুহূর্তেই reconnect করলে সংক্ষিপ্ত সার্ভার ঝলক একটা হুড়োহুড়িতে পরিণত হয় — হাজারটা ক্লায়েন্ট একই সেকেন্ডে দরজায় হাতুড়ি পেটায়, হেঁচকিকে outage-এ পরিণত করে। কিছুটা jitter সহ backoff দরকার যাতে ক্লায়েন্টরা ধাপে ধাপে ফেরে, একসঙ্গে নয়।
function connectWithBackoff(url: string) {
let attempt = 0;
const open = () => {
const ws = new WebSocket(url);
ws.onopen = () => (attempt = 0);
ws.onclose = () => {
const base = Math.min(1000 * 2 ** attempt, 30_000);
const jitter = Math.random() * base; // spread the herd out
attempt++;
setTimeout(open, base + jitter);
};
};
open();
}আর reconnect করা মানে কেবল socket আবার খোলা নয় — এটা state আবার প্রতিষ্ঠা করা। এই ক্লায়েন্ট চলে যাওয়াকালীন কী মিস করল? কোন room-এ ছিল? এমন একটা reconnect যা পাইপ আবার খোলে কিন্তু প্রসঙ্গ ভুলে যায়, সেটা এমন একটা বাগ যা শুধু মাঠে দেখা দেয়।
এমন কিছুর authentication যা তার token-এর চেয়ে বেশি বাঁচে
HTTP auth প্রতি-request-এর ব্যাপার — প্রতিটি request একটা token বহন করে আর আপনি সেটা চেক করেন। একটা WebSocket হলো একটা দীর্ঘস্থায়ী connection যা আপনি একবারই authenticate করেন, handshake-এ ও তারপর এটা কেবল... খোলা থাকে। সম্ভবত ঘণ্টার পর ঘণ্টা। সম্ভবত আপনি যে token গ্রহণ করেছিলেন তা মেয়াদোত্তীর্ণ হওয়ার অনেক পরেও।
তাই ঠিক করতে হয় ইতিমধ্যে খোলা একটা connection-এর জন্য expiry মানে কী। এটা ড্রপ করে reconnect করাবেন? ক্লায়েন্টকে socket দিয়েই token refresh করতে দিয়ে জায়গায় re-validate করবেন? একটাও একক সঠিক উত্তর নেই, কিন্তু একটা ভুল উত্তর আছে: handshake-এ authenticate করে আর কখনো এটা নিয়ে না ভাবা, যাতে বাতিল করা session ডেটা stream করতে থাকে কারণ কেউ socket-কে বলেনি ব্যবহারকারী লগ আউট করেছে।
gap সমস্যা: কেউ শুনছিল না তখন fire হওয়া event
এটাই সবচেয়ে জোরে কামড়ায়, কারণ প্রতিটি ডেমোতে এটা অদৃশ্য। একটা ক্লায়েন্ট আট সেকেন্ডের জন্য disconnect হয়। ওই আট সেকেন্ডে তিনটা event fire করে। ক্লায়েন্ট reconnect করে। ওই তিনটা event-এর কী হয়?
আপনার উত্তর যদি হয় "সার্ভার সেগুলো শূন্যে push করেছে," তাহলে আপনার একটা correctness বাগ আছে, UX খুঁত নয় — ক্লায়েন্টের ভিউ এখন নীরবে ভুল আর অন্য কিছু refresh করতে বাধ্য না করা পর্যন্ত ভুলই থাকবে। বেরোনোর দুটি সৎ উপায় আছে। at-least-once ডেলিভারি নিশ্চিত করতে পারেন: প্রতি-ক্লায়েন্ট event buffer করুন আর reconnect-এ gap replay করুন। অথবা — সাধারণত সহজ ও বেশি মজবুত — socket-কে একটা ঠেলা হিসেবে দেখুন, source of truth নয় ও প্রতিটি reconnect-এ ক্লায়েন্টকে authoritative state আবার fetch করান।
আমি দ্বিতীয় পদ্ধতির উপর প্রথমটার চেয়ে অনেক বেশি বার নির্ভর করি। "reconnect-এ সার্ভারের কাছে বর্তমান সত্য চাও" একটা পুরো শ্রেণির buffering ও ordering বাগ এড়িয়ে যায়। socket আপনাকে বলে কিছু বদলেছে, এসে দেখো; আসল state আসে এমন একটা সাধারণ request থেকে যা সঠিক করতে আপনি আগেই জানেন।
socket-কে একটা ঠেলা হিসেবে দেখুন, source of truth নয়। এটা আপনাকে বলে কিছু বদলেছে — এসে দেখো। আসল state আসে এমন একটা সাধারণ request থেকে যা সঠিক করতে আপনি আগেই জানেন।
দুই নম্বর সার্ভারে যে দেয়ালে ধাক্কা খান
উপরের সবকিছু একটা সার্ভারে দারুণভাবে কাজ করতে পারে, কারণ একটা সার্ভার প্রতিটি connection মেমরিতে রাখতে পারে আর ঠিক জানে কে সংযুক্ত। ক্ষমতা বা redundancy-র জন্য দ্বিতীয় instance চালানোর মুহূর্তে সেই অনুমান ভেঙে পড়ে।
User A instance 1-এ সংযুক্ত। User B instance 2-এ সংযুক্ত। B-র জন্য একটা event instance 1-এ উৎপন্ন হয়। Instance 1-এ B-র socket নেই — সেটা অন্য মেশিনে। In-memory connection state, যা সবকিছু সহজ করেছিল, এখন সক্রিয়ভাবে ভুল।
মানসম্মত সমাধান হলো routing কোনো এক process-এর মেমরিতে রাখা বন্ধ করে instance-গুলোর মধ্যে একটা শেয়ারড backbone বসানো — একটা pub/sub স্তর (Redis সাধারণ পছন্দ) যেখানে প্রতিটি instance publish করে ও subscribe করে। Instance 1 publish করে "B-র জন্য event," প্রতিটি instance শোনে ও যেটার কাছে B-র socket আছে সেটা ডেলিভার করে। এটা exotic নয়, কিন্তু এটা একটা বাস্তব architectural অংশ যা যোগ করতে হয় ও scale করার আগে সেটা জানা ভালো, ইনসিডেন্টের সময় আবিষ্কার করার চেয়ে যখন আপনার অর্ধেক ব্যবহারকারী আপডেট পাওয়া বন্ধ করে দিয়েছে।
ডিভাইসের কাজ কীভাবে এসব বাস্তব করে তুলল
ফিজিক্যাল ডিভাইসকে একটা backend-এর সঙ্গে সেতুবন্ধন এই প্রতিটি সমস্যা নিয়ে কঠিনতা বাড়িয়ে দেয়। একটা ডিভাইস আর আপনার সার্ভারের মধ্যে নেটওয়ার্ক সত্যিই বৈরী: এটা ড্রপ করে, দেরি করে, একই মেসেজ দুবার দেয় ও দুই প্রান্তের ঘড়ি আলাদা হয়ে যায়। ধরে নিতে পারবেন না যে মেসেজ পৌঁছায়, একবার পৌঁছায়, বা ক্রমে পৌঁছায়।
ওই পরিবেশের সংস্পর্শে টিকে থাকা মানসিকতা হলো মেসেজ দেরিতে, দুবার বা কখনোই না আসার জন্য ডিজাইন করা। handler-কে idempotent করুন যাতে duplicate ক্ষতিকর না হয়। timestamp বহন করুন আর "দ্বিতীয়টা পাওয়া গেছে" মানে "দ্বিতীয়টা ঘটেছে" — এটা বিশ্বাস করবেন না। প্রতিটি ডেলিভারিকে best-effort ধরুন আর stream বিশ্বাস করার বদলে authoritative state-এর সঙ্গে মিলিয়ে নিন। একবার পুড়লে এর কোনোটাই exotic নয় — কিন্তু ডেমো কখনো আপনাকে শেখাবে না, কারণ ডেমো localhost-এ চলে যেখানে নেটওয়ার্ক নিখুঁত আর কিছুই কখনো ড্রপ করে না।
রিয়েল-টাইম মূল্যবান যখন আপনার সত্যিই এটা দরকার। শুধু জেনে ঢুকুন যে পাঁচ-মিনিটের জাদুর ট্রিকটা অনেক দীর্ঘ, অনেক আকর্ষণীয় একটা সমস্যার প্রথম পাঁচ মিনিট।