Интеграция через файлы: блокировки, атомарный claim и практические рекомендации
· Обновлено: · Го Комура · Интеграция через файлы, Блокировки, Проектирование, Разработка Windows
История изменений (4 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- В начало статьи добавлен раздел «Карта знаний этой статьи». Это сводка понятий и связей из основного текста: краткое резюме, схема и ссылка на страницу сведений. Утверждения в тексте не менялись.
- Текст обновлён по итогам внешнего ревью (1283 замечания). Содержание отдельных правок см. в записи ниже.
- Добавлен раздел с заранее зафиксированными терминами и таблица соответствия антипаттернов и мер. Добавлены четыре оговорки для общих папок (rename через разные share не атомарно; rename падает, если файл просто открыт; метки времени гарантированы только при закрытии handle; уведомления об изменениях теряются) и источник того, что byte-range lock игнорируется для memory-mapped файлов.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619637)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Интеграция через файлы: блокировки, атомарный claim и практические рекомендации. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619637 https://comcomponent.com/ru/blog/2026/03/07/001-file-integration-locking-best-practices-komurasoft-style/
- DOI (последняя версия)
- 10.5281/zenodo.21619637
- DOI (эта версия)
- 10.5281/zenodo.21619638
Исключительный доступ при интеграции через файлы почти неизбежно всплывает на общих папках, в ночных batch-заданиях и при обмене между процессами. В поиске чаще всего спрашивают: хватит ли одной файловой блокировки; как не дать нескольким воркерам взять один и тот же файл; как не прочитать файл, который ещё пишется.
В этой статье исключительный доступ при интеграции через файлы разбирается вокруг файловых блокировок, атомарного claim, схемы temp -> rename и идемпотентности.
Сначала зафиксируем термины
В этой области много английских слов, которые так и прижились. Пока смысл плавает, текст читать тяжело. Ниже — значения, которые используются в статье.
| Термин | Что это значит здесь |
|---|---|
| Атомарный (atomic) | Операция, промежуточное состояние которой снаружи не видно. Либо она полностью прошла, либо ничего не произошло |
| claim | Захват права обработки: «этот файл обрабатываю я». В статье это в основном схема, где владельцем становится только тот, кому удалось сделать rename из incoming в processing/<worker>/ |
| Атомарный claim | Тот же claim, выполненный одной операцией. Если проверка и захват разделены, в щель успевает другой процесс (3.1) |
| lease | Владение со сроком действия. В lock-файл пишут «кто» и «до какого момента», чтобы по истечении срока другой воркер мог подобрать работу (4.4) |
| stale | Lock или claim, оставшиеся после аварийного завершения владельца. Если нельзя отличить «ещё жив» от «уже умер», останавливаются все (2.3) |
| manifest | Отдельный файл с описанием содержимого: имя, размер, хеш, число записей и т. п. Приёмник сверяет по нему данные. done-файл — его минимальный вариант (4.2) |
| Идемпотентность (idempotency) | Свойство: повторная обработка того же входа не меняет результат (4.5) |
| advisory lock | Блокировка, которая работает, только если все участники соблюдают соглашение. ОС её не навязывает, поэтому программу, которая читает и пишет в обход, написать легко. К этому типу относится Linux flock |
| byte-range lock | Блокировка не всего файла, а указанного диапазона. Типичный пример — Windows LockFileEx; её ОС уже принудительно соблюдает. Но есть исключения (3.5) |
Карта знаний этой статьи
Статья разбирает файловый обмен через общую папку и ночные пакетные задания не как ставку на блокировки ОС, а как проектирование протокола передачи. Право на обработку захватывают атомарным claim до чтения; файл в процессе создания держат под temp-именем, после close публикуют rename на final-имя; завершение не угадывают по размеру или метке времени, а явно фиксируют done и manifest. Если используют lock file, делают его lease с ownerId и expiresAt, чтобы быть готовыми к stale lock; учитывают, что byte-range lock Windows не действует на файл, отображённый в память, а advisory lock бесполезен против стороны, которая не соблюдает договорённость; в итоге на практике силён дизайн, который добивает остаток идемпотентностью: повторная обработка того же ввода не ломает результат.
flowchart LR
accTitle: Карта знаний: исключающий доступ при файловом обмене
accDescr: Схема, которая показывает, как протокол передачи сочетает атомарный claim, публикацию temp→rename, done/manifest, lock file в форме lease и idempotency, и каким антипаттернам это противостоит, чтобы предотвратить двойную обработку и чтение файла во время записи
file_handoff_protocol["протокол передачи файлов"]
atomic_claim["атомарный claim"]
temp_then_rename_publish["публикация через temp -> close -> rename/replace"]
done_manifest_file["файл done/manifest"]
lock_file_lease["lock-файл с арендой (lease)"]
idempotent_processing["обработка с расчётом на идемпотентность"]
os_file_lock["файловая блокировка ОС"]
atomic_creation["атомарное создание (CreateNew / O_CREAT|O_EXCL)"]
duplicate_processing["двойная обработка (двойной учёт, повторная отправка, lost update)"]
exists_then_create_antipattern["двухшаговая проверка Exists→Create"]
direct_final_write_antipattern["прямая запись в конечное имя файла"]
partial_write_read["чтение файла во время записи"]
size_stability_completion_check["проверка завершения по стабилизации размера файла"]
shared_file_mutual_update_antipattern["антипаттерн взаимного обновления общего файла"]
stale_lock["stale lock"]
byte_range_lock["byte-range lock (блокировка диапазона)"]
heterogeneous_system_integration["интеграция разнородных систем"]
advisory_lock["advisory lock"]
cross_volume_rename_fallback["fallback rename между томами (copy + delete)"]
smb_share_file_handoff["обмен файлами через общую папку (SMB)"]
rename_fails_on_open_handle["сбой rename при открытом файле"]
file_timestamp_unreliability["ненадёжность меток времени файла"]
periodic_directory_listing["периодический обход каталога"]
change_notification_loss["потеря уведомлений об изменениях"]
file_handoff_protocol -->|"использует"| atomic_claim
file_handoff_protocol -->|"использует"| temp_then_rename_publish
file_handoff_protocol -->|"использует"| done_manifest_file
file_handoff_protocol -->|"использует"| lock_file_lease
file_handoff_protocol -->|"использует"| idempotent_processing
file_handoff_protocol -.->|"использует"| os_file_lock
atomic_claim -.->|"использует"| atomic_creation
atomic_claim -->|"предотвращает"| duplicate_processing
exists_then_create_antipattern -->|"может вызвать"| duplicate_processing
atomic_claim -->|"рекомендуется для"| exists_then_create_antipattern
atomic_creation -->|"рекомендуется для"| exists_then_create_antipattern
direct_final_write_antipattern -->|"может вызвать"| partial_write_read
temp_then_rename_publish -->|"предотвращает"| partial_write_read
size_stability_completion_check -.->|"может вызвать"| partial_write_read
done_manifest_file -->|"рекомендуется для"| size_stability_completion_check
shared_file_mutual_update_antipattern -->|"может вызвать"| duplicate_processing
idempotent_processing -->|"рекомендуется для"| duplicate_processing
lock_file_lease -->|"рекомендуется для"| stale_lock
lock_file_lease -->|"требует"| atomic_creation
os_file_lock -->|"использует"| byte_range_lock
os_file_lock -.->|"снижает"| duplicate_processing
os_file_lock -->|"не рекомендуется"| heterogeneous_system_integration
advisory_lock -.->|"не рекомендуется"| heterogeneous_system_integration
temp_then_rename_publish -.->|"несовместимо с"| cross_volume_rename_fallback
smb_share_file_handoff -.->|"может вызвать"| cross_volume_rename_fallback
smb_share_file_handoff -.->|"может вызвать"| rename_fails_on_open_handle
done_manifest_file -->|"рекомендуется для"| file_timestamp_unreliability
periodic_directory_listing -->|"рекомендуется для"| change_notification_loss
smb_share_file_handoff -.->|"может вызвать"| change_notification_loss
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 29, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
Содержание
- Сначала вывод (коротко)
- Типичные гонки при интеграции через файлы (схемы)
- 2.1. Читают файл, который ещё пишется
- 2.2. Несколько воркеров одновременно берут один файл
- 2.3. Из-за stale lock останавливаются все
- Антипаттерны
- 3.1. Двухшаговая проверка
Exists -> Create - 3.2. Писать сразу в final-имя
- 3.3. Считать файл готовым, когда размер перестал расти
- 3.4. Общий файл обновляют все подряд
- 3.5. Считать API блокировок универсальным
- 3.1. Двухшаговая проверка
- Практические рекомендации
- 4.1. Публиковать по схеме
temp -> close -> rename / replace - 4.2. Явно фиксировать готовность через
done/ manifest - 4.3. Приёмник атомарно берёт claim
- 4.4. Если опираетесь на lock-файл, делайте его lease
- 4.5. Заранее исходить из идемпотентности
- 4.1. Публиковать по схеме
- Псевдокод (фрагменты)
- Как выбрать подход
- Итог
- Источники
В интеграции через файлы ломается не столько сам код, сколько «договорённость о передаче». Юнит-тесты проходят, а на боевой общей папке или в ночном batch иногда падает — и воспроизвести трудно. Такое бывает довольно часто.
Чаще всего виноваты не API файлового ввода-вывода, а три размытых места:
- когда уже можно читать;
- кто владеет правом обработки;
- как восстанавливаться после сбоя.
В этой статье исключительный доступ при интеграции через файлы не сводится к блокировкам ОС: его разбирают как протокол передачи.
Код из статьи опубликован на GitHub как полный набор, который собирается и запускается: библиотека, демонстрация гонки claim между двумя воркерами и перехвата lease, юнит-тесты на гонки, порчу данных и stale lock.
file-integration-locking-best-practices-komurasoft-style - komurasoft-blog-samples (GitHub)
1. Сначала вывод (коротко)
- Главное в интеграции через файлы — к моменту, когда видно final-имя, состояние уже должно быть «можно читать»
- Состояния «собирается / опубликовано / в обработке / обработано» нужно выражать именами файлов и каталогами
- Если воркеров несколько, claim берут атомарно, до чтения
- Lock-файл и блокировки ОС — вспомогательные средства; в конце всё ловит идемпотентность
Иначе говоря, в интеграции через файлы суть не столько во взаимном исключении, сколько в протоколе передачи. Одним вызовом функции блокировки дело не заканчивается.
2. Типичные гонки при интеграции через файлы (схемы)
2.1. Читают файл, который ещё пишется
Если писать сразу в final-имя, случается именно это. В JSON не будет закрывающей скобки, в CSV не хватит строк, ZIP просто окажется повреждён.
sequenceDiagram
participant 送信 as Отправитель
participant 共有 as Общая папка
participant 受信 as Получатель
送信->>共有: Создаёт orders.csv под final-именем
送信->>共有: Пишет строки с 1-й по 5000-ю
受信->>共有: Обнаруживает orders.csv
受信->>共有: Сразу начинает читать
Note over 受信: Файл ещё не готов
送信->>共有: Дописывает остаток
Note over 受信: Не хватает строк / ошибка разбора / обработана только часть
2.2. Несколько воркеров одновременно берут один файл
Схема «посмотрели список, открыли, если ещё не обработан» позволяет двум воркерам схватить один и тот же файл. Отсюда начинаются двойной учёт и повторная отправка.
sequenceDiagram
participant W1 as Воркер 1
participant W2 as Воркер 2
participant Dir as incoming
W1->>Dir: Находит a.csv
W2->>Dir: Находит a.csv
W1->>Dir: Начинает читать
W2->>Dir: Начинает читать
Note over W1,W2: Один и тот же вход обрабатывается дважды
2.3. Из-за stale lock останавливаются все
Схема, в которой просто кладут lock-файл, легко застревает при аварийном завершении. Если непонятно, чей это lock, жив ли владелец и до какого момента он действует, следующие будут ждать бесконечно.
sequenceDiagram
participant A as Воркер A
participant Lock as lock-файл
participant B as Воркер B
A->>Lock: Создаёт lock
Note over A: Здесь аварийно завершается
B->>Lock: Проверяет, что lock есть
B->>Lock: Откладывает старт обработки
B->>Lock: Ждёт дальше
Note over B,Lock: Нельзя понять, stale это или нет, — все стоят
3. Антипаттерны
3.1. Двухшаговая проверка Exists -> Create
Проблема в том, что «проверить» и «занять» — разные операции. Между ними может вклиниться другой процесс, поэтому взаимного исключения нет.
sequenceDiagram
participant A as Процесс A
participant B as Процесс B
participant FS as Файловая система
A->>FS: Проверяет, что lock нет
B->>FS: Проверяет, что lock нет
FS-->>A: Нет
FS-->>B: Нет
A->>FS: Создаёт lock
B->>FS: Создаёт lock
Note over A,B: Оба идут дальше
Типичный плохой пример выглядит так.
if (!File.Exists(lockPath))
{
File.WriteAllText(lockPath, Environment.ProcessId.ToString());
ProcessFile();
}
Нужно, чтобы «создать, если нет» было одной операцией.
В .NET это семейство FileMode.CreateNew, в POSIX — атомарное создание вроде O_CREAT | O_EXCL.
3.2. Писать сразу в final-имя
Если приёмник понимает появление имени как «уже можно читать», запись сразу в final-имя — уже проигрыш. Базовое правило: быть видимым и быть готовым к чтению — не одно и то же.
flowchart LR
A[Видно final-имя] --> B[Приёмник обнаруживает файл]
B --> C[Отправитель ещё пишет]
C --> D[Читают неполные данные]
using var writer = OpenForWrite(finalPath); // Здесь finalPath уже виден
foreach (var row in rows)
{
writer.WriteLine(row);
}
Так вы сами вызываете аварию из раздела 2.1.
3.3. Считать файл готовым, когда размер перестал расти
На вид удобно, на деле довольно опасно. Копирование по сети, пауза на стороне отправителя, буферизация и повторные попытки легко дают ложную «стабильность».
sequenceDiagram
participant 送信 as Отправитель
participant 共有 as Общая папка
participant 受信 as Получатель
送信->>共有: Начинает копировать data.zip
送信->>共有: На середине делает паузу
受信->>共有: Размер 10 секунд не меняется
Note over 受信: Ошибочно считает файл готовым
受信->>共有: Начинает читать
送信->>共有: Возобновляет копирование
if (currentLength == lastLength && stableSeconds >= 10)
{
return Ready;
}
Если готовность угадывают, общие папки и большие файлы рано или поздно подводят. Надёжнее явно сообщать о завершении через manifest или done-файл.
3.4. Общий файл обновляют все подряд
Схема, в которой все читают и правят один status.csv или counter.json, обычно заканчивается тем, что побеждает последний записавший.
Как только файловый обмен начинают использовать как упрощённую БД, боль начинается именно здесь.
sequenceDiagram
participant A as Batch A
participant B as Batch B
participant F as status.csv
A->>F: Читает v1
B->>F: Читает v1
A->>F: Пишет v2-A
B->>F: Пишет v2-B
Note over F: Обновление от A пропадает
Можно уйти в append-only, но смысл этой схемы плавает в зависимости от файловой системы и размещения. Если общее обновление действительно нужно, файловым обменом здесь лучше не насиловать задачу.
3.5. Считать API блокировок универсальным
API блокировок важны, но работают только когда все участники играют по одним правилам. При интеграции разнородных систем на них лучше не опираться слишком сильно.
Дополнительно:
- Linux
flock— advisory lock, поэтому программу, которая игнорирует соглашение, написать легко - Windows byte-range lock не действует на memory-mapped файлы
- Иными словами, на одни блокировки ОС не стоит вешать ещё и уведомление о готовности, и владение
Второй пункт прямо зафиксирован в спецификации Windows. В Microsoft Learn, Locking and Unlocking Byte Ranges in Files, сразу после фразы, что доступ другого процесса к уже заблокированному диапазону всегда завершается ошибкой (то есть диапазонная блокировка Windows не advisory, а принудительная), стоит предупреждение: для memory-mapped файлов byte-range lock игнорируется. Если другая сторона трогает тот же файл через CreateFileMapping, ваша блокировка её не остановит.
Диапазонную блокировку в .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-mapped файл она не действует, и нет гарантии, что другая система вообще посмотрит на эту метку. Поэтому основу задаёт протокол передачи из главы 4.
4. Практические рекомендации
Сначала — соответствие антипаттернам из главы 3. Если узнаёте один из них, можно сразу перейти к парному разделу.
| Антипаттерн | Что происходит | Что делать |
|---|---|---|
3.1. Двухшаговая проверка Exists -> Create |
В щель между проверкой и захватом вклинивается другой процесс, и оба идут дальше | 4.3 брать claim атомарно (rename или FileMode.CreateNew) |
| 3.2. Писать сразу в final-имя | Приёмник читает файл, который ещё пишется | 4.1 публиковать по схеме temp -> close -> rename / replace |
| 3.3. Считать файл готовым, когда размер перестал расти | Паузу копирования принимают за завершение | 4.2 явно фиксировать готовность через done / manifest |
| 3.4. Общий файл обновляют все подряд | Поздняя запись затирает более раннюю, обновление пропадает | 4.3 сузить писателя до одного, 4.5 поглотить повторную обработку. Если этого мало — критерий отступления в главе 6 |
| 3.5. Считать API блокировок универсальным | Схему ломает сторона, которая не соблюдает соглашение, или доступ через memory-mapped файл | 4.4 lock-файл как lease и 4.5 идемпотентность как последняя линия |
4.1. Публиковать по схеме temp -> close -> rename / replace
Это проверенный путь. Пока файл собирается, он живёт под temp-именем; после close его переключают на final-имя. Приёмник смотрит только на final-имя.
flowchart LR
A[Создать уникальное temp-имя] --> B[Записать в temp всё содержимое]
B --> C[flush / close]
C --> D[rename / replace на final-имя в том же каталоге]
D --> E[Приёмник следит только за final-именем]
На что смотреть:
- temp и final лежат в одном каталоге, как минимум на одном томе / в одной файловой системе
- в Windows / .NET можно рассмотреть семейство
File.Replace - договорённость: как только видно final-имя, содержимое уже готово
Если temp положить на другой диск, rename фактически превращается в копирование, либо Replace падает.
Предпосылка скучная, но очень важная.
Через общую папку (SMB) плавают ещё четыре пункта. Именно это — основная сцена статьи, поэтому вынесем отдельно.
- rename внутри одного каталога одной share выполняется на стороне сервера. Свойство «промежуточное имя не видно» при этом сохраняется. Если же переходить с
\\server\shareAна\\server\shareB, это уже другой том: WindowsMoveFileExприMOVEFILE_COPY_ALLOWEDподменяет перемещение копированием и удалением. Атомарности нет, промежуточное состояние видно. Держать temp и final,incomingиprocessingвнутри одной share на общих папках ещё важнее, чем локально - rename падает, даже если файл просто кем-то открыт. На общую папку заходят антивирус, поисковый индексер, клиенты с других площадок — стороны, о которых вы можете не знать. Rename при публикации и claim лучше считать обычной веткой, а не аварией: короткая пауза и повтор
- Метки времени — плохой критерий. В Microsoft Learn, File Times, гарантировано только одно: метка корректно отражается в момент закрытия handle, которым меняли файл. Время последнего изменения во время записи полностью не обновляется, пока не закрыты все handle на запись. Гранулярность зависит от файловой системы: на FAT время изменения идёт с шагом 2 секунды, на NTFS время последнего доступа может отставать до часа. Через SMB метку ставит часы сервера, поэтому при расхождении часов клиента и сервера съезжает и правило «обработать через N минут после изменения». Именно поэтому готовность смотрят не по времени и размеру, а по
done/ manifest из 4.2 - Уведомления об изменениях тоже теряются. Не опирайтесь только на события: стабильнее сочетать их с периодическим перечислением каталога. Это разобрано в практическом руководстве по FileSystemWatcher — пропуски и дубли
4.2. Явно фиксировать готовность через done / manifest
Если помимо самих данных отдельно сказать, «что именно готово», приёмник становится устойчивее. Особенно это помогает при интеграции разнородных систем.
flowchart TD
A[Собрать data.tmp] --> B[Опубликовать как data.csv]
B --> C[Создать data.done / manifest.json]
C --> D[Приёмник обнаруживает done / manifest]
D --> E[Проверяет имя файла, размер и хеш]
В manifest обычно кладут примерно такие поля.
- имя целевого файла
- размер
- хеш
- число записей
- ID интеграции / ключ идемпотентности
- время генерации
Порядок тоже важен.
Если положить done раньше публикации самих данных, это уже не уведомление о готовности, а предупреждение о будущем сбое.
4.3. Приёмник атомарно берёт claim
Если за одним incoming следят несколько воркеров, понятнее всего «переложить файл к себе до чтения».
Обрабатывает файл только тот воркер, у которого прошёл rename из incoming в processing/<worker>/.
sequenceDiagram
participant W1 as Воркер 1
participant W2 as Воркер 2
participant IN as incoming
participant PR as processing
W1->>IN: Находит a.csv
W2->>IN: Находит a.csv
W1->>PR: Делает rename a.csv
W2->>PR: Делает rename a.csv
Note over W1,W2: Владение получает только тот, кто успел первым
На практике каталоги тоже лучше развести — так проще отслеживать состояние.
flowchart LR
T[temp] -->|publish| I[incoming]
I -->|claim| P[processing]
P -->|успех| A[archive]
P -->|ошибка| E[error]
И rename для claim предполагается на той же файловой системе.
4.4. Если опираетесь на lock-файл, делайте его lease
Lock-файл — не пустышка, а запись о владении со сроком действия. Lock, про владельца которого ничего не известно, позже обязательно станет предметом спора.
flowchart TD
L[lock.json] --> A[ownerId]
L --> B[host]
L --> C[pid]
L --> D[acquiredAt]
L --> E[expiresAt]
L --> F[heartbeatAt]
На что смотреть:
- создание атомарно
- остановку обновлений используют как признак stale
- удаляет, как правило, только создатель
- восстановление продумывают заранее, исходя из того, что блокировку могут не снять
Lock-файл — всего лишь маркер для согласования. Пытаться одним таким файлом гарантировать полную согласованность обычно оказывается тяжело.
4.5. Заранее исходить из идемпотентности
Исключительный доступ важен, но в реальной эксплуатации «иногда приходит дважды» и «перезапуск на середине» до нуля не свести. В конце выручает схема, которая не ломается, даже если тот же вход обработать ещё раз.
flowchart LR
A[Вход + ключ идемпотентности] --> B{Уже обработано?}
B -- да --> C[Считать успехом без повторного выполнения]
B -- нет --> D[Выполнить обработку]
D --> E[Записать в журнал обработанных]
Например, каждому принятому файлу дают ID интеграции и пишут его в журнал обработанных. Если результат не задваивается даже после разового нарушения исключительного доступа, жить с системой гораздо проще.
5. Псевдокод (фрагменты)
MakeTempPathSameDirectory и TryClaimBundleByRename ниже — вымышленные имена функций, чтобы показать порядок. Рабочая реализация есть в наборе примеров, на который ссылается начало статьи.
5.1. Типичный проигрышный паттерн
var lockPath = finalPath + ".lock";
if (!File.Exists(lockPath))
{
File.WriteAllText(lockPath, "");
using var writer = OpenForWrite(finalPath); // Пишем сразу в final-имя
WritePayload(writer);
File.Delete(lockPath);
}
Проблем три.
ExistsиWriteAllText— разные операцииfinalPathстановится виден ещё во время записи- при аварийном завершении
lockостаётся
5.2. Пример в правильную сторону (грубо)
var tempPath = MakeTempPathSameDirectory(finalPath);
WritePayload(tempPath);
FlushAndClose(tempPath);
PublishByRenameOrReplace(tempPath, finalPath); // Предполагается одна ФС / один том
PublishDoneFile(finalPath + ".done", new
{
FileName = Path.GetFileName(finalPath),
Size = GetFileSize(finalPath),
Hash = ComputeHash(finalPath),
IdempotencyKey = integrationId
});
if (!TryClaimBundleByRename(baseName, incomingDir, processingDir))
{
return; // Другой воркер захватил файл раньше
}
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);
Здесь важнее не детали реализации, а порядок. Если не смешивать «записать», «опубликовать», «взять владение» и «отметить обработанным», ломается заметно реже.
6. Как выбрать подход
- Один writer / один reader / один хост — уже одной схемы
temp -> renameчасто хватает для приличной устойчивости - Несколько consumer — добавьте claim-rename
incoming -> processing - Разнородные системы, NAS, общие папки — безопаснее довести до manifest / done и идемпотентности
- Несколько writer хотят обновлять одно логическое состояние — не выжимайте файловый обмен сверх меры, рассмотрите БД или очередь
- Блокировки ОС полезны внутри одной группы приложений с одними допущениями, но протокол передачи они не заменяют
Последний пункт — ещё и критерий отступления. Есть задачи, которые файлами решать действительно тяжело.
7. Итог
Исключительный доступ при интеграции через файлы — это не вызов функции блокировки, а договорённость о переходах состояния. В этом суть статьи. Состояния «собирается / опубликовано / в обработке / обработано» выражают именами и каталогами; двухшаговой проверки Exists -> Create, записи сразу в final-имя, ожидания «размер замер», взаимного обновления общего файла и слепой веры в API блокировок избегают. Если поверх этого сочетать temp -> close -> rename / replace, done / manifest, claim-rename, lease и идемпотентность, аварий на общих папках становится заметно меньше.
Приём простой: не отождествлять «файл технически читается» с «его уже можно читать». Одно это разделение сильно сокращает сбои, которые проявляются только ночью.
8. Источники
- Полный набор примеров кода для этой статьи (библиотека, демонстрация, юнит-тесты) - komurasoft-blog-samples (GitHub)
- LockFileEx function (Win32)
- Locking and Unlocking Byte Ranges in Files (Win32)
- Moving and Replacing Files (Win32)
- MoveFileEx function (Win32)
- File Times (Win32)
- FileStream.Lock Method (.NET)
- File.Replace Method (.NET)
- rename — POSIX
- open — POSIX (
O_CREAT | O_EXCL) - flock(2) — Linux manual page
- open(2) — Linux manual page
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Почему в Windows стоит предпочитать ожидание события, а не Sleep(1)
В Windows точность короткого timed wait зависит от гранулярности системных часов и планирования. Если ждать нужно появление работы, завер...
Минимальный чек-лист безопасности при разработке Windows-приложений
Чек-лист базовых мер безопасности для бизнес-приложений на WPF / WinForms / WinUI / C++ / C#: права, подпись кода, обновления, секреты, H...
Зачем использовать .NET Generic Host и BackgroundService в десктопных приложениях
Как с помощью Generic Host и BackgroundService собрать запуск, периодическую работу, завершение, логи, конфигурацию и DI в Windows-инстру...
Практическое руководство по FileSystemWatcher — как не терять уведомления и что делать с дублями
Как пользоваться FileSystemWatcher и где он подводит: потерянные уведомления, дубли, ловушки определения готовности, повторное сканирован...
Практическое руководство: soft real-time на обычной Windows
Как стабилизировать периодическую обработку и низкую задержку на обычной Windows: разрешение таймера, приоритеты, питание и способы измер...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
В Windows-приложениях с обменом через общие папки и ночными batch-заданиями качество исключительного доступа сразу видно в качестве реализации.
Технические консультации и ревью дизайна
Если сначала нужно развести роли блокировок, атомарного claim и идемпотентности, это можно оформить как техническую консультацию и ревью проекта.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Достаточно ли API блокировок для исключительного доступа при интеграции через файлы?
- Часто нет. Linux flock — advisory lock: программа, которая игнорирует соглашение, спокойно читает и пишет. Windows byte-range lock не действует на memory-mapped файлы. Блокировки ОС полезны внутри одной группы приложений с одними допущениями, но это вспомогательное средство. Основу стоит строить как протокол передачи: temp -> rename, done/manifest, атомарный claim и идемпотентность.
- Как не дать прочитать файл, который ещё пишется?
- Проверенный способ — публиковать по схеме temp -> close -> rename/replace. Пока файл собирается, он живёт под temp-именем; после close его переключают на final-имя в том же каталоге, а приёмник смотрит только на final-имя. Исходная предпосылка: temp и final лежат в одном каталоге, как минимум на одном томе / в одной файловой системе. Договорённость: как только видно final-имя, содержимое уже готово.
- Как не дать нескольким воркерам обработать один и тот же файл?
- Claim нужно брать атомарно, до чтения. Обрабатывает файл только тот воркер, у которого прошёл rename из incoming в processing/<worker>/. Двухшаговая проверка Exists -> Create взаимного исключения не даёт: «проверить» и «занять» — разные операции, и между ними может вклиниться другой процесс. Для атомарного создания берите семейство FileMode.CreateNew в .NET или O_CREAT | O_EXCL в POSIX.
- На что смотреть, если используете lock-файл?
- Это не пустой файл, а lease — запись о владении со сроком действия: ownerId, host, pid, acquiredAt, expiresAt, heartbeatAt. Создавайте атомарно, остановку обновлений используйте как признак stale, удаляйте, как правило, только создатель, и заранее решите, как восстанавливаться, если блокировку не сняли. Одним lock-файлом полную согласованность обычно не закрывают: на практике сильнее работает схема, где в конце всё ловит идемпотентность.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.