فاز 13 · درس 29

قابلیت اطمینان، لغو عملیات و کنترل جریان داده در MCP

نوع
ساختنی
زمان
حدود 120 دقیقه
زبان‌ها
Python
یک شیر فلکه‌ی مکانیکی برنجی بزرگ متصل به لوله‌های پرفشار صنعتی که کنترل جریان داده و انشعابات فشارشکن را تنظیم می‌کند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. لغو درخواست وابسته به بستر انتقال است
  4. stdio
  5. Streamable HTTP
  6. کاربرد محدود لغو از سمت سرور
  7. لغو یک رقابت هم‌زمانی است
  8. پیروزی لغو
  9. پیروزی تکمیل
  10. مهلت‌های زمانی به دو زمان‌سنج نیاز دارند
  11. لغو درخواست با tasks/cancel یکی نیست
  12. شناسه‌ی JSON-RPC جدید به معنای یکتایی تراکنش نیست
  13. مرز دفترکل باید اتمیک و پایدار باشد
  14. ماتریس تلاش مجدد
  15. پس‌زدگی بخشی از صحت سیستم است
  16. بافر کردن در پراکسی
  17. اتصال مجدد یعنی واکشی مجدد
  18. جلوگیری از هجوم هم‌زمان در اتصال مجدد
  19. پیاده‌سازی از صفر
  20. RequestCoordinator
  21. MutationLedger
  22. DurableTaskService
  23. BoundedSseBuffer
  24. توابع کمکی بازیابی
  25. استفاده از ابزارهای واقعی
  26. آزمایشگاه تعاملی
  27. تمرین عملی
  28. خروجی این درس
  29. اعتبارسنجی
  30. حالت‌های شکست در محیط عملیاتی
  31. پیوند با پروژه‌ی پایانی
  32. اصطلاحات کلیدی
  33. برای مطالعه‌ی بیشتر

وقتی همه‌چیز در شبکه و کد درست کار می‌کند، سیستم‌ها رفتار بی‌نقصی دارند؛ اما هزینه‌بارترین باگ‌های سیستم‌های توزیع‌شده دقیقاً زمانی پدیدار می‌شوند که رویدادهایی مثل قطع ارتباط، اتمام مهلت زمانی، یا کندی مصرف‌کننده رخ می‌دهد. اگر کلاینت ارتباط خود را قطع کند و هم‌زمان با یک شناسه‌ی جدید درخواست را تکرار کند، چه تضمینی وجود دارد که یک تراکنش مالی یا تغییر در دیتابیس دو بار اجرا نشود؟ پروتکل MCP ساختار پیام‌ها را استاندارد می‌کند، اما ضمانت‌های تجاری و پایداری در شرایط بحرانی همچنان وظیفه‌ی توسعه‌دهنده است.

در این درس یاد می‌گیرید که چگونه لغو درخواست‌ها را متناسب با هر بستر انتقال پیاده‌سازی کنید، رقابت هم‌زمانی میان تکمیل و لغو را مهار سازید و به کمک کلیدهای یکتاییِ تراکنش (idempotency keys) و صف‌های دارای محدودیت ظرفیت، مانع از نشت حافظه و اجرای تکراری عوارض جانبی شوید.

شناسه‌ی درخواست (request ID) پیام‌ها را به هم مرتبط می‌کند؛ اما عوارض جانبی (side effect) را امن نمی‌سازد، جلوی اجرای ورکر (worker) را نمی‌گیرد و جریان داده (stream) را در برابر مصرف‌کننده‌ی کند محافظت نمی‌کند.

اهداف یادگیری

  • پیاده‌سازی سیگنال لغوِ درست برای stdio و Streamable HTTP.
  • حل رقابت‌های هم‌زمانی میان تکمیل و لغو، بدون ارسال پیام پس از لغو شدن.
  • تفکیک لغو درخواست از معناشناسیِ پایدار در tasks/cancel.
  • تصمیم‌گیری برای تلاش مجدد (retry) بر اساس عوارض جانبی و کلیدهای صریح یکتایی تراکنش (idempotency keys).
  • محدودسازی صف‌های پیشرفت در عین حفظ پاسخ‌های نهایی.
  • بازیابی جریان‌های داده از طریق اتصال مجدد، واکشی مجدد (refetch) و عقب‌گرد دارای نوسان تصادفی (jittered backoff).

صورت مسئله

مسیر بی‌دردسر (happy path) پرهزینه‌ترین باگ‌های سیستم‌های توزیع‌شده را پنهان می‌کند.

کلاینت ابزاری را فراخوانی می‌کند. سرور کار را شروع می‌کند. گزارش‌های پیشرفت از راه می‌رسند. یک پراکسی جریان داده را بافر می‌کند. مهلت زمانی (timeout) کلاینت سر می‌رسد و ارتباطش قطع می‌شود. سرور تنها 1 میلی‌ثانیه بعد کار را به پایان می‌رساند. کلاینت با یک شناسه‌ی JSON-RPC جدید دوباره تلاش می‌کند. عملیات تغییردهنده (mutation) دو بار اجرا می‌شود.

تک‌تک بخش‌ها در سطح محلی درست رفتار کردند، اما کل سیستم در سطح کلان شکست خورد.

پروتکل MCP رفتار پیام‌ها و بستر انتقال (transport) را مشخص می‌کند، اما مدیریت موارد زیر همچنان بر عهده‌ی برنامه‌ی شماست:

  • بودجه‌های زمانی؛
  • یکتایی در لایه‌ی منطق کسب‌وکار؛
  • صف‌های محدودشده؛
  • دسته‌بندی تلاش‌های مجدد؛
  • وضعیت پایدار تسک‌ها؛
  • سیاست اتصال مجدد و واکشی مجدد.

در این درس این تصمیم‌ها را درون یک شبیه‌ساز قطعی (deterministic) پیاده‌سازی می‌کنیم. در اینجا خبری از توابع sleep، سوکت‌ها یا خطاهای تصادفی نیست؛ شما ترتیب رویدادهای لغو را مستقیماً کنترل می‌کنید. همچنین با یک تست چندنخی هماهنگ‌شده، دو کلاینتِ دفترکل را وادار می‌کنیم برای تصاحب یک کلید یکتاییِ مشترک رقابت کنند.

لغو درخواست وابسته به بستر انتقال است

نیت در تمام بسترهای انتقال یکسان است: کلاینت دیگر نیازی به نتیجه‌ی در حال پردازش ندارد؛ اما سیگنال ارسالی روی شبکه فرق می‌کند.

stdio

بستر stdio از یک کانال دوطرفه‌ی مشترک استفاده می‌کند. کلاینت یک اعلان (notification) می‌فرستد:

{
  "jsonrpc": "2.0",
  "method": "notifications/cancelled",
  "params": {
    "requestId": 41,
    "reason": "User closed the operation"
  }
}

این اعلان یک‌طرفه (fire-and-forget) ارسال می‌شود و سرور هیچ پاسخ JSON-RPC در جواب آن نمی‌دهد.

سرور باید کار را متوقف کند، منابع را آزاد سازد و برای درخواست لغوشده پاسخی نفرستد. اگر درخواست ناشناخته باشد، قبلاً تمام شده باشد، یا نشود آن را با ایمنی متوقف کرد، سرور می‌تواند اعلان لغو را نادیده بگیرد.

اعلان‌های لغو با ساختار نادرست، ناشناخته، یا مربوط به درخواست‌هایی که قبلاً تکمیل شده‌اند نادیده گرفته می‌شوند؛ تبدیل این شرایط رقابتی به خطاهای تازه، رقابت‌های ناخواسته‌ی بیشتری می‌سازد.

Streamable HTTP

در نسخه‌ی نوین Streamable HTTP، هر درخواست پاسخ HTTP یا جریان پاسخ SSE اختصاصی خودش را دارد. کلاینت برای لغو، جریان پاسخ همان درخواست را می‌بندد.

برای یک درخواست معمولی HTTP، اعلان notifications/cancelled را با متد POST ارسال نکنید؛ بستن جریان داده، خودِ سیگنال لغو است.

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

کاربرد محدود لغو از سمت سرور

سرور برای لغو فراخوانی‌های دلخواه کلاینت از notifications/cancelled استفاده نمی‌کند. در stdio، لغو ارسال‌شده از سمت سرور صرفاً به پایان دادن به درخواست subscriptions/listen اختصاص دارد. این مسیر را از لغو عادی درخواست‌های کلاینت جدا نگه دارید.

لغو یک رقابت هم‌زمانی است

هر دو ترتیبِ رویداد معتبر هستند.

پیروزی لغو

request starts
client sends cancellation signal
server marks request cancelled
worker reaches completion
server suppresses the response

پیروزی تکمیل

request starts
worker commits the result
server sends the response
cancellation arrives late
server ignores the late notification

کلاینت نیز باید پاسخ دیررس درخواستی را که قبلاً رها کرده نادیده بگیرد. تاخیر شبکه یعنی هیچ‌کدام از طرفین نمی‌توانند اثبات کنند طرف مقابل کدام رویداد را زودتر دیده است.

کلاس RequestCoordinator در این درس فقط یک وضعیت پایانی ذخیره می‌کند؛ متد complete() بعد از لغو شدن هیچ پاسخی برنمی‌گرداند و یک لغو دیرهنگام هم نمی‌تواند رکوردی را که تکمیل شده تغییر دهد.

مهلت‌های زمانی به دو زمان‌سنج نیاز دارند

یک زمان‌سنجِ بی‌عملی (inactivity timer) ساده کافی نیست.

از دو محدودیت استفاده کنید:

  1. مهلت بی‌عملی (Idle timeout): حداکثر زمانی که درخواست می‌تواند بدون هیچ فعالیت مفیدی سپری کند.
  2. مهلت حداکثری (Maximum timeout): بودجه‌ی مطلق زمان واقعی (wall-clock) از لحظه‌ی شروع درخواست.

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

start: 0 ms
progress: 400 ms
progress: 800 ms
progress: 1200 ms
idle timeout: 500 ms
maximum timeout: 2000 ms

در لحظه‌ی 1500 میلی‌ثانیه، درخواست همچنان فعال است؛ چون آخرین پیشرفت تنها 300 میلی‌ثانیه قبل رخ داده است. در لحظه‌ی 2000 میلی‌ثانیه، مهلت نهایی فارغ از اینکه رویداد پیشرفت دیگری در 1999 میلی‌ثانیه رسیده باشد یا نه، درخواست را لغو می‌کند.

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

مقادیر پیشرفت در MCP باید افزایشی باشند. پس از تکمیل یا لغو، ارسال اعلان‌ها متوقف می‌شود. برای ارسال پیشرفت محدودیت نرخ (rate-limit) بگذارید تا یک ورکرِ سریع بستر انتقال را اشباع نکند.

لغو درخواست با tasks/cancel یکی نیست

این دو سازوکار برای طول‌عمرهای متفاوتی طراحی شده‌اند.

سازوکار هدف سیگنال معنای موفقیت
لغو درخواست در stdio یک RPC در حال پردازش notifications/cancelled کلاینت درخواست را رها کرده؛ سرور در صورت امکان باید کار را متوقف کند
لغو درخواست در HTTP یک جریان پاسخ در حال پردازش بستن جریان داده کلاینت درخواست را رها کرده؛ سرور در صورت امکان باید کار را متوقف کند
tasks/cancel یک تسک پایدار درخواست معمولی MCP سرور نیت لغو را تایید و ثبت کرده است

نتیجه‌ی موفق در tasks/cancel اثبات نمی‌کند که ورکر متوقف شده است. تسک ممکن است تا رسیدن ورکر به یک چک‌پوینت برای بررسی فلگ، همچنان در وضعیت working بماند؛ حتی ممکن است کار قبل از رسیدن به آن چک‌پوینت به پایان برسد.

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

شناسه‌ی JSON-RPC جدید به معنای یکتایی تراکنش نیست

شناسه‌های JSON-RPC درخواست‌ها و پاسخ‌ها را به هم پیوند می‌دهند، نه اینکه یک عملیات تجاری را مشخص کنند.

فرض کنید کلاینت تراکنش پرداختی با شناسه‌ی 41 ارسال می‌کند، پاسخش را دریافت نمی‌کند و با شناسه‌ی 42 دوباره می‌فرستد. سرور دو پیام کاملاً متفاوت می‌بیند و بدون یک کلید در لایه‌ی برنامه، راهی ندارد بفهمد هر دو مربوط به یک خرید بوده‌اند.

کلید یکتایی تراکنش نیت کسب‌وکار را مشخص می‌کند:

{
  "name": "charge_account",
  "arguments": {
    "account": "acct-7",
    "cents": 1200,
    "idempotencyKey": "checkout-7"
  }
}

سرور این موارد را ذخیره می‌کند:

  • کلید؛
  • اثر انگشت (هش) آرگومان‌های عملیات؛
  • نتیجه‌ی commit شده.

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

مرز دفترکل باید اتمیک و پایدار باشد

این ترتیب اجرا ناامن است:

check key
run mutation
store result

دو ورکر می‌توانند هم‌زمان نبودِ کلید را ببینند و هر دو تغییر را اعمال کنند. وقوع خرابی (crash) پس از اعمال اثر جانبی ولی پیش از ذخیره‌سازی، در تلاش مجدد ابهام مشابهی می‌سازد.

این درس از یک دفترکل SQLite ذخیره‌شده در فایل استفاده می‌کند. دستور BEGIN IMMEDIATE بررسی کلید، اثر تجاری شبیه‌سازی‌شده، شمارنده‌ی اجرا و نتیجه‌ی ذخیره‌شده را در قالب یک تراکنش واحد و متوالی درمی‌آورد. بنابراین، دو اتصال مستقل به دفترکل که هم‌زمان با یک کلید رقابت می‌کنند، یک نتیجه‌ی commit شده و فقط یک بار اجرا را ثبت خواهند کرد؛ بستن و باز کردن مجدد دفترکل نیز این رکورد را حفظ می‌کند.

هر مقدار بازگشتی از روی JSON ذخیره‌شده بازسازی می‌شود. فراخواننده هرگز شیء تغییرپذیری را که در اختیار دفترکل است دریافت نمی‌کند، بنابراین تغییر دادن دیکشنری دریافتی نمی‌تواند نتایج بازپخش‌های بعدی را خراب کند.

اثر تجاری در این شبیه‌ساز، رسید و شمارنده‌ی اجرا درون همان تراکنش SQLite است. ثبت یک پرداخت واقعی، دیپلوی، یا فراخوانی API خارجی صرفاً با نوشتن در یک جدول محلی اتمیک نمی‌شود. محیط عملیاتی نیازمند تراکنش مشترک در دیتابیس پایدار، الگوی صندوق خروجی تراکنشی (transactional outbox) یا سرویس‌دهنده‌ای بالادستی است که همان کلید یکتایی را اعمال کند. قفل‌های درون حافظه‌ای در سطح فرایند به‌تنهایی در برابر چند نسخه (replica) یا ری‌استارت دوام نمی‌آورند.

ماتریس تلاش مجدد

پیش از پیاده‌سازی منطق تلاش مجدد، عملیات را دسته‌بندی کنید.

دسته مثال قانون تلاش مجدد
امن خواندن قطعی بدون عوارض جانبی تلاش مجدد با شناسه‌ی JSON-RPC جدید پس از مشخص شدن مرز شکست
مشروط تغییر وضعیت همراه با کلید یکتایی پایدار تلاش مجدد با همان کلید و آرگومان‌های کاملاً یکسان
ناامن تغییر وضعیت بدون سازوکار حذف موارد تکراری در بیزنس تلاش خودکار ممنوع؛ ابتدا مغایرت‌گیری کنید

انوتیشن‌های ابزار مانند readOnlyHint و idempotentHint صرفاً پیشنهادهایی غیرقابل‌اعتماد هستند. قرارداد سطح برنامه و پیاده‌سازی سرور است که امنیت تلاش مجدد را تعیین می‌کند.

پس‌زدگی بخشی از صحت سیستم است

تولیدکننده‌ی SSE می‌تواند با سرعتی فراتر از توان کلاینت، پراکسی یا شبکه، پیام‌های پیشرفت تولید کند. یک صف با ظرفیت نامحدود، کندی مصرف‌کننده را به اتمام حافظه تبدیل می‌کند.

از صف محدودشده (bounded queue) استفاده کنید و مشخص کنید چه داده‌هایی می‌توانند از بین بروند.

پیشرفت قابل جایگزینی است؛ مقدار جدیدترِ پیشرفت برای یک توکن مشخص، جایگزین مقدار قبلی می‌شود. اما پاسخ نهایی JSON-RPC هرگز قابل حذف نیست.

بافر پیاده‌سازی‌شده در این درس از این سیاست پیروی می‌کند:

  1. ادغام (coalesce) پیام‌های پیشرفت متوالی برای یک توکن یکسان.
  2. حذف قدیمی‌ترین پیام پیشرفت در صورت پر شدن ظرفیت.
  3. علامت‌گذاری جریان داده با این وضعیت که به واکشی مجدد معتبر نیاز دارد.
  4. حفظ حتمی پاسخ نهایی.
  5. رد هر وضعیتی که در آن حفظ پاسخ نهایی مستلزم دور ریختن پاسخ نهایی دیگری باشد.

این یعنی اتلاف محدودِ کنترل‌شده همراه با بازیابی صریح؛ نادیده گرفتن خاموش خطاها استراتژی به حساب نمی‌آید.

بافر کردن در پراکسی

سرور ممکن است داده‌ها را به‌درستی استریم کند، اما یک پراکسی معکوس (reverse proxy) رویدادها را در بافر نگه دارد.

برای پاسخ‌های SSE هدرهای زیر را بفرستید:

Content-Type: text/event-stream
Cache-Control: no-cache
X-Accel-Buffering: no

مشخصات Streamable HTTP در سال 2026 هدر X-Accel-Buffering: no را توصیه می‌کند تا پراکسی‌های سازگار رویدادها را بی‌درنگ تحویل دهند.

برای جریان‌های کم‌ترافیک و طولانی، دوره‌ای یک کامنت SSE بفرستید:

:

کلاینت خطوط کامنت را نادیده می‌گیرد؛ اما واسطه‌های شبکه ترافیک را می‌بینند و ارتباطِ بدون فعالیت را کمتر می‌بندند.

سیگنال زنده‌ماندن (keepalive) پیشرفت کاری نیست؛ مهلت زمانی معناییِ یک عملیات نباید صرفاً با رسیدن یک کامنت در لایه‌ی انتقال بازنشانی شود.

اتصال مجدد یعنی واکشی مجدد

نسخه‌ی کنونی Streamable HTTP از ادامه‌ی دریافت SSE با مکانیزم Last-Event-ID پشتیبانی نمی‌کند.

پس از قطع شدن جریان subscriptions/listen:

  1. یک درخواست لیسن جدید با یک شناسه‌ی JSON-RPC تازه باز کنید.
  2. فیلتر اشتراک مورد نظر را دوباره اعمال کنید.
  3. ابزارها، منابع، پرامپت‌ها یا تسک‌های تحت‌تاثیر را از طریق متدهای مرجع مجدداً واکشی کنید.
  4. وضعیت برنامه را بر اساس شناسه‌های پایدار یکتا کنید (deduplicate).
  5. عملیات ناامن تغییردهنده را صرفاً به خاطر گم شدن پاسخش دوباره اجرا نکنید.

برنامه‌ی بازیابیِ نمونه صراحتاً sendLastEventId را روی مقدار false قرار می‌دهد و منابعی را که باید واکشی مجدد شوند فهرست می‌کند.

جلوگیری از هجوم هم‌زمان در اتصال مجدد

اگر 10,000 کلاینت دقیقاً بعد از 1 ثانیه دوباره وصل شوند، سروری که در حال بالا آمدن است دوباره از کار می‌افتد.

از عقب‌گرد نمایی (exponential backoff) با نوسان تصادفی (jitter) و سقف مشخص (cap) استفاده کنید. در این درس، نوسان تصادفی به شیوه‌ای قطعی از روی شناسه‌ی کلاینت و شماره‌ی تلاش محاسبه می‌شود تا تست‌ها تکرارپذیر بمانند:

attempt 0: up to 250 ms
attempt 1: up to 500 ms
attempt 2: up to 1000 ms
...
cap: 8000 ms

در محیط عملیاتی می‌توانید از تصادفی‌سازی امن یا توابع محیط اجرا (runtime) استفاده کنید؛ اصل تغییرناپذیر، توزیع بار در طول زمان است نه یک فرمول ریاضی خاص.

پیاده‌سازی از صفر

فایل code/main.py پنج بخش کوچک مربوط به قابلیت اطمینان را پیاده‌سازی می‌کند.

RequestCoordinator

  • شروع یک درخواست در حال پردازش همراه با مهلت‌های بی‌عملی و حداکثری؛
  • تولید اعلان‌های پیشرفت به صورت صعودی و یکنواخت؛
  • تولید سیگنال لغو صحیح برای stdio یا HTTP؛
  • نادیده گرفتن اعلان‌های لغو نامعتبر؛
  • شفاف‌سازی رقابت نهایی میان لغو و تکمیل کار؛
  • اختصاص دادن لغو ارسالی از سمت سرور به اشتراک‌های stdio.

MutationLedger

  • اثبات اینکه دو شناسه‌ی JSON-RPC بدون کلید بیزنس دو بار اجرا می‌شوند؛
  • استفاده از تراکنش SQLite در فایل برای بررسی کلید، اثر شبیه‌سازی‌شده، شمارنده‌ی اجرا و commit قطعی نتیجه؛
  • جلوگیری از اجرای تکراری برای آرگومان‌های یکسان ذیل یک کلید یکتایی، در اتصال‌های مستقل به دفترکل؛
  • رد کردن استفاده‌ی مجدد از یک کلید با آرگومان‌های متفاوت؛
  • بازگرداندن کپی‌های ایمن (defensive copies) و حفظ رکوردهای ذخیره‌شده پس از بستن و باز کردن مجدد فایل دیتابیس.

DurableTaskService

  • تایید دریافت درخواست لغو؛
  • نگه داشتن تسک در وضعیت working تا زمان رسیدن ورکر به چک‌پوینت؛
  • نشان دادن اینکه چرا تایید دریافت، به منزله‌ی وضعیت نهایی نیست.

BoundedSseBuffer

  • ادغام یا حذف پیام‌های پیشرفت تحت فشار بار بالا؛
  • ثبت نیاز سیستم به واکشی مجدد از منبع مرجع؛
  • عدم حذف پاسخ نهایی تحت هیچ شرایطی.

توابع کمکی بازیابی

  • ارائه‌ی هدرهای مناسب SSE برای پراکسی‌ها و کامنت‌های زنده‌ماندن؛
  • ساخت برنامه‌ی اتصال مجدد و واکشی داده‌ها؛
  • توزیع تلاش‌های مجدد با استفاده از عقب‌گرد نمایی قطعی و نوسان تصادفی.

استفاده از ابزارهای واقعی

از ریشه‌ی مخزن اجرا کنید:

cd phases/13-tools-and-protocols/29-mcp-reliability-cancellation-and-flow-control/code
python3 main.py
python3 -m unittest discover tests -v

این دمو هر دو سمت رقابت هم‌زمانی را اجرا می‌کند، عملیات تغییر داده را با حذف موارد تکراریِ مبتنی بر تراکنش در یک دیتابیس موقت SQLite به تصویر می‌کشد، بافر محدود پیشرفت را تحت بار شدید قرار می‌دهد و تغییر وضعیت یک تسک پایدار را از لغو تاییدشده تا اعمال نهایی توسط ورکر نشان می‌دهد.

آزمایشگاه تعاملی

چهار سناریوی ترتیب رویدادها را بدون اضافه کردن توابع sleep اجرا کنید.

  1. درخواست A را شروع کنید، آن را لغو کنید و سپس متد complete() را فراخوانی کنید.
  2. درخواست B را شروع کنید، آن را تکمیل کنید و سپس سیگنال لغو را تحویل دهید.
  3. درخواست C را شروع کنید، قبل از هر مهلت بی‌عملی رویداد پیشرفت بفرستید، و سپس از مهلت زمانی حداکثری عبور کنید.
  4. درخواست D را روی Streamable HTTP شروع کنید و جریان پاسخ آن را ببندید.

برای هر سناریو این موارد را ثبت کنید:

  • وضعیت نهایی درخواست؛
  • آیا پاسخ نهایی وجود دارد یا خیر؛
  • سیگنال لغوی که روی بستر شبکه ارسال شد؛
  • رویدادی که کلاینت باید نادیده بگیرد.

سپس بستر انتقال D را به stdio تغییر دهید. عملیات کاملاً یکسان است، اما سیگنال لغو روی شبکه باید تغییر کند.

تمرین عملی

عملیات reserve_inventory را به MutationLedger اضافه کنید.

الزامات:

  1. کلید یکتایی باید SKU، مقدار (quantity)، شناسه‌ی مستاجر (tenant) و نام عملیات را در بر بگیرد.
  2. تلاش مجدد با همان کلید و همان آرگومان‌ها، اطلاعات همان رزرو اول را برگرداند.
  3. تلاش مجدد با مقدار تغییریافته، بدون ایجاد رزرو جدید با خطا مواجه شود.
  4. اجرایی که تراکنش آن commit شده اما پاسخش از بین رفته، بتواند بر اساس کلید مغایرت‌گیری شود.
  5. نتیجه‌ی عملیات حاوی هیچ رمز یا داده‌ی پرداختی حساسی نباشد.
  6. در صورتی که کلاینت کلیدی ارسال نکرده باشد، تلاش مجدد خودکار غیرفعال شود.
  7. یک قطعی فرضی در اشتراک شبیه‌سازی کنید و قبل از هر اقدامی وضعیت موجودی را مجدداً واکشی کنید.
  8. دو اتصال مجزا به دفترکل را پشت یک مانع هم‌زمانی (barrier) متوقف کنید و سپس با یک کلید یکسان به طور هم‌زمان درخواست بفرستید. بررسی و تایید (assert) کنید که دقیقاً یک رزرو commit شده است.
  9. شیء رزرو بازگردانده‌شده در مرحله‌ی قبل را دستکاری کنید. کلید را بازپخش کنید و ثابت کنید دیتای ذخیره‌شده تغییری نکرده است.
  10. فایل دفترکل را ببندید و دوباره باز کنید، سپس رزرو را با استفاده از کلید بازیابی کنید.

اصول مهندسی را دقیق رعایت کنید: اگر سیستم موجودی انبار در سرویس دیگری قرار دارد، مشخص کنید که آیا آن سرویس همین کلید یکتایی را می‌پذیرد یا اینکه برای اتصال تراکنش محلی به سیستم بیرونی به الگوی transactional outbox نیاز دارید.

خروجی این درس

فایل outputs/skill-mcp-reliability-reviewer.md یک مهارت کامل برای بازبینی قابلیت اطمینان است. به آن یک عملیات MCP، بستر انتقال، سیاست مهلت زمانی، رفتار تلاش مجدد، سیاست صف و برنامه‌ی بازیابی بدهید؛ در پاسخ جدول رقابت‌های هم‌زمانی، دسته‌بندی تلاش مجدد، مرز یکتایی، بررسی‌های کنترل جریان داده و سناریوهای تست شکست را تحویل می‌گیرید.

اعتبارسنجی

این درس زمانی کامل می‌شود که گزاره‌های زیر برقرار باشند:

  • لغو در stdio اعلان notifications/cancelled را ارسال کند و هیچ پاسخی دریافت نکند.
  • لغو در Streamable HTTP جریان درخواست را ببندد و هیچ متد POST لغوی ارسال نکند.
  • لغو پیش از تکمیل، مانع از ارسال پاسخ نهایی شود.
  • تکمیل پیش از لغو، پاسخ را حفظ کند و لغو دیررس را نادیده بگیرد.
  • رویداد پیشرفت بتواند مهلت بی‌عملی را ریست کند، اما هرگز مهلت حداکثری را تغییر ندهد.
  • فرستادن شناسه‌ی JSON-RPC جدید به‌تنهایی، عملیات را مجدداً اجرا کند.
  • یک کلید یکتایی همراه با آرگومان‌های یکسان، تحت رقابت هم‌زمان دو کانکشن فقط یک بار اجرا شود.
  • رکورد commit شده پس از بستن و باز کردن مجدد فایل سالم بماند و بازپخش، یک کپی ایمن برگرداند.
  • دستکاری نتیجه‌ی بازگشتی نتواند داده‌ی ذخیره‌شده در دفترکل را تغییر دهد.
  • بافر محدودشده از سقف ظرفیت خود فراتر نرود و پاسخ نهایی را تحت هیچ شرایطی پاک نکند.
  • اتصال مجدد از درخواست جدید استفاده کند، هدر Last-Event-ID نفرستد، و وضعیت‌های لازم را دوباره واکشی کند.
  • تایید درخواست tasks/cancel تسک را در وضعیت غیرنهایی نگه دارد تا زمانی که ورکر آن را مشاهده کند.

حالت‌های شکست در محیط عملیاتی

شکست نشانه‌ی قابل مشاهده واکنش صحیح
کلاینتِ HTTP اعلان لغو را با POST بفرستد ناهماهنگی کلاینت و سرور درباره‌ی چرخه‌ی حیات درخواست بستن جریان پاسخِ SSE در همان درخواست
ارسال پاسخ از سرور پس از پذیرفتن لغو کلاینت پاسخی دیرهنگام و غیرقابل استفاده دریافت می‌کند توقف کار و عدم ارسال پیام‌های بعدی در صورت برنده شدن لغو
ریست شدن تمام مهلت‌ها با دریافت پیشرفت کارهای قفل‌شده تا ابد فعال می‌مانند حفظ یک مهلت زمانی حداکثری و مطلق به صورت جداگانه
تلقی کردن شناسه‌ی جدید RPC به عنوان فیلتر تکرار عملیات شارژ حساب، دیپلوی یا حذف دو بار اجرا می‌شود افزودن یک کلید پایدار یکتایی در لایه‌ی منطق برنامه
جداسازی بررسی کلید از اعمال تغییر ورکر‌های هم‌زمان هر دو نبودِ کلید را می‌بینند commit هم‌زمانِ ادعای کلید، گزارش تغییر و نتیجه به صورت اتمیک
استفاده از دفترکل درون حافظه در چند نسخه سرور راه‌اندازی مجدد یا ورکر دیگر موارد قبلی را فراموش می‌کند استفاده از دیتابیس مشترک پایدار یا سرویس‌های بالادستی با قابلیت تضمین یکتایی
بازگرداندن مستقیم نتیجه‌ی تغییرپذیر ذخیره‌شده تغییر آبجکت توسط فراخواننده نتایج بعدی را خراب می‌کند سریال‌سازی نتایج ثبت‌شده و بازگرداندن کپی‌های ایمن
استفاده‌ی مجدد از کلید با آرگومان‌های تغییریافته یک کلید نماینده‌ی دو نیت کاری متفاوت می‌شود ذخیره و مقایسه‌ی اثر انگشت آرگومان‌ها
صف نامحدود برای پیام‌های پیشرفت افزایش مصرف حافظه هنگام مواجهه با مصرف‌کننده‌ی کند ادغام و دور ریختن پیام‌های جایگزین‌پذیر در محدوده‌ی تعیین‌شده
افتادن پاسخ نهایی در اثر پر شدن صف کلاینت از نتیجه‌ی واقعی کار بی‌خبر می‌ماند رزرو ظرفیت یا حذف پیام‌های پیشرفت، و هرگز حذف نکردن پاسخ نهایی
بافر شدن داده‌های SSE در پراکسی پیام‌ها ناگهانی یا پس از مهلت زمانی تحویل می‌شوند غیرفعال کردن بافر پراکسی و تنظیم مهلت‌های متناسب
اتکا به Last-Event-ID کلاینت تلاش به ادامه‌ی وضعیتی می‌کند که سرور پشتیبانی نمی‌کند اتصال مجدد با درخواست جدید و واکشی دوباره‌ی داده‌ها
اتصال مجدد هم‌زمان تمام کلاینت‌ها تلاش برای بازیابی، قطعی مجدد در سرور ایجاد می‌کند استفاده از عقب‌گرد نمایی محدودشده به همراه نوسان تصادفی
فرض قطعی دانستن لغو با دریافت ack در تسک کارگر با وجود نمایش توقف در رابط کاربری به کار ادامه می‌دهد پولینگ وضعیت تسک تا زمان رسیدن به وضعیت نهایی

پیوند با پروژه‌ی پایانی

در پروژه‌ی پایانی اکوسیستم ابزارها، با قابلیت اطمینان باید مثل شواهد (evidence) قابل اجرا رفتار شود، نه فقط یک پاراگراف داخل مستندات معماری.

این مستندات و خروجی‌ها را الزامی کنید:

  • لاگ متنی از رقابت لغو در هر کدام از بسترهای انتقال؛
  • جدول رفتار تلاش مجدد برای تک‌تک عملیات تغییردهنده؛
  • ساختار کلیدهای یکتایی و نمونه سناریوی عدم تطابق آرگومان‌ها؛
  • لاگ اجرای هم‌زمان با کلید یکسان، بررسی دوام پس از بازگشایی مجدد، و بررسی عدم امکان تحریف داده‌ها با نام‌های مستعار (mutation-alias check)؛
  • گزارش تست سرریز بافر محدود؛
  • هدرهای مورد نیاز پراکسی معکوس برای SSE و سیاست‌های دوره‌ی بی‌عملی؛
  • طرح اتصال مجدد همراه با نام‌بردن از متدهای مرجع برای واکشی اطلاعات؛
  • مسیر تغییرات لغو در تسک‌های پایدار، در صورتی که پروژه از تسک‌ها استفاده می‌کند.

یک درخواستِ سبز در محیط محلی فقط درستی مسیر بی‌دردسر را ثابت می‌کند. پروژه‌ی پایانی زمانی آماده‌ی محیط پروداکشن است که برای پاسخ‌های گم‌شده، لغوهای دیرهنگام، مصرف‌کنندگان کند و هجوم‌های اتصال مجدد، رفتارهای کاملاً قطعی و پیش‌بینی‌پذیر داشته باشد.

اصطلاحات کلیدی

اصطلاح معنا
لغو درخواست (Request cancellation) رها کردن یک درخواست در حال پردازش MCP
رقابت لغو (Cancellation race) رقابت هم‌زمانی بین دو رویداد پایانیِ تکمیل و لغو
مهلت بی‌عملی (Idle timeout) سقف مجاز زمانی از آخرین فعالیت مفید درخواست
مهلت حداکثری (Maximum timeout) سقف مطلق زمانی از ابتدای درخواست، بدون تاثیرپذیری از رویدادهای پیشرفت
کلید یکتایی تراکنش (Idempotency key) شناسه‌ای در سطح برنامه که مانع از اجرای مجدد یک نیت تجاری می‌شود
دفترکل اتمیک (Atomic ledger) مرزی پایدار که ثبت کلید، رکورد عملیات و نتیجه را در یک پکیج واحد commit می‌کند
پس‌زدگی (Backpressure) سازوکار کنترلی زمانی که سرعت تولیدکننده بیشتر از مصرف‌کننده است
ادغام پیشرفت (Progress coalescing) جایگزین کردن وضعیت‌های قدیمی پیشرفت با مقدار جدیدتر
واکشی مجدد (Refetch) خواندن مجدد وضعیت فعلی پس از قطع ارتباط یا بروز شکاف در جریان داده
نوسان تصادفی (Jitter) تغییرات تصادفی و عمدی در فواصل زمانی برای توزیع تلاش‌های مجدد

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسپاسخ یک mutation از دست می‌رود و کلاینت با یک شناسه JSON-RPC جدید درخواست را تکرار می‌کند. چه چیزی این تلاش مجدد (retry) را امن می‌سازد؟
  2. حین درسکلاینت در پروتکل MCP نسخه 2026-07-28، چگونه یک درخواست در حال اجرا را روی stdio لغو می‌کند؟
  3. حین درسکلاینت چگونه یک درخواست در حال اجرای Streamable HTTP را لغو می‌کند؟
  4. حین درسپیام‌های Progress مرتباً قبل از مهلت تایم‌اوت بیکاری (idle) می‌رسند. کدام ددلاین همچنان باید به درخواست بی‌پایان خاتمه دهد؟
  5. پس از درسپاسخ موفقیت‌آمیز tasks/cancel چه چیزی را اثبات می‌کند؟
  6. پس از درساستریم SSE یک سابسکرایب پس از دریافت چند رویداد progress قطع می‌شود. کدام برنامه بازیابی با MCP نسخه 2026-07-28 همخوانی دارد؟

سؤالات متداول

سیگنال لغو درخواست در بستر Streamable HTTP چگونه ارسال می‌شود؟

در Streamable HTTP، کلاینت نباید متد POST اعلان لغو ارسال کند؛ بلکه با بستن جریان پاسخ اختصاصی همان درخواست (استریم SSE)، سیگنال لغو را به سرور منتقل می‌کند.

چرا ارسال پیام‌های پیشرفت نباید مهلت زمانی حداکثری را بازنشانی کند؟

رویدادهای پیشرفت تنها مجاز به ریست کردن مهلت بی‌عملی (idle timeout) هستند. اگر مهلت حداکثری مطلق را بازنشانی کنند، فرایندهای معیوب یا حلقه‌های بی‌پایان تا ابد منابع سیستم را اشغال می‌کنند.

چرا شناسه‌ی JSON-RPC جدید برای تضمین یکتایی تراکنش (Idempotency) کافی نیست؟

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

ما رو توی گوگل بیشتر ببین

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