آنچه مجموعه داده نشان میدهد
نوشتههای سازمانی درباره عاملها در محیط تولید حول مجموعه محدودی از شکستها همگرا میشوند و بیشتر آنها شکست مدل نیستند. آنها شکست رابط هستند. همان اصطکاک در استقرارهای ظاهراً نامرتبط نیز دیده میشود:
- ابزارها موفقیت بدون ساختار برمیگردانند. پاسخی مانند
{"status": "ok"}یا یک HTTP 200 خالی با بدنه قابل خواندن برای انسان، عامل را وادار میکند استنتاج کند که آیا اثر مورد نظر واقعاً رخ داده است یا نه. - شکست جزئی پنهان است. یک عملیات دستهای که برخی موارد را پردازش کرده و برخی را نادیده گرفته، موفقیت گزارش میکند چون انتقال موفق بوده است.
- یک قصد نیازمند یک آیین چندفراخوانی است. ایجاد یک منبع یعنی فراخوانی یک endpoint ایجاد، سپس نظرسنجی از یک endpoint وضعیت، و بعد فراخوانی یک endpoint نهاییسازی. هر فراخوانی فرصتی برای توقف در میانه است و حالتهای میانی هیچجا در توصیف ابزار مدلسازی نشدهاند.
- تلاشهای مجدد علیه endpointهایی اجرا میشوند که هرگز حالت نهایی را علامت نمیدهند. گزارشهای حادثه عاملهایی را توصیف میکنند که روی فراخوانیای حلقه میزنند که مدام خطای موقتینما برمیگرداند، چون هیچچیز در پاسخ بین «دوباره تلاش کن» و «این هرگز کار نخواهد کرد» تمایز قائل نمیشود.
الگو ثابت است: از سطح ابزار خواسته میشود معناشناسیای را حمل کند که هرگز برای بیان آن طراحی نشده بود. عامل آنقدرها هم بدرفتاری نمیکند، بلکه تنها تفسیری را کامل میکند که رابط اجازه میدهد.
مغالطه پوشش
پیچیدن یک API انسانی موجود در یک طرحواره JSON، سطح ابزار تولید نمیکند. یک فراخوانی رویه از راه دور با توصیفی نازک تولید میکند. طرحواره به مدل میگوید چه فیلدهایی بفرستد؛ تقریباً هیچ نمیگوید که فراخوانی چه معنایی دارد، چه زمانی معتبر است، یا یک پاسخ معین چه چیزی درباره جهان دلالت میکند.
APIهای انسانی برای خوانندهای طراحی شدهاند که میتواند اطلاعات گمشده را جبران کند. آن خواننده میتواند قصد را از زمینه اطراف استنتاج کند، از همکاری بپرسد که یک کد وضعیت چه معنایی دارد، یا متوجه شود که نتیجهای کهنه به نظر میرسد و بررسی کند. هیچیک از این مسیرهای بازیابی برای عاملی که درون یک حلقه کار میکند وجود ندارد. وقتی رابط مبهم است، تنها استراتژی در دسترس عامل حدس زدن، تلاش مجدد، یا رها کردن است و هر سه در محیط تولید پرهزینهاند.
رویکرد پوشش همچنین فرضهایی را وارد میکند که از ترجمه جان سالم به در نمیبرند. یک API انسانی ممکن است یک 200 با بدنه خطا را بهعنوان قراردادی در نظر بگیرد که همه اعضای تیم میدانند. ممکن است یک لیست خالی را یا «بدون نتیجه» یا «پرسوجو بدشکل بود» در نظر بگیرد، چون یک انسان بررسی میکرد. ممکن است عملیات مخرب را غیرتکرارپذیر بگذارد چون یک انسان دکمه را دو بار کلیک نمیکرد. هر یک از اینها برای فراخوان انسانی یک تصمیم طراحی معقول و برای فراخوان ماشینی یک نقص است.
آنچه عاملها بهجایش نیاز دارند سطحی با پیششرطهای صریح، پسشرطهای صریح، و یک طبقهبندی خطای نهایی است. یک پیششرط بیان میکند که پیش از معتبر بودن فراخوانی چه چیزی باید درست باشد. یک پسشرط بیان میکند که پس از یک فراخوانی موفق چه چیزی درست خواهد بود. یک طبقهبندی خطای نهایی بیان میکند که کدام شکستها قابل تلاش مجدد هستند، کدام دائمیاند، و کدام نیازمند یک فراخوانی کاملاً متفاوتاند.

یک فراخوانی ابزار، یک قصد
یک فراخوانی ابزار باید یک قصد واحد را بیان کند. اگر دستیابی به یک هدف نیازمند دنبالهای از فراخوانیها بدون راهی برای ازسرگیری باشد، سطح، تکمیل جزئی و تلاشهای مجدد مبهم را دعوت میکند. جایی که یک فرایند چندمرحلهای اجتنابناپذیر است، حالتهای میانی را صریح مدلسازی کنید و هر گذار را به یک عملیات درجهیک با پیششرطهای خودش تبدیل کنید، بهجای پنهان کردن آنها پشت آیینی که انتظار میرود فراخوان آن را درست انجام دهد.
هر پاسخ یکی از سه چیز است
هر پاسخ باید بهعنوان موفقیت، شکست قابل تلاش مجدد، یا شکست نهایی قابل طبقهبندی باشد. حالت چهارمی وجود ندارد. پاسخی که مبهم است فراخوان را وادار میکند سیاستی اختراع کند و سیاستهای اختراعی همان جایی هستند که طوفانهای تلاش مجدد از آن میآیند. بهطور مشخص، این یعنی یک پاکت پاسخ با مجموعهای کوچک و بسته از انواع نتیجه، و قاعدهای که انواع نتیجه ناشناخته بهعنوان شکست نهایی در نظر گرفته شوند نه اینکه دوباره تلاش شوند.
یک مکانیزم مفید، یک پاکت پاسخ با یک متمایزکننده صریح نتیجه و یک راهنمای تلاش مجدد کراندار است:
{
"outcome": "retryable_failure",
"reason": "upstream_unavailable",
"retry_after_ms": 2000,
"observed_state": null
}
ویژگی مهم، نامهای مشخص فیلدها نیست، بلکه این است که فراخوان میتواند بدون تفسیر انشعاب کند. terminal_failure یعنی توقف و آشکارسازی خطا. retryable_failure یعنی تلاش مجدد در کران اعلامشده. success یعنی پسشرط برقرار است.
عملیات مخرب با کلید تکرارپذیرند
عملیات مخرب باید با کلید تکرارپذیر باشند تا تلاشهای مجدد بهطور ساختاری ایمن باشند. فراخوان یک کلید تکرارپذیری پایدار مشتقشده از قصد فراهم میکند و سطح برای یک کلید تکراری همان نتیجه را برمیگرداند بهجای اینکه اثر را دو بار انجام دهد. این نیاز را از عامل میگیرد که درباره این استدلال کند که آیا تلاش قبلی انجام شده است یا نه، که دقیقاً همان استدلالی است که عامل در آن
یک پاسخ باید حالت مشاهدهشده را در کنار نتیجه ادعاشده برگرداند تا فراخوان بتواند تأیید کند نه اینکه اعتماد کند. اگر ابزار ادعا میکند منبعی ایجاد شده است، باید حالت قابل مشاهده فعلی منبع را برگرداند. آنگاه تأیید به یک مقایسه تبدیل میشود که عامل میتواند انجام دهد، نه یک باور که باید نگه دارد. این همچنین چیزی است که موفقیت با داده کهنه را قابل تشخیص میکند: نتیجه ادعاشده و حالت مشاهدهشده با هم اختلاف دارند.

مصالحههای صادقانه
این قواعد رایگان نیستند. پاکتهای نتیجه صریح پاسخها را بزرگتر میکنند، که زمینه را مصرف میکند و میتواند توصیف یک ابزار را از نقطهای که مدل بهطور قابلاعتماد به آن توجه میکند فراتر ببرد. کلیدهای تکرارپذیری نیازمند حالت سمت سرور و یک پنجره نگهداری هستند، که سربار عملیاتی برای تیمهایی است که پیشتر هیچکدام را نداشتند. تقسیم یک آیین به عملیاتهای صریح و قابل ازسرگیری ابزارهای بیشتری تولید میکند و ابزارهای بیشتر یعنی ابهام انتخاب بیشتر برای فراخوان. پرسش طراحی این نیست که آیا این هزینهها را بپردازیم یا نه، بلکه این است که کجا حالت شکست بدون آنها بدتر است. در بیشتر سیستمهای تولیدی، یک تلاش مجدد مبهم پرهزینهتر از یک پاسخ بزرگتر است.
پیامدهای ارزیابی
اگر سطوح ابزار مسئله هستند، پس درستی فراخوانی ابزار چیز اشتباهی برای امتیازدهی تنها بر اساس تکمیل وظیفه است. اجرایی که یک وظیفه را با تلاش مجدد از میان یک شکست نهایی کامل میکند، اجرای درستی نیست؛ اجرایی است که در برابر سطحی که در برقراری ارتباط شکست خورده، شانس آورده است. امتیازدهی باید دو بُعد را جدا کند:
- نگاشت قصد به فراخوانی. آیا عامل ابزار درست را با آرگومانهای درست برای قصد بیانشده انتخاب کرد، از جمله مواردی که رفتار درست فراخوانی نکردن هیچچیز است؟
- برخورد با طبقه شکست. برای هر طبقه شکستی که سطح میتواند منتشر کند، آیا عامل با سیاست درست پاسخ داد: تلاش مجدد در کران، توقف و آشکارسازی، یا تغییر به یک فراخوانی متفاوت؟
این یعنی مجموعههای رگرسیون به پاسخهای ابزار خصمانه بهعنوان فیکسچرهای درجهیک نیاز دارند، نه بهعنوان آزمایشهای آشوب گاهبهگاه. چهار موردی که بیشترین اهمیت را دارند عبارتند از موفقیت کند، موفقیت جزئی، شکست خاموش، و موفقیت با داده کهنه. هر یک فرض متفاوتی را میآزماید:
- موفقیت کند میآزماید که آیا سیاست مهلت عامل بین تأخیر و شکست تمایز قائل میشود.
- موفقیت جزئی میآزماید که آیا حالت مشاهدهشده را میخواند نه نتیجه ادعاشده را.
- شکست خاموش میآزماید که آیا اصلاً پسشرطها را تأیید میکند.
- موفقیت با داده کهنه میآزماید که آیا مُهرهای زمانی یا نسخهها را مقایسه میکند بهجای اعتماد به پاسخ.
سطحی که نمیتواند این پاسخها را در یک بستر آزمون تولید کند، سطحی است که حالتهای شکستش تا زمانی که در محیط تولید ظاهر شوند ناشناختهاند. ساختن فیکسچرها بخشی از ساختن رابط است.
سخن پایانی
سطوح ابزار یک مسئله طراحی رابط هستند. رویکرد پوشش با رابط بهعنوان یک لایه ترجمه روی یک API انسانی موجود رفتار میکند و هر فرضی را که آن API درباره خوانندهای که میتواند جبران کند، به ارث میبرد. طراحی برای یک فراخوان ماشینی یعنی صریح کردن پیششرطها، پسشرطها، و معناشناسی خطای نهایی، و پذیرش هزینههایی که با آن میآید. جایگزین، سیستمی است که شکستهایش تا زمانی که کسی لاگها را نخواند از موفقیتهایش قابل تشخیص نیستند.
