Locking ב-file handoff: file lock ו-atomic claim בפועל

· עודכן בתאריך: · · file handoff, locking, תכנון, פיתוח Windows

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 7 Mar 2026)
פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173298)

מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.

Go Komura (2026). Locking ב-file handoff: file lock ו-atomic claim בפועל. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173298 https://comcomponent.com/he/blog/file-integration-locking-best-practices-komurasoft-style/

DOI (הגרסה האחרונה)
10.5281/zenodo.22173298
DOI (הגרסה הזו)
10.5281/zenodo.22173299

locking ב-file handoff כמעט תמיד הופך לבעיה בתיקייה משותפת, ב-batch לילי, ובהעברה בין processes נפרדים. מה שחוזר בחיפושים: האם file lock לבד מספיק, איך מונעים מכמה workers לתפוס את אותו קובץ, ואיך נמנעים מקובץ שעדיין נכתב.

המאמר עובר על locking ב-file handoff סביב file lock, atomic claim, temp -> rename ו-idempotency.

מונחים שכדאי ליישר קודם

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

מונח המשמעות במאמר
atomic פעולה שהמצב באמצע לא נראה מבחוץ. יש רק שתי אפשרויות: הצלחה, או שלא קרה כלום
claim תפיסת זכות עיבוד: “אני מעבד את הקובץ הזה”. במאמר הכוונה בעיקר לצורה שבה רק מי שהצליח לעשות rename מ-incoming ל-processing/<worker>/ הופך לבעלים
atomic claim ביצוע ה-claim בפעולה אחת. אם הבדיקה והתפיסה נפרדות, process אחר יכול להיכנס ביניהן (3.1)
lease ownership עם תוקף. כותבים ב-lock file מי ועד מתי, כדי שכשהתוקף פג worker אחר יוכל לקחת (4.4)
stale מצב של lock או claim שנשאר אחרי שהבעלים נפל באופן חריג. אם אי אפשר לקבוע אם הוא חי או מת, כולם נעצרים (2.3)
manifest קובץ נפרד שמתאר את התוכן. שם קובץ, גודל, hash, מספר רשומות וכו’, לאימות בצד המקבל. קובץ done הוא הגרסה המינימלית (4.2)
idempotency תכונה שעיבוד חוזר של אותו קלט לא משנה את התוצאה (4.5)
advisory lock lock שעובד רק אם כל המשתתפים שומרים על ההסכם. ה-OS לא אוכף אותו, אז אפשר לכתוב תוכנית שמתעלמת וקוראת/כותבת בכל זאת. flock ב-Linux הוא מהסוג הזה
byte-range lock lock על טווח מסוים, לא על כל הקובץ. LockFileEx ב-Windows הוא הדוגמה הבולטת, וה-OS אוכף אותו. יש חריג (3.5)

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 29, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

תוכן עניינים

  1. המסקנה בקצרה
  2. דפוסי התנגשות ב-file handoff (תרשים)
    • 2.1. קוראים קובץ שעדיין נכתב
    • 2.2. כמה workers תופסים את אותו קובץ במקביל
    • 2.3. כולם נעצרים בגלל stale lock
  3. Anti-patterns
    • 3.1. בדיקה דו-שלבית Exists -> Create
    • 3.2. כתיבה ישירה לשם הסופי
    • 3.3. גודל הקובץ נעצר, אז מתייחסים לזה כסיום
    • 3.4. כולם מעדכנים קובץ משותף
    • 3.5. חושבים ש-lock API הוא פתרון-על
  4. שיטות עבודה מומלצות
    • 4.1. מפרסמים בסדר temp -> close -> rename / replace
    • 4.2. מציינים שלמות במפורש עם done / manifest
    • 4.3. צד הקבלה לוקח claim באופן אטומי
    • 4.4. אם מסתמכים על lock file, הופכים אותו ל-lease
    • 4.5. מניחים idempotency מראש
  5. פסאודו-קוד (קטעים)
  6. חלוקה גסה לפי מצב
  7. סיכום
  8. מקורות

file handoff הוא תחום שבו “הסכם המסירה” נשבר בקלות רבה יותר מהקוד עצמו. זה עובר ב-unit tests, אבל נשבר מדי פעם דווקא בתיקייה המשותפת בפרודקשן או ב-batch הלילי. וקשה לשחזר. זה די שכיח.

רוב הגורמים אינם ב-API של file I/O עצמו, אלא בכך ששלושת הדברים האלה מעורפלים:

  • מתי מותר לקרוא
  • מי מחזיק בזכות העיבוד
  • איך מתאוששים כשיש כישלון

המאמר לא נעצר בסיפור של OS lock. הוא מסדר locking ב-file handoff כפרוטוקול מסירה.

הקוד שמופיע כאן מפורסם ב-GitHub כסט דוגמאות שאפשר לבנות ולהריץ: ספרייה, הדגמה של תחרות claim בין שני workers ותפיסת lease, ו-unit tests שמשחזרים תחרות, נתונים פגומים ו-stale lock.

file-integration-locking-best-practices-komurasoft-style - komurasoft-blog-samples (GitHub)

1. המסקנה בקצרה

  • הדבר הכי חשוב ב-file handoff הוא ליצור מצב שבו ברגע ששם הקובץ הסופי נראה, כבר מותר לקרוא
  • לייצג בהפרדה, דרך שם קובץ או תיקייה, את המצבים: ביצירה / פורסם / בעיבוד / עובד
  • אם יש כמה workers, לקחת claim באופן אטומי לפני הקריאה
  • להשתמש ב-lock file וב-OS lock כעזר, ובסוף לתפוס כפילות עם idempotency

במילים אחרות, ב-file handoff הגוף הוא לא “locking” אלא תכנון פרוטוקול המסירה. זה לא נגמר בקריאה אחת לפונקציית lock.

2. דפוסי התנגשות ב-file handoff (תרשים)

2.1. קוראים קובץ שעדיין נכתב

אם מתחילים לכתוב ישירות לשם הסופי, קורית התקלה הזו. ב-JSON חסר סוגר, ב-CSV חסרות שורות, וב-ZIP הקובץ פשוט פגום.

קריאה של קובץ שעדיין נכתבהצד השולח יוצר את הקובץ ישירות בשם הסופי וכותב בהדרגה, בעוד צד הקבלה מזהה ומתחיל לקרוא באמצע. התוצאה: שורות חסרות או parse שנכשל.המקבלתיקייה משותפתהשולחהמקבלתיקייה משותפתהשולחעדיין באמצעחוסר שורות / parse שנכשל / עיבוד חלקייוצר את orders.csv בשם הסופיכותב שורות 1 עד 5000מזהה את orders.csvמתחיל לקרוא כפי שהואכותב את השאר

2.2. כמה workers תופסים את אותו קובץ במקביל

בזרימה של “מסתכלים ברשימה, ואם לא עובד פותחים”, שני workers יכולים לתפוס את אותו קובץ. זו ההתחלה של חיוב כפול או שליחה כפולה.

שני workers תופסים את אותו קובץשני workers מוצאים את אותו קובץ ב-incoming ומתחילים לקרוא במקביל, ולכן אותו קלט מעובד פעמיים.incomingworker 2worker 1incomingworker 2worker 1אותו קלט מעובד פעמייםמוצא את a.csvמוצא את a.csvמתחיל לקרואמתחיל לקרוא

2.3. כולם נעצרים בגלל stale lock

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

כולם נעצרים כי אי אפשר לקבוע אם ה-lock עדיין תקףworker A יוצר lock ונופל באופן חריג. worker B מוצא את הקובץ, נמנע מלהתחיל, וממשיך להמתין כי אי אפשר לקבוע אם זה stale.worker Block fileworker Aworker Block fileworker Aנופל כאן באופן חריגאי אפשר לקבוע אם זה stale, וכולם נעצריםיוצר lockבודק שה-lock קייםנמנע מלהתחיל לעבדממשיך להמתין

3. Anti-patterns

3.1. בדיקה דו-שלבית Exists -> Create

הבעיה: הבדיקה והתפיסה הן שתי פעולות נפרדות. process אחר יכול להיכנס ביניהן, ולכן זה לא נותן exclusive access.

בדיקה דו-שלבית מאפשרת לשני processes להתקדםprocess A ו-process B בודקים שניהם שאין lock, שניהם מקבלים שאין, ולכן שניהם יוצרים lock ומתקדמים במקביל.file systemprocess Bprocess Afile systemprocess Bprocess Aשניהם מצליחים להתקדםבודק שאין lockבודק שאין lockאיןאיןיוצר lockיוצר lock

דוגמה טיפוסית לא טובה נראית כך.

if (!File.Exists(lockPath))
{
    File.WriteAllText(lockPath, Environment.ProcessId.ToString());
    ProcessFile();
}

מה שנדרש הוא להפוך את “אם אין — ליצור” לפעולה אחת. ב-.NET משתמשים במשפחת FileMode.CreateNew, ובמערכות POSIX ביצירה אטומית כמו O_CREAT | O_EXCL.

3.2. כתיבה ישירה לשם הסופי

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

הכשל שנגרם מכתיבה ישירה לשם הסופיברגע ששם final נראה, צד הקבלה מזהה אותו בעוד השולח עדיין כותב, ולכן נקראים נתונים לא שלמים.שם final נראהצד הקבלה מזהההשולח עדיין כותבנקראים נתונים לא שלמים
using var writer = OpenForWrite(finalPath); // כאן finalPath כבר נראה
foreach (var row in rows)
{
    writer.WriteLine(row);
}

השיטה הזו מזמינה בעצמה את התקלה מסעיף 2.1.

3.3. גודל הקובץ נעצר, אז מתייחסים לזה כסיום

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

זיהוי שגוי של סיום לפי התייצבות גודל הקובץהשולח מתחיל להעתיק ומשתהה באמצע. המקבל רואה שהגודל לא השתנה 10 שניות, מזהה בטעות כסיום, ומתחיל לקרוא לפני שההעתקה הסתיימה.המקבלתיקייה משותפתהשולחהמקבלתיקייה משותפתהשולחמזהה בטעות כסיוםמתחיל להעתיק את data.zipמשתהה זמנית באמצעהגודל לא משתנה 10 שניותמתחיל לקרואממשיך בהעתקה
if (currentLength == lastLength && stableSeconds >= 10)
{
    return Ready;
}

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

3.4. כולם מעדכנים קובץ משותף

תכנון שבו כולם קוראים ומעדכנים status.csv או counter.json יחיד, בדרך כלל מסתיים בכך שמי שכתב אחרון מנצח. כשמתחילים להשתמש ב-file handoff כמסד נתונים פשוט, כאן זה נהיה קשה.

עדכון הדדי בקובץ משותף מאבד עדכוןbatch A ו-batch B שתיהן קוראות את אותה גרסה v1 של status.csv וכל אחת כותבת גרסה משלה. גרסה B נכתבת אחרונה ומוחקת את העדכון של A.status.csvbatch Bbatch Astatus.csvbatch Bbatch Aהעדכון של A נעלםקורא v1קורא v1כותב v2-Aכותב v2-B

יש גם רעיון לברוח ל-append-only, אבל המשמעות שלו משתנה לפי file system ואופן הפריסה. אם נדרש עדכון משותף, עדיף לא להתאמץ יתר על המידה עם file handoff כאן.

3.5. חושבים ש-lock API הוא פתרון-על

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

הערה:

  • flock ב-Linux הוא advisory lock, ולכן אפשר בקלות לכתוב צד שמתעלם מההסכם
  • byte-range lock ב-Windows מתעלמים ממנו כשמדובר ב-memory-mapped file
  • כלומר, עדיף לא להטיל על OS lock לבדו גם את התכנון של הודעת סיום ו-ownership

הנקודה השנייה מפורשת במפרט של Windows. ב-Locking and Unlocking Byte Ranges in Files של Microsoft Learn, מיד אחרי המשפט שגישה של process אחר לטווח נעול תמיד תיכשל (כלומר נעילת הטווח ב-Windows אינה advisory אלא נאכפת), מופיעה הערה שכאשר משתמשים ב-memory-mapped file, ה-byte-range lock מתעלמים ממנה. אם הצד השני נוגע באותו קובץ דרך CreateFileMapping, ה-lock שלכם פשוט לא רלוונטי עבורו.

אם רוצים לתפוס range lock ב-.NET, זה FileStream.Lock / Unlock (במקרה של Windows).

using var stream = new FileStream(
    path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);

// נועלים באופן בלעדי רק את הבייט הראשון, כתג של "בעיבוד"
stream.Lock(0, 1);
try
{
    // כאן מתבצעת הקריאה/כתיבה של הגוף עצמו
}
finally
{
    // תמיד משחררים לפני הסגירה
    stream.Unlock(0, 1);
}

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

4. שיטות עבודה מומלצות

קודם, ההתאמה ל-anti-patterns בפרק 3. אם יש תחושה שאתם עושים אחד מהם, אפשר לקרוא ישר את הסעיף המתאים.

Anti-pattern מה קורה הפתרון המתאים
3.1. בדיקה דו-שלבית Exists -> Create process אחר נכנס בין הבדיקה לתפיסה, ושני processes מתקדמים במקביל 4.3 לוקחים claim באופן אטומי (rename או FileMode.CreateNew)
3.2. כתיבה ישירה לשם הסופי צד הקבלה קורא קובץ שעדיין נכתב 4.1 מפרסמים בסדר temp -> close -> rename / replace
3.3. גודל הקובץ נעצר, אז מתייחסים לזה כסיום השהיה זמנית בהעתקה מזוהה בטעות כסיום 4.2 מציינים סיום במפורש עם done / manifest
3.4. כולם מעדכנים קובץ משותף הצד שכותב מאוחר יותר דורס, והעדכון נעלם 4.3 מצמצמים לכותב יחיד, ו-4.5 תופס כפילות. אם זה עדיין לא מספיק — החלטת נסיגה בפרק 6
3.5. חושבים ש-lock API הוא פתרון-על נשבר מול צד שלא שומר על ההסכם או ניגש דרך memory mapping 4.4 lock file בתור lease, ו-4.5 idempotency שתופס את זה

4.1. מפרסמים בסדר temp -> close -> rename / replace

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

חמשת השלבים של פרסום בטוחיצירת שם temp ייחודי, כתיבת כל התוכן ל-temp, flush/close, rename/replace לשם final באותה תיקייה, וצד הקבלה עוקב רק אחרי שם final.יוצרים שם temp ייחודיכותבים את כל התוכן ל-tempעושים flush / closerename / replace לשם final באותה תיקייהצד הקבלה עוקב רק אחרי שם final

נקודות מרכזיות:

  • temp ו-final נמצאים באותה תיקייה, לפחות באותו volume / file system
  • ב-Windows / .NET אפשר לשקול את משפחת File.Replace
  • ההסכם: ברגע ששם final נראה, התוכן כבר הושלם

אם temp נמצא בכונן אחר, ה-rename עלול להיות שקול להעתקה בלבד, או ש-Replace עלול להיכשל. ההנחה הזו נראית שקטה, אבל היא חשובה מאוד.

דרך תיקייה משותפת (SMB), עוד ארבע נקודות מתנדנדות. זה בדיוק שדה הקרב המרכזי של המאמר, ולכן הן בנפרד.

  • rename בתוך אותה תיקייה של אותו share רץ בצד השרת. לכן התכונה “לא נראה שם ביניים” נשמרת. לעומת זאת, כשחוצים shares, כמו מ-\\server\shareA ל-\\server\shareB, זה נחשב ל-volume נפרד. כש-MoveFileEx ב-Windows מקבל MOVEFILE_COPY_ALLOWED, הוא מחליף את המעבר בהעתקה ומחיקה. כלומר זה מפסיק להיות atomic, ונראה מצב ביניים. ההנחה שממקמים את temp ו-final, ואת incoming ו-processing, בתוך אותו share, חשובה עוד יותר מאשר במצב מקומי
  • rename נכשל רק כי מישהו פתח את הקובץ. לתיקייה משותפת ניגשים גורמים שלא הכרתם: antivirus, search indexer, או לקוחות מסניפים אחרים. מעשי להתייחס לכישלון rename של publish ו-claim לא כחריגה אלא כענף רגיל, עם המתנה קצרה ו-retry
  • timestamp לא יכול לשמש כלי החלטה. ב-File Times של Microsoft Learn כתוב שמה שמובטח לגבי זמני קובץ הוא רק שהוא ישקף נכון את השינוי בזמן שסוגרים את ה-handle שביצע אותו. זמן העדכון האחרון בזמן כתיבה לא מתעדכן במלואו עד שכל ה-handles לכתיבה נסגרים. גם הגרנולריות תלויה ב-file system: זמן העדכון האחרון ב-FAT מתעדכן ביחידות של 2 שניות, וזמן הגישה האחרון ב-NTFS מתעדכן באיחור של עד שעה. בנוסף, דרך SMB הזמן מסומן לפי שעון השרת. אם יש הפרש בין שעון הלקוח לשרת, גם קביעה כמו “לעבד N דקות אחרי העדכון” תתעוות בהתאם. זו הסיבה שקביעת סיום לא נעשית לפי זמן או גודל, אלא עם done / manifest מסעיף 4.2
  • גם change notifications מפספסות. לא כדאי להסתמך רק על התראות אירועים לניטור תיקייה משותפת. שילוב עם סריקת ספרייה מחזורית מייצב. הנושא מסוכם ב-FileSystemWatcher בפועל: miss ו-duplicate

4.2. מציינים שלמות במפורש עם done / manifest

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

זרימת פרסום עם done ומעקב manifestיוצרים data.tmp, מפרסמים כ-data.csv, יוצרים data.done או manifest.json, צד הקבלה מזהה את ה-done או ה-manifest, ומאמת שם קובץ, גודל ו-hash.יוצרים את data.tmpמפרסמים כ-data.csvיוצרים data.done / manifest.jsonצד הקבלה מזהה done / manifestמאמתים שם קובץ, גודל ו-hash

הפריטים שכדאי לכלול ב-manifest הם בערך אלה.

  • שם הקובץ הרלוונטי
  • גודל
  • hash
  • מספר רשומות
  • מזהה העברה / idempotency key
  • זמן יצירה

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

4.3. צד הקבלה לוקח claim באופן אטומי

אם כמה workers מסתכלים על אותו incoming, קל להבין את הגישה “להעביר לעצמך לפני שקוראים”. רק ה-worker שהצליח לעשות rename מ-incoming ל-processing/<worker>/ מעבד.

רק ה-worker שמצליח ב-rename תופס ownershipשני workers מוצאים את אותו קובץ ומנסים rename ל-processing. רק מי שמצליח ראשון תופס ownership על העיבוד.processingincomingworker 2worker 1processingincomingworker 2worker 1רק מי שהצליח ראשון תופס ownershipמוצא את a.csvמוצא את a.csvעושה rename ל-a.csvעושה rename ל-a.csv

מבחינה תפעולית, קל יותר לעקוב אם גם מפרידים תיקיות.

מסלול התיקיות מ-temp עד archive או errorקובץ עובר מ-temp דרך פרסום ל-incoming, משם דרך claim ל-processing, ולבסוף בהצלחה ל-archive או בכישלון ל-error.פרסוםclaimהצלחהכישלוןtempincomingprocessingarchiveerror

גם ה-rename עבור ה-claim מניחים שהוא רץ על אותו file system.

4.4. אם מסתמכים על lock file, הופכים אותו ל-lease

אם משתמשים ב-lock file, כדאי שהוא יהיה מידע ownership עם תוקף, ולא רק קובץ ריק. lock שלא ברור מי תפס אותו, בהכרח יגרום לבעיות בהמשך.

השדות של קובץ ה-lease ב-lock.jsonlock.json מכיל שישה שדות: ownerId, host, pid, acquiredAt, expiresAt ו-heartbeatAt.lock.jsonownerIdhostpidacquiredAtexpiresAtheartbeatAt

נקודות מרכזיות:

  • היצירה אטומית
  • הפסקת עדכונים משמשת כלי לקביעת stale
  • המחיקה, כעיקרון, רק על ידי מי שיצר
  • קובעים מראש נוהל recovery, בהנחה שיש מקרים שלא משחררים

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

4.5. מניחים idempotency מראש

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

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

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

5. פסאודו-קוד (קטעים)

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

המימוש של הפסאודו-קוד הזה (ספרייה, הדגמת תחרות claim בין שני workers, unit tests) - komurasoft-blog-samples (GitHub)

5.1. דפוס כשל טיפוסי

var lockPath = finalPath + ".lock";

if (!File.Exists(lockPath))
{
    File.WriteAllText(lockPath, "");
    using var writer = OpenForWrite(finalPath); // כותב ישירות לשם הסופי
    WritePayload(writer);

    File.Delete(lockPath);
}

יש שלוש בעיות.

  • Exists ו-WriteAllText הן שתי פעולות נפרדות
  • finalPath נראה כבר באמצע הכתיבה
  • lock נשאר בסיום חריג

5.2. דוגמה לכיוון נכון (בגדול, ככה)

var tempPath = MakeTempPathSameDirectory(finalPath);
WritePayload(tempPath);
FlushAndClose(tempPath);

PublishByRenameOrReplace(tempPath, finalPath); // בהנחת אותו file system / אותו volume
PublishDoneFile(finalPath + ".done", new
{
    FileName = Path.GetFileName(finalPath),
    Size = GetFileSize(finalPath),
    Hash = ComputeHash(finalPath),
    IdempotencyKey = integrationId
});
if (!TryClaimBundleByRename(baseName, incomingDir, processingDir))
{
    return; // worker אחר תפס קודם
}

var manifest = ReadDoneFile(Path.Combine(processingDir, baseName + ".done"));
VerifyPayload(Path.Combine(processingDir, baseName), manifest);

if (AlreadyProcessed(manifest.IdempotencyKey))
{
    MoveBundle(processingDir, archiveDir, baseName);
    return;
}

Process(Path.Combine(processingDir, baseName));
RecordProcessed(manifest.IdempotencyKey);
MoveBundle(processingDir, archiveDir, baseName);

כאן הסדר חשוב יותר מפרטי המימוש. כשלא מערבבים “לכתוב”, “לפרסם”, “לתפוס ownership” ו”לתעד עיבוד” — זה נשבר פחות.

6. חלוקה גסה לפי מצב

  • עם writer יחיד, reader יחיד ואותה מכונה, גם temp -> rename בלבד כבר יציב למדי
  • אם יש כמה consumers, מוסיפים claim rename מ-incoming ל-processing
  • בשילוב בין מערכות שונות, NAS או תיקייה משותפת, בטוח יותר להוסיף גם manifest / done וגם idempotency
  • אם כמה writers רוצים לעדכן את אותו מצב לוגי, כדאי לשקול DB או queue, ולא להתאמץ יתר על המידה עם file handoff
  • OS lock יעיל בתוך אותה קבוצת אפליקציות ואותן הנחות, אבל הוא לא תחליף לפרוטוקול מסירה

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

7. סיכום

locking ב-file handoff הוא לא קריאה לפונקציית lock, אלא קביעת מעברי מצב. זה עצם הרעיון של המאמר. מייצגים בשם או בתיקייה את המצבים ביצירה / פורסם / בעיבוד / עובד, ונמנעים מבדיקה דו-שלבית Exists -> Create, מכתיבה ישירה לשם הסופי, מהמתנה להתייצבות גודל, מעדכון הדדי של קובץ משותף, ומהסתמכות יתר על lock API. מעבר לכך, שילוב של temp -> close -> rename / replace, done / manifest, claim rename, lease ו-idempotency מונע תקלות רבות ב-file handoff דרך תיקייה משותפת.

הטריק ב-file handoff הוא לא לזהות בין “אפשר לקרוא” לבין “מותר לקרוא”. רק ההפרדה הזו מקטינה משמעותית תקלות מהסוג שמופיע רק בלילה.

8. מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

האם locking ב-file handoff מספיק עם lock API בלבד?
לרוב לא. flock ב-Linux הוא advisory lock, אז אפשר לכתוב צד שפשוט מתעלם מההסכם. byte-range lock ב-Windows מתעלמים ממנו על memory-mapped file. OS lock עובד בתוך אותה קבוצת אפליקציות ואותן הנחות, אבל כעזר. הגוף הוא פרוטוקול המסירה: temp -> rename, done/manifest, atomic claim ו-idempotency.
איך מונעים קריאה של קובץ שעדיין נכתב?
הדרך הסטנדרטית היא לפרסם בסדר temp -> close -> rename/replace. בזמן היצירה הקובץ נשאר תחת שם temp. אחרי close מחליפים לשם final באותה תיקייה, וצד הקבלה מסתכל רק על השם הסופי. temp ו-final חייבים להיות באותה תיקייה, לפחות באותו volume / file system. ההסכם: ברגע שהשם הסופי נראה, התוכן כבר הושלם.
איך מונעים מכמה workers לעבד את אותו קובץ במקביל?
לוקחים claim באופן אטומי לפני הקריאה. בפועל, רק ה-worker שהצליח לעשות rename מ-incoming ל-processing/<worker>/ הוא זה שמעבד. בדיקה דו-שלבית Exists ואז Create לא נותנת exclusive access, כי הבדיקה והתפיסה הן שתי פעולות נפרדות, ותהליך אחר יכול להיכנס ביניהן. אם צריך יצירה אטומית, משתמשים ב-FileMode.CreateNew של .NET או ב-O_CREAT | O_EXCL של POSIX.
על מה לשים לב כשמשתמשים ב-lock file?
לא קובץ ריק. עושים ממנו lease עם תוקף: ownerId, host, pid, acquiredAt, expiresAt ו-heartbeatAt. היצירה אטומית. הפסקת עדכונים משמשת לזיהוי stale. מחיקה כעיקרון רק על ידי מי שיצר. מכינים מראש recovery בהנחה שיש מקרים שלא משחררים. בפועל עדיף לא לנסות להבטיח שלמות מלאה עם lock file אחד, אלא לתכנן כך שבסוף idempotency תופס גם כפילות.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג