ДВС ЖКГ: асинхронна модель взаємодії

Продовжую ділитися досвідом взаємодії з ДВС ЖКГ. Наступним завданням, після встановлення захищеного з'єднання, стала організація обміну повідомленнями. Розробники ДВС ЖКГ пропонують дві моделі взаємодії: синхронну й асинхронну. Деякі розробники вибирають синхронну модель через її простоти і доступності. В цій статті спробую пояснити, чому потрібно використовувати саме асинхронну модель і дати підказки по реалізації на C#.

Вибір моделі взаємодії

Синхронна модель передбачає класичний «запит-відповідь». Ви формуєте об'єкт запиту, відправляєте його в ГІС ЖКГ, і система утримує з'єднання до того моменту як не буде сформований відповідь або з'єднання не розірветься по таймауту.

Асинхронна модель будується на двох операціях: постановка завдання на виконання дії і запит стану виконання дії (при необхідності можна повторити кілька разів). Саме ця модель взаємодії називалася розробниками ДВС ЖКГ як краща і рекомендована. Далі ми докладно розглянемо обидві ці операції.

Постановка завдання на виконання дії
Для отримання інформації або відправки даних в ГІС ЖКГ, необхідно заповнити об'єкт запиту. Саме в ньому знаходиться інформація «по якому будинку потрібно отримати інформацію» або «які саме особові рахунки потрібно створити в ГІС ЖКГ».

У повідомленні також вказується ідентифікатор повідомлення (MessageGUID), він однозначно ідентифікує повідомлення в інформаційній системі. Запит, з одним і тим же MessageGUID можна кілька разів відправляти в ГІС ЖКГ, і ГІС ЖКГ гарантує, що він виконається один раз. Наприклад, при постановці завдання на створення особових рахунків запит впав по таймауту або зв'язок раптово перервався, ми можемо потім ще раз надіслати цей запит з упевненістю, що зайвих особових рахунків ми не створимо.

Якщо виконується відправка масиву об'єктів, кожному об'єкту присвоюється TransportGUID, він дозволяє зіставити об'єкт запиту та відповіді. Наприклад, з результату обробки ми зможемо дізнатися, чому саме цей особовий рахунок з усього повідомлення не приймається ДВС ЖКГ.

У відповідь ми отримаємо інший MessageGUID – ідентифікатор запиту в ГІС ЖКГ. Саме з цього ідентифікатора буде запитуватися стан виконання запиту в ГІС ЖКГ.

Запит стану виконання дії
Для отримання стану виконання потрібно виконати короткий запит getStateResult. У ньому вказується MessageGUID, присвоєний в ГІС ЖКГ. Цей запит можна відправляти кілька разів.

У відповіді ми отримаємо стан обробки повідомлення: прийнято, в обробці, готово. Якщо повідомлення оброблено, ДВС ЖКГ повертає результат обробки (отримується об'єкт або інформацію про відправку даних), або повідомляє про помилки, що сталися при обробці повідомлення. Найпоширеніші помилки: «EXP001000: Внутрішня помилка.», «Доступ заборонений для постачальника даних організація ".*" повноваження ".*"», «Віддалений сервер повернув несподівану відповідь: (502) Bad Gateway.», «Прослуховування на api.dom.gosuslugi.ru* не виконувала ні одна кінцева точка, яка могла б прийняти повідомлення.» та інше. По суті, вони ділять повідомлення на два типи: ті повідомлення, які можна відправити ще раз і ті, які вже немає сенсу повторно відправляти. Для себе вирішили, що, якщо отримуємо оброблену помилку ДВС ЖКГ, то повторно запит не відправляємо. Якщо запит відвалився по таймауту або проблема в нашому коді, відправляємо повідомлення ще раз.

Типові помилки ДВС ЖКГ

  1. Виконання обробки повідомлення на стороні ДВС ЖКГ проходить не в транзакції.
    Наприклад, ми відправляємо повідомлення на створення 30 особових рахунків, в результаті отримуємо «EXP001000: Внутрішня помилка.». Ми справедливо очікуємо, що жоден з особових рахунків не призвела, однак при контрольній перевірці бачимо, що ВСІ особові рахунки створилися.
  2. Деякі запити «зависають» у статусі «прийнято» або «в обробці» на невизначений час.
    Зазвичай результат обробки повідомлення можна отримати через кілька секунд, але деякі повідомлення висять в необробленому статус кілька днів, такі повідомлення на своїй стороні позначати, щоб не питати стан обробки в черговий раз.
Із-за таких «особливостей» процес відправки інформації в ГІС ЖКГ ділиться на три етапи:
  1. Завантажуємо інформацію з ДВС ЖКГ для звірки поточного стану
  2. Відправка потрібної інформації в ГІС ЖКГ
  3. Контрольна перевірка завантаженої інформації, може бути, щось знову впало з «EXP001000: Внутрішня помилка.», а інформація була створена.

Технічна сторона взаємодії

Будь-який процес взаємодії з ДВС ЖКГ складається з трьох етапів:
  1. Отримання інформації для повідомлень, збереження її в БД
  2. Створення проксі-об'єктів ГІС ЖКГ (нагадаю, ми працюємо через WCF), відправлення повідомлення, обробка відповіді, збереження MessageGUID ДВС ЖКГ
  3. Отримання результату обробки, обробка результату
Створення повідомлень
Для кожного типу взаємодії ми створюємо таблицю в БД, наприклад, ExportHouseInfoMessages або ImportLsMessages, в ній ми зберігаємо всю необхідну інформацію для створення проксі-об'єкта повідомлення, яку будемо відправляти в ГІС ЖКГ. Саме на цьому етапі ми створюємо MessageGuid повідомлення.

На цьому етапі потрібно створити повідомлення тільки по тим даним, за якими ще не були створені повідомлення і не отриманий результат обробки, інакше можна задублировать дані.

Головна складність цього етапу – дістати дані, що необхідні для надсилання, зі своєї інформаційної системи. Наприклад, у ГІС ЖКГ потрібно відправляти всі зміни в особових рахунках, а у нас не було історії зміни у потрібному розрізі.
Приклад реалізації на C#
/// < 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} за {секундомір.Elapsed}");
}
catch (Exception ex)
{
_logger.Error(this.GetType(), $"Відбулося виключення при обробці {coreInitData}", ex);
}
}
}


Відправка повідомлень
На цьому етапі:
  • Піднімаємо повідомлення з БД, які потрібно відправити в ГІС ЖКГ
  • Створюємо за ним проксі об'єкти повідомлень
  • Відправляємо в ГІС ЖКГ
  • Отримуємо MessageGUID ДВС ЖКГ
  • Зберігаємо його в повідомленні в БД
Приклад реалізації на C#
/// < 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)} будуть відправлені повторно, за {секундомір.Elapsed}");
}
catch (Exception ex)
{
_logger.Error(this.GetType(), $"Відбулося виключення при обробці {coreInitData}", ex);
}
}
}


Отримання результату обробки повідомлення
На цьому етапі:
  • Піднімаємо повідомлення з БД, за яким потрібно отримати результати
  • Формуємо проксі об'єкт отримання стану обробки
  • Відправляємо в ГІС ЖКГ
  • Якщо повідомлення ще не опрацьовано, зберігаємо в БД, що повідомлення не опрацьовано. Якщо після відправлення повідомлення пройшло занадто багато часу, помічаємо повідомлення.
  • Якщо за повідомленням є результат, його потрібно обробити. Зазвичай це збереження прив'язок ідентифікаторів об'єктів в ГІС ЖКГ і в нашій ІС.
Приклад реалізації на C#
/ / / < 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, значить повідомлення ще не опрацьовано
//якщо так, значить можна отримувати результат обробки
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)} будуть відправлені повторно, за {секундомір.Elapsed}");
}
catch (Exception ex)
{
_logger.Error(this.GetType(),$"Відбулося виключення при обробці {coreInitData}", ex);
}
}
}


Висновок

Асинхронна модель взаємодії дозволяє контролювати відправляється інформацію в ГІС ЖКГ за рахунок угоди «один MessageGUID — одне виконане дію». Рекомендую!

github виклав базові класи, якими ми користуємося для взаємодії, постарався написати найбільш докладні коментарі. Якщо використовувати такий підхід, то залишається лише реалізовувати логіку підйому даних зі своєї інформаційної системи і обробляти результат.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.