לדלג לתוכן

עצירה והתאוששות של שירות

נניח שקיבלנו עשר בקשות עיבוד והתחלנו לעבוד על שתיים. עכשיו מנהל המחשב מבקש לעצור את השירות. אפשר להפסיק לקבל בקשות, אבל מה נעשה עם השמונה שממתינות? ומה נעשה אם אחת הפעולות הפעילות כבר כתבה חצי מהתוצאה לקובץ? הבקשה לעצור לא מבטלת את ההתחייבויות שכבר נתנו ללקוחות.

לכן נגדיר שתי מכונות מצבים. הראשונה היא המצב שאנחנו מדווחים לSCM: אתחול, ריצה, עצירה בתהליך ועצירה שהסתיימה. השנייה היא מצב המנוע שלנו: קבלה פתוחה, קבלה סגורה, ניקוז פעולות, שמירת מצב וניקוי. הדיווח למערכת צריך להתאים להתקדמות האמיתית של המנוע.

המצבים והמעברים המתועדים מופיעים בחוזה מעברי המצב של שירות. בפרט, דיווח SERVICE_STOP_PENDING הוא התחייבות ללכת לכיוון סיום, ולא שלב ביניים שממנו נחזור סתם לrunning מפני שהעצירה התארכה.

שירות קטן שאפשר לעצור

נתחיל משירות שאין לו עדיין תור משימות. הוא ממתין לevent שהcontrol handler מסמן. נפריד בין thread הניהול לבין הthread שממתין ומסיים, ונגן על המצב שהם חולקים. שמרו בקובץ lab_service.cpp.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <iostream>

#pragma comment(lib, "advapi32.lib")

namespace {
    wchar_t name[] = L"AmitTechLab";
    SERVICE_STATUS_HANDLE status_handle = nullptr;
    SRWLOCK state_lock = SRWLOCK_INIT;
    HANDLE stop_event = nullptr;
    DWORD current_state = SERVICE_START_PENDING;

    void publish(DWORD state, DWORD error = NO_ERROR,
                 DWORD checkpoint = 0, DWORD hint = 0) {
        SERVICE_STATUS status{};
        status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
        status.dwCurrentState = state;
        status.dwControlsAccepted = state == SERVICE_RUNNING
            ? SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN : 0;
        status.dwWin32ExitCode = error;
        status.dwCheckPoint = checkpoint;
        status.dwWaitHint = hint;
        if (!SetServiceStatus(status_handle, &status)) {
            DWORD failure = GetLastError();
            ExitProcess(failure ? failure : ERROR_GEN_FAILURE);
        }
    }

    DWORD WINAPI control(DWORD code, DWORD, void*, void*) {
        if (code == SERVICE_CONTROL_INTERROGATE) return NO_ERROR;
        if (code != SERVICE_CONTROL_STOP && code != SERVICE_CONTROL_SHUTDOWN)
            return ERROR_CALL_NOT_IMPLEMENTED;
        AcquireSRWLockExclusive(&state_lock);
        if (current_state == SERVICE_RUNNING) {
            current_state = SERVICE_STOP_PENDING;
            publish(SERVICE_STOP_PENDING, NO_ERROR, 1, 3000);
            if (!SetEvent(stop_event)) ExitProcess(GetLastError());
        }
        ReleaseSRWLockExclusive(&state_lock);
        return NO_ERROR;
    }

    void WINAPI service_main(DWORD, wchar_t**) {
        status_handle = RegisterServiceCtrlHandlerExW(name, control, nullptr);
        if (!status_handle) return;
        publish(SERVICE_START_PENDING, NO_ERROR, 1, 3000);

        HANDLE created = CreateEventW(nullptr, TRUE, FALSE, nullptr);
        if (!created) {
            DWORD error = GetLastError();
            publish(SERVICE_STOPPED, error);
            return;
        }
        AcquireSRWLockExclusive(&state_lock);
        stop_event = created;
        current_state = SERVICE_RUNNING;
        publish(SERVICE_RUNNING);
        ReleaseSRWLockExclusive(&state_lock);

        DWORD wait = WaitForSingleObject(created, INFINITE);
        DWORD error = wait == WAIT_OBJECT_0 ? NO_ERROR : GetLastError();

        AcquireSRWLockExclusive(&state_lock);
        current_state = SERVICE_STOPPED;
        CloseHandle(stop_event);
        stop_event = nullptr;
        ReleaseSRWLockExclusive(&state_lock);

        publish(SERVICE_STOPPED, error);
    }
}

int wmain() {
    SERVICE_TABLE_ENTRYW table[] = {
        {name, service_main},
        {nullptr, nullptr}
    };
    if (!StartServiceCtrlDispatcherW(table)) {
        std::cerr << "dispatcher error=" << GetLastError() << '\n';
        return 1;
    }
}

בנו את השירות והעתיקו אותו לתיקיית המעבדה C:\AmitTechLab. נרשום רק את שירות הניסוי שלנו, בחשבון LocalService וללא הפעלה אוטומטית. פקודות ההתקנה דורשות טרמינל מנהל במכונת המעבדה; הן אינן סיבה להריץ את כל כלי הפיתוח כמנהל.

cl /std:c++20 /EHsc /W4 lab_service.cpp
New-Item -ItemType Directory -Force C:\AmitTechLab
Copy-Item .\lab_service.exe C:\AmitTechLab\lab_service.exe
sc.exe create AmitTechLab binPath= C:\AmitTechLab\lab_service.exe start= demand obj= 'NT AUTHORITY\LocalService'
sc.exe start AmitTechLab
sc.exe queryex AmitTechLab
sc.exe stop AmitTechLab
sc.exe queryex AmitTechLab

אם יצירת השירות הצליחה אבל התחלתו נכשלה, בדקו קודם שהחשבון יכול לקרוא ולהריץ את הקובץ ושלא העתקתם build שחסרה לו תלות. בסיום הניסוי, אחרי שהשירות הגיע לstopped, אפשר להסיר את הרשומה באמצעות sc.exe delete AmitTechLab. אין צורך לשנות שירותים קיימים של המערכת.

איפה עובר הthread

הcontrol handler רץ כחלק ממנגנון השליטה של השירות. אם נבצע בו המתנה לכל המשימות, נעכב את המנגנון שאמור לנהל אותנו. בדוגמה הוא רק מפרסם מעבר, מסמן event וחוזר. החוזה של פונקציית הטיפול בבקרות מסביר למה עבודה ארוכה צריכה לעבור למסלול אחר.

הנעילה שומרת על שני invariants: אין סגירה של הevent בזמן שcallback משתמש בו, ואין פרסום running אחרי שכבר התחלנו עצירה. היא אינה מוחזקת בזמן ההמתנה לevent. אם היינו מחזיקים אותה שם, הcallback היה ממתין לנעילה, והthread הראשי היה ממתין לcallback שיסמן את הevent.

הקריאה לSetServiceStatus נעשית בתוך המעבר המוגן כשצריך לסדר את שני הthreadים. בעבודת המנוע לא נשמור את הנעילה הזאת בזמן I/O או join. במקרה הנדיר שבו פרסום המצב נכשל, דוגמת המעבדה מסיימת את התהליך; מוצר אמיתי יוסיף מסלול אבחון מוגבל שלא תלוי במנגנון שנכשל. לא נמשיך לקבל עבודה כשאי אפשר לקיים את חוזה הניהול.

התקדמות שאפשר להאמין לה

הערך dwWaitHint הוא אומדן זמן לפעולה הממתינה, והcheckpoint מסמן התקדמות. נניח שאתחול כולל אימות תצורה, פתיחת journal ושחזור משימות. נוכל להתקדם בcheckpoint אחרי שכל שלב הושלם. לא נפתח timer שרק מגדיל את המונה בזמן שהשחזור תקוע.

המערכת אינה יכולה להסיק מהמונה לבדו שבדקנו עוד רשומה. אנחנו אחראים לקשר אותו לעבודה ממשית. אחרת הכלי הניהולי רואה שירות שמתקדם לנצח אף ששום byte לא השתנה. גם בדיווח סיום יש כלל חשוב: מנקים לפני SERVICE_STOPPED, מדווחים אותו פעם אחת ומסיימים. הכללים האלה מפורטים בחוזה SetServiceStatus.

ניקוז או ביטול

בניקוז אנחנו סוגרים קבלה של עבודה חדשה ומסיימים את מה שכבר התקבל. בביטול אנחנו מבקשים מהעבודה הפעילה להגיע לנקודת סיום מתאימה ומבטלים עבודה ממתינה לפי החוזה. בשני המקרים צריך להחליט מה יקבל לקוח שהבקשה שלו נקלטה בדיוק בזמן סגירת הקבלה.

נחבר את שינוי מצב הקבלה והכנסת הבקשה לתור לאותה נעילה או לאותו פרוטוקול אטומי. אם ההכנסה הצליחה לפני הסגירה, הבקשה שייכת לקבוצת הניקוז. אם הגיעה אחרי הסגירה, נחזיר תשובת דחייה מפורשת. בדיקה של דגל ואחריה הכנסה לתור בלי סנכרון משותף משאירה חלון שבו בקשה נעלמת בין שתי הקבוצות.

גם timeout לעצירה אינו הוכחה שהI/O בוטל. אחרי CancelIoEx נמשיך לקיים את חיי הbuffer עד ההשלמה המתאימה, כפי שלמדנו בפרק האסינכרוני. אם הגענו למועד שבו המוצר חייב לסיים בכוח, נצטרך recovery שמסוגל לטפל במצב שנשאר בדיסק. לא נסמן את כל המשימות כהושלמו רק כדי להוריד את המונה לאפס.

הפעלה מחדש

אפשר להגדיר לSCM פעולות אחרי כשל, כולל הפעלה מחדש והשהיה. ברשימת הפעולות, הכשל הבא בוחר את הפעולה הבאה, ואחרי סוף הרשימה חוזרים על האחרונה; תקופת reset משפיעה על מניין הכשלים. כדאי לקרוא את חוזה פעולות הכשל לפני שמניחים ששלוש רשומות פירושן שלושה ניסיונות בלבד.

נניח שקובץ התצורה פגום. הפעלה מחדש כל שנייה לא תתקן אותו, אבל יכולה למלא לוגים ולהעמיס על האחסון. נבדיל בין כשל זמני לבין כשל שמצריך תיקון חיצוני, ונגדיר סוף לניסיונות או השהיה מתאימה. בעלייה מחדש נקרא את journal ונכריע אילו משימות מותר לנסות שוב. ההפעלה מחדש מחזירה תהליך; היא לא מחזירה אוטומטית את משמעות העבודה שלו.