При разработке интеграций, обменов с веб-сервисами или выполнении параллельных фоновых заданий часто возникает необходимость вести непрерывный текстовый журнал событий. Однако при попытке нескольких сеансов одновременно писать в один и тот же файл разработчики сталкиваются с системной ошибкой совместного доступа.
Проанализируем причину такого поведения платформы 1С и рассмотрим несколько надежных подходов к организации многопоточного логирования.
Выясним причину конфликта: объект платформы ЗаписьТекста открывает дескриптор файла исключительно в монопольном режиме. Даже если в конструкторе указан параметр Дописывать = Истина, среда исполнения не передает флаг разделяемого доступа на запись (FILE_SHARE_WRITE). Как только один сеанс открыл файл на запись, второй сеанс при попытке инициализации ЗаписьТекста сразу получает исключение блокировки операционной системы.
Постоянные попытки открывать файл в цикле с перехватом исключений не рекомендуются, так как они создают паразитную нагрузку на процессор и диск, а также приводят к деградации производительности при росте числа потоков.
Разберем наиболее стабильный и архитектурно правильный подход: каждый параллельный процесс ведет собственный отдельный лог-файл. Чтобы имена файлов не конфликтовали, к имени лога добавляют уникальный идентификатор сеанса, пользователя или задания.
Посмотрим на пример универсальной функции, которая формирует путь к персональному файлу лога для текущего контекста исполнения:
Функция ПолучитьИмяФайлаЛога(БазовыйКаталог, ПрефиксЛога)
ИдентификаторПотока = "";
// Проверяем, выполняется ли код в фоновом задании
ТекущееЗадание = ФоновыеЗадания.ТекущееФоновоеЗадание();
Если ТекущееЗадание <> Неопределено Тогда
ИдентификаторПотока = "job_" + СтрЗаменить(Строка(ТекущееЗадание.УникальныйИдентификатор), "-", "");
Иначе
// Для обычных пользовательских или регламентных сеансов
СеансИнфо = ПолучитьТекущийСеансИнформационнойБазы();
ИдентификаторПотока = "session_" + Строка(СеансИнфо.НомерСеанса);
КонецЕсли;
ТекущаяДатаСтрока = Формат(ТекущаяДатаСеанса(), "ДФ=yyyyMMdd");
ИмяФайла = СтрШаблон("%1_%2_%3.log", ПрефиксЛога, ТекущаяДатаСтрока, ИдентификаторПотока);
Возврат ОбъединитьПути(БазовыйКаталог, ИмяФайла);
КонецФункции
Разберем запись строки в полученный файл с гарантированным закрытием ресурса:
Процедура ЗаписатьВЛог(БазовыйКаталог, ПрефиксЛога, ТекстСообщения)
ИмяФайлаЛога = ПолучитьИмяФайлаЛога(БазовыйКаталог, ПрефиксЛога);
Запись = Новый ЗаписьТекста(ИмяФайлаЛога, КодировкаТекста.UTF8, Символы.ПС, Истина);
СтрокаЛога = СтрШаблон("[%1] %2", Формат(ТекущаяДатаСеанса(), "ДФ='yyyy-MM-dd HH:mm:ss'"), ТекстСообщения);
Запись.ЗаписатьСтроку(СтрокаЛога);
Запись.Закрыть();
КонецПроцедуры
Преимущества подхода: полное отсутствие взаимных блокировок, максимальная скорость записи и простота последующей очистки или архивации устаревших логов по дате и сеансам.
Если в конечном итоге требуется один общий файл, эффективным решением будет разделение этапов записи и агрегации:
Новый УникальныйИдентификатор) в специальный каталог очереди.Посмотрим на реализацию процедуры агрегации логов:
Процедура СобратьЛогиВОдинФайл(КаталогОчереди, ИтоговыйФайлЛога)
НайденныеФайлы = НайтиФайлы(КаталогОчереди, "*.tmp");
Если НайденныеФайлы.Количество() = 0 Тогда
Возврат;
КонецЕсли;
ИтоговаяЗапись = Новый ЗаписьТекста(ИтоговыйФайлЛога, КодировкаТекста.UTF8, Символы.ПС, Истина);
Для Каждого ФайлФрагмента Из НайденныеФайлы Цикл
Попытка
ТекстФрагмента = Новый ЧтениеТекста(ФайлФрагмента.ПолноеИмя, КодировкаТекста.UTF8);
Содержимое = ТекстФрагмента.Прочитать();
ТекстФрагмента.Закрыть();
Если ЗначениеЗаполнено(Содержимое) Тогда
ИтоговаяЗапись.Записать(Содержимое);
КонецЕсли;
УдалитьФайлы(ФайлФрагмента.ПолноеИмя);
Исключение
// Если файл еще дописывается сеансом, пропускаем его до следующего цикла
КонецПопытки;
КонецЦикла;
ИтоговаяЗапись.Закрыть();
КонецПроцедуры
Если архитектура строго требует писать именно в один файл без промежуточных таблиц и очередей, можно переложить задачу дозаписи на уровень операционной системы. Системные вызовы при использовании оператора конвейера >> открывают файл, производят атомарный append и мгновенно освобождают дескриптор.
Посмотрим на пример вызова через командную строку Windows:
Процедура ДописатьСтрокуОС(ПутьКФайлуЛога, ТекстСтроки)
Команда = СтрШаблон("cmd.exe /c chcp 65001 > nul & echo [%1] %2 >> ""%3""",
Формат(ТекущаяДатаСеанса(), "ДФ='yyyy-MM-dd HH:mm:ss'"),
ТекстСтроки,
ПутьКФайлуЛога);
ЗапуститьПриложение(Команда, , Истина);
КонецПроцедуры
Важно: вызов внешних процессов операционной системы создает накладные расходы на запуск процессов cmd.exe или bash, поэтому данный метод подходит только для редких событий (например, фиксация только ошибок обмена), но не для высоконагруженных потоков данных.
Рассмотрим альтернативные платформенные способы фиксации событий, которые избавят от необходимости работать с файловой системой вручную:
ЗаписьЖурналаРегистрации оптимизирован под многопоточную работу кластера серверов. Чтобы журнал не разрастался, в него рекомендуется писать только критические события и ошибки, а штатную очистку настроить регламентом.logcfg.xml позволяет собирать системные сетевые события и HTTP-вызовы на уровне самого сервера 1С без написания программного кода логирования.