שיטת חקירה ודוח לאחר אירוע¶
להתחיל מתיאור שאפשר להפריך¶
נניח שמישהו מדווח שהשרת נתקע אחרי כמה בקשות. המשפט אינו אומר אם התהליך חי, אם הלקוחות עדיין מתחברים, אם המעבד עסוק או אם הבקשות נדחות. לפני פתיחת כלי חקירה ננסח תצפית מדויקת יותר. למשל, ארבע בקשות שגויות גורמות לכך שבקשה תקינה מקבלת דחייה, ומספר המתארים נשאר קבוע. התיאור החדש כבר מצמצם את סוגי ההסברים האפשריים.
נפריד בין עובדה שנמדדה לבין הסבר. העובדה יכולה להיות שמונה מסוים נשאר גבוה לאחר סיום בקשה. ההסבר יכול להיות שהמונה אינו יורד בנתיב שגיאה. ההסבר נשמע סביר, אבל הוא עדיין השערה עד שנמצא את הנתיב ונבצע ניסוי שמבדיל אותו מחלופות. כתיבת ההפרדה ביומן העבודה מונעת מאיתנו להפוך ניחוש מוקדם להנחת יסוד.
תקלה קטנה עם סימפטום מטעה¶
נבנה תוכנית שאינה זקוקה לרשת כדי לשחזר כשל בניהול מכסה. השירות המדומה מקבל מספר, מכפיל אותו ומחזיר תוצאה. הקוד הבא מכיל במכוון פגם של חשבונאות משאבים, אך אינו משתמש בהתנהגות לא מוגדרת. נשמור אותו בקובץ investigate.rs ונבנה בעזרת rustc --edition=2024 investigate.rs -o investigate.
#[derive(Debug)]
struct Budget { used: usize, limit: usize }
fn request(budget: &mut Budget, input: &str) -> Result<u64, &'static str> {
if budget.used == budget.limit { return Err("capacity"); }
budget.used += 1;
let value = input.parse::<u64>().map_err(|_| "parse")?;
let answer = value.checked_mul(2).ok_or("overflow")?;
budget.used -= 1;
Ok(answer)
}
fn main() {
let mut budget = Budget { used: 0, limit: 3 };
for (id, input) in ["7", "bad", "bad", "bad", "9"].into_iter().enumerate() {
let before = budget.used;
let result = request(&mut budget, input);
println!("id={id} before={before} after={} result={result:?}", budget.used);
}
}
הפלט הצפוי מציג הצלחה לבקשה הראשונה, שלוש שגיאות פענוח ודחיית מכסה לבקשה האחרונה. התוכנית אינה מחזיקה שלוש עבודות פעילות ברגע הדחייה. היא מחזיקה שלוש יחידות חשבונאיות שלא הוחזרו. אם היינו מסתכלים רק על מספר ההצלחות, היינו עלולים להסיק שהמפענח תקין ושמקור התקלה נמצא במקום אחר.
בחירת הניסוי הבא¶
נרשום שלוש השערות אפשריות. הראשונה היא שהמכסה קטנה מדי לעומס. השנייה היא שבקשות תקינות מחזיקות משאב למשך זמן ארוך. השלישית היא שנתיב שגיאה אינו מחזיר משאב. נבחר קלט קצר שמבדיל ביניהן: נריץ הרבה בקשות תקינות ברצף, ואז נריץ כמה בקשות שגויות בלי שום מקביליות.
אם בקשות תקינות רבות אינן מגדילות את המונה, ההשערה על עומס לבדו נחלשת. אם בקשות שגויות בודדות מגדילות אותו גם כאשר כל קריאה כבר חזרה, ההשערה על עבודה ארוכה נחלשת. כעת נבדוק את נקודות היציאה של הפונקציה. האופרטור ? מחזיר את השגיאה לפני ההפחתה, ולכן הוא מסביר את התצפית וגם מנבא שפגם דומה יופיע בנתיב גלישת המספר.
הניבוי הוא חלק מרכזי מההסבר. הסבר שאומר רק "יש בעיה בשגיאות" אינו מספיק כדי לבחור תיקון. הסבר שמזהה כל יציאה לאחר רכישת מכסה מנבא משפחה של קלטים, וניתן לבדוק אותו גם במקרים שלא שימשו לגילוי התקלה.
תיקון שמבטא אינווריאנט¶
נגדיר את האינווריאנט כך: בכל רגע מספר היחידות התפוסות שווה למספר ההיתרים החיים. נייצג היתר באובייקט שההשמדה שלו מחזירה את היחידה. התוכנית הבאה היא תיקון מלא למודל החד־תהליכי, ואינה מימוש של מכסה מקבילית.
use std::cell::Cell;
struct Budget { used: Cell<usize>, limit: usize }
struct Permit<'a> { budget: &'a Budget }
impl Budget {
fn acquire(&self) -> Result<Permit<'_>, &'static str> {
let used = self.used.get();
if used >= self.limit { return Err("capacity"); }
self.used.set(used + 1);
Ok(Permit { budget: self })
}
}
impl Drop for Permit<'_> {
fn drop(&mut self) {
self.budget.used.set(self.budget.used.get() - 1);
}
}
fn request(budget: &Budget, input: &str) -> Result<u64, &'static str> {
let _permit = budget.acquire()?;
let value = input.parse::<u64>().map_err(|_| "parse")?;
value.checked_mul(2).ok_or("overflow")
}
fn main() {
let budget = Budget { used: Cell::new(0), limit: 3 };
for input in ["7", "bad", "18446744073709551615", "9"] {
println!("input={input} result={:?}", request(&budget, input));
assert_eq!(budget.used.get(), 0);
}
}
ההיתר חי לאורך כל הפעולה, וגם החזרה מוקדמת עוברת דרך ההשמדה שלו. השם שמתחיל בקו תחתון עדיין יוצר משתנה שמחזיק את הערך עד סוף התחום. כתיבה של דפוס ההתעלמות הבודד במקום המשתנה הייתה דורשת בדיקה אחרת של חיי הערך. זוהי דוגמה לכך שהסבר על בעלות צריך להתאים בדיוק לקוד ולא רק לכוונה.
המודל אינו מבטיח השמדה לאחר סיום כפוי של התהליך או דליפה מכוונת של הערך. עבור מונה שחי רק בתהליך, סיום התהליך מסיים גם את מצב החשבונאות שלו. עבור משאב חיצוני עמיד נצטרך פרוטוקול התאוששות אחר. גבולות התיקון צריכים להופיע בדוח כדי שלא יהפוך להבטחה כללית על כל סוג משאב.
ציר זמן וגבול הידע¶
בדוח אירוע נרשום מתי החלה החריגה, איזו גרסה רצה, מה היה קצב הבקשות ומה השתנה לפני התסמין. נקשר אירועים לפי מזהה בקשה או שעון מתאים. זמני קיר ממחשבים שונים אינם בהכרח מסונכרנים היטב, ולכן סדר שנראה ביומן אינו תמיד סדר סיבתי. בתוך תהליך נעדיף גם משכי זמן משעון מונוטוני לצורך מדידת המתנה.
נשמור את הקלט המינימלי, פקודת הבנייה ותוצאת הבדיקה המקורית. כאשר אין אפשרות לשחזר את תנאי הייצור, נכתוב בדיוק אילו הנחות החלפנו במודל. המודל כאן משמר רכישה, החזרה מוקדמת ושחרור, אך אינו מדמה תזמון תהליכונים או התנהגות רשת. לכן הוא מוכיח פגם חשבונאי, ולא מסביר לבדו כל איטיות שנצפתה בשרת גדול.
תיקון קטן ובדיקת משפחה¶
נבדוק קלט תקין, שגיאת פענוח וגלישה מספרית. לאחר כל קריאה שנגמרת נבדוק שהמכסה חזרה לערכה הקודם. לאחר מכן נבדוק כמה היתרים חיים יחד כדי להוכיח שהמגבלה עצמה עדיין נאכפת. תיקון שמחזיר את המונה לאפס בכל שגיאה אולי יעבור בדיקה סדרתית, אבל ישבור חשבונאות כאשר קיימות עבודות אחרות.
נוסיף בדיקת רגרסיה שמצהירה על האינווריאנט, ולא רק משווה מחרוזת פלט של המקרה הראשון. כך בדיקה שנכתבה בעקבות שגיאת פענוח תגן גם על נתיב שגיאה חדש שיתווסף בעתיד. בדיקה שמחקה את פעולת ההפחתה המדויקת של המימוש תהיה פחות מועילה מבדיקה של הקשר בין היתרים חיים לחשבונאות.
דוח שאפשר לפעול לפיו¶
הדוח הסופי יתחיל בהשפעה שנצפתה, יראה את הראיות, יסביר את המסלול הסיבתי ויתאר את התיקון. אחריו נציין כיצד נבדק התיקון ואילו שאלות נשארו פתוחות. אין צורך להציג כל פקודה שנוסתה בדרך אם אינה תורמת להבנת המסקנה. כן צריך לשמור את חומרי הגלם כדי שחוקר אחר יוכל לבדוק את ההסבר.
ניסוח כגון "הגדלנו את המכסה והבעיה נעלמה" מתאר הקלה אפשרית, אך אינו מוכיח שהגורם תוקן. במקרה שלנו הגדלת המכסה רק מאפשרת יותר שגיאות לפני הדחייה. התיקון המבני קשור להחזרת ההיתר בכל מסלול סיום, והניסוי צריך להראות שהמונה נשאר יציב לאורך רצף שגיאות ארוך.