package ca.uwaterloo.fydp.xcde;
import ca.uwaterloo.fydp.ossp.*;
import ca.uwaterloo.fydp.ossp.std.*;
import ca.uwaterloo.fydp.ossp.impl.OSSPImpl;
import java.util.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import ca.uwaterloo.fydp.ossp.impl.OSSPSimpleSynchronizer;
public class XCDEStressTest {
/**
* Connects the specified number of clients to the specified server
* and has the clients make random concurrent changes
* to the server as fast as possible for the indicated amount of time.
* At the end, it has each client re-download the state and check it
* versus the local copy. The process is repeated the specified number
* of times. The results are printed to System.out.
*
* @param addr
* @param port
* @param numberOfClients
* @param timeInMillis
* @param repeats
*/
public static void stressTest(InetAddress addr, int port, final int numberOfClients, long timeInMillis, int repeats) {
final Object ob = new Object();
for (int trial = 0; trial < repeats; trial++) {
final boolean[] running = new boolean[1];
final boolean[] passed = new boolean[1];
final boolean[] disconnected = new boolean[1];
passed[0] = true;
running[0] = true;
disconnected[0] = false;
final int[] runningClients = new int[1];
runningClients[0] = 0;
final Object runningClientsSync = new Object();
// We make all clients share the same synchronizer, simply so that
// the text on standard out won't be garbled.
final OSSPSynchronizer sync = new OSSPSimpleSynchronizer();
for (int i = 0; i < numberOfClients; i++) {
final OSSPLink client = OSSPImpl.getInstance().makeTCPLink(addr, port, sync);
client.setReceiver(new OSSPHalfLink() {
private java.util.Random rnd = new java.util.Random();
private XCDERootDirectory localState = null;
public void openResourceSet(OSSPMask mask) {
throw new RuntimeException("Broken remote server.");
}
public void closeResourceSet(OSSPMask close, OSSPMask post, OSSPState current) {
throw new RuntimeException("Broken remote server.");
}
public void answerOpenResourceSet(OSSPState state, OSSPMask mask) {
if (state == null) {
// Ignore.
return;
}
if (localState == null) {
// First download. Starts everything.
localState = (XCDERootDirectory)state;
new Thread(new Runnable() {
private static final int maxDepth = 10;
private OSSPDirectoryElement randomNewDirectoryElement(OSSPDirectory dir, int depth) {
// Make a new directory element unlikely
final double createDirProb = 0.5;
String name = randomUnusedElementName(dir);
double val = rnd.nextDouble();
if (val < createDirProb && depth < maxDepth) {
// Create a dir.
return new OSSPDirectoryElement(name, randomDirectory(depth));
} else {
// Create a file.
return new OSSPDirectoryElement(name, randomDocument());
}
}
private XCDEDocument randomDocument() {
return new XCDEDocument(randomString());
}
private OSSPStimulus randomEdit(OSSPDirectory dir, int depth) {
// First randomly decide to create, delete, or edit.
final double createProb = 0.33, deleteProb = 0.33;
double val = rnd.nextDouble();
if ((val < createProb) || dir.elements.isEmpty()) {
return new OSSPDirectoryStimulusInitiateCreate(randomNewDirectoryElement(dir, depth+1));
} else if (val < (createProb + deleteProb)) {
// Delete a directory.
return new OSSPDirectoryStimulusDelete(randomElement(dir).name);
} else {
// Edit an element.
OSSPDirectoryElement el = randomElement(dir);
if (el.state instanceof OSSPDirectory) {
return new OSSPDirectoryStimulusChange(el.name, randomEdit((OSSPDirectory)el.state, depth+1));
} else {
return new OSSPDirectoryStimulusChange(el.name, randomEdit((XCDEDocument)el.state));
}
}
}
private String randomUnusedElementName(OSSPDirectory dir) {
outer: for (;;) {
String s = randomString();
for (ListIterator i = dir.ls.listIterator(); i.hasNext(); ) {
String name = ((OSSPDirectoryListingElement)i.next()).name;
if (s.equals(name)) {
continue outer;
}
}
return s;
}
}
private OSSPDirectoryElement randomElement(OSSPDirectory dir) {
return (OSSPDirectoryElement)dir.elements.get(randomInt(dir.elements.size()-1));
}
private OSSPStimulus randomEdit(XCDEDocument file) {
final double insertProb = 0.5;
double val = rnd.nextDouble();
int pos = randomInt(file.content.length());
int sel = randomInt(file.content.length() - pos);
if (val < insertProb) {
// Insert
return new XCDEDocChangeInsertion(randomEditingUser(file), pos, randomString());
} else { // i.e. delete text
// Delete
return new XCDEDocChangeDeletion(randomEditingUser(file), pos, sel);
}
}
private String randomEditingUser(XCDEDocument doc) {
/*
int i = randomInt(doc.users.size());
if (i >= doc.users.size()) {
return "anon";
}
return ((XCDERootDirectoryUserState)doc.users.get(i)).userName;
*/
return "anon";
}
private int randomInt(int max) {
return (int)((max + 1) * rnd.nextDouble());
}
private String randomString() {
final int longestString = 32;
/**
* Add 1 to the random number b/c element names of length 0 are
* illegal and inserts of length 0 are pointless.
*/
int length = 1 + randomInt(longestString);
StringBuffer buf = new StringBuffer();
for (int i = 0; i < length; i++) {
// Choose a random ASCII letter or digit.
char c;
for (;;) {
c = (char)(byte)(rnd.nextDouble()*256);
if ((('a' <= c) && (c <= 'z')) || (('A' <= c) && (c <= 'Z')) || (('0' <= c) && (c <= '9'))) {
break;
}
}
buf.append(c);
}
return buf.toString();
}
private OSSPDirectory randomDirectory(int depth) {
final int maxInitialElements = 5;
int numElements = randomInt(maxInitialElements);
OSSPDirectory dir = new OSSPDirectory();
for (int i = 0; i < numElements; i++) {
OSSPDirectoryElement el = randomNewDirectoryElement(dir, depth+1);
dir.ls.add(new OSSPDirectoryListingElement(el.name, el.state.getClass().getName()));
dir.elements.add(el);
}
return dir;
}
private String randomUsedUserName(XCDERootDirectory doc) {
return ((XCDERootDirectoryUserState)doc.users.get(randomInt(doc.users.size()-1))).userName;
}
private String randomUnusedUserName(XCDERootDirectory doc) {
outer: for (;;) {
String s = randomString();
for (ListIterator i = doc.users.listIterator(); i.hasNext(); ) {
String name = ((XCDERootDirectoryUserState)i.next()).userName;
if (s.equals(name)) {
continue outer;
}
}
return s;
}
}
private XCDEDocumentSelection randomSelection(XCDEDocument file) {
int pos = randomInt(file.content.length());
int sel = randomInt(file.content.length() - pos);
return new XCDEDocumentSelection(pos, sel);
}
private XCDERootDirectoryUserState randomViewingUserState(String name, Vector path, OSSPDirectory dir) {
if (dir.elements.isEmpty()) {
// We'll abort doing a viewing user and instead do a non-viewing user.
return null;
}
OSSPDirectoryElement el = (OSSPDirectoryElement)dir.elements.get( randomInt(dir.elements.size()-1) );
path.add(el.name);
if (el.state instanceof OSSPDirectory) {
return randomViewingUserState(name, path, (OSSPDirectory)el.state);
} else {
// Found a file. Decide if we want to have a cursor in it or not.
final double selecting = 0.75;
double val = rnd.nextDouble();
if (val < selecting) {
return new XCDERootDirectoryUserState(name, (String[])path.toArray(new String[path.size()]), randomSelection((XCDEDocument)el.state), rnd.nextBoolean(), rnd.nextBoolean());
} else {
return new XCDERootDirectoryUserState(name, (String[])path.toArray(new String[path.size()]), null, rnd.nextBoolean(), rnd.nextBoolean());
}
}
}
private XCDERootDirectoryUserState randomUserState(String name, XCDERootDirectory root) {
final double viewingProb = 0.5;
double val = rnd.nextDouble();
if (val < viewingProb) {
XCDERootDirectoryUserState tmp = randomViewingUserState(name, new Vector(), root);
if (tmp != null) {
return tmp;
}
// Else we ended up at an empty directory instead of a file. There might be files somewhere
// in the repository, but let's just not view anything.
}
return new XCDERootDirectoryUserState(name, null, null, rnd.nextBoolean(), rnd.nextBoolean());
}
OSSPStimulus randomRootEdit(XCDERootDirectory root) {
// Randomly choose to do a user edit or "normal" edit.
double userProb = 0.25;
double val = rnd.nextDouble();
if (val < userProb) {
final double addUserProb = 0.33, delUserProb = 0.66;
val = rnd.nextDouble();
if (val < addUserProb || root.users.isEmpty()) {
return new XCDERootDirectoryUserStimulusAdd(randomUserState(randomUnusedUserName(root), root));
} else if (val < delUserProb) {
return new XCDERootDirectoryUserStimulusRemove(randomUsedUserName(root));
} else {
final double sameName = 0.75;
val = rnd.nextDouble();
if (val < sameName) {
String name = randomUsedUserName(root);
return new XCDERootDirectoryUserStimulusChange(name, randomUserState(name, root));
} else {
return new XCDERootDirectoryUserStimulusChange(randomUsedUserName(root), randomUserState(randomUnusedUserName(root), root));
}
}
} else {
// Normal edit.
return randomEdit(root, 0);
}
}
public void run() {
synchronized (runningClientsSync) {
runningClients[0]++;
if (runningClients[0] == numberOfClients) {
runningClientsSync.notify();
}
}
while (running[0]) {
// Randomly edit the current state as fast as possible.
client.pace();
synchronized (sync) {
try {
if (disconnected[0]) {
return;
}
OSSPStimulus ch = randomRootEdit(localState);
client.sendStimulus(ch);
localState = (XCDERootDirectory)ch.apply(localState);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// We now stop editing. We check our correctness
// by re-downloading the state and comparing. After
// the download, the connection is severed.
client.pace();
synchronized (sync) {
client.openResourceSet(new OSSPFullMask());
}
}
}).start();
} else {
// Second download. Compare with local state and then
// disconnect.
boolean usersCheckPass = true;
outer2: for (ListIterator k = ((XCDERootDirectory)state).users.listIterator(); k.hasNext(); ) {
XCDERootDirectoryUserState user = (XCDERootDirectoryUserState)k.next();
for (ListIterator k2 = localState.users.listIterator(); k2.hasNext(); ) {
XCDERootDirectoryUserState user2 = (XCDERootDirectoryUserState)k2.next();
if (user2.equals(user)) {
k2.remove();
continue outer2;
}
}
System.out.println("Missing user on root: " + user);
System.out.println("(Users on the other copy are: " + localState.users + ")");
usersCheckPass = false;
break outer2;
}
// Equal as long as there's no unremoved user states in localState.
if (usersCheckPass && !localState.users.isEmpty()) {
System.out.println("Missing users on root: " + localState.users);
usersCheckPass = false;
}
if (!usersCheckPass || !destructiveEquals(localState, (OSSPDirectory)state)) {
System.out.println("ERROR!! Final state for this client does not match server.");
passed[0] = false;
} else {
System.out.println("SUCCESS!! Final state for this client matches server.");
}
client.disconnect();
synchronized (runningClientsSync) {
runningClients[0]--;
if (runningClients[0] == 0) {
runningClientsSync.notify();
}
}
}
}
public void sendStimulus(OSSPStimulus stim) {
localState = (XCDERootDirectory)stim.apply(localState);
}
public void disconnect() {
disconnected[0] = true;
throw new RuntimeException("Unexpected remote server disconnect during stress test.");
}
});
client.pace();
synchronized (sync) {
client.openResourceSet(new OSSPFullMask());
}
}
// Wait for all threads to start.
synchronized (runningClientsSync) {
try {
runningClientsSync.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// Run for the indicated amount of time.
synchronized (ob) {
try {
ob.wait(timeInMillis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// Start stopping the threads.
running[0] = false;
// Wait for all threads to stop.
synchronized (runningClientsSync) {
try {
runningClientsSync.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (passed[0]) {
System.out.println("SUCCESS!! All clients match the server.");
} else {
System.out.println("ERROR!! Some clients didn't match the server. Halting trials.");
return;
}
}
System.out.println("SUCCESS!! All trials completed without incident.");
}
/**
* Determines whether dir1 and dir2 are equivalent, except that in doing so
* it destroys dir1.
*
* @param dir1
* @param dir2
* @return
*/
private static boolean destructiveEquals(OSSPDirectory dir1, OSSPDirectory dir2) {
// Note: we ignore the .ls field, because we assume they'll be correct
// for the .elements field.
outer: for (ListIterator i = dir2.elements.listIterator(); i.hasNext(); ) {
OSSPDirectoryElement el = (OSSPDirectoryElement)i.next();
for (ListIterator j = dir1.elements.listIterator(); j.hasNext(); ) {
OSSPDirectoryElement tmp = (OSSPDirectoryElement)j.next();
if (el.name.equals(tmp.name)) {
// Found the entry in dir1.
if (el.state instanceof OSSPDirectory) {
if (!(tmp.state instanceof OSSPDirectory)) {
System.out.println("Element class mismatch.");
return false;
}
if (!destructiveEquals((OSSPDirectory)tmp.state, (OSSPDirectory)el.state)) {
return false;
}
} else {
// Must be XCDEDocument.
if (!(tmp.state instanceof XCDEDocument)) {
System.out.println("Element class mismatch.");
return false;
}
if (!((XCDEDocument)el.state).content.equals(((XCDEDocument)tmp.state).content)) {
System.out.println("Content discrepancy for file named " + tmp.name);
System.out.println("<<<FILE #1's CONTENT>>>");
System.out.println();
System.out.println();
System.out.println();
System.out.println();
System.out.println();
System.out.println(((XCDEDocument)tmp.state).content);
System.out.println("<<<FILE #2's CONTENT>>>");
System.out.println();
System.out.println();
System.out.println();
System.out.println();
System.out.println();
System.out.println(((XCDEDocument)el.state).content);
return false;
}
/*
outer2: for (ListIterator k = ((XCDEDocument)el.state).users.listIterator(); k.hasNext(); ) {
XCDERootDirectoryUserState user = (XCDERootDirectoryUserState)k.next();
for (ListIterator k2 = ((XCDEDocument)tmp.state).users.listIterator(); k2.hasNext(); ) {
XCDERootDirectoryUserState user2 = (XCDERootDirectoryUserState)k2.next();
if (user2.equals(user)) {
k2.remove();
continue outer2;
}
}
// Couldn't find it.
System.out.println("Missing user in file " + tmp.name + ": " + user);
return false;
}
// Equal as long as there's no unremoved user states in tmp.
if (!((XCDEDocument)tmp.state).users.isEmpty()) {
System.out.println("Missing users in file " + tmp.name + ": " + ((XCDEDocument)tmp.state).users);
return false;
}
*/
}
j.remove();
continue outer;
}
}
// No entry for this in the other directory.
System.out.println("Missing element: " + el.name);
return false;
}
// Equal as long as there's no unremoved elements in dir1.
if (!dir1.elements.isEmpty()) {
System.out.println("Missing elements: " + dir1.elements);
return false;
}
return true;
}
/**
* args[0] = server machine name as string
* args[1] = server port number as string
* args[2] = number of clients to connect with
* args[3] = milliseconds to run each trial for
* args[4] = max. number of trials to do
*
* @param args
*/
public static void main(String[] args) throws UnknownHostException {
stressTest(InetAddress.getByName(args[0]), Integer.parseInt(args[1]),
Integer.parseInt(args[2]), Integer.parseInt(args[3]), Integer.parseInt(args[4]));
}
}