אמינות בין תהליכים¶
נניח שלקוח שלח בקשה להגדיל מונה. השרת ביצע אותה, אבל החיבור נסגר לפני שהתשובה הגיעה. אם הלקוח שולח שוב בקשה חדשה, המונה יגדל פעמיים. אם הוא לא שולח, הוא עלול להישאר בלי תשובה לפעולה שכן קרתה. זו בעיית ידע, לא רק בעיית חיבור.
נגדיר מזהה בקשה יציב ומדיניות replay. השרת שומר את תוכן הבקשה ואת תוצאתה, ומחזיר את אותה תוצאה כאשר מתקבל ניסיון חוזר זהה. בקשה עם אותו מזהה ותוכן אחר נדחית. כדי שההבטחה תשרוד קריסה, גם המידע הזה צריך להיכלל במצב העמיד.
חזרה לתוצאה אינה ביצוע מחדש¶
התוכנית הבאה מדגימה את ההיגיון בזיכרון. היא מיועדת ל־thread יחיד, ומשתמשת בהעתקי מצב כדי לשמור על ערבות חזקה מול כשל הקצאה:
#include <cassert>
#include <cstdint>
#include <map>
#include <optional>
struct Put {
std::uint64_t id;
std::uint32_t key;
std::uint32_t value;
};
enum class Status { ok, conflicting_id, capacity };
struct Result {
Status status;
std::uint32_t value;
};
struct Saved {
std::uint32_t key;
std::uint32_t input;
Result result;
};
class Store {
std::map<std::uint32_t, std::uint32_t> values_;
std::map<std::uint64_t, Saved> requests_;
public:
Result put(Put input) {
if (auto found = requests_.find(input.id);
found != requests_.end()) {
if (found->second.key != input.key ||
found->second.input != input.value)
return {Status::conflicting_id, 0};
return found->second.result;
}
if (requests_.size() == 128) return {Status::capacity, 0};
auto candidate_values = values_;
auto candidate_requests = requests_;
candidate_values[input.key] = input.value;
const Result result{Status::ok, input.value};
candidate_requests.emplace(input.id,
Saved{input.key, input.value, result});
values_.swap(candidate_values);
requests_.swap(candidate_requests);
return result;
}
std::optional<std::uint32_t> get(std::uint32_t key) const {
auto found = values_.find(key);
if (found == values_.end()) return std::nullopt;
return found->second;
}
};
int main() {
Store store;
assert(store.put({1, 7, 9}).status == Status::ok);
assert(store.put({2, 7, 10}).status == Status::ok);
const auto repeated = store.put({1, 7, 9});
assert(repeated.status == Status::ok && repeated.value == 9);
assert(store.get(7) == 10);
assert(store.put({1, 7, 11}).status == Status::conflicting_id);
assert(store.get(7) == 10);
}
הניסיון החוזר של בקשה אחת מחזיר תשע, אבל הערך הנוכחי נשאר עשר שנכתב בבקשה שתיים. אילו היינו מבצעים מחדש את ה־put הישן, היינו מחזירים את המצב לאחור. שמירת תוצאה והחלת פעולה הן שתי פעולות שונות.
ההעתקים יקרים, אבל מאפשרים להסביר את גבול הפרסום. אם הקצאה נכשלת לפני ה־swapים, המצב הישן נשאר. בפרויקט המוגבל נשתמש באותה מדיניות עבור מספר קטן של רשומות, ונפרסם רק אחרי כתיבה עמידה. מערכת גדולה תצטרך מבנה יעיל יותר עם הוכחה מתאימה.
מה הלקוח רשאי להסיק¶
תשובה יכולה לציין קבלה לתור, ביצוע בזיכרון או commit עמיד. נבחר משמעות אחת ונקודד אותה במפורש. אם הלקוח צריך עמידות, תשובת "התקבל" אינה מספיקה. אם קריאת התשובה נכשלת, הלקוח יודע רק שאינו יודע את התוצאה, אלא אם פרוטוקול נוסף מוכיח אחרת.
מזהה הבקשה צריך להישאר זהה בכל retry של אותה פעולה. מזהה חדש מציין פעולה חדשה. מצד שני, מזהה אינו הרשאה: לקוח שאינו מורשה אינו רשאי לקרוא תוצאות של לקוח אחר רק משום שניחש מספר. במערכת רב־משתמשים נכלול גם זהות לקוח או session בתחום המפתח.
זיכרון מוגבל והבטחה מוגבלת¶
הדוגמה שומרת לכל היותר מאה עשרים ושמונה מזהים. כאשר המקום נגמר היא דוחה בקשה חדשה, ואינה מוחקת בשקט את הישנה ביותר. מחיקה שקטה הייתה מאפשרת ל־retry ישן להתבצע מחדש.
מערכת אמיתית יכולה להגדיר חלון retention, epochs או אישור מפורש מן הלקוח שאין עוד צורך ב־replay. כל בחירה משנה את ההבטחה. אין משמעות מעשית לטענה כללית של exactly once ללא גבולות על אחסון, זהויות, כשלים ומשך הזמן שבו מותר לנסות שוב.
חיבור ליומן¶
בפרויקט נכתוב את מזהה הבקשה, המפתח והערך בתוך זוג הרשומות העמיד. בזמן recovery נבנה גם את מפת הערכים וגם את טבלת הבקשות. כך קריסה אחרי flush ולפני תשובה אינה יוצרת פעולה חדשה כאשר הלקוח חוזר עם אותו מזהה.
אם ה־flush נכשל, לא נאשר. ייתכן שבפתיחה הבאה נמצא זוג שלם ונוכל לשחזר אותו; הלקוח עדיין צריך לשאול עם אותו מזהה. ההבטחה היא לגבי אפקט עסקי ותוצאה שנשמרו לפי הפרוטוקול, לא לגבי מספר הפעמים שהרשת העבירה bytes.
בדיקת חלון אי־הוודאות¶
נוסיף נקודת קריסה מבוקרת אחרי flush ולפני WriteFile של התשובה. לאחר הפעלה מחדש נשלח את אותה בקשה ונבדוק שמתקבלת התוצאה השמורה בלי append חדש. נשלח גם את אותו מזהה עם ערך אחר ונדרוש שגיאת קונפליקט.
הבדיקה הזו מחברת תקשורת, אחסון ובעלות. קוד pipe תקין וקוד WAL תקין בנפרד אינם מספיקים אם הסדר ביניהם מאפשר אישור לפני עמידות או replay שמשנה שוב את המצב.
קריאה להעמקה¶
החוזים של named pipes ושל FlushFileBuffers מגדירים את פעולות המערכת. משמעות מזהה הבקשה, retention ותשובת commit היא חוזה הפרוטוקול שבנינו.