package com.samknows.measurement.statemachine;
import android.support.annotation.NonNull;
import java.io.Serializable;
import java.util.PriorityQueue;
import com.samknows.libcore.SKConstants;
import com.samknows.libcore.SKPorting;
import com.samknows.measurement.SK2AppSettings;
import com.samknows.measurement.SKApplication;
import com.samknows.measurement.schedule.TestGroup;
import com.samknows.measurement.schedule.condition.ConditionGroupResult;
import com.samknows.measurement.schedule.condition.DatacapCondition;
import com.samknows.measurement.schedule.failaction.RetryFailAction;
import com.samknows.measurement.TestRunner.TestContext;
import com.samknows.measurement.TestRunner.TestExecutor;
import com.samknows.measurement.util.SKDateFormat;
import com.samknows.measurement.util.TimeUtils;
public class ScheduledTestExecutionQueue implements Serializable{
private static final long serialVersionUID = 1L;
private long startTime;
private long endTime; //end time for the queue
private final PriorityQueue<QueueEntry> entries = new PriorityQueue<>();
private transient TestContext tc;
private long accumulatedTestBytes = 0L;
private ScheduledTestExecutionQueue() {}
public ScheduledTestExecutionQueue(TestContext tc) {
this.tc = tc;
accumulatedTestBytes = 0L;
startTime = endTime = System.currentTimeMillis();
long daysDiff = TimeUtils.daysToMillis(SKConstants.TEST_QUEUE_MAX_SIZE_IN_DAYS);
long newEndTime = startTime + daysDiff;
populate(newEndTime);
}
private void extendSize() {
long minSize = TimeUtils.daysToMillis(SKConstants.TEST_QUEUE_NORMAL_SIZE_IN_DAYS);
long currentSize = endTime - System.currentTimeMillis();
if (currentSize < minSize) {
SKPorting.sLogD(this, "extending queue");
long maxSize = TimeUtils.daysToMillis(SKConstants.TEST_QUEUE_MAX_SIZE_IN_DAYS);
long newEndSize = System.currentTimeMillis() + maxSize;
populate(newEndSize);
} else {
SKPorting.sLogD(this, "no need to extend queue, endTime: " + TimeUtils.logString(endTime));
}
}
private synchronized void populate(long newEndTime) {
long timeNow = System.currentTimeMillis();
startTime = endTime >= timeNow ? endTime : timeNow;
endTime = newEndTime;
SKPorting.sLogD(this, "populating test queue from: " + TimeUtils.logString(startTime) + " to " + TimeUtils.logString(endTime));
for(TestGroup tg: tc.config.backgroundTestGroups){
for(Long t:tg.getTimesInInterval(startTime, endTime)){
SKPorting.sLogD(this, "Add test group id "+ tg.id +" at time: "+TimeUtils.logString(t) );
addEntry(t, tg);
}
}
SKPorting.sLogD(this, "queue populated with: " + entries.size());
}
public void addEntry(long time, TestGroup tg){
entries.add(new QueueEntry(time, tg.id, tc.config.backgroundTestGroups.indexOf(tg)));
SKPorting.sLogD(this, "scheduling test group at: "+TimeUtils.logString(time));
}
/*
public void addEntry(long time, TestDescription td) {
entries.add(new QueueEntry(time, td.id, tc.config.tests.indexOf(td)));
Logger.d(this, "scheduling test at: " + new SimpleDateFormat().format(new Date(time)));
}
*/
public void setTestContext(TestContext tc) {
this.tc = tc;
}
public long getAccumulatedTestBytes() {
return accumulatedTestBytes;
}
/**
* @return reschedule time duration in milliseconds
*/
public long executeReturnRescheduleDurationMilliseconds() {
// TODO - is the following assertion something that should be re-activated?
//SKLogger.sAssert(getClass(), (accumulatedTestBytes == 0L));
TestExecutor scheduledTestExecutor = new TestExecutor(tc);
long time = System.currentTimeMillis();
// The events in the queue are run through, until all are processed.
// So, we must start by clearing-out all tests that pre-date the current time.
while (true){
QueueEntry entry = entries.peek();
if (entry != null && !canExecute(entry, time) && entry.getSystemTimeMilliseconds() < time) {
entries.remove();
SKPorting.sLogD(this, "removing test scheduled at: " + entry.getSystemTimeAsDebugString());
} else {
break;
}
}
boolean result = true;
boolean fail = false;
if (canExecute(time))
{
scheduledTestExecutor.start();
while (canExecute(time))
{
QueueEntry entry = entries.remove();
long maximumTestUsage = tc.config == null ? 0: tc.config.maximumTestUsage;
//if data cap is going to be breached do not run test
//the datacap condition is successful if the datacap is not reached
boolean dataCapLikelyToBeReachedFlag = SK2AppSettings.getSK2AppSettingsInstance().isDataCapLikelyToBeReached(maximumTestUsage);
// If we're not going to reach the limit, then success is true.
// If we're going to reach the limit, then success is false.
boolean dataCapSuccessFlag = !dataCapLikelyToBeReachedFlag;
DatacapCondition dc = new DatacapCondition(dataCapSuccessFlag);
if (dc.isSuccess()) {
ConditionGroupResult tr = scheduledTestExecutor.executeBackgroundTestGroup(entry.groupId);
accumulatedTestBytes += scheduledTestExecutor.getAccumulatedTestBytes();
result = tr.isSuccess;
} else {
SKPorting.sLogD(this, "Active metrics won't be collected due to potential datacap breach");
scheduledTestExecutor.getResultsContainer().addFailedCondition(DatacapCondition.JSON_DATACAP);
}
scheduledTestExecutor.getResultsContainer().addCondition(dc.getCondition());
}
scheduledTestExecutor.stop();
scheduledTestExecutor.save("scheduled_tests", -1);
}
extendSize();
if (fail) {
RetryFailAction failAction = tc.config.retryFailAction;
if (failAction != null) {
return tc.config.retryFailAction.delay; //reschedule
} else {
SKPorting.sAssertE(this, "can't find on test fail action, just skipping the test.");
entries.remove();
}
}
return getSleepTimeDurationMilliseconds();
}
private boolean canExecute(long time) {
QueueEntry entry = entries.peek();
if (entry == null) {
return false;
} else {
return canExecute(entry, time);
}
}
// The queue is populated with all events for a given day.
// The events in the queue are run through, until all are processed.
public boolean canExecute(QueueEntry entry, long time) {
long timeUntilEntry = Math.abs(entry.getSystemTimeMilliseconds() - time);
long timeWindow = SK2AppSettings.getSK2AppSettingsInstance().getTestStartWindow();
if (timeWindow == 60000) {
if (timeUntilEntry < timeWindow) {
return true;
}
}
timeWindow /= 2;
if (timeUntilEntry < timeWindow) {
return true;
}
// This allows us to override the value in the debugger...
return false;
}
private long getSleepTimeDurationMilliseconds() {
if (entries.isEmpty()) {
return TimeUtils.daysToMillis(SKConstants.TEST_QUEUE_NORMAL_SIZE_IN_DAYS);
} else {
QueueEntry entry = entries.peek();
long value = entry.getSystemTimeMilliseconds() - System.currentTimeMillis();
return value;
}
}
public int size() {
return entries.size();
}
class QueueEntry implements Serializable, Comparable<QueueEntry>{
private static final long serialVersionUID = 1L;
private final long systemTimeMilliseconds;
private final String systemTimeAsDebugString;
final long groupId;
final int orderIdx;
public long getSystemTimeMilliseconds() {
return systemTimeMilliseconds;
}
public String getSystemTimeAsDebugString() {
return systemTimeAsDebugString;
}
public QueueEntry(long time, long groupId, int orderIdx) {
super();
this.systemTimeMilliseconds = time;
this.systemTimeAsDebugString = new SKDateFormat(SKApplication.getAppInstance()).UITime(systemTimeMilliseconds);
this.groupId = groupId;
this.orderIdx = orderIdx;
}
@Override
public int compareTo(@NonNull QueueEntry another) {
if (systemTimeMilliseconds == another.getSystemTimeMilliseconds()) {
//if time is the same we want to the save original order from config
return Integer.valueOf(orderIdx).compareTo(another.orderIdx);
}
return Long.valueOf(systemTimeMilliseconds).compareTo(another.getSystemTimeMilliseconds());
}
@Override
public String toString() {
return groupId + " : " + getSystemTimeAsDebugString();
}
}
}