פתרון פרויקט מערכת עבודות שאפשר להסביר¶
הפתרון הזה הוא תכנון ייחוס עם מפת חיבור לרכיבים שמומשו לאורך הקורס, מודל מצבים רץ ובדיקות קבלה. פרויקט גמר מאפשר בחירות שונות של UI, התמחות וstorage, ולכן אין executable יחיד שמחליף את ההגשה. ההוכחה צריכה להתאים למימוש שבחרתם ולמכונה שבה הוא נבדק.
הגבולות של פתרון הייחוס¶
הממשק המנוהל מציג רשימת עבודות לפי מזהה ומחזיק רק snapshot של מצב השירות. השירות הוא הבעלים של קבלת העבודה, הזהות והjournal. המנוע הטבעי מחשב על bytes שכבר נבדקו ונמסרו לו בבעלות ברורה. הworker מבודד כשל חישובי מהשירות ומנוהל באמצעות job.
החלון יכול להיסגר בלי לסיים את העבודה. בקשת cancel מפורשת עוברת לשירות ונרשמת כבקשה, אך הצלחה שמגיעה לפני הכרעת הביטול נשארת הצלחה. המערכת אינה משכתבת תוצאה רק כדי להתאים לציפייה של הכפתור.
המסלול המקומי משתמש בזהות peer שנלקחה מהtransport המורשה. המפתח של עבודה הוא זהות ומזהה client, והpayload קשור אליו. שימוש חוזר באותו מפתח עם תוכן אחר מחזיר conflict. כל query בודק הרשאה מחדש לפי החוזה, ולא מניח שמי שמכיר מספר עבודה הוא בעליה.
מאילו רכיבים מרכיבים את המערכת¶
הניהול של process, יצירה suspended וjob מגיע מפרויקט פרק 4. תור העבודה והcompletion נבנים לפי פרק 6. הפורמט העמיד והגבולות של flush מגיעים מפרק 7. מעטפת השירות, access checks וscoped impersonation מגיעים מפרק 8.
הגבול של C ABI והשלמת callbacks מגיע מפרויקט פרק 12. החיבור לUI משתמש בdispatcher ובסיום המתואם שנלמדו שם. האבחון, dump, ETW, packaging ומזהה build מגיעים מפרק 10. ההתאוששות משתמשת במצבים ובחלונות הקריסה של 14.7, והחקירה של completion מאוחר משתמשת ב14.9.
החיבור דורש ליישב שמות, versions וחוזי שגיאה. אי אפשר להעתיק שתי דוגמאות שכל אחת מגדירה משמעות אחרת לtimeout ולהכריז שהמערכת מוכנה. לכל רכיב יש adapter קטן שמתרגם את החוזה המקומי שלו לחוזה השירות, בלי לאבד מצב לא ידוע או שגיאת בעלות.
חוזה הבקשה¶
הבקשה מכילה magic, גרסת header, kind, אורך payload ומזהה בקשה. השירות מגביל payload למגהבייט בדוגמת הייחוס, בודק חיבור אורכים בלי overflow ומקבל רק kind שהוא מכיר. הזהות אינה שדה בheader שניתן לזייף.
התגובה מכילה מזהה, מצב ומידע תוצאה או שגיאה. המצבים הם rejected לפני קבלה, accepted לאחר commit של intent, running, succeeded, canceled, failed וunknown כאשר אי אפשר עדיין לברר את תוצאת הניסיון. התוצאה העמידה נשמרת לפני אישור succeeded לclient.
הבקשה accepted אינה הבטחה להצלחה, אבל היא התחייבות שהשירות זוכר את העבודה לפי חוזה ההתמדה. אם הqueue מלא לפני הcommit מחזירים rejected. אם הכשל קרה אחרי הcommit, חייבים להמשיך להחזיר מצב שאפשר לשאול עליו.
מודל רץ של חיי ניסיון¶
הקוד הבא בודק את החלק שאסור לשכבות לשנות בשקט. הוא אינו journal אמיתי ואינו מבצע I/O. המטרה היא לקבל oracle קטן שאפשר להריץ מול רצפי אירועים של המערכת המשולבת.
#include <cassert>
#include <cstdint>
#include <stdexcept>
enum class State { accepted, running, succeeded, canceled, failed, unknown };
enum class Completion { success, canceled, failure };
class JobModel {
State state = State::accepted;
std::uint64_t generation = 0;
bool cancel_requested = false;
bool inflight = false;
bool durable_result = false;
public:
std::uint64_t start() {
if (state != State::accepted || inflight) throw std::logic_error("cannot start");
if (generation == UINT64_MAX) throw std::overflow_error("generation exhausted");
state = State::running; inflight = true;
return ++generation;
}
void cancel() { cancel_requested = true; }
bool cancellation_requested() const { return cancel_requested; }
void client_timeout() const { /* No server state transition. */ }
void worker_lost(std::uint64_t attempt) {
if (!inflight || attempt != generation) throw std::logic_error("bad loss");
inflight = false; state = State::unknown;
}
void resolve_not_applied() {
if (state != State::unknown) throw std::logic_error("not unknown");
state = State::accepted;
}
void complete(std::uint64_t attempt, Completion completion) {
if (!inflight || attempt != generation) throw std::logic_error("stale completion");
inflight = false;
state = completion == Completion::success ? State::succeeded :
completion == Completion::canceled ? State::canceled : State::failed;
}
void commit_result() {
if (state != State::succeeded && state != State::canceled && state != State::failed)
throw std::logic_error("not terminal");
durable_result = true;
}
bool may_ack_result() const { return durable_result; }
State status() const { return state; }
};
int main() {
JobModel race;
auto attempt = race.start();
race.cancel();
race.client_timeout();
assert(race.status() == State::running && race.cancellation_requested());
race.complete(attempt,Completion::success);
assert(race.status() == State::succeeded && !race.may_ack_result());
race.commit_result();
assert(race.may_ack_result());
JobModel recovery;
auto old_attempt = recovery.start();
recovery.worker_lost(old_attempt);
assert(recovery.status() == State::unknown);
recovery.resolve_not_applied();
auto new_attempt = recovery.start();
bool stale = false;
try { recovery.complete(old_attempt,Completion::success); }
catch (const std::logic_error&) { stale = true; }
assert(stale && recovery.status() == State::running);
recovery.complete(new_attempt,Completion::canceled);
recovery.commit_result();
assert(recovery.status() == State::canceled && recovery.may_ack_result());
}
הפעולה resolve_not_applied דורשת ראיה חיצונית שהניסיון לא יצר effect, או חוזה idempotency מתאים לפני retry. היא אינה כפתור שמותר לקרוא אליו רק כי חלף זמן. בעבודת checksum טהורה על קלט שנשמר אפשר לחשב שוב; בפעולה שמשנה מערכת חיצונית דרושה הוכחה נוספת.
המודל דורש commit_result לפני אישור תוצאה. במימוש, כשל commit משאיר מצב שאינו מאושר גם אם החישוב הצליח. הrecovery צריך להיות מסוגל לשחזר אותו או לחשב שוב בהתאם לחוזה. המודל אינו מחליף את בדיקת אטומיות הjournal; הוא קובע מתי מותר לשכבה מעליו להבטיח משהו.
הסדר של shutdown¶
השירות מפסיק קבלת בקשות חדשות וממשיך query עבור עבודה שכבר התקבלה לפי חלון הזמן שהוגדר. הוא מכריע בין drain לביטול, ממתין להשלמות הדרושות ומסיים את הjournal. רק אחרי quiescence משחררים callback context, engine וhandles.
הUI מפסיק לקבל עדכונים לפני שפריקת החלון מסיימת את חיי אובייקטי התצוגה. הnative wrapper ממשיך להחזיק context עד join או אישור quiescence. הפסקת subscription אינה הוכחה שcallback שכבר התחיל יצא.
הdriver נסגר לפי פרוטוקול פרק 13. הסרת interface, ניקוז requests וסיום work items מתועדים בנפרד מסגירת הservice. מסלול surprise removal או כשל התקן אינו מורשה להשאיר request ללא completion.
תוכנית הבדיקה של הפתרון¶
הבדיקה הראשונה מגישה אותה עבודה פעמיים ומקבלת אותה זהות תוצאה. שינוי payload עם אותו מזהה מחזיר conflict. משתמש אחר אינו יכול לקרוא את התוצאה גם אם הוא מכיר את המזהה.
הבדיקה השנייה עוצרת בכל גבול commit של 14.7 ומריצה recovery. משווים בין רשומות שהלקוח כבר קיבל עליהן אישור לבין המצב ששוחזר. לא מסתפקים בכך שהשירות עלה שוב.
הבדיקה השלישית מכריחה cancel מול success וcompletion ישן מול ניסיון חדש. הoracle שלמעלה והמודל של 14.9 קובעים איזה מעבר מותר. הtrace מצרף מזהה עבודה ומזהה ניסיון כדי שאפשר יהיה להסביר חריגה.
הבדיקה הרביעית ממלאת כל תור עד המגבלה. בודקים דחייה מפורשת, memory וhandle count לאורך מחזורים, ואת יכולת הסגירה כשאין לקוח שקורא תשובות. הדוח של 14.8 כולל גם failed וtimeout.
הבדיקה החמישית מתקינה בלי IDE, משדרגת עם נתונים קיימים ומבצעת את מסלול החזרה שהוגדר. המאמת של 14.10 משווה את החבילה למקור המהימן, והsymbols נשמרים לפי build.
איך נראה פתרון שמגיע לסיום¶
ההגשה מחברת לכל טענה קוד, ניסוי וראיה. הטענה שהUI מגיב קשורה לtrace ולבדיקת סגירה תחת עומס. הטענה שאין callback אחרי שחרור קשורה לחוזה join ולבדיקת המרוץ. הטענה שהעבודה לא אבדה קשורה לנקודת האישור ולrecovery.
הדרישה החדשה מוסיפה kind נוסף דרך אותם גבולות. אם נדרש שינוי של כל הפרוטוקול בלי דרך לדחות client ישן, התכנון צריך תיקון. אם ההרחבה עוברת את הבדיקות ושומרת את המגבלות, יש לנו מערכת שאפשר להמשיך לפתח מתוך הבנה של מה שקורה מתחתיה.