یه باگ بد و یک طومار حرف جاواسکریپتی - حکایتی از اسنپ مارکت

سلام ، من جعفر رضائی هستم، یکی از بچه‌های تیم فرانت‌اند توی اسنپ‌مارکت.

این پست رو دارم ساعت ۳:۴۴ دقیقه بامداد می‌نویسیم و الان نصف شب بعد همون روزی هست که یه مشکلاتی روی مدیریت کوکی‌های nodejs‌ برای server side rendering پروژه نیوسایت پیش اومد.

ذره بین و عکس
ذره بین و عکس


اگه در جریان مشکلی که پیش اومد نیستین، بزارین اول بگم که مشکل چی بود... این مشکل که حدودا ۲ ساعت و ۲۷ دقیقه روی production و برای ۱۰ درصد از ترافیک سایت اصلی که به صورت A/B تست به سایت جدید منتقل میشدن، رخ داد و باعث می‌شد کاربرایی که در لحظه بازکردن سایت همزمانی زیادی داشتن توکن jwtشون با داده کاربر دیگه‌ای پر بشه و بعد از لود شدن صفحه بجای پنل خودشون، پنل یه فرد دیگه رو ببینن.

البته طبق سفارشات ثبت شده، این مورد برای موارد کمی رخ داده و ۸۷ درصد سفارشات این بازه با آدرس صحیح خودشون ثبت شدن که مشخص می‌کنه داشتن با پنل خودشون کار می‌کردن.



خب تا اینجای صحبتم سکانس اول بود که مشکل پیش اومده رو توصیف می‌کرد، سکانس دوم رو مسائل فنی، دلیل بروز این مشکل و راهکاری که برای حل سریعش انجام دادیم رو میگم.

اول از همه می‌خوام یه سوال بپرسم، اگه به هر دلیلی یهو این مشکل در وب‌سایت شما پیش بیاد چیکار می‌کنین؟ من امروز این سوال رو چند بار از خودم پرسیدم که در اون لحظه چطوری می‌تونستم با کمترین زمان دقیق‌ترین تصمیم رو بگیرم! البته واقعیتشو بگم قیافه‌م بعد دیدن باگ این شکلی شد:


حالت‌های چهره - از چپ به راست
حالت‌های چهره - از چپ به راست


خب اولین کاری که کردم و احتمالا شما هم می‌کردین این بود که دیپلوی اخیر که ممکن بود مشکل از اون بوده رو revert کردم و دوباره فرستادم بالا... مشکل حل نشد! چون تغییرات قبل از اون هم مربوط به چیزی نبود که فکر کنم مشکلی ایجاد کرده باشه، پس رفتیم کدها رو بخونیم.


چند تا مسئله‌ای که ممکن بود مشکل از اونا باشه رو در آوردیم.

  • وجود باگ در سرویس authentication
  • اشتباه بودن jwt توکن و وجود باگ توی توکنی که refresh توکن می‌کنه
  • رندر شدن markup جابجا برای کاربران توی express
  • کوکی‌های روی سرور nodejs

۲ تا تئوری اول با یه بررسی ساده رد شدن. مورد سوم اصلا امکان پذیر نبود و بیشتر حاصل تفکر عجولانه توی اون لحظه بود و تنها مورد باقی مونده cookie بود...

یه بررسی کوتاه بچه‌های تیم روی کوکی، بی نتیجه‌ بود و خبری از وجود باگ نداشت، بعلاوه روی استیج این مشکل رو نداشتیم.


آقای سوباسا
آقای سوباسا


البته در نظر بگیرین که کارتون سوباسا پخش نمی‌شد و هر لحظه که بررسی‌ها بیشتر طول می‌کشید خطا روی پروداکشن بیشتر در جریان بود. پس باید هر چه سریع‌تر تصمیمی گرفته می‌شد، توی اون لحظات، دو راه وجود داشت:

  • یه روش پیدا کنیم که سریع جلوی مشکل رو بگیریم و بعدا یه فیکس دقیق روش بزنیم
  • بشینیم کامل بررسی کنیم مشکل رو و اساسی باگ رو حل کنیم

قطعا راه اول رو باید انتخاب می‌‌‌کردیم، ولی خب چه‌جوری باید مشکل رو فیکس کنیم؟! دقیقا چی باعث این مشکل میشه!

اینجا یه لحظه به stream شدن و امکان ایجاد خطا بوسیله Readable stream توی nodejs فکر کردم. قبلا توی گیت‌هاب یه صحبتی با سازنده Preact آقای developit داشتم(لینک) که از یکی از پکیج‌هاشون توی نیوسایت و با stream استفاده کنیم ولی پاسخ خیلی طولانی ایشون خیلی از مسائل رو برای من شفاف کرده بود.


https://github.com/GoogleChromeLabs/critters/issues/53

یه issue ، یه کلاس درس!
یه issue ، یه کلاس درس!



یکی از نکته‌های خوب این صحبت، تشریح نحوه stream شدن توی nodejs بود. برای مثال اینکه nodejs هر سایز از دیتا برای stream که داشته باشیم میاد توی بلاک‌های 16 کیلوبایتی به کلاینت ارسال می‌کنه، یعنی اگر شما ۶۰ کیلوبایت html می‌خوایید stream کنید این اتفاق توی 4 بلاک انجام میشه.


پس فکر به اینکه نکنه داریم یه مشکلی رو با stream کردن بوجود می‌آریم پیش اومد. با توجه به معماری منعطفی که برای server side rendering روی نیوسایت ایجاد کردیم(که احتمالا در قالب یه پست دیگه توضیح میدم یا به شکل open source منتشر میشه) اولین و راحت‌ترین کار تبدیل کردن نحوه رندر از stream به string و دیپلوی کردنش بود.

خوشبختانه بعد از این دیپلوی خطا روی سیستم بچه‌ها دیده نشد و گویا مشکل حل شده بود، ولی مسئله این بود که یه مشکل دیگه‌ای هنوز وجود داشت!

کاربرایی که قبلا لاگین کردن باید از حساب فردی که خودشون نیستن خارج میشدن... برای این موضوع هم یه پچ نوشته شد و مرحله اول یعنی جلوگیری از وقوع بیشتر خطا انجام شد. ولی خب تا اینجا فقط تونسته بودیم بدون متوجه شدن دلیل اصلی رخدادن باگ، از رخ دادن بیشتر اون جلوگیری کنیم.




حالا سکانس سوم: یه کمی اوضاع آرومتره و می‌خواییم مشکل اصلی رو پیدا و حل کنیم... مشکل دقیقا چی بوده؟! یعنی استریم نباید انجام بدیم؟! مگه میشه!

اگه دقت کرده بودین یه بخشی از صحبت‌هام گفته شد که ساختار منعطفی برای SSR فراهم کردیم. این ساختار که کلا به شکل ماژولار داره کار می‌کنه و سعی شده حتی بدون تغییر کد بتونه CSR بشه و از lazy و suspense روی ssr پشتیبانی می‌کنه و کلا حتی نحوه درخواست api هم per component هست نه per route و کلی ویژگی‌های متفاوت دیگه.(میدونم فعلا رو خود ری‌اکت هم suspense برای سرور نیومده ولی ssr-prepass از kitten می‌تونه امکان suspense رو node رو بهتون بده :) )

https://github.com/kitten
https://github.com/kitten


این معماری متفاوت باعث میشه ما یه سری چالش‌های متفاوت‌تر داشته باشیم، یکی از اون چالش‌ها که در راستای ماژولار بودن سیستم پیش اومده بود، نحوه مدیریت کوکی‌ها بوده. خب میدونیم که روی سرور اگه با express داریم کار می‌کنیم می‌تونیم به ازای هر درخواست روی res.cookie برای کلاینت کوکی بفرستیم.

ولی اگه ما برای درخواست‌های httpها یه ماژول داشته باشیم که با یه ماژول دیگه که jwt توکن رو مدیریت می‌کنه ارتباط برقرار می‌کنه، چطوری می‌تونیم res رو توی اون context داشته باشیم؟! شاید بپرسین چرا به res نیاز داریم؟ چون مثلا با فرض اینکه شما jwt توکن دارین و اون توکن منقضی شده ما باید بدون اینکه شما در جریان تعویض توکن باشین یه توکن جدید براتون درست کنیم، اون موقع چون می‌تونه درخواست روی سرور باشه پس لازمه توکن شما روی کوکی نوشته بشه...

خب ما برای اینکه یه storage به شکل isolated برای هر request داشته باشیم که مدیریتش هم راحت باشه و نیاز به maintain کردن کانتینر و... مثل redis نباشه. اومدیم از یه پکیج مشهور به اسم node-continuation-local-storage استفاده کردیم

https://github.com/othiym23/node-continuation-local-storage


با این پکیج میشه یه namespace منحصر به فرد برای هر درخواست ایجاد کرد و بهمون اجازه میده یه‌ سری متغیر رو توی یه session نگهداری کنیم و هرجایی از برنامه بهش دسترسی داشته باشیم. یعنی به شکل یه gateway برای تبادل داده عمل می‌کنه.

پس می‌تونه برامون res و req رو توی کل ماژول‌ها قابل دسترس کنه. اما مشکل زمانی پیش میاد که استریم انجام میشه و پکیج‌های 16kb ارسال میشن به مرورگر. توی این حالت اگر concurrent در حال stream باشیم، nodejs میاد برای بهینه بودن اجرای برنامه، از context موجود بین درخواست‌ها به شکل مشترک استفاده می‌کنه و انگار تغییرات یه جا داره انجام میشه. پس توی یه بلاک از استریم دیتا درسته،pipe انجام میشه و توی بلاک بعدی از محل نادرستی از حافظه خونده میشه! پس باگ ایجاد میشه.


الان که ما دیگه رو renderToString هستیم ولی از اون سمت نحوه مدیریت شدن داده بین ماژول‌ها رو تغییر دادیم و بجای کار روی محوریت کوکی، دیتا رو به شکل ساده‌تر توی closure مربوط به همون ماژول مدیریت می‌کنیم.

با توجه به اینکه با رفرش شدن صفحه امکان داشت کوکی شما از middleware احراز هویت رد نشه و نیاز به رفرش داشته باشه و این مورد بدتر باعث میشد که توکن شما مجددا عوض بشه. البته توی این سناریو در بدترین حالت، با توجه به میزان همزمانی کاربران، یه کاربر فعال در اون بازه زمانی اطلاعات ۷ نفر(نفرات ثابت) دیگه رو رفرش کرده و دیده.




امیدوارم توضیحاتم کافی و شفاف بوده باشه، اگه جایی سوالی داشتین خوشحال میشم بپرسین. امیدوارم فرهنگ بیان دلیل رخدادن باگ‌ها و قبول کردن اینکه سیستم‌ها به باگ می‌خورن و هنر ما توسعه‌دهنده‌ها رفع کردن اونا با کمترین هزینه هستش جا بیفته. مورد بعدی اینکه، ما توی پروژه وب‌سایت جدید اسنپ مارکت بخش زیادی از سورس رو به صورت open source جلو بردیم و توی گیت‌هابمون موجوده ، خوشحال می‌شم به گیت‌هاب‌‌مون هم مراجعه کنین و اگه نظری دارین بهمون بگین :) و کلام آخر ، امیدوارم فرهنگ open source بودن رو بیشتر و بیشتر جا بندازیم

https://github.com/snappmarket