پلیبوک تولید Node.js: ۵ روش نشت حافظه در استریمها
خلاصهٔ کاملتر
این مقاله بخش دوم از یه سری دوقسمتیه که روی الگوهای خطرناک نشت حافظه در استریمهای Node.js تمرکز داره. برخلاف باگهای معمولی، این مشکلات توی تست لوکال با دیتاست کوچک دیده نمیشن و فقط زیر بار واقعی بیرون میزنن.
۱. کلاینت قطع میشه، سرور خبر نداره: وقتی کاربر وسط دانلود تب رو میبنده، متد pipe() قدیمی چون فقط به رویداد finish گوش میده — که هیچوقت فایر نمیشه — کوئری دیتابیس و پردازش رو زنده نگه میداره. اما pipeline() بهصورت خودکار با دریافت خطای ERR_STREAM_PREMATURE_CLOSE، کل زنجیره رو تخریب میکنه. همچنین میشه قبل از شروع کار سنگین، با چک کردن req.destroyed از اتلاف منابع جلوگیری کرد.
۲. خروج زودهنگام از استریم: با رویدادهای دستی data، برای توقف استریم باید خودت removeAllListeners و destroy رو صدا میزدی وگرنه استریم پسزمینه به کار خودش ادامه میداد. راهحل مدرن استفاده از for await...of هست — وقتی از حلقه break میکنی، Node.js بهصورت خودکار stream.destroy() رو فراخوانی میکنه:
// Fixed: async iterators automatically destroy the stream on break
for await (const line of fileStream) {
if (line.includes("ERROR")) {
console.log("Found error!");
break; // fileStream.destroy() is called automatically behind the scenes!
}
}۳. Timeout فقط response رو میبنده: اگه timeout فقط res.end() رو صدا بزنه، pipeline() اون رو بهعنوان خطا نمیبینه و upstream (فچ از API خارجی، کرسر دیتابیس) همچنان زنده میمونه. راهحل استفاده از AbortSignal.timeout() هست که کل زنجیره رو یکجا خاموش میکنه: await pipeline(fetchStream, res, { signal: AbortSignal.timeout(30000) }).
۴. کانکشن دیتابیس به سرعت شبکه گره خورده: آزاد کردن کانکشن دیتابیس روی رویداد close از res یعنی یه کاربر موبایل با اینترنت ضعیف میتونه پنج دقیقه یه کانکشن دیتابیس رو نگه داره. راهحل اینه که کانکشن رو به رویداد close کرسر خود دیتابیس وصل کنیم تا به محض تموم شدن واکشی داده آزاد بشه، نه وقتی شبکه کلاینت تموم کرد. stream.finished() هم گزینه دقیقتریه که برای هر سه حالت پایان (موفق، خطا، destroy) کار میکنه.
۵. pipeline() کار کرد، ولی source هنوز داده میفرسته: destroy() غیرهمزمانه و در فاصله بین فیر شدن خطا و رسیدن سیگنال به source، ممکنه چند chunk اضافه وارد حافظه بشه. راهحل اینه که در بلوک catch، بهصورت صریح source.destroy(err) رو صدا بزنیم — اگه از قبل destroy شده باشه این کار no-op هست و ضرری نداره.
در بخش قوانین مدرن، مقاله به یه تله مهم هم اشاره میکنه: اگه برای هندل کردن backpressure دستی منتظر رویداد drain بمونی و استریم قبل از اون خطا بده، await برای همیشه معلق میمونه. راهحل درست استفاده از Promise.race با AbortController هست تا listener بازنده بلافاصله پاکسازی بشه. همچنین برای دیباگ، اضافه کردن یه setInterval که writable.writableLength رو لاگ میکنه میتونه لحظه دقیق فرار producer از consumer رو نشون بده.
نکات کلیدی:
- همیشه از pipeline() بهجای pipe() استفاده کن — teardown خودکار، مدیریت خطا، و propagation backpressure رو یکجا هندل میکنه
- با AbortSignal.timeout() کل زنجیره pipeline رو timeout کن، نه فقط response رو
- کانکشن دیتابیس رو به رویداد close کرسر وصل کن، نه به close از res
- در بلوک catch بعد از pipeline، بهصورت صریح source.destroy(err) رو صدا بزن
- با writable.writableLength میشه نشت حافظه رو در لحظه تشخیص داد
- قبل از production با --max-old-space-size=128 لود تست بگیر تا leakها سریعتر خودشون رو نشون بدن




