diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java index 14427bf..244903e 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java @@ -50,11 +50,16 @@ import cy.agorise.graphenej.models.JsonRpcResponse; import cy.agorise.graphenej.models.OperationHistory; import cy.agorise.graphenej.network.FullNode; import cy.agorise.graphenej.network.LatencyNodeProvider; +import cy.agorise.graphenej.network.NodeLatencyVerifier; import cy.agorise.graphenej.network.NodeProvider; import cy.agorise.graphenej.operations.CustomOperation; import cy.agorise.graphenej.operations.LimitOrderCreateOperation; import cy.agorise.graphenej.operations.TransferOperation; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.Nullable; +import io.reactivex.disposables.Disposable; +import io.reactivex.subjects.PublishSubject; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -76,6 +81,8 @@ public class NetworkService extends Service { public static final String KEY_REQUESTED_APIS = "key_requested_apis"; + public static final String KEY_ENABLE_LATENCY_VERIFIER = "key_enable_latency_verifier"; + /** * Shared preference */ @@ -112,7 +119,17 @@ public class NetworkService extends Service { // Variable used to keep track of the currently obtained API accesses private HashMap mApiIds = new HashMap(); - NodeProvider nodeProvider = new LatencyNodeProvider(); + // Variable used as a source of node information + private NodeProvider nodeProvider = new LatencyNodeProvider(); + + // Class used to obtain frequent node latency updates + private NodeLatencyVerifier nodeLatencyVerifier; + + // PublishSubject used to announce full node latencies updates + private PublishSubject fullNodePublishSubject; + + // Counter used to trigger the connection only after we've received enough node latency updates + private long latencyUpdateCounter; private Gson gson = new GsonBuilder() .registerTypeAdapter(Transaction.class, new Transaction.TransactionDeserializer()) @@ -144,7 +161,7 @@ public class NetworkService extends Service { public void connect(){ OkHttpClient client = new OkHttpClient(); FullNode fullNode = nodeProvider.getBestNode(); - Log.d(TAG,"connect.url: "+fullNode.getUrl()); + Log.v(TAG,"connect.url: "+fullNode.getUrl()); Request request = new Request.Builder().url(fullNode.getUrl()).build(); mWebSocket = client.newWebSocket(request, mWebSocketListener); } @@ -197,18 +214,19 @@ public class NetworkService extends Service { public void onDestroy() { if(mWebSocket != null) mWebSocket.close(NORMAL_CLOSURE_STATUS, null); + + nodeLatencyVerifier.stop(); } @Nullable @Override public IBinder onBind(Intent intent) { - Log.d(TAG,"onBind.intent: "+intent); // Retrieving credentials and requested API data from the shared preferences mUsername = intent.getStringExtra(NetworkService.KEY_USERNAME); mPassword = intent.getStringExtra(NetworkService.KEY_PASSWORD); mRequestedApis = intent.getIntExtra(NetworkService.KEY_REQUESTED_APIS, 0); mAutoConnect = intent.getBooleanExtra(NetworkService.KEY_AUTO_CONNECT, true); - + boolean verifyNodeLatency = intent.getBooleanExtra(NetworkService.KEY_ENABLE_LATENCY_VERIFIER, false); ArrayList nodeUrls = new ArrayList<>(); // If the user of the library desires, a custom list of node URLs can @@ -225,15 +243,61 @@ public class NetworkService extends Service { // Adding the library-provided list of nodes second nodeUrls.addAll(Arrays.asList(Nodes.NODE_URLS)); + // Feeding all node information to the NodeProvider instance for(String nodeUrl : nodeUrls){ nodeProvider.addNode(new FullNode(nodeUrl)); } - if(mAutoConnect) connect(); - + // We only connect automatically if the auto-connect flag is true AND + // we are not going to care about node latency ordering. + if(mAutoConnect && !verifyNodeLatency) { + connect(); + }else{ + // In case we care about node latency ordering, we must first obtain + // a first round of measurements in order to be sure to select the + // best node. + if(verifyNodeLatency){ + ArrayList fullNodes = new ArrayList<>(); + for(String url : nodeUrls){ + fullNodes.add(new FullNode(url)); + } + nodeLatencyVerifier = new NodeLatencyVerifier(fullNodes); + fullNodePublishSubject = nodeLatencyVerifier.start(); + fullNodePublishSubject.observeOn(AndroidSchedulers.mainThread()).subscribe(nodeLatencyObserver); + } + } return mBinder; } + /** + * Observer used to be notified about node latency measurement updates. + */ + private Observer nodeLatencyObserver = new Observer() { + @Override + public void onSubscribe(Disposable d) { } + + @Override + public void onNext(FullNode fullNode) { + latencyUpdateCounter++; + // Updating the node with the new latency measurement + nodeProvider.updateNode(fullNode); + + // Once we have the latency value of all available nodes, + // we can safely proceed to start the connection + if(latencyUpdateCounter == nodeProvider.getSortedNodes().size()){ + connect(); + } + } + + @Override + public void onError(Throwable e) { + Log.e(TAG,"nodeLatencyObserver.onError.Msg: "+e.getMessage()); + } + + @Override + public void onComplete() { } + }; + /** * Class used for the client Binder. Because we know this service always * runs in the same process as its clients, we don't need to deal with IPC. @@ -489,7 +553,7 @@ public class NetworkService extends Service { Log.e(TAG,"onFailure. Exception: "+t.getClass().getName()+", Msg: "+t.getMessage()); // Logging error stack trace for(StackTraceElement element : t.getStackTrace()){ - Log.e(TAG,String.format("%s#%s:%s", element.getClassName(), element.getMethodName(), element.getLineNumber())); + Log.v(TAG,String.format("%s#%s:%s", element.getClassName(), element.getMethodName(), element.getLineNumber())); } // Registering current status isLoggedIn = false; @@ -538,4 +602,12 @@ public class NetworkService extends Service { public List getNodes(){ return nodeProvider.getSortedNodes(); } + + /** + * Returns an observable that will notify its observers about node latency updates. + * @return Observer of {@link FullNode} instances. + */ + public PublishSubject getNodeLatencyObservable(){ + return fullNodePublishSubject; + } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java index c50d777..5d5fb02 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java @@ -51,6 +51,7 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb private int mRequestedApis; private List mCustomNodeUrls = new ArrayList<>(); private boolean mAutoConnect; + private boolean mVerifyLatency; /** * Runnable used to schedule a service disconnection once the app is not visible to the user for @@ -104,7 +105,8 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb .putExtra(NetworkService.KEY_PASSWORD, mPassword) .putExtra(NetworkService.KEY_REQUESTED_APIS, mRequestedApis) .putExtra(NetworkService.KEY_CUSTOM_NODE_URLS, customNodes) - .putExtra(NetworkService.KEY_AUTO_CONNECT, mAutoConnect); + .putExtra(NetworkService.KEY_AUTO_CONNECT, mAutoConnect) + .putExtra(NetworkService.KEY_ENABLE_LATENCY_VERIFIER, mVerifyLatency); context.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); } @@ -182,6 +184,14 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb this.mAutoConnect = mAutoConnect; } + public boolean isVerifyLatency() { + return mVerifyLatency; + } + + public void setVerifyLatency(boolean mVerifyLatency) { + this.mVerifyLatency = mVerifyLatency; + } + /** * Class used to create a {@link NetworkServiceManager} with specific attributes. */ @@ -191,27 +201,53 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb private int requestedApis; private List customNodeUrls; private boolean autoconnect = true; + private boolean verifyNodeLatency; + /** + * Sets the user name, if required to connect to a node. + * @param name User name + * @return The Builder instance + */ public Builder setUserName(String name){ this.username = name; return this; } + /** + * Sets the password, if required to connect to a node. + * @param password Password + * @return The Builder instance + */ public Builder setPassword(String password){ this.password = password; return this; } + /** + * Sets an integer with the requested APIs encoded as binary flags. + * @param apis Integer representing the different APIs we require from the node. + * @return The Builder instance + */ public Builder setRequestedApis(int apis){ this.requestedApis = apis; return this; } + /** + * Adds a list of custom node URLs. + * @param nodeUrls List of custom full node URLs. + * @return The Builder instance + */ public Builder setCustomNodeUrls(List nodeUrls){ this.customNodeUrls = nodeUrls; return this; } + /** + * Adds a list of custom node URLs. + * @param nodeUrls List of custom full node URLs. + * @return The Builder instance + */ public Builder setCustomNodeUrls(String nodeUrls){ String[] urls = nodeUrls.split(","); for(String url : urls){ @@ -221,11 +257,33 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb return this; } + /** + * Sets the autoconnect flag. This is true by default. + * @param autoConnect True if we want the service to connect automatically, false otherwise. + * @return The Builder instance + */ public Builder setAutoConnect(boolean autoConnect){ this.autoconnect = autoConnect; return this; } + /** + * Sets the node-verification flag. This is false by default. + * @param verifyLatency True if we want the service to perform a latency analysis before connecting. + * @return The Builder instance. + */ + public Builder setNodeLatencyVerification(boolean verifyLatency){ + this.verifyNodeLatency = verifyLatency; + return this; + } + + /** + * Method used to build a {@link NetworkServiceManager} instance with all of the characteristics + * passed as parameters. + * @param context A Context of the application package implementing + * this class. + * @return Instance of the NetworkServiceManager class. + */ public NetworkServiceManager build(Context context){ NetworkServiceManager manager = new NetworkServiceManager(context); if(username != null) manager.setUserName(username); @@ -233,6 +291,7 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb if(customNodeUrls != null) manager.setCustomNodeUrls(customNodeUrls); manager.setRequestedApis(requestedApis); manager.setAutoConnect(autoconnect); + manager.setVerifyLatency(verifyNodeLatency); return manager; } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/network/FullNode.java b/graphenej/src/main/java/cy/agorise/graphenej/network/FullNode.java index 4d46173..60eca24 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/network/FullNode.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/FullNode.java @@ -1,10 +1,12 @@ package cy.agorise.graphenej.network; + import cy.agorise.graphenej.stats.ExponentialMovingAverage; /** * Class that represents a full node and is used to keep track of its round-trip time measured in milliseconds. */ public class FullNode implements Comparable { + private String mUrl; private ExponentialMovingAverage latency; diff --git a/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java b/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java index 67befb2..643df1e 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java @@ -1,7 +1,5 @@ package cy.agorise.graphenej.network; -import android.util.Log; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -9,7 +7,7 @@ import java.util.List; import java.util.PriorityQueue; public class LatencyNodeProvider implements NodeProvider { - private final String TAG = this.getClass().getName(); + private final String TAG = "LatencyNodeProvider"; private PriorityQueue mFullNodeHeap; public LatencyNodeProvider(){ @@ -55,8 +53,6 @@ public class LatencyNodeProvider implements NodeProvider { for(FullNode fullNode : nodeList){ if(fullNode != null){ nodeList.add(fullNode); - }else{ - Log.d(TAG,"Found a null node in getSortedNodes"); } } Collections.sort(nodeList); diff --git a/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java b/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java index 5835f63..e10cec1 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java @@ -2,12 +2,14 @@ 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; @@ -18,6 +20,7 @@ import okhttp3.WebSocketListener; * Class that encapsulates the node latency verification task */ public class NodeLatencyVerifier { + private final String TAG = this.getClass().getName(); public static final int DEFAULT_LATENCY_VERIFICATION_PERIOD = 5 * 1000; @@ -30,7 +33,7 @@ public class NodeLatencyVerifier { // Subject used to publish the result to interested parties private PublishSubject subject = PublishSubject.create(); - private HashMap nodeURLMap = new HashMap<>(); + private HashMap nodeURLMap = new HashMap<>(); // private WebSocket webSocket; @@ -100,10 +103,15 @@ public class NodeLatencyVerifier { requestMap.put(fullNode.getUrl(), request); } - client.newWebSocket(request, mWebSocketListener); - if(!nodeURLMap.containsKey(fullNode.getUrl())){ - nodeURLMap.put(fullNode.getUrl(), fullNode); + String normalURL = fullNode.getUrl().replace("wss://", "https://"); + Log.d(TAG,"normal URL : "+normalURL); + if(!nodeURLMap.containsKey(fullNode.getUrl().replace("wss://", "https://"))){ + HttpUrl key = HttpUrl.parse(normalURL); + Log.i(TAG, "Inserting key: "+key.toString()); + nodeURLMap.put(key, fullNode); } + + client.newWebSocket(request, mWebSocketListener); } mHandler.postDelayed(this, verificationPeriod); } @@ -125,14 +133,32 @@ public class NodeLatencyVerifier { 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){ - String url = "wss://" + webSocket.request().url().host() + webSocket.request().url().encodedPath(); - FullNode fullNode = nodeURLMap.get(url); - long after = System.currentTimeMillis(); - long before = timestamps.get(fullNode); - long delay = after - before; - fullNode.addLatencyValue(delay); - subject.onNext(fullNode); + // 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 after = System.currentTimeMillis(); + long before = timestamps.get(fullNode); + long delay = after - before; + fullNode.addLatencyValue(delay); + subject.onNext(fullNode); + }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); } };