📜 ⬆️ ⬇️

Ensuring uninterrupted client-server interaction (WEB)

With this post I will try to describe the protocol with which you can increase the reliability of the WEB application in the event of problems connecting to the server. I will try to describe abstracting from technologies, however, the text will show examples of server-side Java code and JavaScript / SmartClient on the UI for clarity described and for the reason that this protocol was implemented within the framework of an existing project using these technologies.

Suppose we use a web system that provides a user interface for accessing a bank account. We create a new payment document, specify the details, the amount and click the "hold" button. At this point, somewhere between the client browser and the remote server, there is a connection problem. In response to the XHR request, the status comes 503. It is unambiguous to determine whether the request was received by the remote server and whether it was executed is not possible. The client displays information about the connection error. What will happen if we click “hold” for the same document again and the server has already done it? It can be assumed that the server will most likely produce a validation error stating that such a document has already been posted, or else it will duplicate it. Of course, it all depends on the organization of client-server interaction.

I want to bring to your attention one of the possible solutions to this kind of problems based on a fairly simple protocol.

How does this protocol solve connection problems?
- Client requests are transactional *. If an HTTP connection problem occurs, the user interface is blocked. Attempts are made periodically to send a transaction that fails until a response is received from the service.
- If the request was sent to the server but the response was not delivered back for reasons of connection problems, the client will be resubmitted until a response is received from the Backend service. On the server, it will be determined that the transaction has already been completed earlier and it will return the result from the results cache.
- If the transaction was sent to the server and the response was not received due to a connection failure, the server also determined that the task was not completed yet when the connection was restored, then the corresponding response will be sent to the client and attempts to get the execution result will be repeated until it is counted by the server.
')
* The term transaction, in this context, means an atomic task (or an atomic set of tasks) that performs a request to the service, receives the result of the execution, and makes changes to the user interface. In the event of a failure will be repeated attempts to send it.

When does it make sense to use this protocol?
- When the system architecture is built in such a way that all requests from the client go through one point and fall on one dispatcher (which in turn scatters them to the necessary service).
- When connection interruptions occur frequently.
- If there are long-running, synchronous server tasks and requests are timed out somewhere in the infrastructure between the client and the server. These are the tasks that keep the connection until they finish the execution.

On the project in which I am currently involved, frequent interruptions of the connection occur as they are accessed from ships / platforms in the ocean through a satellite channel. In addition, the system has been developed for a long time and it contains synchronous services that can be executed for a long time, requests to which are cut upon reaching a timeout by a proxy server. Of course, such services are not correct in terms of architecture, but there is something there is. The capabilities of this protocol may not be an alternative to the refactoring of such services, but at least a work round of the above described problem.

Protocol Description

Client part
- Each request to the Backend service is wrapped in a transaction
- Each transaction within one browser window is characterized by a unique transaction identifier. For example, the ID of a new transaction will be calculated by incrementing the ID of the previous one.
- Each request is added a unique identifier of the browser window. This is done due to the fact that several browser windows can operate within a single web session, and transaction identifiers are unique within a single browser window. Details will be described below.
- In case of connection problems (HTTP status, 4xx, 5xx response codes or client timeout), the request and response are sent to the connection error handler.
- The connection error handler blocks the user interface and sends the transaction again for execution at a certain time interval, and displays a corresponding message to the user.
- When the connection is restored, the user interface lock is released and the normal processing of the received response continues.

Server part
- When a request is sent to the dispatcher, the browser's window ID and transaction ID are determined from it.
- The response cache is taken from the server user web session object.
- Based on the response cache, browser window ID and transaction ID, it is determined whether the transaction was completed before.
- If the transaction was completed before the dispatcher returns the result.
- If the transaction is in progress, the server returns information to the client about it.
- If it is determined that the transaction has not yet been executed:
1) in the cache there is a label that for a given browser window ID and transaction ID indicating that the transaction is taken into processing
2) the dispatcher transfers the operation (operations) from the transaction for processing to the corresponding service and receives the result from it
3) the dispatcher puts the result of the transaction into the response cache, respectively, the browser window ID and transaction ID
4) the dispatcher gives the result of the transaction in response.
- When the user server web session becomes outdated, the cache is deleted as part of the deletion of the session object.
- Clearing the answer from the cache is provided when the boundary response time in the cache is reached. This is necessary to prevent memory overflow in case of long user sessions.
- In the case of an attempt to get the result of a transaction that was allocated from the cache after the lifetime has passed, the relevant information about this will be transmitted to the client.

Description of some implementation details

Client part JavaScript, SmartClient 6.5
- in SmartClient, all RPC errors are intercepted by default using the RPCManager.handleError (response, request) method

redefine it and add interception of connection problems as follows
..
if (response.status == isc.RPCResponse.STATUS_TRANSPORT_ERROR || response.status == isc.RPCResponse.STATUS_SERVER_TIMEOUT) {
isc.RPCManager.handleHttpError(response, request);
}
..


We implement the RPCManager.handleHttpError (response, request) method in which we block the user interface (with a message about the problem and with a countdown to the next attempt) and re-send the transaction.

Remember that it is necessary to ensure the uniqueness of the browser identifier within the web session. The browser identifier is defined in the static variable RPCManager.clientId which will be set to timestamp isc.timeStamp () during class initialization.
To add a clientId to each request, override the method through which all RPC requests go in 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 server side, SmartClient DMI
In SmartClient, client-server communication is organized according to their internal protocol SmartClient DMI (Direct Method Invocation). There is its server Java implementation.
Here, the entry point is the com.isomorphic.servlet.IDACall servlet that dispatches requests for specific DMI services.
Inherit from this class and define our implementation in web.xml
Overriding method public void processRequest(HttpServletRequest request, HttpServletResponse response)
If there are no servlet sources, remember that you can always decompile the class file. We will need this to understand what is happening in this servlet and to introduce our functionality into the processing of SmartClient DMI.

From the HTTP request we get the browser identifier that is transmitted by the client.
request.getParameter(CLIENT_ID)

The session object is obtained by request.getSession()
Remember that we have to work carefully since the session object is scrolling between many threads of requests to the server within this session. By combining synchronized block by session calls where needed.

The cache of answers is stored in the session object as the setAttribute()/getAttribute() attribute

The cache structure is as follows:
Map<Long, Map<Long, CacheTransaction>> cache

The key is the browser ID in the session.
The value is a transaction card whose key in turn is the transaction ID and the value of the object with the results of the transaction transactions.

We create these maps with thread-free ones using Collections.synchronizedMap()
A thread-safe here will only be atomic calls, so also do not forget about the synchronized block where necessary.

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

Here, the Map<Long, Object> responses - the map of the service responses by request.
The key here is the hash of the request and the value of the object respons.

In the implementation of this protocol, pay attention to performance and memory used by the cache! Each request will be checked for availability in the cache. In this example implementation, this is not a problem, since each session has its own cache. You can adjust the memory used by setting the lifetime of the object in the cache. Also note that there is no need to store requests in the cache, it is convenient to store the hash on request as a key and respond as a value. Here, the request and response are not meant as HttpRequest and HttpResponse, but objects of the request and response of the target service.
In the case of scalable solutions, we remember that we have session objects. Therefore, we organize either replication of sessions between nodes or load balancing with a sticky session.

Source: https://habr.com/ru/post/124410/


All Articles