شکار یه Race Condition در کتابخونهی hyper
خلاصهٔ کاملتر
سرویس Images کلادفلر که با Rust روی Workers ساخته شده، برای مدیریت اتصالها از hyper استفاده میکنه؛ یه کتابخونهی متنباز HTTP برای Rust. به گفتهی نویسندهها، کمی بعد از یه بازطراحی در دسامبر ۲۰۲۵، گزارشهایی رسید که درخواستهای تبدیل تصویر گاهی شکست میخورن؛ اونم فقط برای تصاویر بزرگ و بهصورت پراکنده.
چیزی که ماجرا رو گیجکننده میکرد این بود که پاسخها وضعیت 200 برمیگردوندن و هیچ خطایی لاگ نمیشد، ولی دادهی تصویر وسط راه قطع میشد؛ مثلاً یه پاسخ ۲ مگابایتی فقط چند صد کیلوبایت میرسید. هدر Content-Length چند مگابایت رو وعده میداد، اما بدنه فقط بخش کوچیکی از اون بود.
به گفتهی تیم، این باگ به بازطراحی جدید ربطی نداشت و سالها تو hyper بوده؛ فقط عوضشدن سرویس واسط باعث شد خوانندهی سمت دیگهی سوکت یهکم کندتر بشه. همین چند میلیثانیه فشار برگشتی (backpressure) کافی بود تا بافر خروجی سوکت پر بشه و باگ خودش رو نشون بده. ابزارهای سطح برنامه هیچی نشون نمیدادن، برای همین با strace رفتن سراغ سطح کرنل.
با strace معلوم شد که تو درخواستهای خراب، hyper فقط هدرها و یه تیکهی کوچیک از بدنه رو میفرسته و بلافاصله shutdown رو صدا میزنه، در حالی که مثلاً از ۱۴.۹ مگابایت فقط حدود ۲۱۹ کیلوبایت رفته. ریشهی ماجرا تو حلقهی وضعیت کتابخونه (فایل dispatch.rs) بود که نتیجهی poll_flush رو نادیده میگرفت:
loop {
let _ = self.poll_read(cx)?;
let _ = self.poll_write(cx)?;
let _ = self.poll_flush(cx)?;
if !self.conn.wants_read_again() {
return Poll::Ready(Ok(()));
}
}به گفتهی نویسندهها، مشکل دقیقاً همون let _ قبل از poll_flush ـه. تو Rust این نوشتار نتیجهی عبارت رو دور میندازه، از جمله Poll::Pending رو که یعنی «هنوز flush تموم نشده». پس ممکنه هنوز چند مگابایت تو بافر مونده باشه، ولی حلقه اصلاً خبردار نمیشه و اتصال رو میبنده. hyper نوشتن رو همون لحظهای که داده تو بافر داخلی نشست تمومشده حساب میکرد، نه وقتی واقعاً به سوکت منتقل شده.
راهحل ساده بود: قبل از بستن اتصال، اول هر چی تو بافر مونده رو flush کن. اول این کارو تو حلقهی dispatch انجام دادن، ولی به گفتهی تیم اونجا جای درستی نبود چون میتونست روی اتصالهای keepalive و backpressure اثر بذاره. برای همین دقیقاً همون نقطهی shutdown رو اصلاح کردن تا اول flush بشه و بعد اتصال بسته بشه:
pub(crate) fn poll_shutdown(
&mut self,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
ready!(self.poll_flush(cx)?);
Pin::new(&mut self.io).poll_shutdown(cx)
}نکات کلیدی:
- سرویس Images کلادفلر پاسخهای بزرگ رو گاهی نصفه میفرستاد، اونم با وضعیت ۲۰۰ و بدون خطا
- ریشه یه race condition قدیمی تو کتابخونهی hyper بود که با کندترشدن خوانندهی سوکت آشکار شد
- hyper نتیجهی poll_flush (یعنی Poll::Pending) رو با let _ دور میریخت و زودتر از موعد اتصال رو میبست
- کشف باگ فقط با ابزار سطح کرنل strace ممکن شد، چون ابزارهای سطح برنامه چیزی نشون نمیدادن
- راهحل: قبل از shutdown حتماً بافر flush بشه؛ این وصله به hyperium/hyper هم فرستاده شد




