org.coweb.oe.OperationEngine Maven / Gradle / Ivy
The newest version!
package org.coweb.oe;
import java.util.Map;
import java.util.HashMap;
import java.util.Stack;
public class OperationEngine {
private int siteId;
private ContextVector cv = null;
private ContextVectorTable cvt = null;
private HistoryBuffer hb = null;
private int siteCount = 1;
/**
* Controls the operational transformation algorithm. Provides a public
* API for operation processing, garbage collection, and engine
* synchronization.
*
* @param siteId Unique integer site ID for this engine instance
*/
public OperationEngine(int siteId) throws OperationEngineException {
this.siteId = siteId;
HashMapargs = new HashMap();
args.put("count", siteId + 1);
this.cv = new ContextVector(args);
this.cvt = new ContextVectorTable(this.cv, siteId);
this.hb = new HistoryBuffer();
}
@Override
public String toString() {
StringBuffer b = new StringBuffer();
b.append("{siteId : " + siteId);
b.append(",ContextVector : " + this.cv);
b.append(",ContextVectorTable : " + this.cvt);
b.append(",HistoryBuffer : " + this.hb);
b.append(",siteCount : " + this.siteCount);
b.append("}");
return b.toString();
}
/**
* Gets the state of this engine instance to seed a new instance.
*
* @return {Object[]} Array or serialized state
*/
public Object[] getState() {
int[] frozen = this.cvt.getEquivalents(this.cv,
this.siteId);
Object[] ret = { this.cvt.getState(), this.hb.getState(), new Integer(this.siteId),
frozen };
return ret;
}
/**
* Sets the state of this engine instance to state received from another
* instance.
*
* @param arr Array in the format returned by getState
*/
public void setState(Object[] arr) throws OperationEngineException {
this.cvt.setState((int[][])arr[0]);
this.hb.setState((Object[])arr[1]);
this.cv = this.cvt.getContextVector(((Integer)arr[2]).intValue());
this.cv = this.cv.copy();
this.cvt.updateWithContextVector(this.siteId, this.cv);
this.siteCount = this.cv.getSize();
int[] frozen = (int[]) arr[3];
for (int i = 0; i < frozen.length; i++) {
this.freezeSite(frozen[i]);
}
}
/**
* Makes a copy of the engine context vector representing the local document
* state.
* @throws OperationEngineException
*
* @return Copy of the context vector for the local site
*/
public ContextVector copyContextVector() throws OperationEngineException {
return this.cv.copy();
}
/**
* Factory method that creates an operation object initialized with the
* given values.
*
* @param local True if the operation was originated locally,
* false if not
* @param key Operation key
* @param value Operation value
* @param type Type of operation: update, insert, delete
* @param position Operation integer position
* @param site Integer site ID where a remote op originated.
* Ignored for local operations which adopt the local site ID.
* @param cv Operation context. Ignored for local operations
* which adopt the local site context.
* @param order Place of the operation in the total order. Ignored
* for local operations which are not yet assigned a place in the
* order.
* @throws OperationEngineException
* @return Subclass instance matching the given type
*/
public Operation createOp(boolean local, String key, String value,
String type, int position, int site, int[] cv, int order) throws OperationEngineException {
Map args = new HashMap();
if (local) {
args.put("key", key);
args.put("position", new Integer(position));
args.put("value", value);
args.put("siteId", new Integer(this.siteId));
args.put("contextVector", this.copyContextVector());
args.put("local", true);
} else {
// build cv from raw sites array
HashMap map = new HashMap();
map.put("sites", cv);
ContextVector contextVector = new ContextVector(map);
args.put("key", key);
args.put("position", new Integer(position));
args.put("value", value);
args.put("siteId", new Integer(site));
args.put("contextVector", contextVector);
args.put("order", order);
args.put("local", false);
}
return Operation.createOperationFromType(type, args);
}
/**
* Creates an operation object and pushes it into the operation engine
* algorithm. The parameters and return value are the same as those
* documented for createOp.
* @throws OperationEngineException
*/
public Operation push(boolean local, String key, String value, String type,
int position, int site, int[] cv, int order) throws OperationEngineException {
Operation op = this.createOp(local, key, value, type, position, site,
cv, order);
if (local) {
return this.pushLocalOp(op);
} else {
return this.pushRemoteOp(op);
}
}
/**
* Procceses a local operation and adds it to the history buffer.
*
* @param op Local operation
* @return Reference to the pass parameter
*/
public Operation pushLocalOp(Operation op) {
// update local context vector
this.cv.setSeqForSite(op.getSiteId(), op.getSeqId());
// add to history buffer
this.hb.addLocal(op);
return op;
}
/**
* Procceses a remote operation, transforming it if required, and adds the
* original to the history buffer.
*
* @param op Remote operation
* @throws OperationEngineException
* @return New, transformed operation object or null if
* the effect of the passed operation is nothing and should not be
* applied to the shared state
*/
public Operation pushRemoteOp(Operation op) throws OperationEngineException {
Operation top = null;
if (this.hasProcessedOp(op)) {
// let the history buffer track the total order for the op
this.hb.addRemote(op);
// engine has already processed this op so ignore it
return null;
} else if (this.cv.equals(op.getContextVector())) {
// no transform needed
// make a copy so return value is independent of input
top = op.copy();
} else {
// transform needed to upgrade context
ContextDifference cd = this.cv.subtract(op.getContextVector());
// make the original op immutable
op.setImmutable(true);
// top is a transformed copy of the original
top = this._transform(op, cd);
}
// update local context vector with the original op
this.cv.setSeqForSite(op.getSiteId(), op.getSeqId());
// store original op
this.hb.addRemote(op);
// update context vector table with original op
this.cvt.updateWithOperation(op);
// return the transformed op
return top;
}
/**
* Processes an engine synchronization event.
*
* @param site Integer site ID of where the sync originated
* @param cv Context vector sent by the engine at that site
* @throws OperationEngineException
*/
public void pushSync(int site, ContextVector cv) throws OperationEngineException {
// update the context vector table
this.cvt.updateWithContextVector(site, cv);
}
/**
* Processes an engine synchronization event.
*
* @param site Integer site ID of where the sync originated
* @param sites Array form of the context vector sent by the site
* @throws OperationEngineException
*/
public void pushSyncWithSites(int site, int[] sites) throws OperationEngineException {
// build a context vector from raw site data
HashMap args = new HashMap();
args.put("sites",sites);
ContextVector cv = new ContextVector(args);
this.pushSync(site, cv);
}
/**
* Runs the garbage collection algorithm over the history buffer.
* @throws OperationEngineException
*
* @return Compiuted minimum context vector of the
* earliest operation garbage collected or null if garbage
* collection did not run
*/
public ContextVector purge() throws OperationEngineException {
if (this.getBufferSize() == 0) {
// exit quickly if there is nothing to purge
return null;
}
// get minimum context vector
ContextVector mcv = this.cvt.getMinimumContextVector();
if (mcv == null) {
// exit quickly if there is no mcv
return null;
}
Operation min_op = null;
ContextDifference cd = this.cv.oldestDifference(mcv);
Stack ops = this.hb.getOpsForDifference(cd);
while (ops.size() > 0) {
// get an op from the list we have yet to process
Operation curr = ops.pop();
// if we haven't picked a minimum op yet OR
// the current op is before the minimum op in context
if (min_op == null || curr.compareByContext(min_op) == -1) {
// compute the oldest difference between the document state
// and the current op
cd = this.cv.oldestDifference(curr.getContextVector());
// add the operations in this difference to the list to process
ops.addAll(this.hb.getOpsForDifference(cd));
// make the current op the new minimum
min_op = curr;
}
}
// get history buffer contents sorted by context dependencies
ops = this.hb.getContextSortedOperations();
// remove keys until we hit the min
for (int i = 0; i < ops.size(); i++) {
Operation op = ops.elementAt(i);
// if there is no minimum op OR
// if this op is not the minimium
if (min_op == null
|| (min_op.getSiteId() != op.getSiteId() || min_op
.getSeqId() != op.getSeqId())) {
// remove operation from history buffer
this.hb.remove(op);
} else {
// don't remove any more ops with context greater than or
// equal to the minimum
break;
}
}
return mcv;
}
/**
* Gets the size of the history buffer in terms of stored operations.
*
* @return Integer size
*/
public int getBufferSize() {
return this.hb.getCount();
}
/**
* Gets if the engine has already processed the give operation based on its
* context vector and the context vector of this engine instance.
*
* @param op Operation to check
* @return True if the engine already processed this operation,
* false if not
*/
public boolean hasProcessedOp(Operation op) {
int seqId = this.cv.getSeqForSite(op.getSiteId());
// console.log('op processed? %s: this.cv=%s, seqId=%d, op.siteId=%d,
// op.cv=%s, op.seqId=%d',
// (seqId >= op.seqId), this.cv.toString(), seqId, op.siteId,
// op.contextVector.toString(), op.seqId);
return (seqId >= op.getSeqId());
}
/**
* Freezes a slot in the context vector table by inserting a reference to
* context vector of this engine. Should be invoked when a remote site stops
* participating.
*
* @param site Integer ID of the site to freeze
* @throws OperationEngineException
*/
public void freezeSite(int site) throws OperationEngineException {
// ignore if already frozen
if (this.cvt.getContextVector(site) != this.cv) {
// insert a ref to this site's cv into the cvt for the given site
this.cvt.updateWithContextVector(site, this.cv);
// one less site participating now
this.siteCount--;
}
}
/**
* Thaws a slot in the context vector table by inserting a zeroed context
* vector into the context vector table. Should be invoked before processing
* the first operation from a new remote site.
*
* @param site Integer ID of the site to thaw
* @throws OperationEngineException
*/
public void thawSite(int site) throws OperationEngineException {
// don't ever thaw the slot for our own site
if (site == this.siteId) {
return;
}
// get the minimum context vector
ContextVector cv = this.cvt.getMinimumContextVector();
// grow it to include the site if needed
cv.growTo(site);
// use it as the initial context of the site
this.cvt.updateWithContextVector(site, cv);
// one more site participating now
this.siteCount++;
}
/**
* Gets the number of sites known to be participating, including this site.
*
* @return Integer count
*/
public int getSiteCount() {
return this.siteCount;
};
/**
* Executes a recursive step in the operation transformation control
* algorithm. This method assumes it will NOT be called if no transformation
* is needed in order to reduce the number of operation copies needed.
*
* @param op Operation to transform
* @param cd Context vector difference between the given
* op and the document state at the time of this recursive call
* @throws OperationEngineException
* @return A new operation, including the effects of all
* of the operations in the context difference or null if the
* operation can have no further effect on the document state
*/
private Operation _transform(Operation op, ContextDifference cd) throws OperationEngineException {
// get all ops for context different from history buffer sorted by
// context dependencies
Stack ops = this.hb.getOpsForDifference(cd);
Operation xop = null;
ContextDifference xcd = null;
Operation cxop = null;
Operation cop = null;
// xcd, xop, cxop, cop, i, l;
// copy the incoming operation to avoid disturbing the history buffer
// when the op comes from our history buffer during a recursive step
op = op.copy();
// iterate over all operations in the difference
for (int i = 0; i < ops.size(); i++) {
// xop is the previously applied op
xop = ops.elementAt(i);
if (!op.getContextVector().equals(xop.getContextVector())) {
// see if we've cached a transform of this op in the desired
// context to avoid recursion
cxop = xop.getFromCache(op.getContextVector());
// cxop = null;
if (cxop != null) {
xop = cxop;
} else {
// transform needed to upgrade context of xop to op
xcd = op.getContextVector()
.subtract(xop.getContextVector());
if (xcd.sites == null || xcd.sites.size() == 0) {
throw new OperationEngineException(
"transform produced empty context diff");
}
// we'll get a copy back from the recursion
cxop = this._transform(xop, xcd);
if (cxop == null) {
// xop was invalidated by a previous op during the
// transform so it has no effect on the current op;
// upgrade context immediately and continue with
// the next one
op.upgradeContextTo(xop);
// @todo: see null below
continue;
}
// now only deal with the copy
xop = cxop;
}
}
if (!op.getContextVector().equals(xop.getContextVector())) {
throw new OperationEngineException("context vectors unequal after upgrade");
}
// make a copy of the op as is before transform
cop = op.copy();
// transform op to include xop now that contexts match IT(op, xop)
op = op.transformWith(xop);
if (op == null) {
// op target was deleted by another earlier op so return now
// do not continue because no further transforms have any
// meaning on this op
// @todo: i bet we want to remove this shortcut if we're
// deep in recursion when we find a dead op; instead cache it
// so we don't come down here again
return null;
}
// cache the transformed op
op.addToCache(this.siteCount);
// do a symmetric transform on a copy of xop too while we're here
xop = xop.copy();
xop = xop.transformWith(cop);
if (xop != null) {
xop.addToCache(this.siteCount);
}
}
// op is always a copy because we never entered this method if no
// transform was needed
return op;
}
public int getSiteId() {
return this.siteId;
}
}