רכיב מתקשר תחת כשל¶
הלקוח לחץ על כפתור, השרת ביצע את העבודה, והחיבור נפל רגע לפני שהתשובה חזרה. הלקוח רואה שגיאה. האם צריך לבצע את העבודה שוב? אם מדובר בקריאת מידע, אולי כן. אם מדובר בעדכון בלתי הפיך, אותו retry יכול להיות הבאג החמור ביותר במערכת.
בפרק הזה עברנו דרך sockets, TLS, RPC וCOM. עכשיו נחבר את המנגנונים בלי לאבד את ההבחנות שבנינו. העובדה שקריאה נכתבת בצורה object->Start() אינה אומרת שהיא אטומית, מקומית או חסינה מניתוק.
מפרידים מזהה עבודה ממזהה ניסיון¶
לכוונת המשתמש ניתן request ID יציב. לכל ניסיון תקשורת ניתן attempt ID חדש. אם נשלח שוב את אותה כוונה, request ID נשאר זהה. אם ניצור עבודה חדשה באמת, ניצור מזהה חדש. בנוסף נקשור את המפתח לזהות המאומתת, כדי ששני לקוחות לא ידרסו זה לזה מזהים.
ההבחנה עוזרת גם בtrace. אירוע שייך לעבודה, לניסיון ולשלב: resolve, connect, authenticate, authorize, commit או reply. שני ניסיונות של אותה עבודה אינם שתי עבודות, ואירוע reply חסר אינו הוכחה שהcommit לא התרחש.
כשל שהגיע מאוחר¶
נניח שניסיון ראשון נתקע, התחיל ניסיון שני, ואז callback מהראשון הגיע. בלי מזהה ניסיון, הוא עלול לסגור את החיבור החדש או לדרוס תוצאה חדשה. המספר לא פותר את חיי הbuffer: את הbuffer של הניסיון הראשון עדיין צריך לשחרר לפי השלמה אמיתית. הוא רק מאפשר להחליט האם לתוצאה יש עדיין סמכות לעדכן את המצב הנוכחי.
התוכנית הבאה, attempts.cpp, מבודדת את החלטת הלקוח. זאת מכונת מצבים לוגית, לא מימוש של transport. היא מדגימה למה timeout אחרי שליחה יוצר מצב לא מוכרע, ולמה callback ישן לא יכול להחליף תוצאה חדשה.
#include <cstdint>
#include <cstdio>
#include <limits>
#include <stdexcept>
enum class State { idle, connecting, sent, unknown, succeeded, rejected };
class Request {
State state_ = State::idle;
std::uint64_t attempt_ = 0;
public:
std::uint64_t begin() {
if (state_ != State::idle && state_ != State::unknown)
throw std::logic_error("attempt not allowed");
if (attempt_ == std::numeric_limits<std::uint64_t>::max())
throw std::overflow_error("attempt space exhausted");
++attempt_;
state_ = State::connecting;
return attempt_;
}
bool sent(std::uint64_t attempt) {
if (attempt != attempt_ || state_ != State::connecting) return false;
state_ = State::sent;
return true;
}
bool failed_before_send(std::uint64_t attempt) {
if (attempt != attempt_ || state_ != State::connecting) return false;
state_ = State::idle;
return true;
}
bool lost_reply(std::uint64_t attempt) {
if (attempt != attempt_ || state_ != State::sent) return false;
state_ = State::unknown;
return true;
}
bool reply(std::uint64_t attempt, bool committed) {
if (attempt != attempt_ ||
(state_ != State::sent && state_ != State::unknown)) return false;
state_ = committed ? State::succeeded : State::rejected;
return true;
}
State state() const { return state_; }
};
void require(bool condition) {
if (!condition) throw std::runtime_error("state-machine check failed");
}
int main() {
Request request;
const auto first = request.begin();
require(request.sent(first));
require(request.lost_reply(first));
require(request.state() == State::unknown);
const auto second = request.begin();
require(request.sent(second));
require(!request.reply(first, true));
require(request.reply(second, true));
require(request.state() == State::succeeded);
require(!request.lost_reply(first));
Request unavailable;
const auto attempt = unavailable.begin();
require(unavailable.failed_before_send(attempt));
require(unavailable.state() == State::idle);
std::puts("state-machine checks passed");
}
המעבר מunknown לניסיון חדש מותר כאן רק תחת הנחת פרוטוקול: הניסיון החדש מבצע בירור או חוזר על אותה בקשה עם dedup בצד השרת. המספר הלוגי לבדו אינו הופך פעולה לאידמפוטנטית. אם השרת אינו מספק חוזה כזה, נשאיר את העבודה במצב לא מוכרע ונציג דרך בירור במקום להפעיל אותה שוב.
הדוגמה מסדרת אירועים על thread אחד. ביישום עם כמה callbacks נסדר את ההכרעות דרך lock או executor אחד. גם קריאה וכתיבה אטומית של state_ בלבד אינן מספיקות, כי הבדיקה של attempt והמעבר במצב הם פעולה משותפת.
גרסה חדשה בלי ניחוש¶
בCOM נוסיף IID חדש ליכולת חדשה. בwire protocol נגדיר version, סוג הודעה, אורכים ומגבלות. לא נחליף משמעות של שדה קיים רק משום ששני הצדדים אצלנו במחשב נבנו יחד. במעבדה נריץ במכוון לקוח ישן מול שרת חדש וגם לקוח חדש מול שרת ישן.
אם הלקוח מבקש יכולת שהשרת אינו מספק, הוא יכול לבחור מסלול ישן שקול או להחזיר שגיאה ברורה. אסור להעמיד פנים שהפעולה החדשה הצליחה. גם שדה לא מוכר דורש החלטה: האם ניתן לדלג עליו בבטחה, או שהוא משנה את משמעות הפעולה ולכן מחייב דחייה.
אבטחה בשני שלבים שונים¶
התצורה של COM כוללת הבחנה בין הרשאה להפעיל שרת לבין הרשאה לגשת אליו. אחר כך יש עדיין הרשאה עסקית לפעולה על עבודה מסוימת. לקוח שמותר לו ליצור proxy לא בהכרח רשאי לבטל עבודה של משתמש אחר.
הפונקציה CoInitializeSecurity מגדירה מדיניות תהליך ויש לקרוא לה בזמן המתאים, לפני שפעילות COM כופה אתחול אבטחה אוטומטי. הproxy עצמו יכול להזדקק להגדרת blanket דרך CoSetProxyBlanket. נגדיר את זהות היעד, שירות האימות, רמת ההגנה ומידת ההתחזות לפי החוזה. לא נתקן E_ACCESSDENIED באמצעות ביטול הגנות כלליות במחשב.
במעבדת DCOM נשתמש בשתי VMים, זהות בדיקה ורכיב שבנינו. נבדוק בנפרד launch, activation, access והרשאת הפעולה. נשמור תצורה לפני שינוי ונשנה רק את ההגדרות של רכיב המעבדה. התיעוד של הגדרות COM מסביר את שכבות ההגדרה; את מדיניות הארגון והקשחות גרסת Windows נבדוק בסביבת היעד ולא נבטל כדי לגרום לדוגמה לעבוד.
חוקרים לפי הגבול שנכשל¶
השגיאה REGDB_E_CLASSNOTREG מכוונת אותנו לactivation, לרישום ולארכיטקטורה המתאימה. השגיאה E_NOINTERFACE יכולה להיות פער יכולות או חסר בתמיכת marshalling. השגיאה RPC_E_WRONG_THREAD מצביעה על הפרת הקשר שימוש. השגיאה RPC_E_DISCONNECTED אומרת שהחיבור של האובייקט אינו זמין; היא אינה אומרת מה קרה לעבודה שכבר נשלחה.
נשמור את HRESULT המלא ואת השלב שבו התקבל. הודעה כמו "הרשת נפלה" מוחקת את ההבדל בין certificate שגוי, שרת שלא הופעל ופעולה שנדחתה אחרי אימות. בtrace נמדוד גם המתנה בתור, כדי שלא נייחס כל latency לTCP או לCOM.
מסיימים בלי להשאיר זנב¶
בסגירה נפסיק לקבל בקשות חדשות, נחליט אילו עבודות מתנקזות ואילו מתבטלות, ונמתין להשלמות הדרושות לשחרור buffers. ננתק subscriptions וקישורים שיכולים ליצור מעגל references. רק לאחר מכן נשחרר proxyים, נבטל רישומים ונפרק את הruntime של הthread.
הבדיקה החשובה אינה רק שמספר handles ירד. נבדוק שאין callback שמחזיק context של מסך שנסגר, שאין packet שלא נצרך, ושאין thread שמחכה לSTA שכבר הפסיק pumping. התוצאה הרצויה היא הסבר לכל משאב שנותר חי, לא רק קו שטוח בגרף זיכרון.