/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.waveprotocol.wave.concurrencycontrol.client;
import com.google.common.annotations.VisibleForTesting;
import org.waveprotocol.wave.concurrencycontrol.common.DeltaPair;
import org.waveprotocol.wave.model.operation.TransformException;
import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
import org.waveprotocol.wave.model.wave.ParticipantId;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Queue;
/**
* Ordered list of operations extractable as single-creator deltas. Consecutive
* operations with matching creators are merged into a single delta. Adjacent
* operations with mismatched creators result in two separate deltas. Allows
* transformation of the enqueued client operations against a server delta.
*
*/
class OperationQueue {
/**
* Helper for transforming a client delta against a server delta in such a way
* that can be substituted out for testing.
*/
@VisibleForTesting
interface Transformer {
/**
* Transforms a client delta against a server delta in a manner which can be
* overridden for testing.
*/
DeltaPair transform(Iterable<WaveletOperation> client, Iterable<WaveletOperation> server)
throws TransformException;
}
private enum ItemState {
/**
* This delta has been sent. You are not allowed to take the operations in this
* delta and create a new delta by concatenating the operations together with another delta.
*/
SENT,
/**
* The delta have been optimised.
*/
OPTIMISED,
/**
* This is a newly created, untouched delta
*/
NONE
}
/**
* This class is used to keep additional information with OperationMergingDelta. This
* is what lives internally in the queue.
*/
private static class Item {
final MergingSequence opSequence;
final ItemState state;
/**
* @param delta assumed not null
* @param state The state of the item.
*/
public Item(MergingSequence delta, ItemState state) {
this.opSequence = delta;
this.state = state;
}
@Override
public String toString() {
return "Delta: " + opSequence + ", item state: " + state;
}
}
/** Transforms deltas using {@link DeltaPair#transform()}. */
private static final Transformer TRANSFORMER = new Transformer() {
@Override
public DeltaPair transform(Iterable<WaveletOperation> client, Iterable<WaveletOperation> server)
throws TransformException {
return (new DeltaPair(client, server)).transform();
}
};
/** Number of head deltas to inspect when estimating queue size. */
private static final int ESTIMATE_DELTAS_TO_COUNT = 4;
private final LinkedList<Item> queue;
private ParticipantId tailCreator;
private final Transformer transformer;
/**
* Creates an empty {@link OperationQueue} that will transform deltas using
* {@link DeltaPair#transform()}.
*/
public OperationQueue() {
this(TRANSFORMER);
}
/**
* Creates an empty {@link OperationQueue} which will use the given
* {@link Transformer}.
*/
@VisibleForTesting
OperationQueue(Transformer transformer) {
this.transformer = transformer;
queue = new LinkedList<Item>();
tailCreator = null;
}
/**
* Adds the given operation to the tail of the operation queue. Merges with
* the delta at the tail if the creators match, otherwise creates a new tail
* delta.
*/
public void add(WaveletOperation op) {
ParticipantId creator = op.getContext().getCreator();
if (queue.isEmpty() || !creator.equals(tailCreator) ||
(queue.getLast().state != ItemState.NONE)) {
queue.addLast(new Item(new MergingSequence(), ItemState.NONE));
tailCreator = creator;
}
queue.getLast().opSequence.add(op);
}
/**
* Prepends the given delta onto the queue's head. No merging is
* allows on this delta. This is because we don't know if the server have actually got
* the previously sent delta, we can't change the delta once it's sent.
*
* @param newHead delta to use for the queue. Must only contain operations from a
* single author. May be empty, in which case this call will do
* nothing.
*/
public void insertHead(List<WaveletOperation> newHead) {
if (newHead.isEmpty()) {
return;
}
MergingSequence mergingHead = new MergingSequence(newHead);
Item item = new Item(mergingHead, ItemState.SENT);
ParticipantId creator = mergingHead.get(0).getContext().getCreator();
if (queue.isEmpty()) {
queue.add(item);
tailCreator = creator;
} else {
queue.addFirst(item);
}
}
/** Returns true if there are no pending operations in the queue. */
public boolean isEmpty() {
return queue.isEmpty();
}
/**
* Estimates the number of operations in this queue.
*
* In order to provide a bounded execution time the result is an underestimate
* of the true number of queued operations.
*/
public int estimateSize() {
int estimate = 0;
// Sum delta size for a fixed number of deltas at the start.
int headDeltasToCount = ESTIMATE_DELTAS_TO_COUNT;
Iterator<Item> itr = queue.iterator();
while ((headDeltasToCount > 0) && itr.hasNext()) {
estimate += itr.next().opSequence.size();
--headDeltasToCount;
}
if (itr.hasNext()) {
// Add size of the last delta as new ops are likely to be pushed into it.
estimate += queue.getLast().opSequence.size();
// Add one for each other delta in the queue.
if (queue.size() > (ESTIMATE_DELTAS_TO_COUNT + 1)) {
estimate += queue.size() - (ESTIMATE_DELTAS_TO_COUNT + 1);
}
}
return estimate;
}
/**
* Takes a delta full of operations by the same creator from the head of the
* queue, removing those operations from the queue. It will contain all
* operations from the head of the queue up until but not including the first
* change in creator. Hence if all operations in the queue have the same
* creator, it will contain all those operations and the queue will become
* empty.
*
* @return A non-empty delta without signature or version information,
* containing operations which all have the same creator address in
* their context.
* @throws NoSuchElementException If the queue is empty.
*/
public List<WaveletOperation> take() {
Item item = takeMergedAndOptimisedItem(queue);
if (isEmpty()) {
tailCreator = null;
}
return item.opSequence;
}
/**
* Transforms the given server delta against the queued operations. Updates
* the queued deltas with the results of the transformation and returns the
* transformed server delta.
*
* Queued delta which have all of their operations transformed away and hence
* become empty are discarded.
*
* @throws TransformException If transformation of any operations fails.
*/
public List<WaveletOperation> transform(List<WaveletOperation> serverOps)
throws TransformException {
List<WaveletOperation> transformedServerOps = serverOps;
Queue<Item> newQueue = new LinkedList<Item>();
while (!queue.isEmpty()) {
// Merge in all subsequent consecutive deltas that have the same author and
// optimise the delta before transforming against the server delta because
// it makes transformation more efficient.
Item item = takeMergedAndOptimisedItem(queue);
MergingSequence queuedDelta = item.opSequence;
DeltaPair transformedDeltas = transformer.transform(queuedDelta, transformedServerOps);
// Even if server op is nullified we must still count the nullified op,
// hence we use the input server ops for incrementing the version. This
// is already transformedDelta.getServer() and we don't touch it.
transformedServerOps = transformedDeltas.getServer();
// Discard client deltas which have had all their ops transformed away
if (!transformedDeltas.getClient().isEmpty()) {
newQueue.add(new Item(
new MergingSequence(transformedDeltas.getClient()), item.state));
}
}
queue.addAll(newQueue);
return transformedServerOps;
}
/**
* This removes the first item from the queue and subsequent consecutive items that
* have the same author to produce a single item that contains all the ops in the items
* removed.
*
* We don't merge sent ones due to non-commutativity of transformation and composition.
*
* @return If the returned item does not have the SENT state, it's delta is always optimised.
* @throws NoSuchElementException If the queue is empty.
*/
private Item takeMergedAndOptimisedItem(Queue<Item> queue) {
Item item = queue.remove();
// Cannot change delta of sent delta
if (item.state == ItemState.SENT) {
return item;
}
MergingSequence resultDelta = item.opSequence;
ParticipantId creator = resultDelta.get(0).getContext().getCreator();
boolean needOptimisation = item.state != ItemState.OPTIMISED;
while (!queue.isEmpty()) {
Item nextItem = queue.element();
MergingSequence nextDelta = nextItem.opSequence;
ParticipantId nextCreator = nextDelta.get(0).getContext().getCreator();
// don't merge sent ones due to non-commutativity of transformation
// and composition
if ((nextItem.state != ItemState.SENT) && creator.equals(nextCreator)) {
resultDelta.addAll(nextDelta);
queue.remove();
needOptimisation = true;
} else {
break;
}
}
if (needOptimisation) {
resultDelta.optimise();
}
return new Item(resultDelta, ItemState.OPTIMISED);
}
@Override
public String toString() {
// Empty space before \n intentional to print in browser
return "Operation Queue = " +
"[deltas: " + queue.size() + "] \n" +
"[queue: " + queue + "] \n" +
"[tailCreator: " + tailCreator + "] \n";
}
}