/**
* Master for Two-Phase Commits
*
* @author Mosharaf Chowdhury (http://www.mosharaf.com)
* @author Prashanth Mohan (http://www.cs.berkeley.edu/~prmohan)
*
* Copyright (c) 2012, University of California at Berkeley
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of University of California, Berkeley nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package edu.berkeley.cs162;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Comparator;
import java.util.NavigableMap;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
public class TPCMaster {
// Timeout value used during 2PC operations
public static final int TIMEOUT_MILLISECONDS = 5000;
// Port on localhost to run registration server on
private static final int REGISTRATION_PORT = 9090;
// Cache stored in the Master/Coordinator Server
public KVCache masterCache = new KVCache(100, 10);
// Registration server that uses TPCRegistrationHandler
public SocketServer regServer = null;
// Number of slave servers in the system
public int numSlaves = -1;
// ID of the next 2PC operation
public Long tpcOpId = 0L;
// Datastructure to do Consistent Hashing
public TreeMap<Long, SlaveInfo> treemap = new TreeMap<Long, SlaveInfo>(
(Comparator<? super Long>) new ConsistentComparator());
/**
* Creates TPCMaster
*
* @param numSlaves
* number of slave servers expected to register
* @throws Exception
*/
public TPCMaster(int numSlaves) {
this.numSlaves = numSlaves;
try {
regServer = new SocketServer(InetAddress.getLocalHost()
.getHostAddress(), REGISTRATION_PORT);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
/**
* Calculates tpcOpId to be used for an operation. In this implementation it
* is a long variable that increases by one for each 2PC operation.
*
* @return
*/
public String getNextTpcOpId() {
tpcOpId++;
return tpcOpId.toString();
}
/**
* Start registration server in a separate thread.
*/
public void run() {
AutoGrader.agTPCMasterStarted();
// implement me
TPCRegistrationHandler hander = null; // created var to be used to check
// val
if (this.numSlaves == 0) {
hander = new TPCRegistrationHandler();
} else {
hander = new TPCRegistrationHandler(this.numSlaves);
}
this.regServer.addHandler(hander);
// this.regServer.connect();
ServerRunner regRunner = new ServerRunner(regServer, "regserver",
"register this");
regRunner.start();
AutoGrader.agTPCMasterFinished();
}
/**
* Converts Strings to 64-bit longs. Borrowed from http://goo.gl/le1o0W,
* adapted from String.hashCode().
*
* @param string
* String to hash to 64-bit
* @return long hashcode
*/
public long hashTo64bit(String string) {
long h = 1125899906842597L;
int len = string.length();
for (int i = 0; i < len; i++) {
h = (31 * h) + string.charAt(i);
}
return h;
}
/**
* Compares two longs as if they were unsigned (Java doesn't have unsigned
* data types except for char). Borrowed from http://goo.gl/QyuI0V
*
* @param n1
* First long
* @param n2
* Second long
* @return is unsigned n1 less than unsigned n2
*/
public boolean isLessThanUnsigned(long n1, long n2) {
return (n1 < n2) ^ ((n1 < 0) != (n2 < 0));
}
public boolean isLessThanEqualUnsigned(long n1, long n2) {
return isLessThanUnsigned(n1, n2) || (n1 == n2);
}
/**
* Find primary replica for a given key.
*
* @param key
* @return SlaveInfo of first replica
*/
public SlaveInfo findFirstReplica(String key) {
// 64-bit hash of the key
long hashedKey = hashTo64bit(key.toString());
// implement me
if (treemap.isEmpty()) {
return null;
} else if (!treemap.containsKey(hashedKey)) {
SortedMap<Long, SlaveInfo> tail = treemap.tailMap(hashedKey);
if (tail.isEmpty()) {
hashedKey = treemap.firstKey();
} else {
hashedKey = tail.firstKey();
}
}
return treemap.get(hashedKey);
}
/**
* Find the successor of firstReplica.
*
* @param firstReplica
* SlaveInfo of primary replica
* @return SlaveInfo of successor replica
*/
public SlaveInfo findSuccessor(SlaveInfo firstReplica) {
if (treemap.isEmpty()) {
return null;
} else {
long fkey = firstReplica.getSlaveID();
NavigableMap<Long, SlaveInfo> tail = treemap.tailMap(fkey, false);
if (tail.isEmpty()) {
fkey = treemap.firstKey();
} else {
fkey = tail.firstKey();
}
return treemap.get(fkey);
}
}
/**
* Synchronized method to perform 2PC operations. This method contains the
* bulk of the two-phase commit logic. It performs phase 1 and phase 2 with
* appropriate timeouts and retries. See the spec for details on the
* expected behavior.
*
* @param msg
* @param isPutReq
* boolean to distinguish put and del requests
* @throws KVException
* if the operation cannot be carried out
*/
public synchronized void performTPCOperation(KVMessage msg, boolean isPutReq)
throws KVException {
AutoGrader.agPerformTPCOperationStarted(isPutReq);
// implement me
String key = msg.getKey();
if (key != null) {
WriteLock cacheLock = masterCache.getWriteLock(key);
cacheLock.lock();
SlaveInfo firstReplica = findFirstReplica(key);
SlaveInfo secondReplica = findSuccessor(firstReplica);
// send commit requests, phase 1.
try {
Socket sock1 = firstReplica.connectHost();
Socket sock2 = secondReplica.connectHost();
if (isPutReq) {
KVMessage putReq = new KVMessage("putreq");
putReq.setKey(key);
putReq.setValue(msg.getValue());
putReq.sendMessage(sock1);
putReq.sendMessage(sock2);
} else {
KVMessage delReq = new KVMessage("delreq");
delReq.setKey(key);
delReq.sendMessage(sock1);
delReq.sendMessage(sock2);
}
// receive votes from slaves
KVMessage response1 = new KVMessage(sock1, TIMEOUT_MILLISECONDS);
KVMessage response2 = new KVMessage(sock2, TIMEOUT_MILLISECONDS);
KVMessage finalDecision; // to keep track of the final decision,
// in case we need to re-send to
// slaves
// if all slaves vote "ready", send global-commit to slaves
if (response1.getMsgType().equals("ready")
&& response2.getMsgType().equals("ready")) {
KVMessage commitMsg = new KVMessage("commit");
finalDecision = commitMsg;
sock1 = firstReplica.connectHost();
commitMsg.sendMessage(sock1);
sock2 = secondReplica.connectHost();
commitMsg.sendMessage(sock2);
}
// in any other case (aborts or timeout), send global-abort
else {
KVMessage abortMsg = new KVMessage("abort");
finalDecision = abortMsg;
abortMsg.sendMessage(sock1);
abortMsg.sendMessage(sock2);
}
response1 = new KVMessage(sock1, TIMEOUT_MILLISECONDS); // check
// for
// "ack"
// response
// from
// slaves
response2 = new KVMessage(sock2, TIMEOUT_MILLISECONDS);
// keep re-sending final decision until all slaves "ack"
while (!response1.getMsgType().equals("ack")
|| !response2.getMsgType().equals("ack")) {
finalDecision.sendMessage(sock1);
finalDecision.sendMessage(sock2);
response1 = new KVMessage(sock1, TIMEOUT_MILLISECONDS);
response2 = new KVMessage(sock2, TIMEOUT_MILLISECONDS);
}
firstReplica.closeHost(sock1);
secondReplica.closeHost(sock2);
} catch (KVException e) {
e.printStackTrace();
} finally {
cacheLock.unlock();
}
}
AutoGrader.agPerformTPCOperationFinished(isPutReq);
return;
}
/**
* Perform GET operation in the following manner: - Try to GET from cache,
* return immediately if found - Try to GET from first/primary replica - If
* primary succeeded, return value - If primary failed, try to GET from the
* other replica - If secondary succeeded, return value - If secondary
* failed, return KVExceptions from both replicas Please see spec for more
* details.
*
* @param msg
* Message containing Key to get
* @return Value corresponding to the Key
* @throws KVException
*/
public String handleGet(KVMessage msg) throws KVException {
AutoGrader.aghandleGetStarted();
// implement me
String returnValue = "";
String key = msg.getKey();
WriteLock cacheLock = masterCache.getWriteLock(key);
cacheLock.lock();
if (masterCache.get(key) != null) {
returnValue = masterCache.get(key);
AutoGrader.aghandleGetFinished();
return returnValue;
}
// if key is not in master cache, proceed:
try {
SlaveInfo firstReplica = findFirstReplica(key);
KVMessage getReq = new KVMessage("getreq");
getReq.setKey(key);
// send message:
Socket sock1 = firstReplica.connectHost();
getReq.sendMessage(sock1);
// timeout:
KVMessage response1 = new KVMessage(sock1, TIMEOUT_MILLISECONDS);
firstReplica.closeHost(sock1);
if (response1.getMsgType().equals("resp")) {
returnValue = response1.getValue();
masterCache.put(response1.getKey(), returnValue);
} else { // if 1st slave is unsuccessful:
SlaveInfo secondReplica = findSuccessor(firstReplica);
// send message:
Socket sock2 = secondReplica.connectHost();
getReq.sendMessage(sock2);
// timeout:
KVMessage response2 = new KVMessage(sock2, TIMEOUT_MILLISECONDS);
secondReplica.closeHost(sock2);
if (response2.getMessage().equals("resp")) {
returnValue = response2.getValue();
masterCache.put(response2.getKey(), returnValue);
}
}
} catch (KVException e) { // need two exceptions possibly
e.printStackTrace();
} finally {
cacheLock.unlock();
}
AutoGrader.aghandleGetFinished();
return returnValue;
}
/**
* Implements NetworkHandler to handle registration requests from
* SlaveServers.
*
*/
public class TPCRegistrationHandler implements NetworkHandler {
public ThreadPool threadpool = null;
public TPCRegistrationHandler() {
// Call the other constructor
this(1);
}
public TPCRegistrationHandler(int connections) {
threadpool = new ThreadPool(connections);
}
@Override
public void handle(Socket client) throws IOException {
// implement me
RegistrationHandler handle = new RegistrationHandler(client);
try {
threadpool.addToQueue(handle);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public class RegistrationHandler implements Runnable {
public Socket client = null;
public RegistrationHandler(Socket client) {
this.client = client;
}
@Override
public void run() {
// implement me
try {
KVMessage response = new KVMessage(client);
if (response.getMsgType().equals("register")) {
SlaveInfo slave = new SlaveInfo(response.getMessage());
if (treemap.get(slave.getSlaveID()) != null) {
treemap.remove(slave.getSlaveID());
}
treemap.put(slave.getSlaveID(), slave);
// Todo make a new KVmessage and check if it exist, and
// if it does update instead of make new
// done
response = new KVMessage("resp",
"Successfully registered");
response.sendMessage(slave.connectHost());
}
} catch (KVException e) {
System.out.println(e);
e.printStackTrace();
}
}
}
}
/**
* Data structure to maintain information about SlaveServers
*
*/
public class SlaveInfo {
// 64-bit globally unique ID of the SlaveServer
public long slaveID = -1;
// Name of the host this SlaveServer is running on
public String hostName = null;
// Port which SlaveServer is listening to
public int port = -1;
/**
*
* @param slaveInfo
* as "SlaveServerID@HostName:Port"
* @throws KVException
*/
public SlaveInfo(String slaveInfo) throws KVException {
// implement me
if (slaveInfo == null) {
throw new KVException(new KVMessage("resp", "null failure"));
} else {
int index = slaveInfo.indexOf("@");
this.slaveID = Long.valueOf(slaveInfo.substring(0, index))
.longValue();
int index2 = index++;
index = slaveInfo.indexOf(":");
this.hostName = slaveInfo.substring(index2 + 1, index);
port = Integer.valueOf((String) slaveInfo.subSequence(
index + 1, slaveInfo.length()));
}
}
public long getSlaveID() {
return slaveID;
}
public Socket connectHost() throws KVException {
// TODO: implement me
Socket sock = null;
try {
sock = new Socket(this.hostName, this.port);
} catch (Exception e) {
System.out.println(e);
throw new KVException(new KVMessage("resp",
"Network Error: Could not create socket"));
}
return sock;
}
public void closeHost(Socket sock) throws KVException {
// TODO: implement me
try {
sock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// class to implement the comparator for Treemap
public class ConsistentComparator implements Comparator<Long> {
@Override
public int compare(Long o1, Long o2) {
if (isLessThanUnsigned(o1, o2)) {
return -1;
} else if (o1.equals(o2)) {
return 0;
} else {
return 1;
}
}
}
public static class ServerRunner implements Runnable {
public static final int THREAD_STOP_TIMEOUT_MS = 1000 * 10; // Wait 10
// seconds
public ServerRunner(SocketServer socs, String name, String desc) {
sockserver = socs;
runnerName = name;
runnerDesc = desc;
}
private final SocketServer sockserver;
public final String runnerName;
public final String runnerDesc;
private Thread thread = null;
private boolean isUp = false;
@Override
public void run() {
try {
sockserver.connect();
System.out.format("Running %s...%n", runnerName);
synchronized (this) {
isUp = true;
notifyAll();
}
sockserver.run();
synchronized (this) {
isUp = false;
notifyAll();
}
} catch (Exception e) {
System.out.println(String.format("SERVER-SIDE: Error from %s",
runnerName));
e.printStackTrace();
}
}
public void start() {
if (thread == null) {
thread = new Thread(this, runnerName);
thread.setDaemon(true); // Allow JVM to exit if thread abandoned
System.out.format("INFO ServerRunner.start: Starting %s: %s%n",
runnerName, runnerDesc);
thread.start();
while (!isUp) {
try {
synchronized (this) {
this.wait(100);
}
} catch (InterruptedException e) {
}
}
System.out.format("INFO ServerRunner.start: %s is now up.%n",
runnerName, runnerDesc);
}
}
public void stop() {
System.out.format("INFO ServerRunner: Stopping %s%n", runnerName);
if (sockserver != null) {
sockserver.stop();
}
if (thread != null) {
try {
thread.join(THREAD_STOP_TIMEOUT_MS);
} catch (InterruptedException e) {
System.out.format("ERROR ServerRunner: "
+ "Failed to stop Server (%s), giving up.%n",
runnerName);
}
}
isUp = false;
thread = null;
}
}
}