Продолжаю делиться опытом по взаимодействию с ГИС ЖКХ. Следующей задачей, после установки защищенного соединения, стала организация обмена сообщениями. Разработчики ГИС ЖКХ предлагают две модели взаимодействия: синхронную и асинхронную. Некоторые разработчики выбирают синхронную модель из-за её простоты и доступности. В этой статье постараюсь объяснить, почему нужно использовать именно асинхронную модель и дать подсказки по реализации на C#.
Выбор модели взаимодействия
Синхронная модель подразумевает классический «запрос-ответ». Вы формируете объект запроса, отправляете его в ГИС ЖКХ, и система удерживает соединение до того момента как не будет сформирован ответ или соединение не разорвётся по таймауту.
Асинхронная модель строится на двух операциях: постановка задания на выполнение действия и запрос состояния выполнения действия (при необходимости повторяем несколько раз). Именно эта модель взаимодействия называлась разработчиками ГИС ЖКХ как предпочтительная и рекомендуемая. Далее мы подробно рассмотрим эти обе операции.
Постановка задания на выполнение действия
Для получения информации или отправки данных в ГИС ЖКХ, необходимо заполнить объект запроса. Именно в нём находится информация «по какому дому нужно получить информацию» или «какие именно лицевые счета нужно создать в ГИС ЖКХ».
В сообщении также указывается идентификатор сообщения (MessageGUID), он однозначно идентифицирует сообщение в информационной системе. Запрос, с одним и тем же MessageGUID можно несколько раз отправлять в ГИС ЖКХ, и ГИС ЖКХ гарантирует, что он выполнится один раз. Например, при постановке задания на создание лицевых счетов запрос упал по таймауту или связь внезапно прервалась, мы можем потом ещё раз отправить этот же запрос с уверенностью, что лишних лицевых счетов мы не создадим.
Если выполняется отправка массива объектов, каждому объекту присваивается TransportGUID, он позволяет сопоставить объект запроса и ответа. Например, из результата обработки мы сможем узнать, почему именно этот лицевой счет из всего сообщения не принимается ГИС ЖКХ.
В ответ мы получим другой MessageGUID – идентификатор запроса в ГИС ЖКХ. Именно по этому идентификатору будет запрашиваться состояние выполнения запроса в ГИС ЖКХ.
Запрос состояния выполнения действия
Для получения состояния выполнения нужно выполнить короткий запрос getStateResult. В нём указывается MessageGUID, присвоенный в ГИС ЖКХ. Этот запрос можно отправлять несколько раз.
В ответе мы получим состояние обработки сообщения: принято, в обработке, готово. Если сообщение обработано, ГИС ЖКХ возвращает либо результат обработки (получаемый объект или информацию об отправке данных), либо сообщает об ошибках, произошедших при обработке сообщения. Самые распространённые ошибки: «EXP001000: Внутренняя ошибка.», «Доступ запрещен для поставщика данных организация "*" полномочие "*"», «Удаленный сервер вернул неожиданный ответ: (502) Bad Gateway.», «Прослушивание на api.dom.gosuslugi.ru* не выполняла ни одна конечная точка, которая могла бы принять сообщение.» и прочее. По сути, они делят сообщения на два типа: те сообщения, которые можно отправить ещё раз и те, которые уже нет смысла повторно отправлять. Для себя решили, что, если получаем обработанную ошибку ГИС ЖКХ, то повторно запрос не отправляем. Если запрос отвалился по таймауту или проблема в нашем коде, отправляем сообщение ещё раз.
Типичные ошибки ГИС ЖКХ
- Выполнение обработки сообщения на стороне ГИС ЖКХ проходит не в транзакции.
Например, мы отправляем сообщение на создание 30 лицевых счетов, в результате получаем «EXP001000: Внутренняя ошибка.». Мы справедливо ожидаем, что ни один из лицевых счетов не создался, однако при контрольной проверке видим, что ВСЕ лицевые счета создались. - Некоторые запросы «зависают» в статусе «принято» или «в обработке» на неопределенное время.
Обычно результат обработки сообщения можно получить через несколько секунд, но некоторые сообщения висят в необработанном статусе несколько дней, такие сообщения нужно на своей стороне помечать, чтобы не спрашивать состояние обработки в очередной раз.
Из-за подобных «особенностей» процесс отправки информации в ГИС ЖКХ делится на три этапа:
- Загружаем информацию из ГИС ЖКХ для сверки текущего состояния
- Отправка нужной информации в ГИС ЖКХ
- Контрольная проверка загруженной информации, может быть, что-то опять упало с «EXP001000: Внутренняя ошибка.», а информация была создана.
Техническая сторона взаимодействия
Любой процесс взаимодействия с ГИС ЖКХ состоит их трёх этапов:
- Получение информации для сообщений, сохранение её в БД
- Создание прокси-объектов ГИС ЖКХ (напомню, мы работаем через WCF), отправка сообщения, обработка ответа, сохранение MessageGUID ГИС ЖКХ
- Получение результата обработки, обработка результата
Создание сообщений
Для каждого типа взаимодействия мы создаем таблицу в БД, например, ExportHouseInfoMessages или ImportLsMessages, в ней мы храним всю необходимую информацию для создания прокси-объекта сообщения, которую будем отправлять в ГИС ЖКХ. Именно на этом этапе мы создаем MessageGuid сообщения.
На этом этапе нужно создать сообщения только по тем данным, по которым ещё не были созданы сообщения и не получен результат обработки, иначе можно задублировать данные.
Главная сложность этого этапа – достать данные, необходимые для отправки, из своей информационной системы. Например, в ГИС ЖКХ нужно отправлять все изменения в лицевых счетах, а у нас не было истории изменения в нужном разрезе.
/// <summary>
/// Базовый сервис для первого этапа взаимодействия - создания сообщений
/// </summary>
/// <typeparam name="TMessageDomain">Тип доменного сообщения</typeparam>
/// <typeparam name="TSourceDomain">Тип объекта, который возвращается из информационной системы</typeparam>
public class CreateMessageCoreService<TMessageDomain, TSourceDomain>
where TMessageDomain : MessageDomain
{
private readonly ISourceService<TSourceDomain> _sourceService;
private readonly IMessageDomainConverter<TMessageDomain, TSourceDomain> _messageDomainConverter;
private readonly IMessageDomainService<TMessageDomain> _messageDomainService;
private readonly IOrgPPAGUIDService _orgPPAGUIDService;
private readonly IGisLogger _logger;
public CreateMessageCoreService(ISourceService<TSourceDomain> sourceService,
IMessageDomainConverter<TMessageDomain, TSourceDomain> messageDomainConverter,
IMessageDomainService<TMessageDomain> messageDomainService,
IOrgPPAGUIDService orgPPAGUIDService, IGisLogger logger)
{
_sourceService = sourceService;
_messageDomainConverter = messageDomainConverter;
_messageDomainService = messageDomainService;
_orgPPAGUIDService = orgPPAGUIDService;
_logger = logger;
}
public void CreateMessages(CoreInitData coreInitData)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
try
{
//получаем данные из информационной системы, по которым нужно осуществить взаимодействие
var sourceDomains = _sourceService.GetSourceDomains(coreInitData);
//получаем senderId по УК
var orgPPAGUID = _orgPPAGUIDService.GetOrgPPAGUID(coreInitData.UkId);
//по исходным данным создаем доменные сообщения
var messages = _messageDomainConverter.ToMessageDomain(sourceDomains, coreInitData, orgPPAGUID);
//сохраняем сообщения в базу данных
_messageDomainService.InsertMessageDomains(messages);
stopWatch.Stop();
_logger.Info(this.GetType(), $"Создано {messages.Count} доменных сообщений по УК {coreInitData.UkId} за {stopWatch.Elapsed}");
}
catch (Exception ex)
{
_logger.Error(this.GetType(), $"Произошло исключение при обработке {coreInitData}", ex);
}
}
}
Отправка сообщений
На этом этапе:
- Поднимаем сообщения из БД, которые нужно отправить в ГИС ЖКХ
- Создаем по ним прокси объекты сообщений
- Отправляем в ГИС ЖКХ
- Получаем MessageGUID ГИС ЖКХ
- Сохраняем его в сообщении в БД
/// <summary>
/// Базовый сервис для второго этапа взаимодействия - отправки сообщений
/// </summary>
/// <typeparam name="TMessageDomain">Тип доменного сообщения</typeparam>
/// <typeparam name="TMessageProxy">Тип прокси объекта сообщения</typeparam>
/// <typeparam name="TAckProxy">Тип прокси объекта ответа</typeparam>
public class SendMessageCoreService<TMessageDomain, TMessageProxy, TAckProxy>
where TMessageDomain : MessageDomain
where TAckProxy : IAckRequestAck
{
private readonly IMessageDomainService<TMessageDomain> _messageDomainService;
private readonly IMessageProxyConverter<TMessageDomain, TMessageProxy> _messageProxyConverter;
private readonly ISendMessageProxyProvider<TMessageProxy, TAckProxy> _sendMessageProxyProvider;
private readonly ISendMessageHandler<TMessageDomain, TAckProxy> _sendMessageHandler;
private readonly IGisLogger _logger;
public SendMessageCoreService(IMessageDomainService<TMessageDomain> messageDomainService,
IMessageProxyConverter<TMessageDomain, TMessageProxy> messageProxyConverter,
ISendMessageProxyProvider<TMessageProxy, TAckProxy> sendMessageProxyProvider,
ISendMessageHandler<TMessageDomain, TAckProxy> sendMessageHandler, IGisLogger logger)
{
_messageDomainService = messageDomainService;
_messageProxyConverter = messageProxyConverter;
_sendMessageProxyProvider = sendMessageProxyProvider;
_sendMessageHandler = sendMessageHandler;
_logger = logger;
}
public void SendMessages(CoreInitData coreInitData)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
try
{
//получаем доменные сообщения для отправки
//не обязательно могут быть только новые
//также поднимаются не отправленные с первого раза
var messages = _messageDomainService.GetMessageDomainsForSend(coreInitData);
foreach (var messageDomain in messages)
{
try
{
//по каждому из доменных сообщений создаем прокси сообщение
var proxyMessageRequests = _messageProxyConverter.ToMessageProxy(messageDomain);
//отправляем прокси сообщение
var proxyAck = _sendMessageProxyProvider.SendMessage(proxyMessageRequests);
//обрабатываем успешный результат
_sendMessageHandler.SendSuccess(messageDomain, proxyAck);
}
catch (Exception exception)
{
//обрабатываем исключения
_sendMessageHandler.SendFail(messageDomain, exception);
}
}
stopWatch.Stop();
_logger.Info(this.GetType(), $"По {messages.Count} доменным сообщениям УК {coreInitData.UkId} отправлено " +
$"{messages.Count(x => x.Status == MessageStatus.Sent)} сообщений, " +
$"{messages.Count(x => x.Status == MessageStatus.SendError)} упали с ошибкой, " +
$"{messages.Count(x => x.Status == MessageStatus.SendErrorTryAgain)} будут отправлены повторно, за {stopWatch.Elapsed}");
}
catch (Exception ex)
{
_logger.Error(this.GetType(), $"Произошло исключение при обработке {coreInitData}", ex);
}
}
}
Получение результата обработки сообщения
На этом этапе:
- Поднимаем сообщения из БД, по которым нужно получить результаты
- Формируем прокси объект получения состояния обработки
- Отправляем в ГИС ЖКХ
- Если сообщение ещё не обработано, сохраняем в БД, что сообщение не обработано. Если после отправки сообщения прошло слишком много времени, помечаем сообщение.
- Если по сообщению есть результат, его нужно обработать. Обычно это сохранение привязок идентификаторов объектов в ГИС ЖКХ и в нашей ИС.
/// <summary>
/// Базовый сервис для третьего этапа взаимодействия - получения результата обработки
/// </summary>
/// <typeparam name="TMessageDomain">Тип доменного сообщения</typeparam>
/// <typeparam name="TGetStateResultProxy">Тип прокси объекта запроса результата обработки</typeparam>
/// <typeparam name="TResultProxy">Тип прокси объекта результата обработки сообщения</typeparam>
/// <typeparam name="TResult">Тип объекта результата обработки сообщения</typeparam>
public class GetResultsCoreService<TMessageDomain, TGetStateResultProxy, TResultProxy, TResult>
where TMessageDomain : MessageDomain
where TResultProxy : IGetStateResult
{
private readonly IMessageDomainService<TMessageDomain> _messageDomainService;
private readonly IGetResultProxyProvider<TGetStateResultProxy, TResultProxy> _getResultProxyProvider;
private readonly IGetStateProxyConverter<TGetStateResultProxy, TMessageDomain> _getStateProxyConverter;
private readonly IResultConverter<TResultProxy, TResult> _resultConverter;
private readonly ISaveResultService<TResult, TMessageDomain> _saveResultService;
private readonly IGetResultMessageHandler<TMessageDomain, TResult> _getResultMessageHandler;
private readonly IGisLogger _logger;
/// <summary>
/// Количество дней, через которые считается, что запрос не выполнится никогда
/// </summary>
private const int GET_RESULT_TIMEOUT_IN_DAYS = 3;
public GetResultsCoreService(IMessageDomainService<TMessageDomain> messageDomainService,
IGetResultProxyProvider<TGetStateResultProxy, TResultProxy> getResultProxyProvider,
IGetStateProxyConverter<TGetStateResultProxy, TMessageDomain> getStateProxyConverter,
IResultConverter<TResultProxy, TResult> resultConverter,
ISaveResultService<TResult, TMessageDomain> saveResultService,
IGetResultMessageHandler<TMessageDomain, TResult> getResultMessageHandler, IGisLogger logger)
{
_messageDomainService = messageDomainService;
_getResultProxyProvider = getResultProxyProvider;
_getStateProxyConverter = getStateProxyConverter;
_resultConverter = resultConverter;
_saveResultService = saveResultService;
_getResultMessageHandler = getResultMessageHandler;
_logger = logger;
}
public void GetResults(CoreInitData coreInitData)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
try
{
//получаем доменнные сообщения для проверки результата обработки
var messages = _messageDomainService.GetMessageDomainsForGetResults(coreInitData);
foreach (var messageDomain in messages)
{
try
{
//по доменному сообщению получаем getState для проверки результатов обработки сообщения
var getStateProxy = _getStateProxyConverter.ToGetStateResultProxy(messageDomain);
TResultProxy resultProxy;
//проверяем результат обработки.
//если возвращается false, значит сообщение ещё не обработано
//если true, значит можно получать результат обработки
if (_getResultProxyProvider.TryGetResult(getStateProxy, out resultProxy))
{
//полученный ответ преобразовываем из прокси сущности в нашу бизнес-сущность результата обработки
var result = _resultConverter.ToResult(resultProxy);
//сохраняем результат обработки сообщения
_saveResultService.SaveResult(result, messageDomain);
//проставляем статусы обработки сообщения в доменном сообщении
_getResultMessageHandler.Success(messageDomain, result);
}
else
{
if (messageDomain.SendedDate.HasValue
&& DateTime.Now.Subtract(messageDomain.SendedDate.Value).Days > GET_RESULT_TIMEOUT_IN_DAYS)
{
//в течение таймаута не можем получить результат обработки сообщения, помечаем
_getResultMessageHandler.NoResultByTimeout(messageDomain);
}
else
{
//помечаем, что сообщение ещё не обработалось
_getResultMessageHandler.NotReady(messageDomain);
}
}
}
catch (Exception exception)
{
//обрабатываем исключения во время работы
_getResultMessageHandler.Fail(messageDomain, exception);
}
}
stopWatch.Stop();
_logger.Info(this.GetType(), $"По {messages.Count} доменным сообщениям УК {coreInitData.UkId} получено " +
$"{messages.Count(x => x.Status == MessageStatus.Done)} успешных ответов, " +
$"{messages.Count(x => x.Status == MessageStatus.InProcess)} в обработке, " +
$"{messages.Count(x => x.Status == MessageStatus.ResponseTakingError)} упали с ошибкой, " +
$"{messages.Count(x => x.Status == MessageStatus.ResponseTakingErrorTryAgain)} будут отправлены повторно, за {stopWatch.Elapsed}");
}
catch (Exception ex)
{
_logger.Error(this.GetType(),$"Произошло исключение при обработке {coreInitData}", ex);
}
}
}
Заключение
Асинхронная модель взаимодействия позволяет контролировать отправляемую информацию в ГИС ЖКХ за счет соглашения «один MessageGUID — одно выполненное действие». Рекомендую!
На github выложил базовые классы, которыми мы пользуемся для взаимодействия, постарался написать наиболее подробные комментарии. Если использовать такой подход, то остается лишь реализовывать логику подъема данных из своей информационной системы и обрабатывать результат.
Автор: nkochnev