АрхитектураМикросервисы и интеграцииРазработчик распределённых систем

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

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

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нужно разделить состояния «команда принята» и «операция завершена». Синхронный вызов должен быстро вернуть идентификатор операции и явно сообщить, что результат ещё не готов, а итог нужно получать через запрос состояния или событие завершения.

Такой контракт не блокирует вызывающую сторону и не создаёт ложного впечатления, будто бизнес-операция уже выполнена.

Исторический контекст

Синхронный запрос естественен для коротких операций: клиент отправляет запрос, ждёт обработку и получает результат в одном обмене. В распределённых системах часть операций зависит от очередей, внешних сервисов, ручных проверок или длительных вычислений, поэтому удержание соединения становится ненадёжным.

Асинхронный контракт возник как способ отделить момент приёма работы от момента получения результата. Это уменьшает временную связанность компонентов и позволяет обрабатывать длительные операции независимо от жизненного цикла исходного соединения.

Постановка проблемы

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

Контракт должен явно описывать жизненный цикл операции: она может быть принята, выполняться, успешно завершиться или завершиться ошибкой. Важно также определить, где клиент узнаёт состояние и что означает каждый результат проверки.

Подробное решение

Синхронный слой принимает команду и возвращает идентификатор операции, состояние принята либо эквивалентный ему результат и, при необходимости, ссылку на ресурс состояния. Это подтверждает только принятие работы и доступность её обработки, но не бизнес-результат.

Дальнейший результат можно получать двумя способами:

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

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

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

Запрос состояния должен быть доступен дольше, чем живёт исходное соединение, а срок хранения результата нужно объявить. Событийная доставка полезна для реактивных систем, но сама по себе не заменяет механизм проверки состояния: событие может быть задержано, потеряно на стороне подписчика или обработано позже.

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

Ситуация из практики

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

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

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

Что кандидаты часто упускают

  1. Вопрос: Что означает успешный ответ на первоначальную команду?

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

  2. Вопрос: Как клиенту отличить временную задержку от окончательной ошибки?

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

  3. Вопрос: Почему одного события о завершении недостаточно для надёжного клиентского контракта?

    Ответ: Событие не гарантирует, что конкретный клиент его получил в нужный момент. Клиент может подключиться позже, временно потерять соединение или не иметь прямого доступа к брокеру. Поэтому обычно сохраняют итоговое состояние операции и предоставляют способ его прочитать; событие используется как ускоритель уведомления, а не как единственный источник восстановления результата.