В процессе интеграции различных конфигураций «1С:Предприятия» разработчики часто сталкиваются с задачей оперативной отправки данных из одной базы в другую. На первый взгляд, идея кажется простой и логичной: настроить подписку на событие проведения документа в базе-источнике, при его записи или проведении запускать фоновое задание, которое через COM-соединение или XML-выгрузку мгновенно передает данные в базу-приемник. Однако при реализации этой схемы «в лоб» разработчики неизбежно сталкиваются с тем, что обмен либо зависает без каких-либо записей в журнале регистрации, либо приводит к серьезным блокировкам базы данных.
В этой статье мы подробно разберем, почему выгрузка данных напрямую из транзакции проведения через фоновое задание является грубой архитектурной ошибкой. Мы проанализируем технические причины сбоев, связанные с транзакционностью, спецификой работы COM-соединений на сервере и правами доступа служб. В завершение мы спроектируем и реализуем правильную, отказоустойчивую архитектуру оперативного обмена данными.
Рассмотрим ситуацию, с которой столкнулся автор темы. В базе-источнике (например, «Комплексная автоматизация 1.1») настроены правила обмена через «Конвертацию данных 2». На клиенте интерактивно выгрузка отрабатывает отлично. Но при попытке перенести этот код в общий модуль для выполнения на сервере через фоновое задание в подписке на проведение документа, процесс зависает или не стартует вовсе.
Проанализируем исходный код подписки и фонового вызова:
Процедура ПодпискаНаСобытие1ОбработкаПроведения(Источник, Отказ, РежимПроведения) Экспорт
ФоновыеЗадания.Выполнить("МодульРегламентныхЗаданий.ОбменятьсяДаннымиСБП");
КонецПроцедуры
И процедуру выгрузки на сервере:
Процедура ОбменятьсяДаннымиСБП() Экспорт
ОбработкаОбмена = Обработки.УниверсальныйОбменДаннымиXML_v3.Создать();
ОбработкаОбмена.РежимОбмена = "Выгрузка";
врФайл = ПолучитьИмяВременногоФайла("xml");
МакетПравил = ПланыОбмена.ОбменСБухгалтерияПредприятия3.ПолучитьМакет("ПравилаВыгрузкиКА_БП");
МакетПравил.Записать(врФайл);
ОбработкаОбмена.ИмяФайлаПравилОбмена = vrFile;
ОбработкаОбмена.ЗагружатьДанныеВРежимеОбмена = Истина;
ОбработкаОбмена.ЗаписыватьРегистрыНаборамиЗаписей = Истина;
ОбработкаОбмена.ЗапоминатьЗагруженныеОбъекты = Истина;
// Настройки COM-подключения к базе-приемнику
ОбработкаОбмена.ТипИнформационнойБазыДляПодключения = Ложь; // Файловая/Серверная
ОбработкаОбмена.ВерсияПлатформыИнформационнойБазыДляПодключения = "V83";
ОбработкаОбмена.ИмяИнформационнойБазыНаСервереДляПодключения = "db_buh_v3";
ОбработкаОбмена.ИмяСервераИнформационнойБазыДляПодключения = "s1c2k16";
ОбработкаОбмена.ПользовательИнформационнойБазыДляПодключения = "ИмяПользователя";
ОбработкаОбмена.ПарольИнформационнойБазыДляПодключения = "Пароль";
ОбработкаОбмена.НепосредственноеЧтениеВИБПриемнике = Истина;
ОбработкаОбмена.ЗагрузитьПравилаОбмена();
УстановитьУзелОбменаУСтрокДерева(ОбработкаОбмена.TableПравилВыгрузки.Строки,
ПланыОбмена.ОбменСБухгалтерияПредприятия3.НайтиПоКоду("БП"));
ОбработкаОбмена.ВыполнитьВыгрузку();
КонецПроцедуры
Выясним, какие критические ошибки кроются в этом подходе и почему данная схема неработоспособна в реальных условиях.
Основная и самая опасная ошибка — запуск фонового задания напрямую из обработчика проведения документа. Проведение документа в 1С всегда выполняется внутри неявной транзакции базы данных. Пока процедура проведения не завершилась полностью, транзакция остается открытой.
Когда мы вызываем метод ФоновыеЗадания.Выполнить, система мгновенно создает новый рабочий сеанс на сервере приложений 1С. Этот сеанс пытается прочитать данные проводимого документа, чтобы выгрузить их в приемник. Однако, согласно правилам СУБД (уровень изоляции транзакций Read Committed):
На клиенте интерактивная выгрузка работает без проблем, так как она выполняется в 32-разрядном адресном пространстве под учетной записью текущего пользователя Windows. На сервере ситуация кардинально меняется:
Новый COMОбъект("V83.COMConnector") сервер пытается загрузить 64-битную библиотеку comcntr.dll. Если на сервере зарегистрирована только 32-битная версия платформы или библиотека вовсе не зарегистрирована в системном реестре, вызов завершится критической ошибкой, которая даже не успеет записаться в лог обмена.USR1CV8). У этого пользователя часто отсутствуют права на инициализацию COM-компонентов в Windows (DCOM), нет доступа к сетевым ресурсам и временным папкам сервера, куда метод ПолучитьИмяВременногоФайла пытается сохранить временный файл правил обмена.УниверсальныйОбменДаннымиXML затесался хотя бы один интерактивный вызов (например, функции Предупреждение, Вопрос, Сообщить или открытие формы), серверный процесс мгновенно и навсегда зависнет, ожидая реакции пользователя на несуществующем экране.Для того чтобы регламентное или фоновое задание могло корректно запустить процедуру обмена, необходимо соблюдение строгих правил конфигурирования:
Экспорт.Если эти требования не соблюдены, планировщик заданий просто «не увидит» процедуру, что приведет к ошибке вызова метода.
---
Чтобы построить действительно надежный, быстрый и масштабируемый обмен, мы полностью откажемся от запуска выгрузки внутри транзакции проведения документа. Перейдем к классической двухэтапной схеме с использованием планов обмена и регламентных заданий.
Вместо непосредственного запуска выгрузки в подписке на проведение, мы будем использовать стандартный механизм 1С — регистрацию изменений на узле плана обмена. Это чрезвычайно быстрая операция, которая занимает доли миллисекунды, не создает блокировок и полностью подчиняется транзакции проведения (если транзакция откатится, то и регистрация изменения отменится).
Создадим или используем существующий План обмена (например, ОбменСБухгалтерияПредприятия3). Настроим подписку на событие ПриЗаписи для нужных документов:
Процедура ПодпискаНаСобытиеРегистрацияИзмененийПриЗаписи(Источник, Отказ, РежимЗаписи, РежимПроведения) Экспорт
// Проверим, что документ не находится в режиме обмена (чтобы избежать зацикливания)
Если Источник.ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Получаем узел-приемник
УзелПриемник = ПланыОбмена.ОбменСБухгалтерияПредприятия3.НайтиПоКоду("БП");
Если ЗначениеЗаполнено(УзелПриемник) Тогда
// Регистрируем изменение только для этого конкретного узла
ПланыОбмена.ЗарегистрироватьИзменения(УзелПриемник, Источник);
КонецЕсли;
КонецПроцедуры
Теперь создадим регламентное задание, которое будет запускаться независимо от действий пользователей на сервере, например, каждые 2–3 минуты. Оно будет считывать накопленные изменения из плана обмена, порционно выгружать их и очищать регистрацию только после успешного завершения сеанса обмена.
Напишем процедуру для общего модуля РегламентныеПроцедурыСервер (флаг «Сервер» — Истина, «Экспорт» присутствует):
Процедура ВыполнитьОперативныйОбменСБП() Экспорт
УзелПриемник = ПланыОбмена.ОбменСБухгалтерияПредприятия3.НайтиПоКоду("БП");
Если Не ЗначениеЗаполнено(УзелПриемник) Тогда
ЗаписьЖурналаРегистрации("Обмен данными", УровеньЖурналаРегистрации.Ошибка, , ,
"Не найден узел плана обмена с кодом 'БП'!");
Возврат;
КонецЕсли;
// Проверим, есть ли зарегистрированные изменения, чтобы не запускать тяжелую обработку вхолостую
ВыборкаИзменений = ПланыОбмена.ВыбратьИзменения(УзелПриемник, 1); // получаем маркер изменений
Если Не ВыборкаИзменений.Следующий() Тогда
// Изменений нет, завершаем работу
Возврат;
КонецЕсли;
// Сбрасываем выборку для корректной работы обработки обмена
ВыборкаИзменений = Неопределено;
// Инициализируем обработку выгрузки XML
ОбработкаОбмена = Обработки.УниверсальныйОбменДаннымиXML_v3.Создать();
ОбработкаОбмена.РежимОбмена = "Выгрузка";
// Использование временных файлов на сервере должно быть безопасным
ИмяФайлаПравил = ПолучитьИмяВременногоФайла("xml");
МакетПравил = ПланыОбмена.ОбменСБухгалтерияПредприятия3.ПолучитьМакет("ПравилаВыгрузкиКА_БП");
МакетПравил.Записать(ИмяФайлаПравил);
ОбработкаОбмена.ИмяФайлаПравилОбмена = ИмяФайлаПравил;
ОбработкаОбмена.ЗагружатьДанныеВРежимеОбмена = Истина;
ОбработкаОбмена.ЗаписыватьРегистрыНаборамиЗаписей = Истина;
ОбработкаОбмена.ЗапоминатьЗагруженныеОбъекты = Истина;
// Настройки удаления регистрации изменений после успешной выгрузки
// Режим 2 означает автоматическое удаление регистрации только для выгруженных объектов
ОбработкаОбмена.ТипУдаленияРегистрацииИзмененийДляУзловОбменаПослеВыгрузки = 2;
// Настройки COM-соединения
ОбработкаОбмена.ТипИнформационнойБазыДляПодключения = Ложь; // Серверный вариант базы
ОбработкаОбмена.ВерсияПлатформыИнформационнойБазыДляПодключения = "V83";
ОбработкаОбмена.ИмяИнформационнойБазыНаСервереДляПодключения = "db_buh_v3";
ОбработкаОбмена.ИмяСервераИнформационнойБазыДляПодключения = "s1c2k16";
ОбработкаОбмена.ПользовательИнформационнойБазыДляПодключения = "АдушкинВ";
ОбработкаОбмена.ПарольИнформационнойБазыДляПодключения = "135246";
ОбработкаОбмена.НепосредственноеЧтениеВИБПриемнике = Истина;
Попытка
ОбработкаОбмена.ЗагрузитьПравилаОбмена();
// Устанавливаем узел обмена для выгружаемых правил
НастроитьУзлыОбменаВПравилах(ОбработкаОбмена.ТаблицаПравилВыгрузки.Строки, УзелПриемник);
// Выполняем выгрузку
ОбработкаОбмена.ВыполнитьВыгрузку();
ЗаписьЖурналаРегистрации("Обмен данными", УровеньЖурналаРегистрации.Информация, , ,
"Обмен данными с БП успешно завершен.");
Исключение
ИнформацияОбОшибке = ИнформацияОбОшибке();
ЗаписьЖурналаРегистрации("Обмен данными", УровеньЖурналаРегистрации.Ошибка, , ,
"Ошибка при выполнении обмена: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке));
КонецПопытки;
// Удаляем временный файл правил обмена с диска сервера
Попытка
УдалитьФайлы(ИмяФайлаПравил);
Исключение
// Игнорируем ошибку удаления временного файла
КонецПопытки;
КонецПроцедуры
Процедура НастроитьУзлыОбменаВПравилах(СтрокиДерева, УзелОбмена)
Для Каждого Строка Из СтрокиДерева Цикл
Если Строка.ЭтоГруппа Тогда
НастроитьУзлыОбменаВПравилах(Строка.Строки, УзелОбмена);
Иначе
Строка.СсылкаНаУзелОбмена = УзелОбмена;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Даже при правильной настройке регламентных заданий COM-соединение остается крайне капризной технологией, чувствительной к обновлениям платформы 1С, настройкам DCOM на сервере Windows и разрядности процессов. Современным индустриальным стандартом интеграции систем «1С» является использование HTTP-сервисов.
Рассмотрим, как концептуально перевести обмен на HTTP-протокол, полностью избавившись от проблем с COM-библиотеками:
Пример отправки данных из источника по HTTP-протоколу:
Процедура ОтправитьДанныеПоHTTP(ИмяФайлаДанных)
// Настройка соединения
СерверПриемник = "buh-server.local";
РесурсСервиса = "/buh_base/hs/Exchange/UploadData";
HTTPСоединение = Новый HTTPСоединение(СерверПриемник, , "ПользовательHTTP", "ПарольHTTP", , 30);
HTTPЗапрос = Новый HTTPЗапрос(РесурсСервиса);
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/xml; charset=utf-8");
HTTPЗапрос.УстановитьТелоИзФайла(ИмяФайлаДанных);
Попытка
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния = 200 Тогда
ЗаписьЖурналаРегистрации("HTTP Обмен", УровеньЖурналаРегистрации.Информация, , ,
"Данные успешно переданы в приемник.");
Иначе
ВызватьИсключение "Сервер вернул ошибку: " + HTTPОтвет.КодСостояния + " " + HTTPОтвет.ПолучитьТелоКакСтроку();
КонецЕсли;
Исключение
ИнформацияОбОшибке = ИнформацияОбОшибке();
ВызватьИсключение "Ошибка HTTP-соединения: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке);
КонецПопытки;
КонецПроцедуры
Такой подход гарантирует максимальное быстродействие, независимость баз друг от друга и отсутствие зависаний сеансов при сетевых сбоях.
Чтобы ваш автоматический обмен данными работал стабильно и без сбоев, придерживайтесь следующих правил:
Экспорт.USR1CV8) должен быть доступ на чтение/запись к системным временным папкам и права на запуск DCOM-компонента V83.COMConnector, если вы продолжаете использовать COM.