פתרון הפרויקט: חקירת אירוע ותיקון מוכח¶
הגורמים והתיקון המבני¶
המצב המקורי מערבב שלוש אחריויות באותה נעילה: מכסת עבודה, היסטוריית תצפית והמתנה איטית. נפריד אותן. מונה אטומי יתאר היתרים חיים, שומר בעלות יחזיר היתר בכל מסלול סיום רגיל, ומנעול קצר יגן רק על ההיסטוריה המוגבלת. ההמתנה תתבצע מחוץ למנעול הזה.
נשתמש באותו מבנה Job ובאותו שלד עובדים מההרצאה האחרונה. נחליף את המבנה State ואת הפונקציה handle בקוד הבא, ונוסיף את היבואים המופיעים כאן ליבואים הקיימים. אין ליצור הגדרה שנייה של אותו מבנה במקביל להגדרה הישנה.
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
struct State {
active: AtomicUsize,
history: Mutex<VecDeque<(usize, bool)>>,
}
impl State {
fn new() -> Self {
Self { active: AtomicUsize::new(0), history: Mutex::new(VecDeque::new()) }
}
}
struct Permit { state: Arc<State> }
impl Drop for Permit {
fn drop(&mut self) {
let previous = self.state.active.fetch_sub(1, Ordering::AcqRel);
debug_assert!(previous > 0);
}
}
fn acquire(state: &Arc<State>) -> Result<Permit, &'static str> {
state.active.fetch_update(Ordering::AcqRel, Ordering::Acquire, |active| {
if active < 8 { Some(active + 1) } else { None }
}).map_err(|_| "capacity")?;
Ok(Permit { state: state.clone() })
}
fn handle_with(state: &Arc<State>, job: &Job, wait: impl FnOnce())
-> Result<usize, &'static str> {
let _permit = acquire(state)?;
if job.bad { return Err("invalid"); }
if job.slow { wait(); }
let mut history = state.history.lock().unwrap();
if history.len() == 32 { history.pop_front(); }
history.push_back((job.id, job.slow));
Ok(history.len())
}
fn handle(state: &Arc<State>, job: &Job) -> Result<usize, &'static str> {
handle_with(state, job, || std::thread::sleep(Duration::from_millis(20)))
}
נחליף בפונקציה הראשית את יצירת המצב בשורה הבאה. יתר פעולת יצירת התור והעובדים נשארת כמו במעבדה, והפניה המועברת ל־handle היא כעת הפניה אל בעלות משותפת על המצב.
נחליף את נעילת המצב וההדפסה האחרונה בשורות הבאות. בשלב הזה כבר הצטרפנו לכל העובדים, ולכן הדרישה שהמונה יהיה אפס אינה קריאה רגעית בזמן עבודה חיה.
let active = state.active.load(Ordering::Acquire);
let retained = state.history.lock().unwrap().len();
assert_eq!(active, 0);
assert!(retained <= 32);
println!("wall={:?} errors={errors} active={active} retained={retained} p99_us={}",
begin.elapsed(), times[(times.len() - 1) * 99 / 100]);
המונה אינו משמש לפרסום תוכן ההיסטוריה, ולכן הגנת ההיסטוריה נשארת באחריות המנעול. סדרי הזיכרון שנבחרו למונה חזקים מהנדרש לספירה מבודדת, אבל אינם תחליף לנעילה של מבנה הנתונים. נימנע מהסקה שלפיה כל גישה למצב בטוחה רק מפני שקיים בו שדה אטומי.
בדיקה של החזרת היתר¶
נוסיף את מודול הבדיקות הבא לאותו קובץ. נבנה אותו באמצעות rustc --edition=2024 --test incident_lab.rs -o incident_tests ונריץ את בינארי הבדיקות. הבדיקות משתמשות בטיפוסי המעבדה ובמימוש החדש, ולא דורשות שרת חיצוני.
#[cfg(test)]
mod tests {
use super::*;
fn job(id: usize, bad: bool, slow: bool) -> Job {
Job { id, bad, slow, entered: Instant::now() }
}
#[test]
fn errors_return_permits() {
let state = Arc::new(State::new());
for id in 0..100 {
assert_eq!(handle(&state, &job(id, true, false)), Err("invalid"));
assert_eq!(state.active.load(Ordering::Acquire), 0);
}
assert_eq!(handle(&state, &job(101, false, false)), Ok(1));
}
#[test]
fn capacity_tracks_live_owners() {
let state = Arc::new(State::new());
let mut permits: Vec<_> = (0..8).map(|_| acquire(&state).unwrap()).collect();
assert!(acquire(&state).is_err());
drop(permits.pop());
let replacement = acquire(&state).unwrap();
assert_eq!(state.active.load(Ordering::Acquire), 8);
drop(replacement);
drop(permits);
assert_eq!(state.active.load(Ordering::Acquire), 0);
}
#[test]
fn history_is_bounded() {
let state = Arc::new(State::new());
for id in 0..100 { handle(&state, &job(id, false, false)).unwrap(); }
let history = state.history.lock().unwrap();
assert_eq!(history.len(), 32);
assert_eq!(history.front().unwrap().0, 68);
assert_eq!(history.back().unwrap().0, 99);
}
#[test]
fn waiting_does_not_hold_history_lock() {
let state = Arc::new(State::new());
let (entered_tx, entered_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
let worker_state = state.clone();
let worker = std::thread::spawn(move || {
handle_with(&worker_state, &job(1, false, true), || {
entered_tx.send(()).unwrap();
release_rx.recv().unwrap();
}).unwrap();
});
entered_rx.recv().unwrap();
let history_available = state.history.try_lock().is_ok();
release_tx.send(()).unwrap();
worker.join().unwrap();
assert!(history_available);
assert_eq!(state.active.load(Ordering::Acquire), 0);
}
}
הבדיקה האחרונה אינה מניחה שהעובד יגיע לנקודה בתוך מספר אלפיות שנייה. ערוץ הכניסה מוכיח שהוא נמצא בתוך שלב ההמתנה, וערוץ השחרור מחזיק אותו שם עד שהבדיקה בדקה את הנעילה. כך אנו בודקים אינווריאנט של חיי משאב במקום סף זמן שתלוי בעומס המחשב.
פירוש ניסויי העומס¶
בעומס הלא תקין נצפה לשגיאות קלט מוצדקות בלי הצטברות במונה. ארבעת העובדים אינם אמורים למצות מכסה של שמונה בפעולות הרגילות, ולכן דחיות מכסה שיטתיות לאחר שגיאות ידרשו חקירה נוספת. הבדיקה שמחזיקה שמונה היתרים מוכיחה שהמגבלה עצמה לא בוטלה כדי להסתיר את התקלה.
בעומס הנקי מספר רשומות ההיסטוריה ייעצר בשלושים ושתיים. הזיכרון התושב הכולל עדיין כולל את התורים, התוצאות, הקוד והמקצה, ולכן אין לצפות לערך קבוע לחלוטין בכל גודל ריצה. נמדוד את הרכיב שתוקן ישירות ונשתמש במדדי המערכת כדי לבדוק את ההשפעה הכוללת.
בעומס המעורב ההמתנות יכולות להתקדם בכמה עובדים בלי להחזיק את נעילת ההיסטוריה. בקשות מהירות עדיין עשויות לחכות בתור מאחורי עבודה אחרת, ולכן התיקון אינו מבטיח אפס השהיה. הוא מסיר את הסריאליזציה המסוימת שנבעה מהחזקת המנעול בזמן השינה. אם נדרש חוזה זמן חזק יותר לבקשות קטנות, נצטרך לתכנן תורים, סדרי עדיפות או מכסות נפרדות ולמדוד את מחירם.
הדוח הסופי¶
נצרף לכל משפחה את התצפית המקורית ואת הבדיקה שמסבירה אותה. ההחזרה המוקדמת מוכחת באמצעות חשבונאות, ההיסטוריה הבלתי מוגבלת מוכחת באמצעות אובייקטים שנשארים אחרי סיום, וחסימת העובדים מוכחת באמצעות חיי הנעילה והבדיקה המתוזמנת בערוצים. נפריד תוצאות שנמדדו מהתחזיות המתוארות כאן.
הפתרון אינו בודק קריסת תהליך בזמן כתיבה עמידה ואינו כולל לקוחות רשת אמיתיים. הניסוי הבא בשירות מלא יחבר מזהי בקשות לזמני קבלה, תחילת טיפול וסיום, ויבדוק את אותן תכונות תחת ניתוק ומגבלות משאב. כך חבילת החקירה נשארת בסיס להמשך עבודה ולא רשימת מסקנות רחבה יותר מהראיות שלה.