При работе с фоновыми заданиями в 1С часто возникает необходимость логирования процессов. Самый простой и очевидный путь — использовать объект ЗаписьТекста для записи логов в текстовый файл на диске. Однако при масштабировании системы разработчики неизбежно сталкиваются с проблемой: платформа 1С открывает текстовые файлы на запись в монопольном режиме. Когда несколько параллельных фоновых заданий пытаются одновременно произвести запись в один и тот же файл, возникает системная ошибка совместного доступа (файл занят другим процессом).
В этой статье мы подробно разберем несколько эффективных способов решения этой проблемы: от синхронизации потоков средствами самой платформы 1С до архитектурных изменений процесса логирования.
Если вам критически важно писать именно в один файл в режиме реального времени, мы можем синхронизировать работу фоновых заданий. Для этого используется концепция «критической секции», реализованная с помощью транзакционных блокировок 1С. Мы заставим фоновые задания ждать своей очереди на запись.
Разберем пример кода, который реализует этот подход. Мы будем использовать объект БлокировкаДанных, наложенный на любой неизменяемый или малоиспользуемый объект базы данных (например, пустую ссылку определенного справочника):
Процедура ЗаписатьВЛогСБлокировкой(ИмяФайла, ТекстСообщения) Экспорт
НачатьТранзакцию();
Попытка
// Создаем объект блокировки для синхронизации потоков
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.Номенклатура"); // Можно использовать любой справочник
// Блокируем конкретное уникальное пространство (например, пустую ссылку)
ЭлементБлокировки.УстановитьЗначение("Ссылка", Справочники.Номенклатура.ПустаяСсылка());
// В этот момент поток засыпает и ждет, если другой поток уже удерживает блокировку
Блокировка.Заблокировать();
// Теперь мы гарантированно одни в этой секции кода. Открываем файл в режиме добавления (Параметр Истина)
ЗаписьТекста = Новый ЗаписьТекста(ИмяФайла, КодировкаТекста.UTF8, Символы.ПС, Истина);
ЗаписьТекста.ЗаписатьСтроку(Формат(ТекущаяДатаСеанса(), "ДФ=yyyy-MM-dd_HH:mm:ss") + " | " + ТекстСообщения);
ЗаписьТекста.Закрыть();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// В реальных условиях здесь лучше предусмотреть запись в альтернативное место или повторную попытку
ЗаписьЖурналаРегистрации("Логирование", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Преимущества метода: гарантируется запись в один файл, соблюдается хронология событий, исключаются ошибки конкурентного доступа к файлу на уровне операционной системы.
Недостатки: существенно снижается производительность фоновых заданий, так как они вынуждены простаивать в ожидании снятия блокировки транзакции. Метод не подходит для высоконагруженных систем с высокой частотой логирования.
Чтобы избежать взаимных блокировок, мы можем предоставить каждому фоновому заданию собственный уникальный файл для записи логов. Конкуренция за файл полностью исчезает.
Для создания уникального имени файла можно использовать уникальный идентификатор фонового задания или уникальный идентификатор сеанса:
Функция ПолучитьИмяФайлаПотока(БазовыйПуть)
ИдентификаторПотока = СтрЗаменить(Строка(Новый УникальныйИдентификатор()), "-", "");
Возврат БазовыйПуть + "_" + ИдентификаторПотока + ".log";
КонецФункции
Процедура ЗаписатьВИндивидуальныйЛог(БазовыйПуть, ТекстСообщения) Экспорт
ИмяФайла = ПолучитьИмяФайлаПотока(БазовыйПуть);
ЗаписьТекста = Новый ЗаписьТекста(ИмяФайла, КодировкаТекста.UTF8, Символы.ПС, Истина);
ЗаписьТекста.ЗаписатьСтроку(Формат(ТекущаяДатаСеанса(), "ДФ=yyyy-MM-dd_HH:mm:ss") + " | " + ТекстСообщения);
ЗаписьТекста.Закрыть();
КонецПроцедуры
После завершения работы всех фоновых заданий (или по расписанию раз в час/день) запускается отдельный однопоточный процесс (регламентное задание), который собирает все файлы по маске, склеивает их содержимое в один общий файл лога и удаляет временные файлы потоков.
Преимущества: максимальная скорость работы фоновых заданий, полное отсутствие взаимных блокировок.
Недостатки: логи не доступны в общем файле мгновенно (требуется время на сбор и склейку), нарушается естественная хронология внутри общего файла, если сборщик не выполняет сортировку строк по таймштампу.
Этот паттерн проектирования считается наиболее профессиональным в среде разработки 1С. Вместо того чтобы сразу писать данные на жесткий диск (что является медленной неблокируемой операцией), мы используем базу данных в качестве быстрого буфера.
Проанализируем шаги реализации этого подхода:
БуферЛогов с измерениями ИдентификаторЗаписи (тип УникальныйИдентификатор) и ресурсами ДатаВремя (Дата), ТекстСообщения (Строка), Источник (Строка).СбросБуфераЛоговНаДиск, работающее строго в один поток (например, каждые 30 секунд).Пример кода для записи в буфер из фонового задания:
Процедура ЗаписатьВБуфер(ТекстСообщения, Источник) Экспорт
МенеджерЗаписи = РегистрыСведений.БуферЛогов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ИдентификаторЗаписи = Новый УникальныйИдентификатор();
МенеджерЗаписи.ДатаВремя = ТекущаяУниверсальнаяДатаВМиллисекундах();
МенеджерЗаписи.ТекстСообщения = ТекстСообщения;
МенеджерЗаписи.Источник = Источник;
МенеджерЗаписи.Записать(Истина);
КонецПроцедуры
Преимущества: высочайшая надежность, фоновые задания не тратят время на дисковые операции ввода-вывода (I/O), данные лога упорядочены, нет риска заблокировать файл лога.
Недостатки: незначительное увеличение объема базы данных за счет временного хранения буфера (проблема нивелируется своевременной очисткой регистра регламентным заданием).
Зачастую разработчики пытаются изобрести собственный велосипед для логирования там, где отлично справляется сама платформа 1С. Функция ЗаписьЖурналаРегистрации() полностью берет на себя все вопросы конкурентного доступа и асинхронной записи.
Если вам нужно изолировать технологические логи от основных системных сообщений, вы можете использовать уникальные имена событий:
Процедура ЗаписатьВШтатныйЛог(ИмяСобытия, ТекстСообщения) Экспорт
// Платформа сама оптимизирует запись и предотвратит блокировки
ЗаписьЖурналаРегистрации(
"ФоновоеЗадание." + ИмяСобытия,
УровеньЖурналаРегистрации.Информация,
,
,
ТекстСообщения
);
КонецПроцедуры
В дальнейшем вы можете выгружать эти данные в текстовый файл внешней обработкой по мере необходимости или анализировать их непосредственно в интерфейсе 1С.
Если вам обязательно нужно дописывать данные в один файл напрямую из 1С без использования транзакций и баз данных, можно делегировать эту задачу операционной системе. Сама ОС умеет осуществлять операцию добавления (append) строк в файл без долгосрочного удержания монопольной блокировки.
Для реализации этого метода мы можем использовать вызов командной строки через метод ЗапуститьПриложение():
Процедура ЗаписатьЧерезКоманднуюСтроку(ИмяФайла, ТекстСообщения) Экспорт
// Экранируем спецсимволы в тексте сообщения, если это необходимо
ПодготовленныйТекст = СтрЗаменить(ТекстСообщения, """", """""");
// Формируем команду добавления строки для Windows (команда "echo >>")
Команда = "cmd.exe /c echo " + Формат(ТекущаяДатаСеанса(), "ДФ=yyyy-MM-dd_HH:mm:ss") + " | " + ПодготовленныйТекст + " >> """ + ИмяФайла + """";
// Запуск в фоновом режиме без ожидания завершения для максимальной скорости
ЗапуститьПриложение(Команда, , Ложь);
КонецПроцедуры
При таком подходе операционная система мгновенно выполняет операцию дозаписи строки в файл и сразу освобождает его, сводя вероятность коллизии к минимуму.
Выбор оптимального метода записи логов зависит от архитектурных требований вашей системы: