graphenej/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java

211 lines
7.4 KiB
Java

package cy.agorise.graphenej.network;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.HashMap;
import java.util.List;
import cy.agorise.graphenej.api.android.NetworkService;
import io.reactivex.subjects.PublishSubject;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
/**
* Class that encapsulates the node latency verification task
*/
public class NodeLatencyVerifier {
private final String TAG = this.getClass().getName();
private static final int DEFAULT_LATENCY_VERIFICATION_PERIOD = 5 * 1000;
// Variable used to store the list of nodes that should be verified
private List<FullNode> mNodeList;
// Variable used to store the desired verification period
private long verificationPeriod;
// Subject used to publish the result to interested parties
private PublishSubject<FullNode> subject = PublishSubject.create();
private HashMap<HttpUrl, FullNode> nodeURLMap = new HashMap<>();
// Map used to store the first timestamp required for a RTT (Round Trip Time) measurement.
// If:
// RTT = t2 - t1
// This map will hold the value of t1 for each one of the nodes to be measured.
private HashMap<FullNode, Long> timestamps = new HashMap<>();
private HashMap<String, Request> requestMap = new HashMap<>();
private Handler mHandler = new Handler(Looper.getMainLooper());
private OkHttpClient client;
public NodeLatencyVerifier(List<FullNode> nodes){
this(nodes, DEFAULT_LATENCY_VERIFICATION_PERIOD);
}
public NodeLatencyVerifier(List<FullNode> nodes, long period){
mNodeList = nodes;
verificationPeriod = period;
}
/**
* Method used to start the latency verification task.
* <p>
* The returning object can be used for interested parties to receive constant updates
* regarding new latency measurements for every full node.
* </p>
* @return A {@link PublishSubject} class instance.
*/
public PublishSubject start(){
mHandler.post(mVerificationTask);
return subject;
}
/**
* Method used to cancel the verification task.
*/
public void stop(){
mHandler.removeCallbacks(mVerificationTask);
}
/**
* Node latency verification task.
*/
private final Runnable mVerificationTask = new Runnable() {
@Override
public void run() {
for(FullNode fullNode : mNodeList){
long before = System.currentTimeMillis();
timestamps.put(fullNode, before);
// We want to reuse the same OkHttpClient instance if possible
if(client == null) client = new OkHttpClient();
// Same thing with the Request instance, we want to reuse them. But since
// we might have one request per node, we keep them in a map.
Request request;
if(requestMap.containsKey(fullNode.getUrl())){
request = requestMap.get(fullNode.getUrl());
}else{
// If the map had no entry for the request we want, we create one
// and add it to the map.
request = new Request.Builder().url(fullNode.getUrl()).build();
requestMap.put(fullNode.getUrl(), request);
}
String normalURL = fullNode.getUrl().replace("wss://", "https://");
HttpUrl key = HttpUrl.parse(normalURL);
if(!nodeURLMap.containsKey(key)){
nodeURLMap.put(key, fullNode);
}
client.newWebSocket(request, mWebSocketListener);
}
mHandler.postDelayed(this, verificationPeriod);
}
};
/**
* Listener that will be called upon a server response.
*/
private WebSocketListener mWebSocketListener = new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
handleResponse(webSocket, response);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
handleResponse(webSocket, response);
}
/**
* Method used to handle the node's first response. The idea here is to obtain
* the RTT (Round Trip Time) measurement and publish it using the PublishSubject.
*
* @param webSocket WebSocket instance
* @param response Response instance
*/
private void handleResponse(WebSocket webSocket, Response response){
synchronized (this){
// Obtaining the HttpUrl instance that was previously used as a key
HttpUrl url = webSocket.request().url();
if(nodeURLMap.containsKey(url)){
FullNode fullNode = nodeURLMap.get(url);
long delay;
if(response == null) {
// There is no internet connection, or the node is unreachable. We are just
// putting an artificial delay.
delay = Long.MAX_VALUE;
} else {
long after = System.currentTimeMillis();
long before = timestamps.get(fullNode);
delay = after - before;
}
if(fullNode != null){
fullNode.addLatencyValue(delay);
subject.onNext(fullNode);
}else{
Log.w(TAG,"Could not extract FullNode instance from the map");
}
}else{
// We cannot properly handle a response to a request whose
// URL was not registered at the nodeURLMap. This is because without this,
// we cannot know to which node this response corresponds. This should not happen.
Log.e(TAG,"nodeURLMap does not contain url: "+url);
for(HttpUrl key : nodeURLMap.keySet()){
Log.e(TAG,"> "+key);
}
}
webSocket.close(NetworkService.NORMAL_CLOSURE_STATUS, null);
}
}
};
/**
* Updates the 'isConnected' attribute of a specific node.
* @param fullNode The node we want to update.
*/
public void updateActiveNodeInformation(FullNode fullNode){
for(FullNode node : mNodeList){
if(node.equals(fullNode)){
node.setConnected(fullNode.isConnected());
}
}
}
/**
* Removes the given node from the nodes list
* @param fullNode The node to remove
*/
public void removeNode(FullNode fullNode){
for(FullNode node : mNodeList){
if(node.equals(fullNode)){
mNodeList.remove(node);
String normalURL = node.getUrl().replace("wss://", "https://");
HttpUrl key = HttpUrl.parse(normalURL);
nodeURLMap.remove(key);
node.setRemoved(true);
subject.onNext(node);
break;
}
}
}
public List<FullNode> getNodeList(){
return mNodeList;
}
}