Merge branch 'feat_ping_nodes' into develop

develop
Nelson R. Perez 2018-09-21 13:19:13 -05:00
commit a28775d464
17 changed files with 914 additions and 76 deletions

View File

@ -23,6 +23,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
defaultConfig {
multiDexEnabled true
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
dependencies {
@ -32,6 +36,10 @@ dependencies {
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
implementation group: "org.tukaani", name: "xz", version: "1.6"
androidTestImplementation 'com.android.support:support-annotations:27.1.1'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
// Rx dependencies
api 'io.reactivex.rxjava2:rxandroid:2.0.2'
api 'io.reactivex.rxjava2:rxjava:2.1.16'

View File

@ -0,0 +1,83 @@
package cy.agorise.graphenej;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.api.bitshares.Nodes;
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 io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
import io.reactivex.subjects.PublishSubject;
@RunWith(AndroidJUnit4.class)
public class NodeLatencyVerifierTest {
private final String TAG = this.getClass().getName();
@Test
public void testNodeLatencyTest() throws Exception {
ArrayList<FullNode> nodeList = new ArrayList<>();
nodeList.add(new FullNode(Nodes.NODE_URLS[0]));
nodeList.add(new FullNode(Nodes.NODE_URLS[1]));
nodeList.add(new FullNode(Nodes.NODE_URLS[2]));
final NodeLatencyVerifier nodeLatencyVerifier = new NodeLatencyVerifier(nodeList);
PublishSubject subject = nodeLatencyVerifier.start();
final NodeProvider nodeProvider = new LatencyNodeProvider();
subject.subscribe(new Observer<FullNode>() {
int counter = 0;
@Override
public void onSubscribe(Disposable d) {}
@Override
public void onNext(FullNode fullNode) {
Log.i(TAG,String.format("Avg latency: %.2f, url: %s", fullNode.getLatencyValue(), fullNode.getUrl()));
// Updating node provider
nodeProvider.updateNode(fullNode);
List<FullNode> sortedNodes = nodeProvider.getSortedNodes();
for(FullNode node : sortedNodes){
Log.d(TAG,String.format("> %.2f, url: %s", node.getLatencyValue(), node.getUrl()));
}
// Finish test after certain amount of rounds
if(counter > 3){
synchronized (NodeLatencyVerifierTest.this){
nodeLatencyVerifier.stop();
NodeLatencyVerifierTest.this.notifyAll();
}
}
counter++;
}
@Override
public void onError(Throwable e) {
Log.e(TAG,"onError.Msg: "+e.getMessage());
synchronized (NodeLatencyVerifierTest.this){
NodeLatencyVerifierTest.this.notifyAll();
}
}
@Override
public void onComplete() {
Log.d(TAG,"onComplete");
}
});
try {
synchronized(this) {
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

View File

@ -3,6 +3,7 @@
package="cy.agorise.graphenej">
<uses-sdk android:minSdkVersion="1" />
<uses-permission android:name="android.permission.INTERNET" />
<application>
<service

View File

@ -2,10 +2,8 @@ package cy.agorise.graphenej.api.android;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.gson.Gson;
@ -23,6 +21,7 @@ import cy.agorise.graphenej.Asset;
import cy.agorise.graphenej.AssetAmount;
import cy.agorise.graphenej.BaseOperation;
import cy.agorise.graphenej.LimitOrder;
import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.UserAccount;
@ -49,11 +48,18 @@ import cy.agorise.graphenej.models.HistoryOperationDetail;
import cy.agorise.graphenej.models.JsonRpcNotification;
import cy.agorise.graphenej.models.JsonRpcResponse;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.Memo;
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;
@ -67,7 +73,7 @@ import okhttp3.WebSocketListener;
public class NetworkService extends Service {
private final String TAG = this.getClass().getName();
private static final int NORMAL_CLOSURE_STATUS = 1000;
public static final int NORMAL_CLOSURE_STATUS = 1000;
public static final String KEY_USERNAME = "key_username";
@ -75,6 +81,13 @@ 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
*/
public static final String KEY_AUTO_CONNECT = "key_auto_connect";
/**
* Constant used to pass a custom list of node URLs. This should be a simple
* comma separated list of URLs.
@ -89,8 +102,6 @@ public class NetworkService extends Service {
private WebSocket mWebSocket;
private int mSocketIndex;
// Username and password used to connect to a specific node
private String mUsername;
private String mPassword;
@ -100,13 +111,25 @@ public class NetworkService extends Service {
private String mLastCall;
private long mCurrentId = 0;
private boolean mAutoConnect;
// Requested APIs passed to this service
private int mRequestedApis;
// Variable used to keep track of the currently obtained API accesses
private HashMap<Integer, Integer> mApiIds = new HashMap<Integer, Integer>();
private ArrayList<String> mNodeUrls = new ArrayList<>();
// 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<FullNode> 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())
@ -132,36 +155,15 @@ public class NetworkService extends Service {
// suited for every response type.
private DeserializationMap mDeserializationMap = new DeserializationMap();
@Override
public void onCreate() {
super.onCreate();
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
// Retrieving credentials and requested API data from the shared preferences
mUsername = pref.getString(NetworkService.KEY_USERNAME, "");
mPassword = pref.getString(NetworkService.KEY_PASSWORD, "");
mRequestedApis = pref.getInt(NetworkService.KEY_REQUESTED_APIS, -1);
// If the user of the library desires, a custom list of node URLs can
// be passed using the KEY_CUSTOM_NODE_URLS constant
String serializedNodeUrls = pref.getString(NetworkService.KEY_CUSTOM_NODE_URLS, "");
// Deciding whether to use an externally provided list of node URLs, or use our internal one
if(serializedNodeUrls.equals("")){
mNodeUrls.addAll(Arrays.asList(Nodes.NODE_URLS));
}else{
String[] urls = serializedNodeUrls.split(",");
mNodeUrls.addAll(Arrays.asList(urls));
}
connect();
}
private void connect(){
/**
* Actually establishes a connection from this Service to one of the full nodes.
*/
public void connect(){
OkHttpClient client = new OkHttpClient();
String url = mNodeUrls.get(mSocketIndex % mNodeUrls.size());
Log.d(TAG,"Trying to connect with: "+url);
Request request = new Request.Builder().url(url).build();
client.newWebSocket(request, mWebSocketListener);
FullNode fullNode = nodeProvider.getBestNode();
Log.v(TAG,"connect.url: "+fullNode.getUrl());
Request request = new Request.Builder().url(fullNode.getUrl()).build();
mWebSocket = client.newWebSocket(request, mWebSocketListener);
}
public long sendMessage(String message){
@ -212,19 +214,90 @@ public class NetworkService extends Service {
public void onDestroy() {
if(mWebSocket != null)
mWebSocket.close(NORMAL_CLOSURE_STATUS, null);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
nodeLatencyVerifier.stop();
}
@Nullable
@Override
public IBinder 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<String> nodeUrls = new ArrayList<>();
// If the user of the library desires, a custom list of node URLs can
// be passed using the KEY_CUSTOM_NODE_URLS constant
String customNodeUrls = intent.getStringExtra(NetworkService.KEY_CUSTOM_NODE_URLS);
// Adding user-provided list of node URLs first
if(customNodeUrls != null){
String[] urls = customNodeUrls.split(",");
ArrayList<String> urlList = new ArrayList<>(Arrays.asList(urls));
nodeUrls.addAll(urlList);
}
// 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));
}
// 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<FullNode> 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<FullNode> nodeLatencyObserver = new Observer<FullNode>() {
@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.
@ -241,7 +314,6 @@ public class NetworkService extends Service {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
mWebSocket = webSocket;
// Notifying all listeners about the new connection status
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.CONNECTED, ApiAccess.API_NONE));
@ -481,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;
@ -494,9 +566,8 @@ public class NetworkService extends Service {
}
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE));
mSocketIndex++;
if(mSocketIndex > mNodeUrls.size() * 3){
if(nodeProvider.getBestNode() == null){
Log.e(TAG,"Giving up on connections");
stopSelf();
}else{
@ -516,11 +587,27 @@ public class NetworkService extends Service {
return mApiIds.get(whichApi) != null;
}
public ArrayList<String> getNodeUrls() {
return mNodeUrls;
/**
* Updates the full node details
* @param fullNode Updated {@link FullNode} instance
*/
public void updateNode(FullNode fullNode){
nodeProvider.updateNode(fullNode);
}
public void setNodeUrls(ArrayList<String> mNodeUrls) {
this.mNodeUrls = mNodeUrls;
/**
* Returns a list of {@link FullNode} instances
* @return List of full nodes
*/
public List<FullNode> getNodes(){
return nodeProvider.getSortedNodes();
}
/**
* Returns an observable that will notify its observers about node latency updates.
* @return Observer of {@link FullNode} instances.
*/
public PublishSubject<FullNode> getNodeLatencyObservable(){
return fullNodePublishSubject;
}
}

View File

@ -11,6 +11,9 @@ import android.os.Handler;
import android.os.IBinder;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* This class should be instantiated at the application level of the android app.
@ -20,8 +23,7 @@ import java.lang.ref.WeakReference;
*/
public class NetworkServiceManager implements Application.ActivityLifecycleCallbacks {
private final String TAG = this.getClass().getName();
private final static String TAG = "NetworkServiceManager";
/**
* Constant used to specify how long will the app wait for another activity to go through its starting life
* cycle events before running the teardownConnectionTask task.
@ -43,6 +45,14 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb
// In case we want to interact directly with the service
private NetworkService mService;
// Attributes that might need to be passed to the NetworkService
private String mUserName = "";
private String mPassword = "";
private int mRequestedApis;
private List<String> 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
* more than DISCONNECT_DELAY milliseconds.
@ -77,8 +87,27 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb
public void onActivityStarted(Activity activity) {
mHandler.removeCallbacks(mDisconnectRunnable);
if(mService == null){
// Creating a new Intent that will be used to start the NetworkService
Context context = mContextReference.get();
Intent intent = new Intent(context, NetworkService.class);
// Adding user-provided node URLs
StringBuilder stringBuilder = new StringBuilder();
Iterator<String> it = mCustomNodeUrls.iterator();
while(it.hasNext()){
stringBuilder.append(it.next());
if(it.hasNext()) stringBuilder.append(",");
}
String customNodes = stringBuilder.toString();
// Adding all
intent.putExtra(NetworkService.KEY_USERNAME, mUserName)
.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_ENABLE_LATENCY_VERIFIER, mVerifyLatency);
context.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
}
@ -114,4 +143,156 @@ public class NetworkServiceManager implements Application.ActivityLifecycleCallb
@Override
public void onServiceDisconnected(ComponentName componentName) {}
};
public String getUserName() {
return mUserName;
}
public void setUserName(String mUserName) {
this.mUserName = mUserName;
}
public String getPassword() {
return mPassword;
}
public void setPassword(String mPassword) {
this.mPassword = mPassword;
}
public int getRequestedApis() {
return mRequestedApis;
}
public void setRequestedApis(int mRequestedApis) {
this.mRequestedApis = mRequestedApis;
}
public List<String> getCustomNodeUrls() {
return mCustomNodeUrls;
}
public void setCustomNodeUrls(List<String> mCustomNodeUrls) {
this.mCustomNodeUrls = mCustomNodeUrls;
}
public boolean isAutoConnect() {
return mAutoConnect;
}
public void setAutoConnect(boolean mAutoConnect) {
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.
*/
public static class Builder {
private String username;
private String password;
private int requestedApis;
private List<String> 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<String> 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){
if(customNodeUrls == null) customNodeUrls = new ArrayList<>();
customNodeUrls.add(url);
}
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);
if(password != null) manager.setPassword(password);
if(customNodeUrls != null) manager.setCustomNodeUrls(customNodeUrls);
manager.setRequestedApis(requestedApis);
manager.setAutoConnect(autoconnect);
manager.setVerifyLatency(verifyNodeLatency);
return manager;
}
}
}

View File

@ -9,5 +9,7 @@ public class Nodes {
"wss://dexnode.net/ws", // Dallas, USA
"wss://bitshares.crypto.fans/ws", // Munich, Germany
"wss://bitshares.openledger.info/ws", // Openledger node
"wss://us.nodes.bitshares.ws",
"wss://eu.nodes.bitshares.ws"
};
}

View File

@ -0,0 +1,78 @@
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;
private FullNode(){}
public FullNode(String url){
latency = new ExponentialMovingAverage(ExponentialMovingAverage.DEFAULT_ALPHA);
this.mUrl = url;
}
/**
* Full node URL getter
* @return
*/
public String getUrl() {
return mUrl;
}
/**
* Full node URL setter
* @param mUrl
*/
public void setUrl(String mUrl) {
this.mUrl = mUrl;
}
/**
*
* @return The exponential moving average object instance
*/
public ExponentialMovingAverage getLatencyAverage(){
return latency;
}
/**
*
* @return The latest latency average value
*/
public double getLatencyValue() {
return latency.getAverage();
}
/**
* Method that updates the latency average with a new value.
* @param latency Most recent latency sample to be added to the exponential average
*/
public void addLatencyValue(double latency) {
this.latency.updateValue(latency);
}
@Override
public int compareTo(Object o) {
FullNode node = (FullNode) o;
return (int) Math.ceil(latency.getAverage() - node.getLatencyValue());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FullNode fullNode = (FullNode) o;
return mUrl.equals(fullNode.getUrl());
}
@Override
public int hashCode() {
return mUrl.hashCode();
}
}

View File

@ -0,0 +1,61 @@
package cy.agorise.graphenej.network;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;
public class LatencyNodeProvider implements NodeProvider {
private final String TAG = "LatencyNodeProvider";
private PriorityQueue<FullNode> mFullNodeHeap;
public LatencyNodeProvider(){
mFullNodeHeap = new PriorityQueue<>();
}
@Override
public FullNode getBestNode() {
return mFullNodeHeap.peek();
}
@Override
public void addNode(FullNode fullNode) {
mFullNodeHeap.add(fullNode);
}
@Override
public boolean updateNode(FullNode fullNode) {
mFullNodeHeap.remove(fullNode);
return mFullNodeHeap.offer(fullNode);
}
/**
* Updates an existing node with the new latency value.
*
* @param fullNode Existing full node instance
* @param latency New latency measurement
* @return True if the node priority was updated successfully
*/
public boolean updateNode(FullNode fullNode, int latency){
if(mFullNodeHeap.remove(fullNode)){
fullNode.addLatencyValue(latency);
return mFullNodeHeap.add(fullNode);
}else{
return false;
}
}
@Override
public List<FullNode> getSortedNodes() {
FullNode[] nodeArray = mFullNodeHeap.toArray(new FullNode[mFullNodeHeap.size()]);
ArrayList<FullNode> nodeList = new ArrayList<>();
for(FullNode fullNode : nodeList){
if(fullNode != null){
nodeList.add(fullNode);
}
}
Collections.sort(nodeList);
return Arrays.asList(nodeArray);
}
}

View File

@ -0,0 +1,165 @@
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();
public 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<>();
// private WebSocket webSocket;
// 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 OkHttoClient 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://");
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);
}
};
/**
* 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){
// 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);
}
};
}

View File

@ -0,0 +1,38 @@
package cy.agorise.graphenej.network;
import java.util.List;
/**
* Interface used to describe the high level characteristics of a class that will
* hold and manage a list of {@link FullNode} instances.
*
* The idea is that the class implementing this interface should provide node instances
* and thus URLs for the {@link cy.agorise.graphenej.api.android.NetworkService} with
* different sorting heuristics.
*/
public interface NodeProvider {
/**
* Returns the node with the best characteristics. Returns null if there is no {@link FullNode}
* @return A FullNode instance
*/
FullNode getBestNode();
/**
* Adds a new node to the queue
* @param fullNode {@link FullNode} instance to add.
*/
void addNode(FullNode fullNode);
/**
* Updates the rating of a specific node that is already in the NodeProvider
* @param fullNode
*/
boolean updateNode(FullNode fullNode);
/**
* Returns an ordered list of {@link FullNode} instances.
* @return
*/
List<FullNode> getSortedNodes();
}

View File

@ -0,0 +1,49 @@
package cy.agorise.graphenej.stats;
/**
* Class used to compute the Exponential Moving Average of a sequence of values.
* For more details see <a href="https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average">here</a>.
*/
public class ExponentialMovingAverage {
public static final double DEFAULT_ALPHA = 0.5;
private double alpha;
private Double accumulatedValue;
/**
* Constructor, which takes only the alpha parameter as an argument.
*
* @param alpha The coefficient alpha represents the degree of weighting decrease, a constant
* smoothing factor between 0 and 1. A higher alpha discounts older observations faster.
*/
public ExponentialMovingAverage(double alpha) {
this.alpha = alpha;
}
/**
* Method that updates the average with a new sample
* @param value New value
* @return The updated average value
*/
public double updateValue(double value) {
if (accumulatedValue == null) {
accumulatedValue = value;
return value;
}
double newValue = accumulatedValue + alpha * (value - accumulatedValue);
accumulatedValue = newValue;
return newValue;
}
/**
*
* @return Returns the current average value
*/
public double getAverage(){
return accumulatedValue == null ? 0 : accumulatedValue;
}
public void setAlpha(double alpha){
this.alpha = alpha;
this.accumulatedValue = null;
}
}

View File

@ -0,0 +1,31 @@
package cy.agorise.graphenej.network;
import junit.framework.Assert;
import org.junit.Test;
public class FullNodeTest {
@Test
public void testFullNodeComparable(){
FullNode nodeA = new FullNode("wss://dummy");
FullNode nodeB = new FullNode("wss://dummy");
FullNode nodeC = new FullNode("wss://dummy");
nodeA.addLatencyValue(100);
nodeB.addLatencyValue(200);
nodeC.addLatencyValue(100);
Assert.assertTrue("Makes sure the node nodeA.compareTo(nodeB) returns a negative value", nodeA.compareTo(nodeB) < 0);
Assert.assertTrue("Makes sure nodeA.compareTo(nodeB) returns zero", nodeA.compareTo(nodeC) == 0);
Assert.assertTrue("Makes sure nodeB.compareTo(nodeA) returns a positive value", nodeB.compareTo(nodeA) > 0);
}
@Test
public void testFullNodeAverageLatency(){
FullNode fullNode = new FullNode("wss://dummy");
fullNode.getLatencyAverage().setAlpha(0.5);
fullNode.addLatencyValue(100);
Assert.assertEquals(100.0, fullNode.getLatencyValue());
fullNode.addLatencyValue(50);
Assert.assertEquals(75.0, fullNode.getLatencyValue());
}
}

View File

@ -0,0 +1,63 @@
package cy.agorise.graphenej.network;
import junit.framework.Assert;
import org.junit.Test;
import java.util.List;
public class LatencyNodeProviderTest {
private FullNode nodeA, nodeB, nodeC;
private LatencyNodeProvider latencyNodeProvider;
private void setupTestNodes(){
// Creating 3 nodes with different latencies
nodeA = new FullNode("wss://nodeA");
nodeB = new FullNode("wss://nodeB");
nodeC = new FullNode("wss://nodeC");
// Adding latencies measurements
nodeA.addLatencyValue(100);
nodeB.addLatencyValue(50);
nodeC.addLatencyValue(20);
// Creating a node provider and adding the nodes created previously
latencyNodeProvider = new LatencyNodeProvider();
latencyNodeProvider.addNode(nodeC);
latencyNodeProvider.addNode(nodeA);
latencyNodeProvider.addNode(nodeB);
}
@Test
public void testSorting(){
setupTestNodes();
// Confirming that the best node is nodeC
FullNode bestNode = latencyNodeProvider.getBestNode();
System.out.println("Best node latency: "+bestNode.getLatencyValue());
Assert.assertSame("Check that the best node is nodeC", nodeC, bestNode);
// Improving nodeA score by feeding it with new better latency measurements
latencyNodeProvider.updateNode(nodeA, 10);
latencyNodeProvider.updateNode(nodeA, 10);
latencyNodeProvider.updateNode(nodeA, 10);
latencyNodeProvider.updateNode(nodeA, 10);
// Updating the nodeA position in the provider
latencyNodeProvider.updateNode(nodeA);
bestNode = latencyNodeProvider.getBestNode();
System.out.println("Best node latency after update: "+bestNode.getLatencyValue());
Assert.assertSame("Check that the best node now is the nodeA", nodeA, bestNode);
}
@Test
public void testSortedList(){
setupTestNodes();
// Confirming that the getSortedNodes gives us a sorted list of nodes in increasing latency order
List<FullNode> fullNodeList = latencyNodeProvider.getSortedNodes();
Assert.assertSame(nodeC, fullNodeList.get(0));
Assert.assertSame(nodeB, fullNodeList.get(1));
Assert.assertSame(nodeA, fullNodeList.get(2));
}
}

View File

@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.luminiasoft.labs.sample", appContext.getPackageName());
assertEquals("cy.agorise.labs.sample", appContext.getPackageName());
}
}

View File

@ -16,7 +16,6 @@ import butterknife.ButterKnife;
import cy.agorise.graphenej.RPC;
public class CallsActivity extends AppCompatActivity {
private final String TAG = this.getClass().getName();
@BindView(R.id.call_list)
RecyclerView mRecyclerView;
@ -26,10 +25,9 @@ public class CallsActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calls);
ButterKnife.bind(this);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(linearLayoutManager);
mRecyclerView.addItemDecoration(new DividerItemDecoration(this, linearLayoutManager.getOrientation()));
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
mRecyclerView.setAdapter(new CallAdapter());
}

View File

@ -9,6 +9,7 @@ import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import cy.agorise.graphenej.api.android.NetworkService;
import cy.agorise.graphenej.network.NodeLatencyVerifier;
public abstract class ConnectedActivity extends AppCompatActivity implements ServiceConnection {
private final String TAG = this.getClass().getName();
@ -28,7 +29,6 @@ public abstract class ConnectedActivity extends AppCompatActivity implements Ser
// We've bound to LocalService, cast the IBinder and get LocalService instance
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) service;
mNetworkService = binder.getService();
ConnectedActivity.this.onServiceConnected(className, service);
}
@ -41,8 +41,8 @@ public abstract class ConnectedActivity extends AppCompatActivity implements Ser
@Override
protected void onStart() {
super.onStart();
// Binding to NetworkService
Intent intent = new Intent(this, NetworkService.class);
// Binding to NetworkService
if(bindService(intent, mNetworkServiceConnection, Context.BIND_AUTO_CREATE)){
mShouldUnbindNetwork = true;
}else{

View File

@ -1,10 +1,8 @@
package cy.agorise.labs.sample;
import android.app.Application;
import android.preference.PreferenceManager;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.api.android.NetworkService;
import cy.agorise.graphenej.api.android.NetworkServiceManager;
/**
@ -12,30 +10,25 @@ import cy.agorise.graphenej.api.android.NetworkServiceManager;
*/
public class SampleApplication extends Application {
private final String TAG = this.getClass().getName();
@Override
public void onCreate() {
super.onCreate();
// This variable would hold a list of custom nodes
String customNodes = "wss://mydomain.net/ws,wss://myotherdomain.com/ws";
// Specifying some important information regarding the connection, such as the
// credentials and the requested API accesses
int requestedApis = ApiAccess.API_DATABASE | ApiAccess.API_HISTORY | ApiAccess.API_NETWORK_BROADCAST;
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putString(NetworkService.KEY_USERNAME, "nelson")
.putString(NetworkService.KEY_PASSWORD, "secret")
.putInt(NetworkService.KEY_REQUESTED_APIS, requestedApis)
// .putString(NetworkService.KEY_CUSTOM_NODE_URLS, customNodes)
.apply();
/*
* Registering this class as a listener to all activity's callback cycle events, in order to
* better estimate when the user has left the app and it is safe to disconnect the websocket connection
*/
registerActivityLifecycleCallbacks(new NetworkServiceManager(this));
NetworkServiceManager networkManager = new NetworkServiceManager.Builder()
.setUserName("nelson")
.setPassword("secret")
.setRequestedApis(requestedApis)
.setCustomNodeUrls("wss://eu.nodes.bitshares.ws")
.setAutoConnect(true)
.setNodeLatencyVerification(true)
.build(this);
// Registering this class as a listener to all activity's callback cycle events, in order to
// better estimate when the user has left the app and it is safe to disconnect the websocket connection
registerActivityLifecycleCallbacks(networkManager);
}
}