Как организовать параллельную запись в текстовый лог из нескольких сеансов 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке интеграций, обменов с веб-сервисами или выполнении параллельных фоновых заданий часто возникает необходимость вести непрерывный текстовый журнал событий. Однако при попытке нескольких сеансов одновременно писать в один и тот же файл разработчики сталкиваются с системной ошибкой совместного доступа.

Проанализируем причину такого поведения платформы 1С и рассмотрим несколько надежных подходов к организации многопоточного логирования.

Причина монопольной блокировки объекта ЗаписьТекста

Выясним причину конфликта: объект платформы ЗаписьТекста открывает дескриптор файла исключительно в монопольном режиме. Даже если в конструкторе указан параметр Дописывать = Истина, среда исполнения не передает флаг разделяемого доступа на запись (FILE_SHARE_WRITE). Как только один сеанс открыл файл на запись, второй сеанс при попытке инициализации ЗаписьТекста сразу получает исключение блокировки операционной системы.

Постоянные попытки открывать файл в цикле с перехватом исключений не рекомендуются, так как они создают паразитную нагрузку на процессор и диск, а также приводят к деградации производительности при росте числа потоков.

Способ 1. Разделение файлов логов по сеансам и фоновым заданиям

Разберем наиболее стабильный и архитектурно правильный подход: каждый параллельный процесс ведет собственный отдельный лог-файл. Чтобы имена файлов не конфликтовали, к имени лога добавляют уникальный идентификатор сеанса, пользователя или задания.

Посмотрим на пример универсальной функции, которая формирует путь к персональному файлу лога для текущего контекста исполнения:


Функция ПолучитьИмяФайлаЛога(БазовыйКаталог, ПрефиксЛога)
    
    ИдентификаторПотока = "";
    
    // Проверяем, выполняется ли код в фоновом задании
    ТекущееЗадание = ФоновыеЗадания.ТекущееФоновоеЗадание();
    Если ТекущееЗадание <> Неопределено Тогда
        ИдентификаторПотока = "job_" + СтрЗаменить(Строка(ТекущееЗадание.УникальныйИдентификатор), "-", "");
    Иначе
        // Для обычных пользовательских или регламентных сеансов
        СеансИнфо = ПолучитьТекущийСеансИнформационнойБазы();
        ИдентификаторПотока = "session_" + Строка(СеансИнфо.НомерСеанса);
    КонецЕсли;
    
    ТекущаяДатаСтрока = Формат(ТекущаяДатаСеанса(), "ДФ=yyyyMMdd");
    ИмяФайла = СтрШаблон("%1_%2_%3.log", ПрефиксЛога, ТекущаяДатаСтрока, ИдентификаторПотока);
    
    Возврат ОбъединитьПути(БазовыйКаталог, ИмяФайла);
    
КонецФункции

Разберем запись строки в полученный файл с гарантированным закрытием ресурса:


Процедура ЗаписатьВЛог(БазовыйКаталог, ПрефиксЛога, ТекстСообщения)
    
    ИмяФайлаЛога = ПолучитьИмяФайлаЛога(БазовыйКаталог, ПрефиксЛога);
    
    Запись = Новый ЗаписьТекста(ИмяФайлаЛога, КодировкаТекста.UTF8, Символы.ПС, Истина);
    
    СтрокаЛога = СтрШаблон("[%1] %2", Формат(ТекущаяДатаСеанса(), "ДФ='yyyy-MM-dd HH:mm:ss'"), ТекстСообщения);
    Запись.ЗаписатьСтроку(СтрокаЛога);
    Запись.Закрыть();
    
КонецПроцедуры

Преимущества подхода: полное отсутствие взаимных блокировок, максимальная скорость записи и простота последующей очистки или архивации устаревших логов по дате и сеансам.

Способ 2. Паттерн каталога очереди (Spool-директория)

Если в конечном итоге требуется один общий файл, эффективным решением будет разделение этапов записи и агрегации:

  1. Каждый сеанс записывает порцию событий в свой временный файл с уникальным именем (используя Новый УникальныйИдентификатор) в специальный каталог очереди.
  2. Регламентное фоновое задание или скрипт с заданной периодичностью читает файлы из очереди, дописывает их содержимое в итоговый общий лог и удаляет обработанные фрагменты.

Посмотрим на реализацию процедуры агрегации логов:


Процедура СобратьЛогиВОдинФайл(КаталогОчереди, ИтоговыйФайлЛога)
    
    НайденныеФайлы = НайтиФайлы(КаталогОчереди, "*.tmp");
    Если НайденныеФайлы.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    ИтоговаяЗапись = Новый ЗаписьТекста(ИтоговыйФайлЛога, КодировкаТекста.UTF8, Символы.ПС, Истина);
    
    Для Каждого ФайлФрагмента Из НайденныеФайлы Цикл
        Попытка
            ТекстФрагмента = Новый ЧтениеТекста(ФайлФрагмента.ПолноеИмя, КодировкаТекста.UTF8);
            Содержимое = ТекстФрагмента.Прочитать();
            ТекстФрагмента.Закрыть();
            
            Если ЗначениеЗаполнено(Содержимое) Тогда
                ИтоговаяЗапись.Записать(Содержимое);
            КонецЕсли;
            
            УдалитьФайлы(ФайлФрагмента.ПолноеИмя);
        Исключение
            // Если файл еще дописывается сеансом, пропускаем его до следующего цикла
        КонецПопытки;
    КонецЦикла;
    
    ИтоговаяЗапись.Закрыть();
    
КонецПроцедуры

Способ 3. Дозапись средствами операционной системы

Если архитектура строго требует писать именно в один файл без промежуточных таблиц и очередей, можно переложить задачу дозаписи на уровень операционной системы. Системные вызовы при использовании оператора конвейера >> открывают файл, производят атомарный append и мгновенно освобождают дескриптор.

Посмотрим на пример вызова через командную строку Windows:


Процедура ДописатьСтрокуОС(ПутьКФайлуЛога, ТекстСтроки)
    
    Команда = СтрШаблон("cmd.exe /c chcp 65001 > nul & echo [%1] %2 >> ""%3""", 
        Формат(ТекущаяДатаСеанса(), "ДФ='yyyy-MM-dd HH:mm:ss'"), 
        ТекстСтроки, 
        ПутьКФайлуЛога);
        
    ЗапуститьПриложение(Команда, , Истина);
    
КонецПроцедуры

Важно: вызов внешних процессов операционной системы создает накладные расходы на запуск процессов cmd.exe или bash, поэтому данный метод подходит только для редких событий (например, фиксация только ошибок обмена), но не для высоконагруженных потоков данных.

Способ 4. Встроенные механизмы платформы 1С

Рассмотрим альтернативные платформенные способы фиксации событий, которые избавят от необходимости работать с файловой системой вручную:

  1. Регистр сведений: непериодический регистр сведений с режимом записи «Независимый». Каждая запись лога добавляется как новая строка (в качестве ключевого поля используется GUID). Это позволяет параллельным сеансам выполнять запись транзакционно без блокировок таблицы. По регистру можно построить отчет или выгрузить данные в файл при необходимости.
  2. Журнал регистрации: метод ЗаписьЖурналаРегистрации оптимизирован под многопоточную работу кластера серверов. Чтобы журнал не разрастался, в него рекомендуется писать только критические события и ошибки, а штатную очистку настроить регламентом.
  3. Технологический журнал: настройка файла logcfg.xml позволяет собирать системные сетевые события и HTTP-вызовы на уровне самого сервера 1С без написания программного кода логирования.
← На главную