Pull to refresh

Обеспечение бесперебойного клиент-серверного взаимодействия(WEB)

Reading time 6 min
Views 2.3K
Данным постом постараюсь описать протокол с помощью которого можно повысить надежность ВЕБ-приложения при возникновении проблем соединения с сервером. Постараюсь описать абстрагируясь от технологий однако в тексте будут приведены примеры серверного Java кода и JavaScript/SmartClient на UI для наглядности описуемого и по причине того что данный протокол был реализован в рамках существующего проекта с использованием данных технологий.

Предположим мы пользуемся веб-системой предоставляющей пользовательский интерфейс доступа к банковскому счету. Мы создаем новый платежный документ указываем реквизиты, сумму и нажимаем кнопку “провести”. В этот момент где-то между клиентским браузером и удаленным сервером возникает проблема с соединением. В ответе на XHR запрос приходит статус 503. Однозначно определить был ли получен удаленным сервером запрос и был ли он выполнен не возможно. На клиент выводится информация об ошибке соединения. Что будет если мы еще раз кликнем “провести” для этого же документа а сервером он уже был проведен? Можно сделать предположение что скорее всего сервер выдаст ошибку валидации о том что такой документ уже был проведен, либо же проведет дубликат. Конечно же все зависит от организации клиент-серверного взаимодействия.

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

Как данный протокол решает проблемы соединения?
— Клиентские запросы являются транзакционными*. При возникновении проблем HTTP-соединения пользовательский интерфейс блокируется. Периодично проводятся попытки отправить транзакцию на которой произошёл сбой до тех пор пока не будет получен ответ из сервиса.
— В случае если запрос был передан на сервер но ответ по причинам проблем соединения не был доставлен обратно, на клиенте будет выполнятся повторная отправка транзакции пока не будет получен ответ из Backend-сервиса. На сервере будет определено что транзакция уже была выполнена ранее и он выдаст результат из кеша результатов.
— Если транзакция была отправлена на сервер и ответ не был получен по причине сбоя соединения, также при восстановлении соединения сервер определил что выполнение задачи еще не завершено тогда будет отправлен соответствующий ответ клиенту и будут повторятся попытки получить результат выполнения пока он не будет подсчитан сервером.

* Под термином транзакция, в данном контексте, подразумевается атомарная задача(либо атомарный набор задач) которая выполняет запрос к сервису, получает результат выполнения и проводит изменения пользовательского интерфейса. В случае возникновения сбоя будут проводится повторные попытки ее отправки.

Когда есть смысл использования данного протокола?
— Когда архитектура системы построена таким образом что все запросы с клиента идут через одну точку и попадают на один диспетчер(который в свою очередь раскидывает их на нужные сервиса).
— Когда часто возникают перебои соединения.
— Если есть долго выполняющиеся, синхронные серверные задачи и запросы режутся по таймауту где-то в инфраструктуре между клиентом и сервером. Имеются ввиду те задачи которые держат соединение пока не закончат выполнение.

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

Описание протокола

Клиентская часть
— Каждый запрос к Backend-сервису обворачивается в транзакцию
— Каждая транзакция в рамках одного окна браузера характеризуется уникальным идентификатором транзакции. К примеру ID новой транзакции будет подсчитан путем инкрементации ID предыдущей.
— К каждому запросу добавляется уникальный идентификатор окна браузера. Это выполняется по причине того что в рамках одной веб-сессии могут работать несколько окон браузера а идентификаторы транзакций уникальны в рамках одного окна браузера. Детали будут описаны ниже.
— При возникновении проблем соединения (HTTP статус коды ответа 4хх, 5хх либо клиентский таймаут) запрос и ответ передаются в обработчик ошибок соединения.
— Обработчик ошибок соединения блокирует пользовательский интерфейс и снова отправляет транзакцию на выполнение через определенный интервал времени о чем выводит пользователю соответствующее сообщение.
— При восстановлении соединения блокировка пользовательского интерфейса снимается и продолжается нормальная обработка полученного ответа.

Серверная часть
— При попадании запроса на диспетчер из него определяется ID окна браузера и ID транзакции.
— Из серверного объекта пользовательской веб-сессии берется кеш ответов.
— На основе кеша ответов, ID окна браузера и ID транзакции определяется была ли выполнена транзакция ранее.
— Если транзакция была выполнена ранее диспетчер возвращает результат.
— Если транзакция в процессе выполнения сервер возвращает клиенту информацию об этом.
— Если определено что транзакция еще не попадала на выполнение:
1) в кеше ставится метка что для данного ID окна браузера и ID транзакции о том что транзакция взята в обработку
2) диспетчер передает операцию(операции) из транзакции на обработку в соответствующий сервис и получает результат из него
3) диспетчер кладет результат выполнения транзакции в кеш ответов соответственно ID окна браузера и ID транзакции
4) диспетчер отдает результат выполнения транзакции в респонс.
— При устаревании пользовательской серверной веб-сессии кеш удаляется в рамках удаления объекта сессии.
— Обеспечивается очистка ответа из кеша при достижении граничного времени жизни ответа в кеше. Это необходимо для предотвращения переполнения пространства памяти в случае долгих пользовательских сессий.
— В случае попытки получить результат по транзакции которая была выделена из кеша по истечении времени жизни клиенту будет передана соответствующая информация об этом.

Описание некоторых деталей реализации

Клиентская чась JavaScript, SmartClient 6.5
— в SmartClient все RPC-ошибки перехватываются по умолчанию методом RPCManager.handleError(response, request)

переопределяем его и добавляем перехват проблем соеденения следующим образом
..
if (response.status == isc.RPCResponse.STATUS_TRANSPORT_ERROR || response.status == isc.RPCResponse.STATUS_SERVER_TIMEOUT) {
isc.RPCManager.handleHttpError(response, request);
}
..


Реализовываем метод RPCManager.handleHttpError(response, request) в котором блокируем пользовательский интерфейс(с сообщением о проблеме и с отсчетом времени до выполнения следующей попытки) и выполняем повторную отправку транзакции.

Помним что надо обеспечить уникальность идентификатора браузера в рамках веб-сессии. Идентификатор браузера определяем в статической переменной RPCManager.clientId которая при инициализации класса будет устанавливаться в таймстамп isc.timeStamp().
Для добавления clientId в каждый запрос переопределяем метод через который идут все RPC запросы в SmartClient-е
isc.RPCManager.addClassMethods({
..
sendRequestOriginal : RPCManager.sendRequest,
sendRequest : function (request) {
if (request.params == null) {
request.params = new Object();
}
request.params.clientId = RPCManager.clientId;
isc.RPCManager.sendRequestOriginal(request);
}
..


Серверная часть Java, SmartClient DMI
В SmartClient клиент-серверное взаимодействие организовано по их внутреннему протоколу SmartClient DMI(Direct Method Invocation). Существует его серверная Java реализация.
Здесь точкой входа является сервлет com.isomorphic.servlet.IDACall который диспетчиризирует запросы на конкретные DMI сервисы.
Наследуемся от данного класса и определяем нашу реализацию в web.xml
Переопределяем метод public void processRequest(HttpServletRequest request, HttpServletResponse response)
Если нету исходных кодов сервлета помним что всегда можно декомпилировать класс файл. Это нам понадобится для понимания того что именно происходит в данном сервлете и для внедрения нашего функционала в обработку SmartClient DMI.

Из HTTP реквеста получаем идентификатор браузера который передается клиентом
request.getParameter(CLIENT_ID)

Объект сессии получаем посредством request.getSession()
Помним что тут надо работать внимательно поскольку объект сессии шарится между многими потоками запросов к серверу в рамках данной сессии. Объедением в synchronized блок по сессии вызовы там где это необходимо.

Кеш респонсов храним в объекте сессии как аттрибут setAttribute()/getAttribute()

Структура кеша следующая:
Map<Long, Map<Long, CacheTransaction>> cache

Ключом является идентификатор браузера в сессии
Значением является карта транзакций у которой в свою очередь ключом является ID транзакции а значением объект с результатами операций по транзакции.

Эти карты создаем потокобезапасными посредством Collections.synchronizedMap()
Потокобезопасными здесь будут только атомарные вызовы, потому также не забываем про synchronized блок где это необходимо.

public class CacheTransaction {
private long transactionNum;
private long requestTimestamp;
private Map<Long, Object> responses;
..

Здесь Map<Long, Object> responses — карта респонсов сервисов по их реквестам.
Ключом здесь является хеш по реквесту а значением объект респонса.

В реализации данного протокола обращайте внимание на производительность и на используемую кешом память! Каждый запрос будет проверятся на наличие в кеше. В данном примере реализации это не является проблемой поскольку в каждой сессий свой кеш. Регулировать используемую память можно посредством настройки времени жизни объекта в кеше. Также обратите внимание что нет необходимости хранить запросы в кеше, удобно хранить хеш по запросу как ключ и респонс как значение. Здесь под реквест и респонс подразумевается не HttpRequest и HttpResponse а объекты реквеста и респонса целевого сервиса.
В случае масштабируемых решений помним что у нас есть сессионные объекты. Поэтому организовываем либо репликацию сессий между нодами либо балансировку нагрузки с липкой сессией.
Tags:
Hubs:
-1
Comments 4
Comments Comments 4

Articles