এই পর্বে যা শিখবেন
AbortController,AbortSignal,AbortErrorআরTimeoutError— নিজের বাতিলযোগ্যdelay(ms, signal)বানানো- Timeout-এর দুই পথ, আর search-as-you-type-এর race condition সারানোর দুটি কৌশল
- Exponential backoff, jitter,
Retry-After, idempotency আর circuit breaker-এর ধারণা - Concurrency limit (একটি ছোট pool নিজে লেখা), batching আর in-flight promise cache দিয়ে dedupe
- Debounce, throttle,
requestAnimationFrame, polling থেকে WebSocket, optimistic UI আর অফলাইন queue
১ · কেন “একটি await” যথেষ্ট নয় — বাস্তব অ্যাপের পাঁচটি সমস্যা
পর্ব ৩৩ থেকে ৩৫-এ আপনি Promise, async/await আর fetch শিখেছেন। টিউটোরিয়ালের উদাহরণে সবকিছু সুন্দর: একটি বোতাম, একটি request, একটি await, তারপর ফল দেখানো। কিন্তু একটি সত্যিকারের ই-কমার্স অ্যাপ চালু করলে প্রথম সপ্তাহেই পাঁচটি সমস্যা চোখে পড়বে।
✋ যা আর দরকার নেই
ব্যবহারকারী “শাড়ি” লিখে খুঁজতে শুরু করলেন, তারপর মত বদলে “পাঞ্জাবি” লিখলেন। প্রথম request এখনো চলছে — এর উত্তর আর কারও কাজে লাগবে না। বাতিল করুন।
⏱️ যা শেষই হচ্ছে না
Load-shedding-এর পরে মোবাইল ডেটা দুর্বল; একটি request ৪০ সেকেন্ড ধরে ঝুলে আছে, spinner ঘুরছেই। নির্দিষ্ট সময় পরে হাল ছাড়ুন।
🔁 যা একবার ব্যর্থ হলো
পেমেন্ট গেটওয়ে এক মুহূর্তের জন্য 503 দিল। ব্যবহারকারীকে সাথে সাথে “ব্যর্থ” না দেখিয়ে একটু থেমে আবার চেষ্টা করুন — তবে বুঝে।
🚦 যা অনেকগুলো একসাথে
বিক্রেতা ১০টি পণ্যের ছবি একসাথে আপলোড করছেন। সব একসাথে পাঠালে দুর্বল সংযোগ আটকে যায়। একসাথে কয়টি চলবে, তার সীমা দিন — আর খুব ঘন ঘন আসা event-এর রাশ টানুন।
এই পর্বের নকশাগুলো (pattern) কোনো লাইব্রেরি নয় — কয়েক লাইনের ধারণা। প্রায় সবগুলোই দাঁড়িয়ে আছে তিনটি জিনিসের ওপর, যা আপনি আগেই জানেন: closure (পর্ব ২২ — অবস্থা মনে রাখা), timer আর event loop (পর্ব ৩১–৩২), আর Promise (পর্ব ৩৩). নতুন একটিই মূল উপাদান লাগবে — বাতিল করার একটি সাধারণ ভাষা।
একটি রেস্তোরাঁর রান্নাঘর ভাবুন। ওয়েটার অর্ডার নিয়ে আসেন — এটি request. কাস্টমার মত বদলালে ওয়েটার রান্নাঘরে গিয়ে বলেন “৫ নম্বর টেবিলের বিরিয়ানি বাতিল” — এটি abort. কোনো পদ ২০ মিনিটেও না এলে ম্যানেজার বলেন “আর অপেক্ষা নয়” — timeout. চুলা হঠাৎ নিভে গেলে বাবুর্চি একটু থেমে আবার জ্বালান, প্রতিবার একটু বেশি অপেক্ষা করে — retry with backoff. চুলা মাত্র তিনটি, তাই একসাথে তিনটির বেশি পদ রান্না হয় না — concurrency limit. আর ঈদের ভিড়ে কাস্টমার প্রতি সেকেন্ডে “আমার খাবার কই?” জিজ্ঞেস করলে ওয়েটার প্রতিবার রান্নাঘরে দৌড়ান না — কিছুক্ষণ পরপর একবার খবর নেন — throttle.
২ · AbortController আর AbortSignal — বাতিল করার সাধারণ ভাষা
Promise-এর একটি সীমাবদ্ধতা পর্ব ৩৩-এ হয়তো খেয়াল করেছেন: একবার তৈরি হলে বাইরে থেকে তাকে থামানোর কোনো method নেই। Promise শুধু একটি ভবিষ্যৎ ফলের প্রতিনিধি; যে কাজটি সেই ফল তৈরি করছে, তার নিয়ন্ত্রণ promise-এর হাতে নেই। তাই বাতিলের জন্য ওয়েব প্ল্যাটফর্ম একটি আলাদা, দুই-অংশের ব্যবস্থা দিয়েছে।
🎛️ AbortController — রিমোট
যে বাতিল করবে, তার হাতে থাকে। একটিই কাজের method: controller.abort(reason?).
📡 AbortSignal — রিসিভার
controller.signal — যে কাজ বাতিল হতে পারে, তার হাতে দেওয়া হয়। এতে আছে signal.aborted (true/false), signal.reason, 'abort' event আর signal.throwIfAborted().
ভাগটি ইচ্ছাকৃত। Signal পাওয়া কোড শুধু শুনতে পারে, নিজে বাতিল করতে পারে না; বাতিলের ক্ষমতা থাকে যে controller তৈরি করেছে তার কাছে। একটি signal একাধিক কাজে দেওয়া যায় — একবার abort() করলে সবগুলো একসাথে থামে (যেমন একটি পাতা ছেড়ে যাওয়ার সময় সেই পাতার সব request). আর মনে রাখুন, বাতিল একমুখী: একবার aborted হলে signal আর কখনো আগের অবস্থায় ফেরে না। নতুন কাজের জন্য নতুন controller বানাতে হয়।
fetch-এ signal দেওয়া
const controller = new AbortController();
cancelBtn.onclick = () => controller.abort(); // ব্যবহারকারী “বাতিল” চাপলেন
try {
const res = await fetch("/api/products?q=saree", { signal: controller.signal });
showProducts(await res.json());
} catch (err) {
if (err.name === "AbortError") return; // নিজেরাই বাতিল করেছি — ভুল নয়
showError(err); // আসল নেটওয়ার্ক ভুল
}abort() ডাকলে ব্রাউজার সত্যিই সংযোগটি বন্ধ করে দেয় (ডাউনলোড মাঝপথে থামে), আর fetch-এর promise reject হয় একটি AbortError দিয়ে — এটি একটি DOMException, যার name হলো "AbortError". লক্ষ করুন, বাতিল হওয়াকে সাধারণ ভুলের মতো দেখানো উচিত নয় — ব্যবহারকারী নিজেই চেয়েছেন। তাই catch-এ প্রথম কাজ err.name দেখা। abort(reason)-এ নিজের কারণ দিলে আধুনিক ব্রাউজারে fetch সেই কারণ দিয়েই reject হয়। আর মনে রাখুন: ব্রাউজারের বাতিল সার্ভারের কাজ ফেরত আনে না — অনুরোধ যদি আগেই সার্ভারে পৌঁছে থাকে, সার্ভার হয়তো কাজটি শেষ করে ফেলেছে।
নিজের async function-কে বাতিলযোগ্য করা
Signal কোনো জাদু নয় — এটি একটি সাধারণ EventTarget. যে function signal গ্রহণ করে, তাকে নিজেই তিনটি কাজ করতে হয়: শুরুতে দেখা আগেই বাতিল কি না, 'abort' event শোনা, আর বাতিল হলে নিজের সম্পদ (timer, listener, সংযোগ) পরিষ্কার করে reject করা। একটি বাতিলযোগ্য delay দেখুন — এটিই নিচের retry-এর ভিত্তি:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
signal?.throwIfAborted(); // আগেই বাতিল? তখনই reject
const id = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => {
clearTimeout(id); // timer-ও বন্ধ — নইলে বৃথা চলবে
reject(signal.reason); // সাধারণত DOMException "AbortError"
}, { once: true });
});
}এই নিয়মটিকে বলা হয় cooperative cancellation — বাতিল জোর করে চাপানো হয় না, কাজটি নিজে signal দেখে থামতে রাজি হয়। একটি লম্বা লুপ যদি signal কখনো না দেখে, abort() তাকে থামাতে পারবে না। তাই নিজের লম্বা async কাজে মাঝে মাঝে signal.throwIfAborted() ডাকুন — প্রতিটি await-এর পরে বা প্রতিটি ব্যাচের শুরুতে।
AbortSignal.timeout আর AbortSignal.any
দুটি তৈরি-করা static method কাজ অনেক সহজ করে। AbortSignal.timeout(5000) এমন একটি signal দেয়, যা ৫ সেকেন্ড পরে নিজেই বাতিল হয় — এবার reason-এর name হলো "TimeoutError", "AbortError" নয়। এতে আপনি আলাদা করতে পারেন: ব্যবহারকারী বাতিল করেছেন, না সময় শেষ হয়েছে। AbortSignal.any([a, b]) কয়েকটি signal জুড়ে একটি signal বানায়, যা যেকোনো একটি বাতিল হলেই বাতিল হয়:
const userCancel = new AbortController();
const signal = AbortSignal.any([userCancel.signal, AbortSignal.timeout(8000)]);
const res = await fetch("/api/order-status", { signal });
// বাতিল চাপলে → AbortError; ৮ সেকেন্ড পেরোলে → TimeoutErrorAbortSignal.timeout সব আধুনিক ব্রাউজার আর Node.js 18-এর পর থেকে নিশ্চিতভাবে আছে। AbortSignal.any নতুন — Chrome/Edge 116, Node.js 20.3 থেকে; পুরোনো ব্রাউজার সমর্থন করতে হলে দুটি signal-এর 'abort' event শুনে হাতে একটি নতুন controller বাতিল করুন।
একই signal আজ প্রায় সবখানে চলে: addEventListener(type, fn, { signal }) — signal বাতিল হলে listener নিজেই সরে যায় — থেকে Node.js-এর fs আর timers/promises পর্যন্ত। কেন বাতিল Promise-এর ভেতরে না রেখে আলাদা বস্তু বানানো হলো, সেই গল্প ভাবনার প্রশ্ন ৪-এ।
৩ · Timeout আর race condition — দেরিতে আসা পুরোনো উত্তর
Promise.race দিয়ে timeout — আর তার লুকানো দাম
AbortSignal.timeout আসার আগে timeout দেওয়ার প্রচলিত উপায় ছিল Promise.race (পর্ব ৩৩): আসল কাজ আর একটি টাইমার — যে আগে শেষ হয়, সেটিই জেতে।
function withTimeout(promise, ms) {
let id;
const timer = new Promise((_, reject) => {
id = setTimeout(() => reject(new Error(`${ms}ms পেরিয়ে গেছে`)), ms);
});
return Promise.race([promise, timer]).finally(() => clearTimeout(id));
}
await withTimeout(loadReport(), 3000); // ৩ সেকেন্ডে না এলে rejectRace শুধু অপেক্ষা থামায়, কাজ থামায় না। টাইমার জিতলে আপনার await এগিয়ে যায়, কিন্তু loadReport() পেছনে চলতেই থাকে — নেটওয়ার্কের ডেটা নামাতে থাকে, শেষ হলে তার ফল কেউ দেখে না। এক-আধবার ক্ষতি নেই; কিন্তু ধীর সার্ভারে প্রতি সেকেন্ডে শত শত “হেরে যাওয়া” request জমলে সার্ভারের ভার আরও বাড়ে। সত্যিকারের timeout চাইলে কাজটিকেই একটি signal দিন (AbortSignal.timeout), যাতে সময় শেষে কাজটি নিজেই থামে। .finally(() => clearTimeout(id))-ও ভুলবেন না — নইলে কাজ আগে শেষ হলেও টাইমার পরে বাজবে।
Search-as-you-type: উত্তর আসে ভুল ক্রমে
এবার এই পর্বের সবচেয়ে বাস্তব বাগ। একটি পণ্য খোঁজার বক্সে প্রতিটি টাইপে একটি request যায়। ব্যবহারকারী লিখলেন “pa”, তারপর দ্রুত “panjabi”. দুটি request বেরোল। কিন্তু নেটওয়ার্ক ক্রম মানে না — “pa”-র ফলাফল বড় (হাজার পণ্য), তাই ধীরে এল; “panjabi”-র ফল ছোট, আগে এল। কোড প্রতিটি উত্তর আসামাত্র তালিকা আঁকে। ফল: ব্যবহারকারীর বক্সে লেখা “panjabi”, অথচ পর্দায় “pa”-র ফলাফল — কারণ পুরোনো উত্তরটি শেষে এসে নতুনটিকে মুছে দিয়েছে। একাধিক async কাজের শেষ হওয়ার ক্রমের ওপর ফল নির্ভর করলে, তাকে বলে race condition.
| সময় | ঘটনা | পর্দায় কী |
|---|---|---|
| ০ ms | “pa” টাইপ → request A বেরোল | — |
| ১৫০ ms | “panjabi” টাইপ → request B বেরোল | — |
| ৪০০ ms | B-এর উত্তর এল (ছোট, দ্রুত) | panjabi-র ফল ✔ |
| ৯০০ ms | A-এর উত্তর এল (বড়, ধীর) | pa-র ফল ✘ — পুরোনো উত্তর নতুনটিকে মুছে দিল |
সমাধান দুটি, আর দুটিই কয়েক লাইনের। ১. আগেরটি বাতিল করুন — নতুন টাইপ এলে পুরোনো request abort করুন। এতে নেটওয়ার্কও বাঁচে:
let controller = null;
async function searchProducts(q) {
controller?.abort(); // আগের request বাতিল
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`,
{ signal: controller.signal });
renderList(await res.json());
} catch (err) {
if (err.name !== "AbortError") showError(err);
}
}২. পুরোনো উত্তর উপেক্ষা করুন — যেখানে কাজটি বাতিল করা যায় না (যেমন একটি তৃতীয় পক্ষের SDK), সেখানে প্রতিটি request-কে একটি ক্রমিক নম্বর দিন, আর উত্তর এলে দেখুন সেটি এখনো সবশেষ কি না:
let latest = 0;
async function searchProducts(q) {
const id = ++latest; // এই request-এর নম্বর
const items = await searchSdk(q);
if (id !== latest) return; // ইতিমধ্যে নতুন request গেছে — ফেলে দিন
renderList(items);
}দুটি একসাথেও চলে, আর প্রায় সবসময় সাথে থাকে debounce (অংশ ৬) — যাতে প্রতিটি অক্ষরে request-ই না যায়।
৪ · Retry, exponential backoff আর jitter — বুঝে আবার চেষ্টা
অনেক ভুল ক্ষণস্থায়ী: মোবাইল টাওয়ার বদলের মুহূর্তে সংযোগ কেটে গেল, সার্ভার কয়েক সেকেন্ডের জন্য ব্যস্ত (503 Service Unavailable), বা আপনি সীমার বেশি অনুরোধ পাঠিয়েছেন (429 Too Many Requests). এমন transient error-এ একটু থেমে আবার চেষ্টা করলে প্রায়ই সফল হয়। কিন্তু retry ভুলভাবে করলে সমস্যা সারানোর বদলে বাড়ায়। তিনটি প্রশ্নের উত্তর আগে ঠিক করুন।
- কোন ভুলে? নেটওয়ার্ক ভুল, 408, 429, 500, 502, 503, 504 — হ্যাঁ। 400 (ভুল ডেটা), 401/403 (অনুমতি নেই), 404 (নেই) — না; একশোবার পাঠালেও একই উত্তর আসবে। আর
AbortError— কখনোই না, ব্যবহারকারী নিজে বাতিল করেছেন। - কোন কাজে? শুধু idempotent কাজে — যা একবার বা দশবার করলে ফল একই।
GET(পড়া),PUT(একটি নির্দিষ্ট মান বসানো),DELETEidempotent;POST /ordersনয়। প্রথম POST হয়তো সার্ভারে পৌঁছে অর্ডার তৈরিও করেছে, শুধু উত্তরটি পথে হারিয়েছে — আবার পাঠালে দুটি অর্ডার, দুবার টাকা কাটা। POST নিরাপদে retry করতে চাইলে সার্ভারকে একটি idempotency key দিন (একটি অনন্য id, যা দেখে সার্ভার একই অনুরোধ দ্বিতীয়বার করে না) — অনেক পেমেন্ট API এভাবেই কাজ করে। - কতক্ষণ পরে, কতবার? সাথে সাথে নয়, আর অসীমবার নয় — এখানেই backoff আর jitter.
Exponential backoff — প্রতিবার অপেক্ষা দ্বিগুণ
সার্ভার ব্যস্ত থাকলে সাথে সাথে আবার পাঠানো মানে তার ভার আরও বাড়ানো। তাই প্রতিটি ব্যর্থতার পরে অপেক্ষা বাড়াতে থাকুন: ১০০ ms, ২০০, ৪০০, ৮০০… — সূত্র base × 2^(n−1), সাথে একটি সর্বোচ্চ সীমা (cap) আর মোট চেষ্টার সীমা। একে বলে exponential backoff. একটি পেমেন্টের অবস্থা যাচাইয়ের নকল API ধরুন, যা প্রথম দুবার 503 দেয়, তৃতীয়বার সফল হয়:
1async function retry(task, tries = 3, base = 100) {2 for (let n = 1; ; n++) {3 try { return await task(n); }4 catch (err) {5 if (n === tries) throw err;6 const wait = base * 2 ** (n - 1);7 await delay(wait);8 }9 }10}
| n (চেষ্টা) | |
| সার্ভারের উত্তর | |
| পরের অপেক্ষা (jitter ছাড়া) | |
| full jitter হলে | |
| মোট সময় (প্রতিটি কল ৫০ ms) |
- চেষ্টা ১ — ব্যর্থ।
task(1)৫০ ms পরে 503 দিয়ে reject হলো,catch-এ এলাম। এখনো চেষ্টা বাকি (১ ≠ ৩), তাই অপেক্ষা হিসাব: ১০০ × ২⁰ = ১০০ ms.await delay(100)— এই সময়ে event loop অন্য কাজ করতে পারে, কিছুই আটকে নেই। - চেষ্টা ২ — আবার ব্যর্থ। অপেক্ষা দ্বিগুণ: ১০০ × ২¹ = ২০০ ms. সার্ভার যদি সত্যিই অতিরিক্ত ভারে থাকে, এই বাড়তে থাকা বিরতি তাকে সেরে ওঠার সময় দেয়। পঞ্চম চেষ্টায় অপেক্ষা হতো ১৬০০ ms — তাই বাস্তবে একটি cap (যেমন ১০ সেকেন্ড) রাখা হয়।
- চেষ্টা ৩ — সফল।
return await task(3)ফল নিয়ে পুরো function থেকে বেরিয়ে যায়। Caller শুধু দেখে একটি সফল ফল, প্রায় ৪৫০ ms পরে — মাঝের দুটি ব্যর্থতা তার কাছে অদৃশ্য। লক্ষ করুন, এখানেreturn awaitজরুরি: শুধুreturn task(n)লিখলে rejectiontry-এর বাইরে ঘটত,catchধরতই না। - যদি তৃতীয়বারও ব্যর্থ হতো।
n === tries, তাইthrow err— শেষ ভুলটি caller-এর কাছে যায়, যে ব্যবহারকারীকে “পরে আবার চেষ্টা করুন” দেখাবে। চিরকাল চেষ্টা করা কোড একটি মৃত সার্ভারের দিকে অসীম request পাঠাত।
Jitter — সবাই একসাথে ফিরে আসবেন না
শুধু backoff-এ একটি গোপন সমস্যা আছে। ধরুন সার্ভার এক সেকেন্ডের জন্য পড়ে গেল, আর ঠিক তখন দশ হাজার ব্যবহারকারীর request ব্যর্থ হলো। সবার কোড একই নিয়মে ১০০ ms পরে আবার পাঠাবে — দশ হাজার request একই মুহূর্তে! তারপর ২০০ ms পরে আবার একসাথে। ঢেউয়ের মতো আঘাত সার্ভারকে সেরে উঠতে দেয় না — একে বলে thundering herd. সমাধান jitter: অপেক্ষার সময়ে এলোমেলোতা মেশানো। জনপ্রিয় “full jitter” রূপে অপেক্ষা = ০ থেকে min(cap, base × 2^(n−1))-এর মধ্যে একটি random সংখ্যা। এতে request-গুলো সময়ের ওপর ছড়িয়ে পড়ে।
সার্ভার নিজে বলে দিলে আরও ভালো: 429 বা 503 উত্তরের সাথে Retry-After header আসতে পারে — কত সেকেন্ড পরে (বা কোন তারিখ-সময়ে) আবার আসতে হবে। থাকলে নিজের হিসাবের বদলে সেটিই মানুন। এবার সব নিয়ম একসাথে, একটি বাস্তবধর্মী fetchWithRetry-এ (চালানো যায় না — নেটওয়ার্ক লাগে):
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
async function fetchWithRetry(url, options = {}, tries = 4) {
for (let n = 1; ; n++) {
let res;
try {
res = await fetch(url, options);
if (!RETRYABLE.has(res.status) || n === tries) return res; // সফল বা স্থায়ী ভুল
} catch (err) {
if (err.name === "AbortError" || n === tries) throw err; // বাতিল কখনো retry নয়
}
const retryAfter = Number(res?.headers.get("Retry-After")); // সেকেন্ডে, থাকলে
const cap = Math.min(10_000, 200 * 2 ** (n - 1));
const wait = retryAfter > 0 ? retryAfter * 1000 : Math.random() * cap; // full jitter
await delay(wait, options.signal); // অপেক্ষাও বাতিলযোগ্য
}
}
// ব্যবহার: শুধু GET-এ, বা idempotency key থাকা POST-এএবার মূল ধারণাগুলো নিজে চালিয়ে দেখুন — বাতিলযোগ্য delay, দুবার ব্যর্থ হয়ে তৃতীয়বার সফল হওয়া নকল পেমেন্ট API, ব্যবহারকারীর বাতিল আর AbortSignal.timeout. আউটপুট যাতে প্রতিবার একই আসে, তাই এখানে jitter বন্ধ রাখা হয়েছে:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
signal?.throwIfAborted(); // আগেই বাতিল? তখনই reject
const id = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => { clearTimeout(id); reject(signal.reason); }, { once: true });
});
}
async function retry(task, tries = 3, base = 100) {
for (let n = 1; ; n++) {
try { return await task(n); } catch (err) {
if (n === tries) throw err; // আর নয় — ভুল ওপরে যাক
const wait = base * 2 ** (n - 1); // 100, 200, 400 ...
console.log(`চেষ্টা ${n}: ${err.message} — ${wait}ms পরে আবার`);
await delay(wait);
}
}
}
const checkPayment = async (n) => { // নকল API: দুবার ব্যর্থ
await delay(50);
if (n < 3) throw new Error("503 Service Unavailable");
return `চেষ্টা ${n}: পেমেন্ট সফল`;
};
retry(checkPayment).then(console.log);
const ac = new AbortController();
delay(5000, ac.signal).catch((e) => console.log("বাতিল:", e.name, ac.signal.aborted));
ac.abort(); // ব্যবহারকারী “বাতিল” চাপলেন
delay(5000, AbortSignal.timeout(120)).catch((e) => console.log("সময় শেষ:", e.name));প্রথম লাইন বাতিল: AbortError true — ac.abort() সাথে সাথে ডাকা হয়েছে, তাই ৫ সেকেন্ডের delay-টি listener-এর মাধ্যমে timer বন্ধ করে reject হলো; reason একটি DOMException, যার name "AbortError". তারপর ৫০ ms-এ চেষ্টা 1: 503 Service Unavailable — 100ms পরে আবার. ১২০ ms-এ সময় শেষ: TimeoutError — AbortSignal.timeout-এর reason-এর নাম আলাদা, তাই দুই ধরনের বাতিল আলাদা করা যায়। ২০০ ms-এ চেষ্টা 2: … 200ms পরে আবার, আর প্রায় ৪৫০ ms-এ চেষ্টা 3: পেমেন্ট সফল. তিনটি কাজ event loop-এ পালা করে এগিয়েছে, কেউ কাউকে আটকায়নি। আর দুটি ৫ সেকেন্ডের timer-ই বাতিলের সময় clearTimeout হয়েছে, তাই প্রোগ্রাম ৪৫০ ms-এই চুপ।
Circuit breaker — বারবার ব্যর্থ হলে কিছুক্ষণ চেষ্টাই নয়
Retry একটি request-এর জন্য ভালো। কিন্তু একটি সার্ভিস যদি পুরোপুরি মৃত হয়, প্রতিটি নতুন request তবুও তিনবার চেষ্টা করবে, প্রতিবার timeout পর্যন্ত অপেক্ষা করবে — ব্যবহারকারী বসে থাকবেন, মৃত সার্ভিস আরও চাপ খাবে। Circuit breaker বাড়ির বিদ্যুতের সার্কিট ব্রেকারের মতো: পরপর কয়েকটি ব্যর্থতা হলে সে “খুলে যায়” (open) — কিছুক্ষণ কোনো request পাঠানোই হয় না, সাথে সাথে ভুল বা cache-এর পুরোনো ডেটা ফেরত দেওয়া হয়। কিছুক্ষণ পরে সে “আধখোলা” (half-open) হয়ে একটি পরীক্ষামূলক request পাঠায়; সফল হলে আবার “বন্ধ” (closed). এটি মূলত সার্ভারের জগতের নকশা — বিস্তারিত ভাবনার প্রশ্ন ৩-এ।
৫ · একসাথে কয়টি — concurrency limit, batching আর dedupe
পর্ব ৩৩-এর Promise.all দিয়ে অনেক কাজ একসাথে চালানো যায়। কিন্তু Promise.all(images.map(upload)) লেখার মুহূর্তে সবগুলো upload একসাথে শুরু হয়ে যায় — map সবগুলো function ডেকে ফেলে, Promise.all শুধু অপেক্ষা করে। ১০টি ছবিতে হয়তো চলে যায়; ১০,০০০টি ফাইলে ব্রাউজার বা সার্ভার হাঁপিয়ে ওঠে, অনেক request 429 খায়, মেমরি ভরে যায়। দরকার একটি concurrency limit: একসাথে বড়জোর N টি কাজ, একটি শেষ হলে তবেই পরেরটি।
সবচেয়ে সহজ বাস্তবায়ন হলো “worker pool”: N জন কর্মী, আর একটি ভাগ করা কাজের তালিকা। প্রতিটি কর্মী একটি লুপে তালিকা থেকে পরের কাজ তুলে নেয়, শেষ করে, আবার তোলে — তালিকা ফুরোলে থামে। next++ নিয়ে দুই কর্মীর কাড়াকাড়ির ভয় নেই, কারণ JavaScript-এর কোড একটি thread-এ চলে আর await-এর মাঝখানে কেউ ঢুকতে পারে না (পর্ব ৩১)। ৩টি স্লটে ৬টি কাজ ধাপে ধাপে দেখুন — কাজগুলোর সময় আলাদা (A ৩০০ ms, B ১০০, C ২০০, D ১০০, E ২০০, F ১০০):
1async function mapWithConcurrency(items, limit, fn) {2 const results = new Array(items.length);3 let next = 0;4 async function worker() {5 while (next < items.length) {6 const i = next++;7 results[i] = await fn(items[i], i);8 }9 }10 await Promise.all(Array.from({ length: limit }, worker));11 return results;12}
| সময় | |
| স্লট ১ | |
| স্লট ২ | |
| স্লট ৩ | |
| অপেক্ষায় | |
| next |
- তিনজন কর্মী, তিনটি কাজ।
Array.from({ length: 3 }, worker)তিনবারworkerডাকে। প্রত্যেকেnext++দিয়ে একটি index তুলে নিয়ে কাজ শুরু করে, তারপরawait-এ থামে: A, B, C চলছে,next = 3. D, E, F অপেক্ষায় — এখনো শুরুই হয়নি, এটাইPromise.all(map)-এর সাথে পার্থক্য। - ১০০ ms — B শেষ, স্লট খালি। স্লট ২-এর কর্মীর
awaitশেষ হলো, ফলresults[1]-এ গেল, লুপ ঘুরে সে D তুলে নিল। একসাথে চলছে এখনো ঠিক তিনটি — সীমা কখনো ভাঙে না। - ২০০ ms — দুটি একসাথে শেষ। C আর D দুটোই শেষ; দুই কর্মী পালা করে E আর F তুলে নিল। এখন
next = 6— তালিকা খালি। কেউ নতুন কাজ পাবে না, তবে যারা চলছে তারা শেষ করবে। - ৩০০ ms — A আর F শেষ। স্লট ১ আর ২-এর কর্মী লুপে ফিরে দেখল
next < items.lengthমিথ্যা, তাই থামল — তাদের promise পূর্ণ হলো। লক্ষ করুন, শেষ হওয়ার ক্রম (B, C, D, A, F) শুরুর ক্রম থেকে আলাদা। - ৪০০ ms — E শেষ, সব শেষ। তিন কর্মীর promise-ই পূর্ণ, তাই লাইন ১০-এর
Promise.allশেষ হলো।results-এ ফল আছে মূল ক্রমে, কারণ প্রত্যেকে নিজের index-এ লিখেছে। মোট সময় ৪০০ ms — সব একসাথে চালালে লাগত ৩০০ ms, একটি একটি করে চালালে ১০০০ ms. সীমা মাঝামাঝি একটি নিরাপদ গতি দেয়।
বিক্রেতার ১০টি ছবি এক লাইনে: await mapWithConcurrency(files, 3, uploadImage) — একসাথে ৩টি upload, একটি শেষ হলে পরেরটি। বাস্তবে আরও দুটি জিনিস যোগ করুন: প্রতিটি কাজের ভুল try/catch-এ ধরে { ok: false, error } হিসেবে রাখুন (নইলে একটি ব্যর্থতা পুরো Promise.all ভেঙে দেয় — পর্ব ৩৩-এর allSettled-এর মতো আচরণ চান), আর একটি signal নিন যাতে “আপলোড বাতিল” চাপলে বাকি কাজ আর শুরু না হয়। npm-এর p-limit প্যাকেজ একই ধারণার একটি জনপ্রিয় রূপ।
Batching আর request deduplication
কখনো কখনো সমস্যা উল্টো: কাজ অনেক, কিন্তু প্রতিটি খুব ছোট। একটি পাতায় ৫০টি পণ্যের কার্ড, প্রত্যেকে আলাদাভাবে নিজের দাম চাইছে — ৫০টি request. Batching-এ কয়েক মিলিসেকেন্ড ধরে অনুরোধগুলো জমিয়ে একটিতে পাঠানো হয়: /api/prices?ids=1,2,3,…. GraphQL জগতের DataLoader লাইব্রেরি ঠিক এই কাজ করে।
আরেকটি সাধারণ অপচয়: একই পণ্যের জন্য একসাথে কয়েকটি অনুরোধ। পাতার হেডার, কার্ট আর “আপনার জন্য” অংশ — তিনটি component একই মুহূর্তে পণ্য ৪২-এর তথ্য চায়। তিনটি request পাঠানোর দরকার নেই; প্রথমটির promise-টিই বাকি দুজনকে দিয়ে দিন। একে বলে request deduplication — এটি আসলে পর্ব ২২-এর memoization, শুধু ফলের বদলে চলমান promise জমানো হচ্ছে:
const inFlight = new Map(); // id → চলমান promise
function getProduct(id) {
if (inFlight.has(id)) return inFlight.get(id); // একই id-র request চলছে — সেটিই দিন
const p = fetchProduct(id).finally(() => inFlight.delete(id));
inFlight.set(id, p);
return p;
}
// একসাথে getProduct(42) তিনবার → নেটওয়ার্কে একটিই request, তিনজন একই ফল পানদুটি খুঁটিনাটি খেয়াল করুন। প্রথমত, finally-তে entry মুছে ফেলা হয় — এটি cache নয়, শুধু “এই মুহূর্তে চলছে” তালিকা; শেষ হলে পরের অনুরোধ তাজা ডেটা আনবে, আর ব্যর্থ promise-ও চিরকাল জমে থাকবে না। দ্বিতীয়ত, promise জমানো হচ্ছে await করার আগেই, সাথে সাথে — await-এর পরে জমালে মাঝের সময়ে আসা অনুরোধগুলো আবার নতুন request পাঠাত। React Query বা SWR-এর মতো লাইব্রেরি এই ধারণার পূর্ণ রূপ।
Python-এর asyncio-তে প্রায় হুবহু একই নকশা: asyncio.wait_for(coro, timeout) বা asyncio.timeout() (3.11) সময় শেষে task-টিকে সত্যিই cancel করে, আর asyncio.Semaphore(3) দিয়ে concurrency limit (Python-এর asyncio). Java-তে Executors.newFixedThreadPool(3) নিজেই একটি pool, আর CompletableFuture.orTimeout() (Java 9) timeout দেয় (Java-র Executor আর CompletableFuture). C++-এর standard library-তে তৈরি thread pool নেই, তবে C++20-এর std::stop_token হুবহু AbortSignal-এর মতো সহযোগী বাতিল (C++-এর মাল্টিথ্রেডিং). মূল পার্থক্য: ওই ভাষাগুলোর pool আসল thread চালায়, JavaScript-এর pool একটি thread-এ শুধু অপেক্ষাগুলো ভাগাভাগি করে।
৬ · Debounce — ব্যবহারকারী থামা পর্যন্ত অপেক্ষা
এবার উল্টো দিকের সমস্যা: কাজ ধীর নয়, event খুব দ্রুত। একজন মানুষ সেকেন্ডে ৫–১০টি অক্ষর টাইপ করেন; input event প্রতিটি অক্ষরে আসে। “ঢাকা” লিখতে চারটি event — প্রতিটিতে একটি search request পাঠালে চারটির মধ্যে তিনটির উত্তর কেউ দেখবে না। Debounce-এর নিয়ম: event আসতে থাকলে অপেক্ষা করুন; শেষ event-এর পরে নির্দিষ্ট সময় (যেমন ৩০০ ms) চুপচাপ গেলে তবেই একবার চালান। প্রতিটি নতুন event ঘড়িটি আবার শূন্য থেকে শুরু করায়।
লিফটের দরজার কথা ভাবুন। কেউ ঢুকলে দরজা খোলা থাকে; প্রতিটি নতুন মানুষ এলে দরজার টাইমার আবার শুরু হয়। শেষ মানুষটি ঢোকার পরে কয়েক সেকেন্ড কেউ না এলে তবেই দরজা বন্ধ হয়ে লিফট চলে। লিফট প্রতিটি মানুষের জন্য আলাদা ট্রিপ দেয় না — ভিড় থামার জন্য অপেক্ষা করে।
বাস্তবায়ন একটি closure-এ রাখা একটি timer id: প্রতিটি কলে আগের timer বাতিল, নতুন timer বসানো। চারটি কী-প্রেস — ০, ১০০, ২০০ আর ৬০০ ms-এ — আর ৩০০ ms অপেক্ষা দিয়ে ধাপে ধাপে দেখুন:
1function debounce(fn, wait) {2 let timer;3 return (...args) => {4 clearTimeout(timer);5 timer = setTimeout(() => fn(...args), wait);6 };7}8const search = debounce(callApi, 300);
| সময় | |
| ঘটনা | |
| timer (closure-এ) | |
| API কল মোট |
- ০ ms — প্রথম কী-প্রেস।
timerএখনোundefined;clearTimeout(undefined)কিছুই করে না (নিরাপদ)। নতুন timer বসল: ৩০০ ms-এ বাজবে। Closure-এরtimervariable এখন সেই id ধরে রাখছে — পর্ব ২২-এর মতো কলের পর কলে টিকে থাকা অবস্থা। - ১০০ ms — আবার কী-প্রেস। আগের timer (৩০০-এর) বাতিল, নতুনটি বসল ১০০ + ৩০০ = ৪০০-এ। ঘড়ি আবার শূন্য থেকে। এখনো একটিও API কল হয়নি।
- ২০০ ms — আবার। আবার বাতিল, এবার ৫০০-এ। ব্যবহারকারী যতক্ষণ টাইপ করছেন, কাজটি ততক্ষণ পিছিয়েই যাচ্ছে।
- ৫০০ ms — চুপচাপ ৩০০ ms, timer বাজল। ২০০-র পরে কোনো কী-প্রেস আসেনি, তাই timer টিকে গেল আর
callApi("dha")চলল — শেষ কলের argument নিয়ে। তিনটি event, একটি কল। - ৬০০ ms — ব্যবহারকারী আবার লিখলেন। “dhaka” — নতুন timer, ৯০০-এ বাজবে। (এখানেই অংশ ৩-এর race condition-এর ঝুঁকি: “dha”-র request হয়তো এখনো চলছে। তাই বাস্তবে debounce-এর সাথে abort জুড়ে দিন।)
- ৯০০ ms — দ্বিতীয় কল।
callApi("dhaka"). মোট চারটি কী-প্রেসে দুটি API কল। এই রূপকে বলে trailing debounce — ভিড় থামার শেষে চলে। দাম একটিই: ব্যবহারকারী শেষ অক্ষরের পরে অন্তত ৩০০ ms অপেক্ষা করেন। তাই wait খুব বড় দিলে অ্যাপ ধীর মনে হয়; ২০০–৪০০ ms সাধারণত ভালো।
Leading বনাম trailing: কখনো কখনো প্রথম event-এই সাথে সাথে চালানো দরকার, তারপর ভিড় থামা পর্যন্ত চুপ — যেমন একটি বোতামে দ্রুত দুবার ক্লিক: প্রথম ক্লিকেই কাজ হোক, পরেরগুলো উপেক্ষিত হোক। এটি leading debounce. Lodash-এর debounce(fn, wait, { leading, trailing, maxWait }) দুটোই দেয়; maxWait নিশ্চিত করে যে টানা event আসতে থাকলেও কাজটি একটি নির্দিষ্ট সময়ের বেশি আটকে থাকবে না। সাধারণ ব্যবহার: search box, autosave, resize শেষে layout হিসাব, আর “এই username পাওয়া যাবে কি না” যাচাই।
৭ · Throttle আর requestAnimationFrame — নির্দিষ্ট সময়ে বড়জোর একবার
Scroll করার সময় scroll event সেকেন্ডে কয়েক ডজন বার আসে, আর ব্যবহারকারী হয়তো কয়েক সেকেন্ড ধরে থামেনই না। এখানে debounce খারাপ: “স্ক্রল থামলে তবেই” মানে পুরো স্ক্রলের সময় পাতার উপরের অগ্রগতি-বার নড়বেই না। দরকার নিয়মিত হালনাগাদ, তবে প্রতিটি event-এ নয়। Throttle-এর নিয়ম: নির্দিষ্ট সময়ের জানালায় বড়জোর একবার চালান; মাঝের event বাদ। প্রতি ১০০ ms-এ একটি event আর ৩০০ ms সীমা দিয়ে দেখুন:
1function throttle(fn, limit) {2 let last = -Infinity;3 return (...args) => {4 const now = Date.now();5 if (now - last < limit) return; // বাদ6 last = now;7 fn(...args);8 };9}
| event-এর সময় | |
| now − last | |
| ফল | |
| last | |
| মোট: event / চলা |
- ০ ms — প্রথম event সাথে সাথে চলে।
lastশুরুতে-Infinity, তাই পার্থক্য অসীম — চলল,last = 0. Throttle-এর এই রূপ “leading”: ব্যবহারকারী প্রথম নড়াচড়াতেই সাড়া পান, debounce-এর মতো অপেক্ষা করতে হয় না। - ১০০ আর ২০০ ms — বাদ। পার্থক্য ১০০ আর ২০০, দুটোই ৩০০-র কম। হ্যান্ডলার চলে না — event গুলো চুপচাপ ফেলে দেওয়া হয়। কিন্তু debounce-এর মতো ঘড়ি নতুন করে শুরু হয় না; জানালা মাপা হয় শেষ চলা থেকে।
- ৩০০ ms — জানালা শেষ, চলল। ৩০০ − ০ = ৩০০, যা সীমার কম নয় — চলল,
last = 300. টানা event আসতে থাকলেও হ্যান্ডলার নিয়মিত তালে চলছে। - ৪০০–৬০০ ms — একই তাল। দুটি বাদ, ৬০০-তে চলল। এই নিয়মিততাই throttle-এর মূল্য: ১ সেকেন্ডে ১০টি event থেকে ৪টি চলা — debounce হলে টানা স্ক্রলের মাঝে একটিও চলত না।
- ৭০০–৯০০ ms — আর শেষের প্রশ্ন। ৯০০-তে চলল; মোট ১০টি event, ৪টি চলা। কিন্তু ভাবুন, শেষ event যদি ৮০০-তে হতো? সেটি বাদ পড়ত, আর পাতার শেষ অবস্থাটি কখনো আঁকা হতো না। তাই ভালো throttle-এ একটি “trailing” কল থাকে — বাদ পড়া শেষ event-টি জানালা শেষে একবার চালানো। Lodash-এর
throttleডিফল্টভাবে leading আর trailing দুটোই করে।
| বিষয় | Debounce | Throttle |
|---|---|---|
| নিয়ম | শেষ event-এর পরে wait ms চুপ থাকলে একবার | প্রতি limit ms-এ বড়জোর একবার |
| টানা event চললে | একবারও চলে না (শেষে একবার) | নিয়মিত তালে চলতে থাকে |
| প্রশ্নটি | “ব্যবহারকারী কি শেষ করেছেন?” | “শেষবার কখন চালিয়েছি?” |
| সাধারণ ব্যবহার | search box, autosave, resize শেষে, username যাচাই | scroll, mousemove, drag, গেমের গুলি, বোতামে বারবার চাপ |
| উপরের উদাহরণে | ৪টি কী-প্রেস → ২টি কল (৫০০, ৯০০) | ১০টি event → ৪টি কল (০, ৩০০, ৬০০, ৯০০) |
এবার দুটো একসাথে চালিয়ে দেখুন। টাইপিং আর scroll — দুটোই timer দিয়ে নকল করা হয়েছে, যাতে প্রতিবার একই আউটপুট আসে। এখানে throttle-টি timestamp-এর বদলে একটি “তালা” আর timer দিয়ে লেখা — এটিও খুব প্রচলিত রূপ:
function debounce(fn, wait) {
let timer;
return (...args) => {
clearTimeout(timer); // আগের অপেক্ষা বাতিল
timer = setTimeout(() => fn(...args), wait); // নতুন করে গোনা শুরু
};
}
function throttle(fn, limit) {
let locked = false;
return (...args) => {
if (locked) return; // সময় হয়নি — বাদ
locked = true;
fn(...args);
setTimeout(() => (locked = false), limit);
};
}
const search = debounce((q) => console.log("সার্চ API:", q), 300);
[[0, "d"], [100, "dh"], [200, "dha"], [600, "dhaka"]]
.forEach(([t, q]) => setTimeout(() => search(q), t)); // নকল টাইপিং
const onScroll = throttle((i) => console.log("scroll হ্যান্ডলার, ইভেন্ট", i), 350);
for (let i = 0; i < 10; i++) setTimeout(() => onScroll(i), i * 100);আউটপুট সময়ের ক্রমে মিশে আসে: scroll হ্যান্ডলার, ইভেন্ট 0 (০ ms), ইভেন্ট 4 (৪০০), সার্চ API: dha (৫০০), ইভেন্ট 8 (৮০০), সার্চ API: dhaka (৯০০). Debounce অংশটি stepper-এর হুবহু: চারটি টাইপে দুটি কল। Throttle-এ সীমা ইচ্ছা করে ৩৫০ ms, ৩০০ নয় — ৩০০ হলে ৩০০ ms-এ দুটি timer (তালা খোলা আর event ৩) প্রায় একসাথে বাজত, আর কোনটি আগে চলবে তা কয়েক মিলিসেকেন্ডের হেরফেরে বদলাতে পারত। ৩৫০-এ event ৩ বাদ, event ৪ চলে; তালা ৭৫০ পর্যন্ত, তারপর event ৮. শিক্ষা: সীমানায় পড়া timer-এর ওপর যুক্তি দাঁড় করাবেন না — বাস্তব timer সবসময় একটু দেরিতে বাজে।
“অর্ডার দিন” বোতাম — double-click থেকে বাঁচানো
অধৈর্য ব্যবহারকারী পেমেন্ট বোতামে দুবার চাপলে দুটি অর্ডার — এটি throttle-এর একটি বিশেষ রূপ, যেখানে “জানালা” হলো request শেষ হওয়া পর্যন্ত। সবচেয়ে পরিষ্কার সমাধান হলো request চলার সময় বোতামটিই নিষ্ক্রিয় রাখা, আর সার্ভারের জন্য একটি idempotency key পাঠানো:
const checkoutKey = crypto.randomUUID(); // এই চেকআউটের জন্য একটিই key
orderBtn.addEventListener("click", async () => {
if (orderBtn.disabled) return;
orderBtn.disabled = true; // দ্বিতীয় ক্লিক আর কাজ করবে না
try {
await placeOrder(cart, { idempotencyKey: checkoutKey });
showSuccess();
} finally {
orderBtn.disabled = false; // ভুল হলেও বোতাম ফিরে আসে
}
});ব্রাউজারের এই সুরক্ষা ভালো অভিজ্ঞতার জন্য; আসল নিশ্চয়তা সার্ভারের, যে একই key দ্বিতীয়বার দেখলে নতুন অর্ডার না বানিয়ে আগের ফলটিই ফেরত দেয়। পর্ব ২২-এর once আর এখানের key — দুটো একসাথে থাকলে দুই স্তরের পাহারা।
requestAnimationFrame — পর্দা আঁকার তালে throttle
শুধু দৃশ্যমান কিছু হালনাগাদ করতে হলে (অগ্রগতি-বার, sticky header, টেনে আনা জিনিসের অবস্থান) মিলিসেকেন্ডের সীমার চেয়ে ভালো সঙ্গী হলো requestAnimationFrame. এটি callback চালায় ব্রাউজারের পরের ছবি আঁকার ঠিক আগে — সাধারণত পর্দার refresh rate-এর তালে (৬০ Hz পর্দায় প্রায় ১৬.৭ ms পরপর), আর লুকানো ট্যাবে থেমে যায়। একটি ফ্রেমে একবারের বেশি আঁকার কোনো মানে নেই, কারণ মানুষ তা দেখতেই পাবে না:
let scheduled = false;
window.addEventListener("scroll", () => {
if (scheduled) return; // এই ফ্রেমের কাজ আগেই নির্ধারিত
scheduled = true;
requestAnimationFrame(() => {
const max = document.body.scrollHeight - innerHeight;
bar.style.width = (scrollY / max) * 100 + "%"; // পরের ফ্রেমের ঠিক আগে একবার
scheduled = false;
});
}, { passive: true });৮ · Polling থেকে push, optimistic UI আর অফলাইন queue
সার্ভারের খবর কীভাবে পাবেন
ডেলিভারি রাইডার কোথায়, পেমেন্ট নিশ্চিত হলো কি না, নতুন মেসেজ এল কি না — এসব খবর সার্ভারে তৈরি হয়, কিন্তু HTTP-তে কথা শুরু করে সবসময় ব্রাউজার। চারটি প্রচলিত পথ:
| পথ | কীভাবে | কখন ভালো |
|---|---|---|
| Polling | নির্দিষ্ট সময় পরপর জিজ্ঞেস: “কিছু নতুন?” — বেশিরভাগ উত্তর “না” | সরল, কম ঘন ঘন বদল (প্রতি ১০–৩০ সেকেন্ডে অর্ডারের অবস্থা) |
| Long polling | জিজ্ঞেস করে অপেক্ষা; সার্ভার খবর না হওয়া পর্যন্ত উত্তর আটকে রাখে, তারপর ব্রাউজার আবার জিজ্ঞেস করে | WebSocket ব্যবহার করা না গেলে প্রায় তাৎক্ষণিক খবর |
| Server-Sent Events | একটি খোলা HTTP সংযোগে সার্ভার একদিকে event পাঠাতে থাকে; ব্রাউজারের EventSource নিজেই আবার সংযোগ করে | শুধু সার্ভার থেকে আসা ধারা — নোটিফিকেশন, লাইভ স্কোর, AI-এর উত্তর ধাপে ধাপে |
| WebSocket | একটি স্থায়ী দুইমুখী সংযোগ, দুই পক্ষই যেকোনো সময় বার্তা পাঠায় | চ্যাট, মাল্টিপ্লেয়ার গেম, যৌথ সম্পাদনা |
Polling-এ এই পর্বের সব নকশা লাগে: setInterval-এর বদলে আগের উত্তরের পরে await delay(ms, signal) (ধীর সার্ভারে request জমে না), ব্যর্থতায় backoff, পাতা ছাড়লে বাতিল। SSE আর WebSocket বিস্তারিত পর্ব ৫১-এ।
Optimistic UI — আগে দেখান, পরে নিশ্চিত করুন
Facebook-এ লাইক চাপলে হৃদয়টি সাথে সাথে লাল হয় — সার্ভারের উত্তরের অপেক্ষা করে না। একে বলে optimistic update: সফল হবে ধরে নিয়ে UI আগেই বদলান, request পেছনে যাক; ব্যর্থ হলে আগের অবস্থায় ফিরিয়ে আনুন (rollback) আর জানান।
async function toggleLike(post) {
const before = { liked: post.liked, count: post.count };
post.liked = !post.liked; // ১. সাথে সাথে UI বদল
post.count += post.liked ? 1 : -1;
render(post);
try {
await api.setLike(post.id, post.liked); // ২. পেছনে request (PUT — idempotent)
} catch {
Object.assign(post, before); // ৩. ব্যর্থ — আগের অবস্থায় ফেরত
render(post);
showToast("লাইক সেভ হয়নি, আবার চেষ্টা করুন");
}
}লক্ষ করুন, request-টি “লাইক টগল করা” নয়, বরং “liked = true বসানো” — idempotent; দ্রুত দুবার চাপলেও শেষ অবস্থাই জেতে। Optimistic UI তখনই মানায় যখন সফলতার সম্ভাবনা বেশি আর ভুল হলে ক্ষতি কম (লাইক, বুকমার্ক, তালিকা সাজানো)। টাকা পাঠানো বা অর্ডারে নয় — সেখানে সার্ভারের নিশ্চিতকরণের জন্য অপেক্ষা করুন।
অফলাইন হলে — কাজ জমিয়ে রাখার queue
নেটওয়ার্কহীন গ্রামে একজন মাঠকর্মী সার্ভের ফর্ম জমা দিচ্ছেন — কাজটি হারানো চলবে না। সমাধান একটি queue: কাজগুলো স্থানীয়ভাবে জমা রাখুন (পর্ব ৪০-এর IndexedDB), আর সংযোগ ফিরলে (online event) ক্রম মেনে একটি একটি করে retry-সহ পাঠান, প্রতিটির সাথে idempotency key. পাতা বন্ধ থাকলেও এটি করার উপায় — service worker — পর্ব ৪৪-এ।
- Debounce আর throttle-এর পার্থক্য কী? — debounce শেষ event-এর পরে নির্দিষ্ট সময় চুপ থাকলে একবার চালায়; throttle প্রতি নির্দিষ্ট সময়ে বড়জোর একবার চালায়।
- একটি debounce function লিখুন। — closure-এ একটি timer রাখুন; প্রতিটি কলে
clearTimeoutকরে নতুনsetTimeout(নিচের কোড)। প্রায়ই পরের প্রশ্ন: leading option আরthisধরে রাখা। - একটি fetch কীভাবে বাতিল করবেন? —
AbortControllerতৈরি করে{ signal }দিন, তারপরcontroller.abort(); promiseAbortErrorদিয়ে reject হয়। - Exponential backoff-এ jitter কেন? — একসাথে ব্যর্থ হওয়া অনেক client যাতে একই মুহূর্তে আবার আঘাত না করে — যাকে বলে thundering herd.
- ১০,০০০টি URL আনতে হবে, একসাথে ৫টির বেশি নয় — কীভাবে? — N জন worker-এর একটি pool, যারা একটি ভাগ করা index থেকে পরের কাজ তোলে; ফল নিজের index-এ লেখে।
// ইন্টারভিউ সংস্করণ — this আর argument দুটোই রক্ষা করে
function debounce(fn, wait, { leading = false } = {}) {
let timer = null;
return function (...args) {
const callNow = leading && timer === null;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!leading) fn.apply(this, args);
}, wait);
if (callNow) fn.apply(this, args);
};
}
function throttle(fn, limit) {
let last = -Infinity;
return function (...args) {
const now = Date.now();
if (now - last >= limit) { last = now; fn.apply(this, args); }
};
}৯ · AI-এর যুগে — AI-এর লেখা async নকশা যাচাই করা
“একটি retry function লিখে দাও” বললে AI কয়েক সেকেন্ডে চলার মতো কোড দেয়। কিন্তু এই নকশাগুলোর ভুল ডেমোতে ধরা পড়ে না; ধরা পড়ে হাজার ব্যবহারকারী আর ধীর সার্ভার একসাথে এলে। পাঁচটি ভুল বিশেষভাবে দেখুন।
| AI-এর সাধারণ ভুল | ফল | ঠিক রূপ |
|---|---|---|
সব ভুলে, সব method-এ retry — POST /orders-ও | দুটি অর্ডার, দুবার টাকা কাটা; 400-এ বৃথা চেষ্টা | শুধু ক্ষণস্থায়ী ভুল আর idempotent কাজ; POST-এ idempotency key |
| নির্দিষ্ট বিরতি বা jitter ছাড়া backoff, কোনো সীমা নেই | সার্ভার ফিরে আসার মুহূর্তে retry-এর ঢেউ; অসীম লুপ | exponential backoff + full jitter + cap + সর্বোচ্চ চেষ্টা; Retry-After মানা |
React component-এর ভেতরে প্রতিটি render-এ debounce(...) | প্রতি render-এ নতুন closure, নতুন timer — debounce কাজই করে না | useMemo/useRef দিয়ে একবার তৈরি, unmount-এ বাতিল |
| search বা effect-এ abort নেই | পুরোনো উত্তর নতুনটিকে মুছে দেয়; unmount-এর পরে state বদল | প্রতিটি নতুন request-এ আগেরটির abort(); cleanup-এ abort |
১০,০০০টি কাজে সরাসরি Promise.all(items.map(fn)) | হাজার সংযোগ একসাথে, 429, মেমরি শেষ | mapWithConcurrency/p-limit দিয়ে ৩–১০টি একসাথে |
AI-কে এমন কোড লেখাতে চাইলে নিয়মগুলো আগেই বলে দিন — আর পরীক্ষাও চেয়ে নিন:
AI-এর লেখা async নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১“Debounce” শব্দটি এসেছে ইলেকট্রনিক্স থেকে। একটি যান্ত্রিক সুইচের সমস্যা কীভাবে ওয়েবের সবচেয়ে প্রচলিত একটি নকশার নাম হয়ে গেল?
একটি যান্ত্রিক সুইচ বা কিবোর্ডের বোতাম চাপলে আমাদের কাছে মনে হয় একবার “অন” হলো। কিন্তু ভেতরে দুটি ধাতব পাত এসে ঠোকে, আর স্প্রিং-এর মতো কয়েকবার লাফিয়ে আলাদা হয়ে আবার লাগে — কয়েক মিলিসেকেন্ড ধরে। মানুষের কাছে এটি অদৃশ্য, কিন্তু একটি ডিজিটাল সার্কিট মাইক্রোসেকেন্ডে পড়ে; তার কাছে একটি চাপ মানে অন-অফ-অন-অফ-অন — হয়তো দশটি চাপ। একে বলে contact bounce. ক্যালকুলেটরে একবার “৫” চাপলে “৫৫৫” উঠে যাওয়া এড়াতে প্রকৌশলীরা বহু দশক ধরে “debouncing” করে আসছেন: হার্ডওয়্যারে একটি RC সার্কিট বা latch দিয়ে, বা সফটওয়্যারে — “অবস্থা বদলের পরে কয়েক মিলিসেকেন্ড স্থির না থাকা পর্যন্ত মানবেন না।”
শেষ বাক্যটিই ঠিক JavaScript-এর debounce. ব্যবহারকারীর টাইপিং, জানালার resize বা scroll — এগুলোও “লাফানো” সংকেত: একটি মানুষী ইচ্ছা (“ঢাকা খুঁজব”) অনেকগুলো যান্ত্রিক event হয়ে আসে। ২০০০-এর দশকের শেষে Ajax-নির্ভর অ্যাপে প্রতিটি কী-প্রেসে সার্ভারে request যাওয়ার সমস্যা প্রকট হয়। ২০০৯ সালে John Hann তাঁর ব্লগে “Debouncing Javascript Methods” নামে একটি লেখা প্রকাশ করেন, যা ধারণাটিকে JavaScript সমাজে জনপ্রিয় করে বলে ব্যাপকভাবে উল্লেখ করা হয়। কিছুদিনের মধ্যে Underscore.js লাইব্রেরি _.debounce আর _.throttle যোগ করে, আর পরে Lodash এগুলোকে leading, trailing আর maxWait option-সহ পরিণত রূপ দেয় — Lodash-এর throttle আসলে ভেতরে maxWait-সহ একটি debounce.
মজার ব্যাপার হলো, প্রায় একই নাম নিয়ে দুই জগতে সামান্য অর্থভেদ আছে। হার্ডওয়্যারের debounce সাধারণত প্রথম স্থির পরিবর্তনটি মেনে নিয়ে পরের লাফগুলো উপেক্ষা করে — যা JavaScript-এর “leading” রূপের কাছাকাছি; আর ওয়েবের জনপ্রিয় ডিফল্ট হলো “trailing” — শেষে একবার। দুটোর পেছনের প্রশ্ন একই: কোন সংকেত আসল ইচ্ছা, আর কোনটি শুধু শব্দ (noise)?
মূল উপলব্ধি: সফটওয়্যারের অনেক “নতুন” নকশা আসলে পুরোনো প্রকৌশলের পুনরাবিষ্কার। Debounce মানে মানুষের ইচ্ছা আর যন্ত্রের সংকেতের মধ্যে একটি ছাঁকনি — সুইচের ধাতব পাত হোক বা কিবোর্ডের আঙুল, সমস্যাটি একই।
প্র ০২১৯৭০-এর দশকের Ethernet থেকে ২০১৫ সালের AWS-এর একটি ব্লগ পোস্ট — exponential backoff আর jitter-এর ইতিহাস আমাদের retry লেখা সম্পর্কে কী শেখায়?
১৯৭০-এর দশকের শুরুতে হাওয়াই বিশ্ববিদ্যালয়ের ALOHAnet রেডিওর মাধ্যমে কয়েকটি দ্বীপের কম্পিউটার জুড়েছিল। সবাই একই চ্যানেলে পাঠাত; দুজন একসাথে পাঠালে সংকেত মিশে নষ্ট হতো (collision), আর দুজনকেই আবার পাঠাতে হতো। প্রশ্ন: কখন? দুজনই যদি একই সময় পরে আবার পাঠায়, আবার collision — অসীমকাল। উত্তর ছিল এলোমেলো অপেক্ষা। Xerox PARC-এ Robert Metcalfe আর David Boggs এই ধারণা তারের নেটওয়ার্কে নিয়ে Ethernet তৈরি করেন (তাঁদের বিখ্যাত গবেষণাপত্র ১৯৭৬ সালে)। Ethernet-এর CSMA/CD নিয়মে প্রতিটি collision-এর পরে একটি যন্ত্র ০ থেকে ২n−১ “slot time”-এর মধ্যে একটি random সময় অপেক্ষা করে, যেখানে n হলো পরপর collision-এর সংখ্যা (একটি সীমা পর্যন্ত) — truncated binary exponential backoff. যত বেশি সংঘর্ষ, তত বড় জানালা থেকে random বাছাই; আর অনেক চেষ্টার পরে হাল ছেড়ে দেওয়া।
লক্ষ করুন, এখানে প্রথম দিন থেকেই দুটি উপাদান একসাথে ছিল: exponential (জানালা দ্বিগুণ) আর random (জানালার ভেতরে এলোমেলো বাছাই)। পরের কয়েক দশকে সফটওয়্যার জগৎ প্রায়ই প্রথমটি নিয়েছে, দ্বিতীয়টি ভুলে গেছে — “১, ২, ৪, ৮ সেকেন্ড পরে আবার চেষ্টা”। ২০১৫ সালে AWS Architecture Blog-এ Marc Brooker “Exponential Backoff And Jitter” নামে একটি লেখায় simulation দিয়ে দেখান, jitter ছাড়া backoff-এ প্রতিযোগী client-রা একসাথে ঢেউ হয়ে আসতে থাকে, আর মোট কাজ বাড়ে। তিনি কয়েকটি রূপ তুলনা করেন — “full jitter” (০ থেকে পুরো জানালার মধ্যে random), “equal jitter” আর “decorrelated jitter” — আর দেখান যে jitter যোগ করলে মোট কল ও কাজ শেষের সময় দুটোই উল্লেখযোগ্যভাবে কমে। আজ AWS-এর SDK-সহ অনেক SDK ডিফল্টভাবে jitter-সহ backoff ব্যবহার করে।
শিক্ষা হলো: retry একটি সমষ্টিগত আচরণ। একা দেখলে “২০০ ms পরে আবার” নিরীহ, কিন্তু সেই কোড লাখ ফোনে চলছে, আর সবাই একই মুহূর্তে একই ঘটনা দেখছে। Ethernet-এর যন্ত্রগুলো একটি তার ভাগ করত; আমাদের client-রা একটি সার্ভার ভাগ করে — সমস্যাটি একই।
মূল উপলব্ধি: Backoff সার্ভারকে নিঃশ্বাস নেওয়ার সময় দেয়, jitter সবাইকে একসাথে ফিরতে দেয় না। দুটি ছাড়া retry আসলে একটি সমন্বিত আক্রমণ, যা আপনার নিজের ব্যবহারকারীরাই চালাচ্ছেন।
প্র ০৩Circuit breaker নকশাটি কোথা থেকে এল, আর কেন একটি ধীর সার্ভিস একটি মৃত সার্ভিসের চেয়েও বিপজ্জনক হতে পারে?
২০০৭ সালে Michael Nygard-এর বই “Release It! Design and Deploy Production-Ready Software” প্রকাশিত হয়। বইটি তাত্ত্বিক নয় — বড় বড় ই-কমার্স আর এয়ারলাইন সিস্টেম ভেঙে পড়ার বাস্তব গল্প থেকে লেখা। Nygard দেখান, সিস্টেম সাধারণত একটি অংশের ব্যর্থতায় ভাঙে না; ভাঙে যখন সেই ব্যর্থতা ছড়িয়ে পড়ে — একে বলে cascading failure. তাঁর বর্ণিত “stability patterns”-এর মধ্যে সবচেয়ে বিখ্যাত হয় Circuit Breaker — বাড়ির বৈদ্যুতিক ব্রেকারের অনুকরণে, যা শর্ট সার্কিটে লাইন কেটে দিয়ে পুরো বাড়ি পুড়ে যাওয়া ঠেকায়। পরে Martin Fowler-এর একটি জনপ্রিয় লেখা আর Netflix-এর Hystrix লাইব্রেরি একে microservice জগতের সাধারণ নকশা বানায়।
কেন ধীর সার্ভিস মৃতটির চেয়ে খারাপ? একটি মৃত সার্ভিস সাথে সাথে “connection refused” দেয় — caller দ্রুত জানে, দ্রুত বিকল্প পথে যায়। কিন্তু একটি ধীর সার্ভিস প্রতিটি request ৩০ সেকেন্ড ধরে রাখে। Caller-এর প্রতিটি thread, সংযোগ বা মেমরি সেই অপেক্ষায় আটকে থাকে। কয়েক মিনিটে caller-এর সব সম্পদ শেষ — এবার caller-ও ধীর, তার caller-ও আটকে যায়। একটি ছোট recommendation সার্ভিসের ধীরতা পুরো চেকআউট বন্ধ করে দিতে পারে। এর সাথে retry যোগ করলে অবস্থা আরও খারাপ: প্রতিটি অপেক্ষমাণ request তিনগুণ হয়ে ফিরে আসে।
Circuit breaker এই শিকল কাটে তিন অবস্থা দিয়ে। Closed: স্বাভাবিক, request যায়, ব্যর্থতা গোনা হয়। সীমা পেরোলে Open: কিছুক্ষণ কোনো request যায় না, সাথে সাথে ভুল বা fallback (cache-এর পুরোনো ডেটা, “এখন সুপারিশ দেখানো যাচ্ছে না”) ফেরত — caller-এর সম্পদ বাঁচে, অসুস্থ সার্ভিস বিশ্রাম পায়। সময় শেষে Half-open: কয়েকটি পরীক্ষামূলক request; সফল হলে Closed, ব্যর্থ হলে আবার Open. সাথে থাকে timeout (ধীরকে মৃত বানানো) আর Nygard-এর “bulkhead” — জাহাজের খোপের মতো সম্পদ ভাগ করা; এই পর্বের concurrency limit আসলে একটি ছোট bulkhead.
মূল উপলব্ধি: নির্ভরযোগ্য সিস্টেম মানে এমন সিস্টেম নয় যা কখনো ব্যর্থ হয় না; বরং যা দ্রুত ব্যর্থ হয় (fail fast), ব্যর্থতা আটকে রাখে, আর নিজে থেকে সেরে ওঠার সুযোগ দেয়। Timeout, retry, backoff আর circuit breaker একসাথে একটি দল — একটি ছাড়া অন্যটি অসম্পূর্ণ।
প্র ০৪Promise-এর ভেতরে বাতিলের ক্ষমতা না দিয়ে আলাদা AbortController কেন বানানো হলো? ২০১৫–২০১৭-এর সেই বিতর্ক থেকে API নকশা সম্পর্কে কী শেখা যায়?
২০১৫ সালের দিকে ব্রাউজারে fetch আসে — XMLHttpRequest-এর আধুনিক, Promise-ভিত্তিক বিকল্প। কিন্তু একটি জিনিস বাদ পড়ে: XHR-এর abort(). ডেভেলপাররা সাথে সাথে অভিযোগ করেন — search box, পাতা বদল, বড় ডাউনলোড — সবখানে বাতিল দরকার। WHATWG-এর fetch মানদণ্ডের GitHub-এ “Aborting a fetch” নামের আলোচনা শত শত মন্তব্যে গড়ায়, আর বিতর্কটি দুই বছর চলে।
মূল প্রশ্ন ছিল নকশার: বাতিল কি Promise-এর বৈশিষ্ট্য হওয়া উচিত? এক পক্ষ চাইলেন “cancelable promise” — promise.cancel(), আর একটি তৃতীয় অবস্থা (“canceled”), resolve আর reject-এর পাশাপাশি। TC39-তে এমন একটি প্রস্তাব এগোয়ও, কিন্তু আপত্তি ওঠে গভীর: একটি promise অনেকে ভাগ করে নিতে পারে — একজন then লাগিয়েছে বলেই কি সে সবার জন্য কাজটি থামাতে পারবে? ফল যে পড়ে, তার হাতে বাতিলের ক্ষমতা দেওয়া মানে “পড়ার অধিকার” আর “নিয়ন্ত্রণের অধিকার” মিশিয়ে ফেলা। ২০১৬ সালের শেষে প্রস্তাবটি প্রত্যাহার করা হয়।
শেষে সমাধান আসে DOM মানদণ্ড থেকে, ২০১৭ সালে: আলাদা AbortController (ক্ষমতা) আর AbortSignal (শোনার অধিকার)। এই নকশাকে বলা যায় “capability” ভাগ — যার হাতে controller, শুধু সে বাতিল করতে পারে; signal যাকে খুশি দেওয়া যায়, নিরাপদে। Signal Promise থেকে স্বাধীন, তাই একটি signal fetch, timer, event listener, stream — যেকোনো কিছুতে দেওয়া যায়, আর এক abort()-এ সব থামে। ২০১৭–২০১৯-এর মধ্যে প্রধান ব্রাউজারগুলো এটি চালু করে। Node.js আলাদা পথে না গিয়ে একই API গ্রহণ করে — v15-এ global AbortController আসে, আর আজ fs.readFile, timers/promises, child_process, এমনকি events.once একই signal বোঝে। .NET-এর CancellationToken আর C++20-এর std::stop_token একই দর্শনের আত্মীয়।
মূল উপলব্ধি: ভালো API নকশায় “কে কী করতে পারে” স্পষ্ট ভাগ থাকে। দুই বছরের ধীর বিতর্ক বিরক্তিকর মনে হলেও তার ফল এমন একটি সাধারণ ভাষা, যা ব্রাউজার থেকে সার্ভার পর্যন্ত সব API একইভাবে বোঝে — তাড়াহুড়োর একটি ভুল নকশা যুগ যুগ ধরে বইতে হতো।
প্র ০৫Retry যে সমস্যা সারানোর জন্য, কীভাবে সেটিই বড় outage-এর কারণ হয়? Thundering herd আর retry storm-এর পেছনের গণিত কী?
একটি সহজ হিসাব দিয়ে শুরু করুন। একটি অ্যাপে তিন স্তর: ব্রাউজার → API সার্ভার → ডেটাবেস সার্ভিস। প্রতিটি স্তর ভুলে ৩ বার চেষ্টা করে। ডেটাবেস ধীর হয়ে গেলে API-র প্রতিটি request ডেটাবেসে ৩টি চেষ্টা পাঠায়; ব্রাউজারও API-তে ৩ বার চেষ্টা করে। ফল: ব্যবহারকারীর একটি ক্লিক ডেটাবেসে ৩ × ৩ = ৯টি request হয়ে পৌঁছায়। ঠিক যে মুহূর্তে ডেটাবেসের সবচেয়ে কম ভার দরকার, তখনই সে স্বাভাবিকের নয়গুণ ভার পায়। স্তর বাড়লে এই গুণফল সূচকীয়ভাবে বাড়ে — একে বলে retry amplification বা retry storm. সার্ভার একটু সেরে উঠলেই জমে থাকা retry-এর ঢেউ তাকে আবার ফেলে দেয়, আর সিস্টেম একটি “metastable” অবস্থায় আটকে যায় — মূল কারণ চলে গেলেও নিজে থেকে সেরে ওঠে না।
বাস্তবে এমন ঘটনা বহুবার ঘটেছে। ২০১০ সালের সেপ্টেম্বরে Facebook প্রায় আড়াই ঘণ্টা বন্ধ ছিল: একটি ভুল কনফিগারেশন মান স্বয়ংক্রিয় যাচাই ব্যবস্থাকে বিভ্রান্ত করে, আর প্রতিটি client “ঠিক করতে” ডেটাবেসে প্রশ্ন পাঠাতে থাকে — সেই প্রশ্নের চাপে ডেটাবেস ব্যর্থ হয়, যা আরও প্রশ্ন ডেকে আনে। প্রকৌশলীদের শেষে পুরো সাইট বন্ধ করে ভার থামাতে হয়। ২০১৫ সালে AWS-এর একটি অঞ্চলে DynamoDB-র বড় বিভ্রাটের প্রকাশিত বিশ্লেষণেও একই ছবি — একটি অভ্যন্তরীণ মেটাডেটা সার্ভিস ধীর হলে storage সার্ভারগুলোর বারবার অনুরোধ তার ওপর চাপ আরও বাড়ায়। কাছাকাছি ঘটনা thundering herd: একটি জনপ্রিয় cache entry-র মেয়াদ শেষ হলে হাজার request একসাথে ডেটাবেসে ছুটে যায়।
প্রতিরক্ষাগুলো এই পর্বেই আছে, শুধু সঠিক জায়গায় বসাতে হবে। এক স্তরে retry — সাধারণত সবচেয়ে বাইরের বা সবচেয়ে ভেতরের একটিতে, সব স্তরে নয়। Retry budget — মোট request-এর একটি ছোট অংশের (ধরা যাক ১০%) বেশি retry নয়; Google-এর SRE বই এমন নিয়মের কথা বলে। Jitter — ঢেউ ভাঙা। Circuit breaker — মৃত সার্ভিসে চেষ্টা বন্ধ। আর cache-এর জন্য request coalescing — অংশ ৫-এর dedupe-এর মতো, একই key-এর জন্য একটিই ডেটাবেস প্রশ্ন।
মূল উপলব্ধি: প্রতিটি client-এর কাছে যৌক্তিক আচরণ সমষ্টিতে বিপর্যয় হতে পারে। Retry লেখার সময় প্রশ্ন করুন: “লাখ client একসাথে এটি করলে সার্ভারের কী হবে?” — এই প্রশ্নই একজন সাধারণ প্রোগ্রামার আর একজন সিস্টেম-চিন্তকের পার্থক্য।
অনুশীলন
এগুলোর বেশিরভাগে কোড লিখতে হবে না — আগে কাগজে টাইমলাইন আঁকুন, তারপর console-এ চালিয়ে মিলিয়ে নিন।
-
টাইমলাইন অনুমান করুন:
debounce(fn, 250); কল আসে ০, ১০০, ৪০০, ৫০০ আর ৬০০ ms-এ।fnকখন কখন চলবে? একই কলগুলোতেthrottle(fn, 250)(timestamp-ভিত্তিক, leading, trailing নেই) হলে?Debounce: ০ → ২৫০-এ বাজবে; ১০০ → বাতিল, ৩৫০-এ; ৩৫০-এ কেউ বাতিল করেনি, তাই ৩৫০ ms-এ প্রথম কল. ৪০০ → ৬৫০; ৫০০ → ৭৫০; ৬০০ → ৮৫০ — ৮৫০ ms-এ দ্বিতীয় কল. Throttle: ০ চলে (last = ০); ১০০ বাদ (১০০ < ২৫০); ৪০০ চলে (৪০০ ≥ ২৫০, last = ৪০০); ৫০০ বাদ; ৬০০ বাদ (২০০ < ২৫০). চলে ০ আর ৪০০-এ — আর ৬০০-এর শেষ event হারিয়ে যায়, ঠিক এ কারণেই trailing কল দরকার।
-
Retry করবেন কি না: প্রতিটির জন্য হ্যাঁ/না আর কারণ বলুন — (ক)
GET /products-এ নেটওয়ার্ক ভুল, (খ)POST /payments-এ 503, idempotency key ছাড়া, (গ)GET /orders/7-এ 404, (ঘ)PUT /cart/items/3 {"qty": 2}-এ 429,Retry-After: 5, (ঙ) ব্যবহারকারী বাতিল চাপার পরেAbortError.(ক) হ্যাঁ — পড়া idempotent, ভুলটি ক্ষণস্থায়ী হতে পারে; backoff আর jitter-সহ। (খ) না — 503 পেলেও সার্ভার হয়তো আংশিক কাজ করেছে; key ছাড়া আবার পাঠালে দুবার টাকা কাটার ঝুঁকি। ব্যবহারকারীকে অবস্থা দেখান বা আগে key যোগ করুন। (গ) না — জিনিসটি নেই; আবার চাইলেও নেই। (ঘ) হ্যাঁ — PUT একটি নির্দিষ্ট মান বসায় (idempotent), আর সার্ভার নিজেই বলেছে ৫ সেকেন্ড পরে আসতে; নিজের backoff-এর বদলে ৫ সেকেন্ড অপেক্ষা। (ঙ) কখনো না — ব্যবহারকারী চাননি; retry করলে বাতিল বোতামটিই অর্থহীন।
-
বাগ খুঁজুন: একটি সহকর্মী লিখেছেন
input.addEventListener("input", (e) => debounce(() => search(e.target.value), 300)());— আর অভিযোগ করছেন “debounce কাজ করছে না, প্রতিটি অক্ষরে search হচ্ছে, শুধু ৩০০ ms দেরিতে।” কেন? ঠিক করুন।প্রতিটি
inputevent-এdebounce(...)নতুন করে ডাকা হচ্ছে — অর্থাৎ প্রতিবার নতুন closure, নতুনtimervariable. প্রতিটি debounced function জীবনে একবারই ডাকা হয়, তাই কেউ কারও timer বাতিল করে না; সবগুলোই ৩০০ ms পরে চলে। Debounce-এর পুরো শক্তি একটি ভাগ করা closure-এ। ঠিক রূপ: debounced function একবার বাইরে তৈরি করুন —const debouncedSearch = debounce(search, 300); input.addEventListener("input", (e) => debouncedSearch(e.target.value));. React-এ একই বাগ হয় component-এর ভেতরে প্রতি render-এ debounce বানালে — সমাধানuseMemoবাuseRef. -
কোড লিখুন — বাতিলযোগ্য pool: এই পর্বের
mapWithConcurrency(items, limit, fn)-কে বদলেmapWithConcurrency(items, limit, fn, signal)বানান: signal বাতিল হলে নতুন কোনো কাজ শুরু হবে না, আর পুরো functionsignal.reasonদিয়ে reject হবে। চলমান কাজগুলোকেও থামাতেfn-কে signal দিন।একটি সম্ভাব্য সমাধান:
async function mapWithConcurrency(items, limit, fn, signal) { const results = new Array(items.length); let next = 0; async function worker() { while (next < items.length) { signal?.throwIfAborted(); // নতুন কাজ তোলার আগে দেখুন const i = next++; results[i] = await fn(items[i], i, signal); } } await Promise.all(Array.from({ length: limit }, worker)); return results; }বাতিল হলে প্রতিটি worker পরের কাজ তোলার আগে
throwIfAborted()-এ থামে, আর প্রথম rejection-এPromise.allreject হয়।fnযদি signal-টিfetch-এ দেয়, চলমান upload-ও সাথে সাথে থামে — নইলে সেগুলো শেষ হওয়া পর্যন্ত চলবে, শুধু ফল কেউ নেবে না। একটি কাজ ব্যর্থ হলে বাকিদেরও থামাতে চাইলে ভেতরে নিজের একটি controller রাখুন, যা যেকোনো ভুলেabort()করে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমDebounce আর AbortController-সহ একটি JavaScript search box-এ একটি কী-প্রেস থেকে ফল দেখানো পর্যন্ত ঘটনাগুলো সাজান (▲▼ চেপে বা টেনে)।
- ⌨️ input event এল, debounced function ডাকা হলো
- ⏱️ আগের timer বাতিল, নতুন timer বসল
- 🤫 wait সময় কোনো নতুন কী-প্রেস নেই — timer বাজল
- ✋ আগের চলমান request-এর controller.abort()
- 📡 নতুন AbortController-এর signal দিয়ে fetch পাঠানো
- 🖼️ উত্তর এল, বাতিল হয়নি — তালিকা আঁকা হলো
JavaScript-এ debounce(fn, 300) দিয়ে তৈরি function-কে ০, ১০০, ২০০ আর ৬০০ ms-এ ডাকা হলো। fn কখন কখন চলবে?
- ০ আর ৬০০ ms-এ
- ৩০০ আর ৯০০ ms-এ
- ৫০০ আর ৯০০ ms-এ
- শুধু ৯০০ ms-এ
JavaScript-এ Promise.race([loadReport(), timeout(3000)])-এ timeout জিতলে loadReport()-এর কাজের কী হয়?
- স্বয়ংক্রিয়ভাবে বাতিল হয়ে যায়
- পেছনে চলতেই থাকে, শুধু তার ফল কেউ ব্যবহার করে না
- একটি TimeoutError ছুড়ে প্রোগ্রাম থামিয়ে দেয়
- নতুন করে আবার শুরু হয়
নিচের কোন request ভুল হলে স্বয়ংক্রিয়ভাবে retry করা সবচেয়ে বিপজ্জনক?
- GET /products-এ নেটওয়ার্ক ভুল
- PUT /profile-এ 429 আর Retry-After: 2
- DELETE /cart/5-এ 503
- idempotency key ছাড়া POST /orders-এ timeout
Exponential backoff-এর সাথে jitter (এলোমেলো অপেক্ষা) যোগ করার মূল কারণ কী?
- অনেক client যাতে একই মুহূর্তে আবার আঘাত না করে
- প্রতিটি একক request-এর সফল হওয়ার সম্ভাবনা নিশ্চিতভাবে বাড়াতে
- মোট চেষ্টার সংখ্যা সীমিত করতে
- আক্রমণকারী যাতে request-এর সময় অনুমান না করতে পারে
JavaScript-এ AbortSignal.timeout(5000) দেওয়া একটি fetch ৫ সেকেন্ডে শেষ না হলে promise কোন নামের DOMException দিয়ে reject হয়?
- AbortError
- NetworkError
- TimeoutError
- RangeError
একটি পাতার scroll অনুযায়ী উপরের অগ্রগতি-বার মসৃণভাবে হালনাগাদ করতে কোনটি সবচেয়ে উপযুক্ত?
- ৩০০ ms-এর debounce
- requestAnimationFrame দিয়ে প্রতি ফ্রেমে বড়জোর একবার
- প্রতিটি scroll event-এ সরাসরি হালনাগাদ
- প্রতি ১০ ms-এ setInterval
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।
মনে রাখুন
- বাতিল = AbortController + AbortSignal. Signal দিন
fetchআর নিজের async function-কে; বাতিল হলে timer/listener পরিষ্কার করে reject করুন।AbortErrorভুল নয় —catch-এ আলাদা করুন।AbortSignal.timeoutদেয়TimeoutError. Promise.raceঅপেক্ষা থামায়, কাজ নয়। Search-এ পুরোনো উত্তর নতুনটিকে মুছতে পারে — আগেরটি abort করুন বা request id দিয়ে পুরোনো উত্তর ফেলে দিন।- Retry: শুধু ক্ষণস্থায়ী ভুলে, শুধু idempotent কাজে, exponential backoff + jitter + cap + সর্বোচ্চ চেষ্টা,
Retry-Afterমেনে। বারবার ব্যর্থ হলে circuit breaker. - অনেক কাজে সীমা দিন — worker pool দিয়ে একসাথে N টি; একই key-এর চলমান promise ভাগ করে dedupe, ছোট অনুরোধ জুড়ে batching.
- Debounce থামার জন্য অপেক্ষা করে, throttle তাল বেঁধে চালায়; দৃশ্যমান কাজে
requestAnimationFrame. Optimistic UI-তে rollback রাখুন, অফলাইন কাজ queue-তে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৭ · DOM — tree, খোঁজা আর বদলানোপরবর্তী পর্বমডিউল ৬-এর শুরু: JavaScript দিয়ে পাতা বদলানো।
- JavaScript প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ২২ · scope chain আর closureফিরে দেখুনDebounce, throttle আর dedupe-এর ভেতরের যন্ত্র — closure আর memoization.
- Python-এর asyncio — async/awaitতুলনা করুনwait_for, Semaphore আর task cancel — একই নকশা Python-এ।
- Java-র Executor আর CompletableFutureতুলনা করুনআসল thread pool, timeout আর future-এর জগৎ।