В практике разработки на платформе 1С:Предприятие 8.3 часто встречается архитектурная задача: одно регламентное задание должно скачать файл с внешнего ресурса (например, FTP-сервера), а второе — обработать его и загрузить данные в информационную базу. Однако здесь кроется классическая ловушка «состояния гонки» (race condition). Если второе задание запускается в тот момент, когда первое еще не закончило процесс копирования, оно может попытаться прочитать недозаписанный («битый») файл. В результате возникают ошибки формата XML, создаются файлы размером 0 Кб, или данные загружаются лишь частично.
В данной статье мы подробно разберем несколько стратегий решения этой проблемы: от простых «семафоров» до профессиональных методов атомарного переименования файлов.
Разберем ситуацию на примере. Регламентное задание №1 вызывает метод ФТП.Получить(). Операционная система сразу создает целевой файл в локальном каталоге, но наполнение его данными происходит постепенно. В это же время регламентное задание №2 проверяет наличие файла в папке. Видя, что файл существует, оно мгновенно начинает его чтение. Поскольку файл еще заблокирован процессом скачивания или просто не заполнен до конца, 1С выдает ошибку доступа или считывает пустую структуру. Чтобы этого избежать, нам нужно гарантировать, что обработка начнется только после полного освобождения файла первым процессом.
Это один из самых наглядных способов. Суть метода заключается в создании специального пустого файла-индикатора (флага), который сигнализирует о том, что процесс загрузки еще идет. Рассмотрим алгоритм действий:
ФТП.Получить() создает в каталоге пустой файл с уникальным именем (например, busy.lock).busy.lock. Если он есть — задание завершается, не начиная обработку.Для реализации проверки существования мы будем использовать объект Файл и его метод Существует(). Проанализируем пример логики для первого задания:
// Регламентное задание 1: Скачивание
ПутьКЛоку = КаталогВременныхФайлов() + "ftp_download.lock";
ФайлЛока = Новый Файл(ПутьКЛоку);
// Создаем пустой файл-семафор
ТекстЛока = Новый ЗаписьТекста(ПутьКЛоку);
ТекстЛока.Закрыть();
Попытка
ФТП.Получить(ИмяФайлаНаСервере, ПутьЛокальный);
// После успешного получения удаляем семафор
УдалитьФайлы(ПутьКЛоку);
Исключение
// В случае ошибки тоже важно удалить семафор, чтобы не заблокировать очередь
УдалитьФайлы(ПутьКЛоку);
ЗаписатьЛогСобытия("Ошибка скачивания: " + ОписаниеОшибки());
КонецПопытки;
Более надежный способ на уровне операционной системы — попытка открыть файл в режиме, запрещающем совместный доступ. Если файл еще скачивается методом ФТП.Получить(), ОС не позволит другому процессу открыть его монопольно. Мы можем использовать объект ФайловыйПоток.
Рассмотрим код, который может использовать второе задание для проверки доступности файла:
// Регламентное задание 2: Обработка
ИмяФайла = "data.xml";
ПутьКФайлу = "C:\FTP_Exchange\" + ИмяФайла;
Попытка
// Пытаемся открыть файл исключительно для себя
Поток = Новый ФайловыйПоток(ПутьКФайлу, РежимОткрытияФайла.Открыть, ДоступКФайлу.Чтение, ВозможностьСовместногоДоступа.Нет);
// Если код прошел сюда, значит файл не занят другим процессом
Поток.Закрыть();
Исключение
// Файл еще занят скачиванием или другим процессом
Возврат; // Выходим и ждем следующего запуска задания
КонецПопытки;
// Начинаем безопасную обработку файла
ОбработатьФайл(ПутьКФайлу);
Этот метод считается «золотым стандартом» в системном программировании и интеграциях. Его суть в том, что файл становится «видимым» для обработчика только в тот момент, когда он уже полностью готов. Выясним, как это работает по шагам:
order.xml.tmp.ФТП.Получить() работает с этим временным расширением. Второе задание настроено игнорировать файлы с расширением .tmp.ПереместитьФайл(), чтобы сменить имя на order.xml.Важно понимать, что операция переименования (перемещения) внутри одного логического диска в файловых системах (NTFS/Linux) происходит мгновенно (атомарно). Файл либо имеет старое имя, либо новое, промежуточного состояния «полупереименован» не существует.
Посмотрим на пример кода:
// В первом задании
ИмяФайлаИтоговое = "order.xml";
ИмяФайлаВременное = ИмяФайлаИтоговое + ".tmp";
ФТП.Получить(ИмяФайлаИтоговое, ПутьККаталогу + ИмяФайлаВременное);
// Переименовываем только после успешного завершения скачивания
ПереместитьФайл(ПутьККаталогу + ИмяФайлаВременное, ПутьККаталогу + ИмяФайлаИтоговое);
Аналогично методу с переименованием, мы можем использовать разные папки. Проанализируем этот подход:
/Temp/ и /Ready/./Temp/./Ready/./Ready/.Это позволяет избежать ситуации, когда в основном рабочем каталоге скапливаются недокачанные файлы из-за обрывов связи или ошибок сервера.
Зачастую разработчики избыточно усложняют систему, создавая два разных регламентных задания там, где логически выполняется один процесс. Если обработка файла всегда должна следовать за его скачиванием, самым эффективным решением будет объединение их в одно регламентное задание.
Разберем пример структуры такого задания:
Процедура ВыполнитьОбменСFTP() Экспорт
Если СкачатьФайлССервера() Тогда
// Если скачивание прошло успешно и файл гарантированно закрыт
ОбработатьСкачанныйФайл();
Иначе
ЗаписатьЛогСобытия("Скачивание не удалось, обработка пропущена.");
КонецЕсли;
КонецПроцедуры
Этот подход полностью снимает проблему блокировок, так как процедуры выполняются последовательно в рамках одного сеанса фонового задания.
В некоторых случаях, когда мы не можем управлять процессом записи (например, файл пишет сторонняя программа), можно использовать проверку стабильности размера. Мы считываем размер файла через Файл.Размер(), делаем паузу в 1-2 секунды и считываем снова. Если размер не изменился, велика вероятность, что запись завершена. Однако этот метод менее надежен, чем блокировка или переименование, так как при медленном интернете размер может не меняться несколько секунд, хотя закачка еще идет.
Резюмируем: для стабильной работы обмена в 1С 8.3 рекомендуется использовать метод атомарного переименования (Solution 3) или объединение задач в одну цепочку (Solution 5). Это гарантирует целостность данных и отсутствие ошибок доступа к файлам.