לדלג לתוכן

מעקב שינויים ונעילה

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

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

קריאת הודעות עם גבול זמן

התוכנית הבאה מקבלת תיקייה קיימת. בזמן שהיא ממתינה אפשר ליצור או לשנות קובץ מעבדה בתיקייה מחלון אחר:

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <array>
#include <cstddef>
#include <cstring>
#include <iostream>
#include <string>

struct Handle {
    HANDLE value;
    explicit Handle(HANDLE v) : value(v) {}
    ~Handle() {
        if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value);
    }
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
};

bool print_changes(const std::byte* bytes, std::size_t count) {
    constexpr std::size_t header = 3 * sizeof(DWORD);
    std::size_t offset = 0;
    while (offset < count) {
        if (count - offset < header) return false;
        DWORD next = 0, action = 0, name_bytes = 0;
        std::memcpy(&next, bytes + offset, 4);
        std::memcpy(&action, bytes + offset + 4, 4);
        std::memcpy(&name_bytes, bytes + offset + 8, 4);
        if (name_bytes % sizeof(wchar_t) != 0 ||
            name_bytes > count - offset - header) return false;
        std::wstring name(name_bytes / sizeof(wchar_t), L'\0');
        std::memcpy(name.data(), bytes + offset + header, name_bytes);
        std::wcout << action << L": " << name << L'\n';
        if (next == 0) return true;
        if (next < header + name_bytes || next % sizeof(DWORD) != 0 ||
            next > count - offset) return false;
        offset += next;
    }
    return false;
}

int wmain(int argc, wchar_t** argv) {
    if (argc != 2) return 2;
    Handle directory(CreateFileW(argv[1], FILE_LIST_DIRECTORY,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        nullptr, OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr));
    if (directory.value == INVALID_HANDLE_VALUE) return 1;
    Handle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
    if (!event.value) return 1;
    alignas(DWORD) std::array<std::byte, 16384> buffer{};
    OVERLAPPED operation{};
    operation.hEvent = event.value;
    const DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME |
                         FILE_NOTIFY_CHANGE_SIZE |
                         FILE_NOTIFY_CHANGE_LAST_WRITE;
    if (!ReadDirectoryChangesW(directory.value, buffer.data(),
            static_cast<DWORD>(buffer.size()), FALSE, filter,
            nullptr, &operation, nullptr)) {
        const DWORD error = GetLastError();
        if (error != ERROR_IO_PENDING) {
            std::cerr << error << '\n';
            return 1;
        }
    }

    if (WaitForSingleObject(event.value, 5000) != WAIT_OBJECT_0) {
        if (!CancelIoEx(directory.value, &operation) &&
            GetLastError() != ERROR_NOT_FOUND)
            std::cerr << "cancel request failed\n";
    }
    DWORD transferred = 0;
    if (!GetOverlappedResult(
            directory.value, &operation, &transferred, TRUE)) {
        const DWORD error = GetLastError();
        if (error == ERROR_OPERATION_ABORTED) return 0;
        if (error == ERROR_NOTIFY_ENUM_DIR) {
            std::cout << "rescan-required\n";
            return 3;
        }
        std::cerr << error << '\n';
        return 1;
    }
    if (transferred == 0) {
        std::cout << "rescan-required\n";
        return 3;
    }
    return print_changes(buffer.data(), transferred) ? 0 : 1;
}

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

אובדן פירוט אינו היעדר שינוי

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

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

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

פתיחה חוקית אינה עקביות לוגית

שני writers יכולים לפתוח קובץ בהצלחה לפי share modes ועדיין לדרוס שדות זה של זה. שיתוף קובע אילו פתיחות מותרות, לא כיצד לתאם עסקה. נעילת טווח בתים באמצעות LockFileEx יכולה לתאם גישות מתאימות, אבל צריך לבדוק את גבולות החוזה, כולל התנהגות גישות דרך memory mapping.

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

מנגנון הזדמנות אינו mutex רגיל

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

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

קריאה להעמקה

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