Перейти к содержимому

Идемпотентность обработчиков обмена

Директивный

v8std#std690

#обмен#надёжность

Обработчик приёма данных обязан быть идемпотентным: повторная обработка того же сообщения не должна создавать дубли объектов или повторно применять эффекты.

  • Ключ идемпотентности хранится в ИБ и ищется до создания — внешний идентификатор сообщения/объекта в индексируемом реквизите или в регистре сведений «Соответствие внешних ключей» (измерение — внешний ключ, ресурс — ссылка).
  • Ключ выбирается устойчивый. Если источник даёт идентификатор (ИдСообщения, GUID чека) — используется он. Если нет (периодическая выгрузка без идентификаторов) — ключ составной и естественный: период + источник (касса, подразделение, организация).
  • При параллельной загрузке поиск и создание — под блокировкой по ключу (см. ниже), иначе два потока создадут два объекта.

Каналы доставки допускают повторную доставку: ретраи отправителя, обрыв связи после записи, но до подтверждения, повторный запуск обмена вручную. Без идемпотентности каждый такой повтор — дубль документа и расхождение учёта, которое разбирают руками.

Параллельная загрузка: блокировка по ключу

Заголовок раздела «Параллельная загрузка: блокировка по ключу»

При параллельной загрузке (несколько потоков/фоновых заданий/инстансов отправителя) пара «найти-или-создать» сама по себе не защищает от гонки: два потока одновременно не находят объект и создают два. Поиск и запись — под управляемой блокировкой по ключу идемпотентности:

Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрСведений.СоответствиеВнешнихКлючей");
Элемент.УстановитьЗначение("ВнешнийКлюч", ВнешнийКлюч);
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать(); // второй поток ждёт здесь, потом находит объект
Ссылка = НайтиПоВнешнемуКлючу(ВнешнийКлюч);
Если Ссылка = Неопределено Тогда
Ссылка = СоздатьДокумент(ДанныеСообщения, ВнешнийКлюч);
КонецЕсли;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Возврат Ссылка;

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

flowchart TD
    A[Сообщение получено] --> B{Найден объект по ИдСообщения?}
    B -- Да --> C[Получить существующий объект]
    B -- Нет --> D[Создать новый объект]
    C --> E[Заполнить и записать]
    D --> E
    E --> F[Дублей нет]

Два реальных случая, где видны обе половины правила:

  • Загрузка чеков маркетплейса. Требование пришло от самого отправителя: «важна идемпотентность создания объектов, так как одновременно может быть несколько инстансов». Один и тот же чек физически приходит несколько раз — ретраи, параллельные инстансы, переотправка после таймаута. Ключ — внешний идентификатор чека, поиск и создание — под блокировкой по ключу (шаблон выше), иначе два инстанса создают два документа.
  • Загрузка продаж из ДодоИс. У ежедневной выгрузки нет идентификатора сообщения, и повторный запуск за тот же день задваивал «Отчёты о розничных продажах». Ключ здесь составной и естественный — период + касса/подразделение: повторный запуск находит и обновляет существующий ОРП. Разбор всего конвейера — в сквозном примере.

Чисто читающие обработчики (отдают данные, ничего не записывают) идемпотентны по построению — дополнительный ключ не нужен. Для всего, что создаёт или изменяет данные, исключений нет: даже «одноразовая» загрузка когда-нибудь будет запущена повторно.