package lsr.leader.analyze;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;
import lsr.common.Util;
public class Analyzer {
static class Interval implements Comparable<Interval> {
public final long start;
public final long end;
public Interval(long curGPStart, long date) {
this.start = curGPStart;
this.end = date;
}
@Override
public int compareTo(Interval o) {
long thisVal = this.start;
long anotherVal = o.start;
return (thisVal<anotherVal ? -1 : (thisVal==anotherVal ? 0 : 1));
}
public long getDuration() {
return end-start;
}
public static String getHeader() {
return "Start\tEnd\tDuration";
}
@Override
public String toString() {
return start + "\t" + end + "\t" + getDuration();
}
}
enum State {
Initial,
Up,
Crashed,
Recovering
}
static class RTTPeriod {
public final long startTS;
public final double majrtts[];
public final int leader;
// /** The leader RTT during this period */
// public final double leaderRTT;
// /** How far is leader from optimal */
// public final double gamma;
public RTTPeriod(long startTS, double majrtts[], int leader) {
this.startTS = startTS;
this.majrtts = majrtts;
this.leader = leader;
}
public double getLeaderMajRTT() {
if (leader == -1) {
return -1;
} else {
return majrtts[leader];
}
}
public double getGamma() {
if (leader == -1) {
return -1;
} else {
double gamma = Double.MIN_VALUE;
for (double rtt : majrtts) {
gamma = Math.max(majrtts[leader]- rtt, gamma);
}
return gamma;
}
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(startTS).append("\t");
for (double rtt : majrtts) {
sb.append(Util.toString(rtt)).append("\t");
}
sb.append(leader);
return sb.toString();
}
public static String getHeader(int nProcs) {
StringBuilder s = new StringBuilder();
s.append("Start\t");
for (int i = 0; i < nProcs; i++) {
s.append("p"+i+"\t");
}
s.append("Leader");
return s.toString();
}
}
// State of a process. Recovering
class ProcessState {
public State state = State.Initial;
public final int id;
public int view = 0;
public double[] rtt;
public double majRtt = -1;
public ProcessState(int n, int id) {
this.id = id;
this.rtt = new double[n];
Arrays.fill(rtt, Double.MAX_VALUE);
}
public int getLeader() {
return view % n;
}
@Override
public String toString() {
return "p" + id + " View="+ view + ", state="+state + ", rtt=" + Util.toString(rtt) + "(" + majRtt + ")";
}
}
// Output files
/* all the logs and analysis of relevant events of the run.
* Intended for interpretation of the results and debugging.
*/
private final static String RESULTS_DIR = ".";
private final String RUNLOG_FILE = "log.txt";
private final PrintStream runLog;
private final String STATS_FILE = "stats.txt";
private final String SUSPICIONS_FILE = "suspicions.txt";
private final String ELECTION_FILE = "elections.txt";
private final String GOODPERIODS_FILE = "goodperiods.txt";
private final String RTTS_FILE = "rtts.txt";
private final List<LogRecord> logList;
private final int n;
// State of the system
private final ProcessState[] system;
// The current log being analyzed
private LogRecord curLog;
// Timestamp of the next event
private long nextEvtTS;
// Metrics to be measured
private long runStart = -1;
private long runEnd = -1;
private long leaderAvailableTime = 0;
//////////////////
// Suspicion times
//////////////////
// Auxiliary: Time when the last good period started. -1 means bad period.
private long curGPStart = -1;
private int curLeader = -1;
// Time series with duration of good periods
private final List<Interval> goodPeriods = new ArrayList<Interval>();
//////////////////
// Suspicion times
//////////////////
// Auxiliary variable
// Time when the last leader crashed
private long leaderCrashTime;
// The last leader that crashed, -1 if there is a leader.
private int crashedLeader;
// When the leader was first suspected.
private long suspectTime;
private long falseSuspicions = 0;
private long trueSuspicions = 0;
private long promotions = 0;
// Time series
private List<Interval> suspectTimes = new ArrayList<Interval>();
private List<Interval> electionTimes = new ArrayList<Interval>();
private List<RTTPeriod> rttIntervals = new ArrayList<RTTPeriod>();
public Analyzer(List<LogRecord> logList, int n) throws FileNotFoundException {
this.logList = logList;
this.n = n;
this.system = new ProcessState[n];
for (int i = 0; i < system.length; i++) {
system[i] = new ProcessState(n, i);
}
// runLog = System.out;
runLog = new PrintStream(new File(RESULTS_DIR, RUNLOG_FILE));
}
public void analyze() throws FileNotFoundException {
log("Starting analyzis");
// Should be 0 if logs are normalized
runStart = logList.get(0).getTimestamp();
runEnd = logList.get(logList.size()-1).getTimestamp();
double majrtts[] = new double[n];
Arrays.fill(majrtts, Double.MAX_VALUE);
rttIntervals.add(new RTTPeriod(runStart, majrtts, curLeader));
for (int i = 0; i < logList.size(); i++) {
curLog = logList.get(i);
// If we are at the end, use the current event
// timestamp as the end of the run.
int j = i+1 < logList.size() ? i+1 : i;
nextEvtTS = logList.get(j).getTimestamp();
System.out.println("Analyzing: " + curLog);
log(curLog.toString());
switch (curLog.type) {
case Start:
start((Start)curLog);
break;
case Stop:
stop((Stop)curLog);
break;
case BetterLeader:
betterLeader((BetterLeader)curLog);
break;
case Suspect:
suspect((Suspect)curLog);
break;
case RTT:
rtt((RTT)curLog);
break;
case TimeReply:
timeReply((TimeReply)curLog);
break;
case View:
newView((NewView)curLog);
break;
default:
throw new AssertionError("Unknown log type: " + curLog);
}
updateMetrics();
}
finishRun();
printMetrics();
}
/**
* Performs the final computations at the end of the run
*/
private void finishRun() {
// Close the last good period
if (curGPStart != -1) {
goodPeriods.add(new Interval(curGPStart, runEnd));
curGPStart = -1;
onGPEnd();
}
}
/**
* Checks if there is an elected leader.
* A process p is a leader if
* - p is alive
* - all processes that are up are in the same view as the leader
*
* @return The current leader of the system or -1 if there is no leader
*/
private int getLeader() {
int higestView = -1;
for (ProcessState ps : system) {
// only consider alive processes
if (ps.state == State.Up) {
if (higestView == -1) {
// This is the first alive process
higestView = ps.view;
assert ps.view >= 0;
}
// There is a candidate leader.
// All alive processes must have it as leader
if (higestView != ps.view) {
return -1;
}
}
}
int leader = higestView % n;
if (higestView != -1 && system[leader].state == State.Up) {
return leader;
} else {
return -1;
}
}
private void updateMetrics() {
if (getLeader() != -1) {
leaderAvailableTime += nextEvtTS - curLog.getTimestamp();
}
}
/**
* Should be called only after the state is updated
* with the new log and before processing the next log
*/
private void updateGoodPeriod() {
/* possibilities:
* 1. A new GP starts.
* 2. A GP ends
* 3. Continue on a bad period
* 4. Continue on a GP. Not possible, because the leader
* changed in one process.
*/
int leader = getLeader();
if (curGPStart == -1) {
// not in a good period
if (leader >= 0) {
// A good period started
curGPStart = curLog.getTimestamp();
curLeader = leader;
onGPStart();
} // else still on a bad period.
} else {
// In a GP. It may continue or it may end.
if (leader == -1) {
goodPeriods.add(new Interval(curGPStart, curLog.getTimestamp()));
curGPStart = -1;
curLeader = -1;
onGPEnd();
} else {
assert this.curLeader == leader : "A GP should not start immediately after the previous";
}
}
}
private String aliveProcesses() {
StringBuffer sb = new StringBuffer();
for (ProcessState ps : system) {
if (ps.state == State.Up) {
sb.append(ps.id).append(" ");
}
}
return sb.toString().trim();
}
private void newView(NewView log) {
assert system[log.p].state != State.Crashed : "Crashed process cannot install a new view";
// A process is no longer recovering when
// it installs a view higher than 0.
// TODO: it's also not recovering if it remains on view 0 but
// receives ALIVEs from leader. Or leader is on view 0.
if (system[log.p].state == State.Recovering && log.view != 0) {
log(log.p + " Recovering -> Up");
system[log.p].state = State.Up;
}
// The leader might have changed. Must recheck if it is a good period.
system[log.p].view = log.view;
updateGoodPeriod();
if (suspectTime != -1 && getLeader() >= 0) {
log("Recovery complete");
electionTimes.add(new Interval(suspectTime, log.getTimestamp()));
suspectTime = -1;
}
updateRTTs();
}
private void timeReply(TimeReply log) {
// TODO Auto-generated method stub
}
private void rtt(RTT log) {
ProcessState ps =system[log.p];
ps.rtt = log.rttVector;
ps.majRtt = log.majRtt;
// System.out.println(log.getTimestamp() + " RTT(" + ps.id + ") " + Util.toString(ps.rtt) + " ("+ps.majRtt+")");
double[] majs = new double[system.length];
for (int i = 0; i < system.length; i++) {
majs[i] = system[i].majRtt;
}
log("MAJRTTs: " + Util.toString(majs));
updateRTTs();
}
private void log(String string) {
if (curLog == null) {
runLog.println(string);
} else {
runLog.println(curLog.getTimestamp() + " " + string);
}
}
private void updateRTTs() {
// Update the majrtts of the previous interval
RTTPeriod prevInterval = rttIntervals.get(rttIntervals.size()-1);
double[] majrtts = Arrays.copyOf(prevInterval.majrtts, prevInterval.majrtts.length);
if (curLog instanceof RTT) {
RTT rttLog = (RTT) curLog;
majrtts[rttLog.p] = rttLog.majRtt;
} else {
// the majrtts remain unchanged
if (prevInterval.leader == curLeader) {
// Neither the RTTs nor the leader changed. Skip.
return;
}
}
RTTPeriod rttPeriod = new RTTPeriod(
curLog.getTimestamp(), majrtts, curLeader);
rttIntervals.add(rttPeriod);
log(curLog.getTimestamp() + " " + rttPeriod);
}
private void suspect(Suspect log) {
if (leaderCrashTime != -1 && crashedLeader == log.suspectedProc) {
log("Leader suspected.");
suspectTimes.add(new Interval(leaderCrashTime, log.getTimestamp()));
crashedLeader = -1;
leaderCrashTime = -1;
suspectTime = log.getTimestamp();
trueSuspicions++;
}
// There is an alive leader and log.p is suspecting it.
if (curLeader != -1 && log.suspectedProc == curLeader) {
log("FALSE SUSPICION");
falseSuspicions++;
}
}
private void betterLeader(BetterLeader log) {
log("BETTERLEADER " +
"Prev: " + curLeader + " (" + system[curLeader].majRtt + "), " +
"New " + log.newLeader + " (" + system[log.newLeader].majRtt + ")");
promotions++;
// onGPEnd();
// this.curGPLeader = log.newLeader;
// onGPStart();
}
private void onGPStart() {
log("GPSTART - Leader: " + curLeader +
", Alive: " + aliveProcesses());
}
private void onGPEnd() {
log("GPEND - Alive: " + aliveProcesses());
}
private void stop(Stop log) {
assert system[log.p].state != State.Crashed;
// Suspicion time: if the leader fails, count time until first suspicion
if (log.p == getLeader()) {
log("Leader failed. Waiting for suspicion.");
assert leaderCrashTime == -1;
leaderCrashTime = log.getTimestamp();
crashedLeader = log.p;
}
// Since p crashed, it is no longer the leader
system[log.p].state = State.Crashed;
// Good period is over
updateGoodPeriod();
}
private void start(Start log) {
assert system[log.p].state == State.Initial || system[log.p].state == State.Crashed;
if (system[log.p].state == State.Initial) {
system[log.p].state = State.Up;
} else {
system[log.p].state = State.Recovering;
}
if (log.p == crashedLeader) {
// this is extremely unlikely to happen
log("Leader recovered before being suspected.");
leaderCrashTime = -1;
}
}
private void printMetrics() throws FileNotFoundException {
// General statistics first
PrintStream statsPS = new PrintStream(new File(RESULTS_DIR, STATS_FILE));
// Manually breaking it down is simple and would work as well.
// But this requires fewer lines of code
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
// Must set time zone to neutral, to prevent automatic adjustments.
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
long runLength = runEnd - runStart;
// Output as a latex table. To be included directly on a tex file.
statsPS.println("RunStart: & " + runStart + " \\\\");
statsPS.println("RunEnd: & " + runEnd + " \\\\");
statsPS.println("Duration: & " + runLength + " (" + sdf.format(runLength) + ")\\\\");
double leaderAvailable= (((double)leaderAvailableTime)/runLength)*100;
statsPS.println("Availability: & " + leaderAvailableTime + " (" + Util.toString(leaderAvailable) + "\\%) \\\\" );
statsPS.println("CorrectSuspicions: & " + trueSuspicions + " \\\\");
statsPS.println("FalseSuspicions: & " + falseSuspicions + " \\\\");
statsPS.println("Promotions: & " + promotions + " \\\\");
String[] header = new String[2];
// Sum of good periods
long totalGP = sum(goodPeriods);
statsPS.println("Total Good Period: & " + totalGP + " \\\\");
header[0] = "Good periods";
header[1] = Interval.getHeader();
printList(header, goodPeriods, GOODPERIODS_FILE);
// Time waiting for suspecting a crashed process
long totalSuspect = sum(suspectTimes);
statsPS.println("Suspect time: & " + totalSuspect + " \\\\");
header[0] = "Suspect periods";
printList(header, suspectTimes, SUSPICIONS_FILE);
long totalElectionTime = sum(electionTimes);
statsPS.println("Election time: & " + totalElectionTime);
header[0] = "Election periods";
printList(header, electionTimes, ELECTION_FILE);
header[0] = "RTTs";
header[1] = RTTPeriod.getHeader(n);
printList(header, rttIntervals, RTTS_FILE);
}
private static long sum(List<Interval> list) {
long total = 0;
for (Interval interval : list) {
total += interval.getDuration();
}
return total;
}
// private void printTimeSeriesList(List<RTTPeriod> list, String file)
// throws FileNotFoundException
// {
// PrintWriter pw = new PrintWriter(new File(RESULTS_DIR, file));
// for (RTTPeriod r : list) {
// StringBuilder sb = new StringBuilder();
// sb.append(r.startTS);
// for (double majrtt : r.majrtts) {
// sb.append("\t").append(Util.toString(majrtt));
// }
// sb.append("\t").append(r.leader);
// sb.append("\t").append(Util.toString(r.getGamma()));
// pw.println(sb);
// }
// pw.flush();
// }
private void printList(String header[], List<?> list, String outFile)
throws FileNotFoundException
{
PrintStream ps = new PrintStream(new File(RESULTS_DIR, outFile));
for (String s : header) {
ps.println("%"+s);
}
for (Object r : list) {
ps.println(r);
}
ps.close();
}
// private void printTimeSeries(List<Interval> list, String file)
// throws FileNotFoundException
// {
// PrintWriter pw = new PrintWriter(new File(RESULTS_DIR, file));
// for (Interval r : list) {
// long ts = r.start - runStart;
// pw.println(ts + "\t" + r.getDuration());
// }
// pw.flush();
// }
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (ProcessState ps : system) {
sb.append(ps + "\n");
}
return sb.toString();
}
}