package com.samknows.tests;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import com.samknows.libcore.SKPair;
import com.samknows.libcore.SKPorting;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.json.JSONObject;
//import com.samknows.libcore.SKLogger;
//import com.samknows.measurement.TestRunner.SKTestRunner;
//import com.samknows.measurement.util.SKDateFormat;
public class ClosestTarget extends SKAbstractBaseTest implements Runnable {
/*
* constants for creating a ClosestTarget test
*/
private final String NUMBEROFPACKETS = "numberOfPackets";
private final String DELAYTIMEOUT = "delayTimeout";
private final String INTERPACKETTIME = "interPacketTime";
private final static String VALUE_NOT_KNOWN = "-";
public static final String TESTSTRING = "CLOSESTTARGET";
/*
* Default values for the LatencyTest
*/
private final int _NPACKETS = 5;
private final int _INTERPACKETTIME = 1000000;
private final int _DELAYTIMEOUT = 2000000;
private final int _PORT = 6000;
/*
* Constraints for the test parameters This values are needed to avoid to
* misconfigure the latency test and hence to make the test useless or worst
* to get stuck with the closest target test execution
*/
private final int NUMBEROFPACKETSMAX = 100;
private final int NUMBEROFPACKETSMIN = 5;
private final int INTERPACKETIMEMAX = 60000000;
private final int INTERPACKETIMEMIN = 10000;
private final int DELAYTIMEOUTMIN = 1000000;
private final int DELAYTIMEOUTMAX = 5000000;
private final int NUMBEROFTARGETSMAX = 50;
private final int NUMBEROFTARGETSMIN = 1;
public static final String JSON_CLOSETTARGET = "closest_target";
public static final String JSON_IPCLOSESTTARGET = "ip_closest_target";
private ClosestTarget(List<Param> params) {
synchronized (ClosestTarget.this) {
sClosestTarget = "";
setParams(params);
}
}
public static ClosestTarget sCreateClosestTarget(List<Param> params) {
return new ClosestTarget(params);
}
public class Result {
public int total;
public int completed;
public String currbest_target;
public long curr_best_timeNanoseconds;
}
//Used to collect the results from the individual LatencyTests as soon as the finish
private BlockingQueue<LatencyTest.Result> bq_results = new LinkedBlockingQueue<>();
//public ClosestTarget() {
// synchronized (ClosestTarget.this) {
// sClosestTarget = "";
// }
//}
private boolean between(int x, int a, int b) {
return (x >= a && x <= b);
}
@Override
public boolean isReady() {
if (!between(nPackets, NUMBEROFPACKETSMIN, NUMBEROFPACKETSMAX)) {
SKPorting.sAssert(getClass(), false);
return false;
}
if (!between(interPacketTime, INTERPACKETIMEMIN, INTERPACKETIMEMAX)) {
SKPorting.sAssert(getClass(), false);
return false;
}
if (!between(delayTimeout, DELAYTIMEOUTMIN, DELAYTIMEOUTMAX)) {
SKPorting.sAssert(getClass(), false);
return false;
}
if (!between(targets.size(), NUMBEROFTARGETSMIN, NUMBEROFTARGETSMAX)) {
SKPorting.sAssert(getClass(), false);
return false;
}
return true;
}
@Override
public void runBlockingTestToFinishInThisThread() {
find();
}
@Override
public void run() {
find();
}
@Override
public int getNetUsage() {
return 0;
}
@Override
public boolean isSuccessful() {
return success;
}
private void setNumberOfDatagrams(int n) {
nPackets = n;
}
private void setInterPacketTime(int t) {
interPacketTime = t;
}
private void setDelayTimeout(int t) {
delayTimeout = t;
}
private void setPort(int p) {
port = p;
}
private void addTarget(String target) {
targets.add(target);
}
public void setTargetListEmpty() {
targets = new ArrayList<>();
}
/*
Some networks block UDP traffic; and some might even block raw TCP traffic!
GIVEN: performing a closest target test
WHEN: UDP fails
THEN: we need use HTTP as the ultimate failsafe.
Therefore, as a fall-back from the UDP best-target-selection process:
1. Make three HTTP requests to "/" on each server. Set a 2 second timeout on each request.
Ideally, you should parallelise them (maybe allow up to 6 concurrent requests).
2. Choose the server with the lowest non-zero response time
(not an average of the three requests - just take the one with the absolute lowest)
*/
private static final int CHttpQueryTimeoutSeconds = 2;
// Query the specified URL, and return <status,latency>
private static SKPair<Boolean, Long> sGetHttpResponseAndReturnLatencyMilliseconds(String urlString) {
//AsyncHttpClient client = new AsyncHttpClient();
//client.setTimeout(5000); // This is in milliseconds!
SKPorting.sLogD("ClosestTarget", "DEBUG: fire http closest target query at (" + urlString + ")");
// https://stackoverflow.com/questions/3000214/java-http-client-request-with-defined-timeout
HttpParams httpParams = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParams, CHttpQueryTimeoutSeconds * 1000);
HttpConnectionParams.setSoTimeout(httpParams, CHttpQueryTimeoutSeconds * 1000);
HttpClient httpclient = new DefaultHttpClient(httpParams);
HttpGet httpGet = new HttpGet(urlString);
HttpResponse response;
// Fire the query!
final Date startTime = new Date();
try {
response = httpclient.execute(httpGet);
// Check if server response is valid
StatusLine status = response.getStatusLine();
if (status.getStatusCode() != 200) {
SKPorting.sLogD("TAG", "Invalid response from server: " + status.toString());
SKPorting.sAssert(ClosestTarget.class, false);
} else {
// Successful query, handle the response!
final Date timeNow = new Date();
long measuredLatencyMilliseconds = timeNow.getTime() - startTime.getTime();
SKPorting.sAssert(ClosestTarget.class, measuredLatencyMilliseconds > 0);
SKPorting.sLogD("ClosestTarget", "DEBUG: HTTP/Closest target test - success - measuredLatencyMilliseconds = " + measuredLatencyMilliseconds);
return new SKPair<>(true, measuredLatencyMilliseconds);
}
} catch (SocketTimeoutException ste) {
// Don't fire a debug-time assertion if we have a simple timeout!
SKPorting.sLogD("ClosestTarget", "DEBUG: HTTP/Closest target test - SocketTimeoutException");
} catch (ConnectTimeoutException cte) {
// Don't fire a debug-time assertion if we have a simple timeout!
SKPorting.sLogD("ClosestTarget", "DEBUG: HTTP/Closest target test - ConnectTimeoutException");
} catch (UnknownHostException uhe) {
// Don't fire a debug-time assertion if the host cannot be found - it might just be down.
SKPorting.sLogD("ClosestTarget", "DEBUG: HTTP/Closest target test - UnknownHostException");
} catch (Exception e) {
// This might show up if e.g. all network connections are disabled.
SKPorting.sAssert(ClosestTarget.class, false);
}
return new SKPair<>(false, -100L);
}
private static final String TAG = ClosestTarget.class.getName();
private static String sGetTAG() {
return TAG;
}
private final int cQueryCountPerServer = 3;
private ArrayList<Integer> finishedTestsPerServer = new ArrayList<>();
private boolean mbInHttpTestingFallbackMode = false;
private boolean mbUdpClosestTargetTestSucceeded = false;
public boolean getUdpClosestTargetTestSucceeded() {
return mbUdpClosestTargetTestSucceeded;
}
// http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/CountDownLatch.html
class WorkerRunner extends Thread {
private int serverIndex;
private String target;
private String urlString;
private CountDownLatch startSignal;
private CountDownLatch doneSignal;
private long measuredLatencyMilliseconds = -100L;
LatencyTest latencyTest = null;
public long getMeasuredLatencyMilliseconds() {
return measuredLatencyMilliseconds;
}
WorkerRunner(int inServerIndex, String inTarget, String inUrlString, CountDownLatch inStartSignal, CountDownLatch inDoneSignal) {
super();
serverIndex = inServerIndex;
target = inTarget;
urlString = inUrlString;
startSignal = inStartSignal;
doneSignal = inDoneSignal;
latencyTest = latencyTests[serverIndex];
SKPorting.sAssert(getClass(), latencyTest.getTarget().equals(target));
}
@Override
public void run() {
try {
startSignal.await();
doWork();
} catch (InterruptedException ex) {
SKPorting.sAssert(getClass(), false);
}
SKPorting.sLogD("RUN()", "Finished run() in WorkerRunner!");
}
void doWork() {
SKPair<Boolean, Long> latencyQueryResult = ClosestTarget.sGetHttpResponseAndReturnLatencyMilliseconds(urlString);
if (latencyQueryResult.first) {
// Succeeded!
measuredLatencyMilliseconds = latencyQueryResult.second;
SKPorting.sAssert(getClass(), measuredLatencyMilliseconds > 0L);
} else {
// Failed to get a latency measurement!
measuredLatencyMilliseconds = -100L;
}
doneSignal.countDown();
synchronized (ClosestTarget.this) {
// This allows the UI to update itself, according to our current best guess...
// used for UI reporting!
if (measuredLatencyMilliseconds > 0) {
if (measuredLatencyMilliseconds * 1000000 < curr_best_Nanoseconds) {
curr_best_Nanoseconds = measuredLatencyMilliseconds * 1000000;
curr_best_target = target;
closestTarget = target;
}
}
// Increment "finished" only when the last async test for the server is completed.
int value = finishedTestsPerServer.get(serverIndex);
value++;
finishedTestsPerServer.set(serverIndex, value);
if (value == cQueryCountPerServer) {
finished++;
}
latencyTest.status = SKAbstractBaseTest.STATUS.DONE;
latencyTest.finished = true;
// Add a new result to the queue for display...
// Will be ignored if not the best yet!
LatencyTest.sCreateAndPushLatencyResultNanoseconds(bq_results, closestTarget, curr_best_Nanoseconds);
}
}
}
// This only returns when done...
private boolean blockingTryHttpClosestTargetTestIfUdpTestFails() {
mbInHttpTestingFallbackMode = true;
mbUdpClosestTargetTestSucceeded = false;
long timeAtStart = new Date().getTime();
// Prevent weird warnings from the outer UI query thread...
success = false;
finished = 0;
closestTarget = VALUE_NOT_KNOWN;
String TAG = getClass().getName();
SKPorting.sLogD("ClosestTarget", "DEBUG: tryHttpClosestTargetTestIfUdpTestFails");
// 3 Threads per server!
int serverCount = targets.size();
SKPorting.sLogD("ClosestTarget", "DEBUG: tryHttpClosestTargetTestIfUdpTestFails - serverCount=" + serverCount);
{
int tempIndex;
for (tempIndex = 0; tempIndex < serverCount; tempIndex++) {
SKPorting.sLogD("ClosestTarget", "DEBUG: tryHttpClosestTargetTestIfUdpTestFails - targets[" + tempIndex + "]=" + targets.get(tempIndex));
finishedTestsPerServer.add(0);
}
}
int queriesToRun = serverCount * cQueryCountPerServer;
SKPorting.sLogD("ClosestTarget", "DEBUG: tryHttpClosestTargetTestIfUdpTestFails - queriesToRun=" + queriesToRun);
// http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/CountDownLatch.html
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch queryCompleteCountdown = new CountDownLatch(queriesToRun);
//queryCompleteCountdown = queriesToRun;
ArrayList<Long> bestLatencyMillisecondsPerServer = new ArrayList<>();
//
// Fire this off in separate async tasks, and block waiting for them all to complete!
//
int serverIndex;
for (serverIndex = 0; serverIndex < serverCount; serverIndex++) {
// -100 means - no successful response - yet!
bestLatencyMillisecondsPerServer.add(-100L);
}
ArrayList<WorkerRunner> workerRunnableArray = new ArrayList<>();
for (serverIndex = 0; serverIndex < serverCount; serverIndex++) {
String target = targets.get(serverIndex);
String urlString = "http://" + target + "/";
int queryIndexForServer;
for (queryIndexForServer = 0; queryIndexForServer < cQueryCountPerServer; queryIndexForServer++) {
// http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/CountDownLatch.html
WorkerRunner workerRunnable = new WorkerRunner(serverIndex, target, urlString, startSignal, queryCompleteCountdown);
workerRunnableArray.add(workerRunnable);
workerRunnable.start();
}
}
//
// Block until all the async task queries have completed!
//
// Tell the async tasks to start working!
startSignal.countDown();
// Block, waiting for all the async task queries to complete.
try {
queryCompleteCountdown.await();
} catch (InterruptedException e) {
SKPorting.sAssert(getClass(), false);
}
//
// We have finished the tests, for all servers!
//
long now = new Date().getTime();
SKPorting.sLogD("HTTPClosestTarget", "time in seconds taken to run all HTTP tests = " + ((double) (now - timeAtStart)) / 1000.0);
// Return true if we succeed - in which case :
// closestTarget = set to the right target
// ipClosestTarget = the ip address of the closest target
if (closestTarget.equals(VALUE_NOT_KNOWN)) {
// This can happen e.g. if all networking is off... but it is quite unlikely.
SKPorting.sAssert(getClass(), false);
} else {
success = true;
InetAddress address = null;
try {
address = InetAddress.getByName(closestTarget);
ipClosestTarget = address.getHostAddress();
} catch (UnknownHostException e) {
SKPorting.sAssert(getClass(), false);
}
}
// This tells the UI monitor thread to finish.
finished = targets.size() + 1;
return success;
}
private boolean find() {
boolean ret = false;
if (targets.size() == 0) {
return ret;
}
ArrayList<Thread> threads = new ArrayList<>();
latencyTests = new LatencyTest[targets.size()];
for (int i = 0; i < targets.size(); i++) {
LatencyTest lt = new LatencyTest(targets.get(i), port, nPackets,
interPacketTime, delayTimeout);
lt.setBlockingQueueResult(bq_results);
latencyTests[i] = lt;
if (latencyTests[i].isReady()) {
Thread t = new Thread(latencyTests[i]);
threads.add(t);
t.start();
}
}
for (int i = 0; i < targets.size(); i++) {
try {
threads.get(i).join();
} catch (InterruptedException ie) {
ie.printStackTrace();
SKPorting.sAssert(getClass(), false);
}
}
int minDist = Integer.MAX_VALUE;
for (int i = 0; i < targets.size(); i++) {
if (latencyTests[i].isSuccessful()) {
success = true;
int avg = (int) latencyTests[i].getAverageMicroseconds();
if (avg < minDist) {
closestTarget = targets.get(i);
ipClosestTarget = latencyTests[i].getIpAddress();
minDist = avg;
}
}
}
if (closestTarget.equals(VALUE_NOT_KNOWN)) {
// Run the Http-based Closest Target test as a fall-back condition!
// This will return only when fully done...
mbUdpClosestTargetTestSucceeded = false;
finished = 0;
// WHEN:
// - if closest target UDP test failed (NB: this is ALWAYS run first in manual testing)
// THEN:
// - notify the app to display test as UDP skipped
// NOTE: Doesn't actually matter if we're running a manual test or not - if we're not running a manual test,
// the user interface will ignore this event.
//SKTestRunner.sDoReportUDPFailedSkipTests();
ret = blockingTryHttpClosestTargetTestIfUdpTestFails();
// HTTP (blocking) test succeeded!
mbFinishedSelectingClosestTarget = true;
} else {
// UDP test succeeded!
mbUdpClosestTargetTestSucceeded = true;
mbFinishedSelectingClosestTarget = true;
ret = true;
}
return ret;
}
public String getClosest() {
if (closestTarget.equals(VALUE_NOT_KNOWN)) {
return null;
}
return closestTarget;
}
private Long mTimestamp = SKAbstractBaseTest.sGetUnixTimeStampSeconds();
@Override
public synchronized void finish() {
mTimestamp = SKAbstractBaseTest.sGetUnixTimeStampSeconds();
status = STATUS.DONE;
}
@Override
public long getTimestamp() {
return mTimestamp;
}
@Override
public void setTimestamp(long timestamp) {
mTimestamp = timestamp;
}
@Override
public JSONObject getJSONResult() {
ArrayList<String> o = new ArrayList<>();
Map<String, Object> output = new HashMap<>();
//string id
o.add(TESTSTRING);
output.put(JsonData.JSON_TYPE, TESTSTRING);
//TIME
o.add(mTimestamp + "");
output.put(JsonData.JSON_TIMESTAMP, mTimestamp);
output.put(JsonData.JSON_DATETIME, SKPorting.sGetDateAsIso8601String(new java.util.Date(mTimestamp * 1000)));
//status
boolean status = true;
if (closestTarget.equals(VALUE_NOT_KNOWN)) {
status = false;
}
synchronized (ClosestTarget.class) {
sClosestTarget = closestTarget;
}
o.add(status ? "OK" : "FAIL");
output.put(JsonData.JSON_SUCCESS, status);
//closest target - might be VALUE_NOT_KNOWN...
o.add(closestTarget);
output.put(JSON_CLOSETTARGET, closestTarget);
//ip closest target
o.add(ipClosestTarget);
output.put(JSON_IPCLOSESTTARGET, ipClosestTarget);
//setOutput(o.toArray(new String[1]));
JSONObject json_output = new JSONObject(output);
return json_output;
}
private LatencyTest[] latencyTests = null;
private ArrayList<String> targets = new ArrayList<>();
private int nPackets = _NPACKETS;
private int interPacketTime = _INTERPACKETTIME;
private int delayTimeout = _DELAYTIMEOUT;
private int port = _PORT;
private String closestTarget = VALUE_NOT_KNOWN;
private boolean success = false;
private String ipClosestTarget = VALUE_NOT_KNOWN;
// This is used purely by the UI, to display a count-down of the target servers latency test.
// it is accessed via getPartialResults()...
private int finished = 0;
private boolean mbFinishedSelectingClosestTarget = false;
private long curr_best_Nanoseconds = Long.MAX_VALUE;
private String curr_best_target;
private static String sClosestTarget = VALUE_NOT_KNOWN;
public static String sGetClosestTarget() {
synchronized (ClosestTarget.class) {
return sClosestTarget;
}
}
public static void sSetClosestTarget(String inTarget) {
synchronized (ClosestTarget.class) {
sClosestTarget = inTarget;
//SKTestRunner.sDoReportClosestTargetSelected(inTarget);
}
}
@Override
public int getProgress0To100() {
if (latencyTests == null) {
return 0;
}
int min = 100;
for (LatencyTest t : latencyTests) {
if (t != null) {
int curr = t.getProgress0To100();
min = curr < min ? curr : min;
}
}
return min;
}
public Result getPartialResults() {
//first result when no test is finished yet
if (finished == 0) {
Result ret = new Result();
ret.completed = 0;
ret.total = targets.size();
ret.curr_best_timeNanoseconds = 0;
ret.currbest_target = "";
if (mbInHttpTestingFallbackMode == false) {
finished++; // Do this ONLY if no in HTTP-based testing!
}
return ret;
}
if (mbFinishedSelectingClosestTarget == true) { // (finished == targets.size() + 1){
// This tells the UI monitor to finish!
return null;
}
Result ret = new Result();
try {
LatencyTest.Result r = bq_results.take();
ret.completed = finished;
if (mbInHttpTestingFallbackMode == false) {
finished++; // Do this ONLY if no in HTTP-based testing!
}
ret.total = targets.size();
if (r.rttMicroseconds > 0 && (r.rttMicroseconds * 1000L) < curr_best_Nanoseconds) {
curr_best_Nanoseconds = (r.rttMicroseconds * 1000L);
curr_best_target = r.target;
}
ret.curr_best_timeNanoseconds = curr_best_Nanoseconds;
ret.currbest_target = curr_best_target;
} catch (InterruptedException ie) {
ie.printStackTrace();
ret = null;
}
return ret;
}
// @Override
// public HumanReadable getHumanReadable() {
// // TODO Auto-generated method stub
// return null;
// }
@Override
public String getStringID() {
return TESTSTRING;
}
private void setParams(List<Param> params) {
try {
for (Param param : params) {
String value = param.getValue();
if (param.contains(TARGET)) {
addTarget(value);
} else if (param.contains(PORT)) {
setPort(Integer.parseInt(value));
} else if (param.contains(NUMBEROFPACKETS)) {
setNumberOfDatagrams(Integer.parseInt(value));
} else if (param.contains(DELAYTIMEOUT)) {
setDelayTimeout(Integer.parseInt(value));
} else if (param.contains(INTERPACKETTIME)) {
setInterPacketTime(Integer.parseInt(value));
} else {
initialised = false;
break;
}
}
} catch (NumberFormatException nfe) {
initialised = false;
}
}
}