package com.sap.runlet.operationaltransformation;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* A very simple peer implementation for operational transformations that starts
* out with a provided initial state and then receives operations which it applies
* to its state. It manages a set of clients. Using the <tt>server</tt> constructor
* parameter one can configure whether this peer is a client or a server.
*
* @author Axel Uhl D043530
*
* @param <O> type of change operations to be processed by this server
* @param <S> type of state managed by this server to which the operations can be applied
*/
public class PeerImpl<O extends Operation<S>, S> implements Peer<O, S> {
private String name;
private StatesAndOperations<O, S> statesAndOperations;
/**
* Tells if this peer acts in the server or client role. This is used to determine in which
* direction to apply the operational transformation.
*/
private Role role;
/**
* Tells if this peer is currently actively running, with a certain number of pending / running requests
*/
private int running = 0;
/**
* Queues operations sent out to a peer in {@link #updatePeers(Operation, Peer)} and whose
* merging has not yet been confirmed by that peer. Those messages are transformed when an
* operation from that peer is applied. When applying an operation to the peer has been
* confirmed, the first operation from the queue is removed because operations sent
* from this peer to the remote peer are processed in order.
*/
private Map<Peer<O, S>, UnmergedOperationsQueue<O, S>> unmergedOperationsForPeer = new HashMap<Peer<O, S>, UnmergedOperationsQueue<O, S>>();
/**
* Remembers per peer how many of that peer's operations this peer has already
* applied locally.
*/
private Map<Peer<O, S>, Integer> numberOfMergedOperations = new HashMap<Peer<O, S>, Integer>();
private Transformer<O> transformer;
/**
* The background task handler which sends out the asynchronous updates to the peers.
*/
private ExecutorService merger;
public PeerImpl(Transformer<O> transformer, S initialState, Role role) {
this.transformer = transformer;
statesAndOperations = new StatesAndOperations<O, S>(initialState);
this.role = role;
this.merger = Executors.newSingleThreadExecutor();
}
public PeerImpl(String name, Transformer<O> transformer, S initialState, Role role) {
this(transformer, initialState, role);
this.name = name;
}
/**
* Constructs a client peer and connects it to the <tt>server</tt> peer, establishing
* the two-way link between client and server peers. The initial state is taken from
* the server.
*/
public PeerImpl(Transformer<O> transformer, Peer<O, S> server) {
this.transformer = transformer;
S initialState = server.addPeer(this);
statesAndOperations = new StatesAndOperations<O, S>(initialState);
this.role = Role.CLIENT;
this.merger = Executors.newSingleThreadExecutor();
addPeer(server);
}
/**
* Constructs a client peer with a name and connects it to the
* <tt>server</tt> peer, establishing the two-way link between client and
* server peers. The initial state is taken from the server.
*/
public PeerImpl(String name, Transformer<O> transformer, Peer<O, S> server) {
this(transformer, server);
this.name = name;
}
@Override
public void finalize() {
merger.shutdown();
}
private Transformer<O> getTransformer() {
return transformer;
}
@Override
public S addPeer(Peer<O, S> peer) {
if ( role == Role.CLIENT && getPeers().size() > 0) {
throw new RuntimeException("A client must be connected to at most one server");
}
unmergedOperationsForPeer.put(peer, new UnmergedOperationsQueue<O, S>());
numberOfMergedOperations.put(peer, 0);
return getCurrentState();
}
private Collection<Peer<O, S>> getPeers() {
return unmergedOperationsForPeer.keySet();
}
public S getCurrentState() {
return statesAndOperations.getCurrentState();
}
@Override
public synchronized void apply(O operation) {
taskStarted();
statesAndOperations.apply(operation);
updatePeers(operation, /* except */ null);
taskFinished();
}
@Override
public synchronized void apply(final Peer<O, S> source, O operation,
final int numberOfOperationsSourceHasMergedFromThis) {
taskStarted();
if (!getPeers().contains(source)) {
throw new RuntimeException("Peer "+source+" not registered with peer "+this);
}
// Starting from base up to current, compute transformed operation sequence to send
// to client; this will create a sequence of states for the client which eventually
// leads up to a state that equals the server's current state.
O transformedOp = operation;
UnmergedOperationsQueue<O, S> unmergedOperationsForSource = unmergedOperationsForPeer.get(source);
int localOpNumber = numberOfOperationsSourceHasMergedFromThis;
for (O unconfirmedOperation : unmergedOperationsForSource.getUnmergedOperations(numberOfOperationsSourceHasMergedFromThis)) {
if (role == Role.SERVER) {
ClientServerOperationPair<O> pair = getTransformer().transform(transformedOp, unconfirmedOperation);
transformedOp = pair.getClientOp();
unmergedOperationsForSource.updateWithTransformed(localOpNumber, pair.getServerOp());
} else {
ClientServerOperationPair<O> pair = getTransformer().transform(unconfirmedOperation, transformedOp);
transformedOp = pair.getServerOp();
unmergedOperationsForSource.updateWithTransformed(localOpNumber, pair.getClientOp());
}
localOpNumber++;
}
statesAndOperations.apply(transformedOp); // produce a new current state
final int numberOfMergedOperationsFromSource = numberOfMergedOperations.get(source)+1;
numberOfMergedOperations.put(source, numberOfMergedOperationsFromSource);
// It's important that the following call to confirm is synchronized with the
// sending of updates to the source peer. Therefore, the confirm call is executed
// in the same serializing background thread:
scheduleTask(new Runnable() {
public void run() {
source.confirm(PeerImpl.this, numberOfMergedOperationsFromSource);
}
});
updatePeers(transformedOp, /* except */source);
taskFinished();
}
/**
* Propagates the changes described by <tt>operation</tt> to all registered
* peers (except the peer identified by <tt>except</tt> if not <tt>null</tt>
* ). For each peer, the operation is sent along to that peer's
* {@link #apply(Peer, Operation, int)} method together with the
* {@link #numberOfMergedOperations number of operations currently merged
* from that peer}.
*/
private synchronized void updatePeers(final O operation, Peer<O, S> except) {
for (final Peer<O, S> peer : getPeers()) {
if (except == null || !except.equals(peer)) {
unmergedOperationsForPeer.get(peer).sentOutOperation(operation);
// remember the number of merged operations while still in synchronized block
final int numberOfMergedOpsForPeer = numberOfMergedOperations.get(peer);
scheduleTask(new Runnable() {
public void run() {
/*
* Thoughts on locking: The peer runs the folling apply(...) call synchronized.
* Therefore, it can't take other apply calls (local or from other peers) during
* that time. It may, though, have pending tasks in its updatePeers that can
* continue to run. This may include updates for this peer which would be received
* by this peer's apply(...) method.
*/
peer.apply(PeerImpl.this, operation, numberOfMergedOpsForPeer);
}
});
}
}
}
public String toString() {
if (name != null) {
return name;
} else {
return (role == Role.SERVER?"server":"client")+" "+super.toString();
}
}
@Override
public void confirm(Peer<O, S> source, int numberOfMergedOperations) {
unmergedOperationsForPeer.get(source).confirm(numberOfMergedOperations);
}
/**
* Records that a new task is scheduled by incrementing {@link #running} by one, then
* schedules the <tt>runnable</tt> with the {@link #merger} executor service. The
* <tt>runnable</tt> is wrapped such that when its execution has finished,
* {@link #taskFinished()} will be called such that {@link #running} is decremented
* properly again and waiting clients in {@link #waitForNotRunning()} can be unblocked.
*/
private void scheduleTask(final Runnable runnable) {
taskStarted();
merger.execute(new Runnable() {
public void run() {
runnable.run();
taskFinished();
}
});
}
private void taskStarted() {
running++;
}
/**
* Must be called by all tasks submitted to the {@link #merger} when finished.
* Unblocks {@link #waitForNotRunning()} if the merger's queue got empty by this.
*/
private synchronized void taskFinished() {
running--;
if (running == 0) {
notifyAll();
}
}
@Override
public synchronized void waitForNotRunning() {
while (running > 0) {
try {
wait();
} catch (InterruptedException e) {
// ignore interruption, try again
}
}
}
}