מחזור חיי שירות ותצפית מובנית¶
שירות יכול להחזיק PID ולהאזין לסוקט בלי להיות מוכן לטפל בבקשות. אם היומן עדיין עובר recovery או שה־workers עדיין לא הופעלו, הודעת ready מוקדמת תגרום ללקוח לקבל הבטחה שאין מי שיקיים. נבנה מכונת מצבים שמגדירה מתי admission מותר, מה קורה בזמן drain ואיזה אירוע מסמן shutdown שהסתיים.
המודל הבא הוא תוכנית מלאה בלי I/O. הוא מפריד את מספר הפעולות שהתקבלו משלב מחזור החיים, ובודק מעברים שאסור לאפשר.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Phase { Starting, Ready, Draining, Stopped }
struct Lifecycle { phase: Phase, active: usize }
impl Lifecycle {
fn ready(&mut self) -> Result<(), &'static str> {
if self.phase != Phase::Starting { return Err("invalid ready"); }
self.phase = Phase::Ready;
Ok(())
}
fn accept(&mut self) -> Result<(), &'static str> {
if self.phase != Phase::Ready { return Err("not accepting"); }
self.active = self.active.checked_add(1).ok_or("overflow")?;
Ok(())
}
fn drain(&mut self) {
if self.phase == Phase::Stopped { return; }
self.phase = if self.active == 0 { Phase::Stopped } else { Phase::Draining };
}
fn finish(&mut self) -> Result<(), &'static str> {
self.active = self.active.checked_sub(1).ok_or("no active request")?;
if self.phase == Phase::Draining && self.active == 0 {
self.phase = Phase::Stopped;
}
Ok(())
}
}
fn main() {
let mut service = Lifecycle { phase: Phase::Starting, active: 0 };
assert!(service.accept().is_err());
service.ready().unwrap();
service.accept().unwrap();
service.drain();
assert!(service.accept().is_err());
service.finish().unwrap();
assert_eq!(service.phase, Phase::Stopped);
}
בשרת מקבילי כל החלטת accept ושינוי ל־Draining צריכים להיות מסונכרנים ביחס לאותו invariant. אם נבדוק דגל ready, נשחרר את הנעילה ורק אחר כך נגדיל active, shutdown יכול לראות אפס ולסגור משאב רגע לפני שהבקשה משתמשת בו. אפשר להחזיק admission permit שה־Drop שלו מקטין את הספירה, אך אז צריך לדעת האם ביטול future באמת מסיים גם את עבודת ה־kernel או ה־spawn_blocking שהוא התחיל.
ה־drain עוצר קבלת אחריות חדשה ומשלים את מה שכבר התקבל. ה־abort מבקש להפסיק גם עבודה קיימת, אבל עדיין צריך להמתין להחזרת משאבים שנמצאים בשימוש. במערכת עם יומן, עבודה עמידה יכולה להישאר במצב queued לשחזור הבא במקום להסתיים בזמן הכיבוי. נכתוב איזו מדיניות חלה על כל מצב, כדי ש־shutdown timeout לא יהפוך להבטחה שקרית שהכול הושלם.
פעולת reload צריכה לפרש ולאמת תצורה חדשה בצד לפני פרסומה. אם תקרת הבקשות החדשה נמוכה ממספר הבקשות הפעילות, אפשר לעצור admission עד שהמספר ירד; אין לבטל באופן אוטומטי חוזי בעלות שכבר נמסרו. פרסום Arc של תצורה immutable מאפשר לכל בקשה להשתמש בגרסה עקבית, תוך ציון version בלוגים. שינוי של שדה אחר שדה תחת locks שונים יכול ליצור תצורה שמעולם לא היתה תקפה.
בתצפית נפריד counters של accepted, rejected, completed ו־recovered. מדד active הוא gauge של מצב נוכחי, ואילו histogram של latency דורש נקודת התחלה וסיום מוגדרות. מזהה בקשה עובר בכל הרכיבים, אבל payload רגיש אינו צריך להופיע בלוג כדי לקשר אירועים. נרשום קוד שגיאה, שלב וגרסת build, ונמנע מהפיכת כל בקשה ל־label נפרד במערכת metrics שעלולה לגדול ללא גבול.
ה־systemd יכול להמתין להודעת READY=1 בשירות Type=notify, אבל האפליקציה חייבת לשלוח אותה רק אחרי recovery והכנת המאזינים. שירות Type=exec מאשר הצלחת exec, לא מוכנות עסקית. ה־watchdog צריך לבדוק התקדמות אמיתית של המערכת, ולא טיימר שממשיך לפעום גם כשהתור מת. התיעוד של systemd.service מגדיר את מנגנון התיאום; מכונת המצבים שלנו מגדירה מה ראוי לדווח לו.
נבחן shutdown שמתחיל בזמן שבקשה ממתינה ל־fsync. סגירת המאזין עוצרת חיבורים חדשים, אבל אינה מסיימת את הבקשה שכבר קיבלה permit. השירות צריך להמשיך להחזיק את היומן ואת state של הבקשה עד שתוצאת ה־commit ידועה או עד שמנהל חיצוני מפסיק את התהליך. אם ה־timeout של שכבת הרשת פג, הלקוח מפסיק לחכות, אך רכיב האחסון עדיין יכול לסיים ולשמור תוצאה לבירור הבא. אירוע request-timeout ואירוע commit-complete יכולים לכן להופיע באותו trace בלי סתירה. רק אירוע resources-returned מאפשר למכונת מחזור החיים להגיע ל־Stopped באופן רגיל.