// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed 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 com.google.collide.client.collaboration.cc;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.waveprotocol.wave.model.operation.OperationPair;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/*
* Forked from Wave. Currently, the only changes are exposing some otherwise
* internal state (such as queuedClientOps).
*/
/**
* Simple implementation of main concurrency control logic, independent of
* transport concerns.
*
* <p>
* For efficiency, client ops are also compacted before transforming and before
* sending.
*/
public class TransformQueue<M> {
public interface Transformer<M> {
OperationPair<M> transform(M clientOp, M serverOp);
List<M> compact(List<M> clientOps);
}
private final Transformer<M> transformer;
private int revision = -1;
@VisibleForTesting int expectedAckedClientOps = 0;
@VisibleForTesting List<M> serverOps = new LinkedList<M>();
@VisibleForTesting List<M> unackedClientOps = Collections.emptyList();
@VisibleForTesting List<M> queuedClientOps = new LinkedList<M>();
boolean newClientOpSinceTransform = false;
public TransformQueue(Transformer<M> transformer) {
this.transformer = transformer;
}
public void init(int revision) {
Preconditions.checkState(this.revision == -1, "Already at a revision (%s), can't init at %s)",
this.revision, revision);
Preconditions.checkArgument(revision >= 0, "Initial revision must be >= 0, not %s", revision);
this.revision = revision;
}
public void serverOp(int resultingRevision, M serverOp) {
checkRevision(resultingRevision);
Preconditions.checkState(expectedAckedClientOps == 0,
"server op arrived @%s while expecting %s client ops",
resultingRevision, expectedAckedClientOps);
this.revision = resultingRevision;
if (!unackedClientOps.isEmpty()) {
List<M> newUnackedClientOps = new LinkedList<M>();
for (M clientOp : unackedClientOps) {
OperationPair<M> pair = transformer.transform(clientOp, serverOp);
newUnackedClientOps.add(pair.clientOp());
serverOp = pair.serverOp();
}
unackedClientOps = newUnackedClientOps;
}
if (!queuedClientOps.isEmpty()) {
if (newClientOpSinceTransform) {
queuedClientOps = transformer.compact(queuedClientOps);
}
newClientOpSinceTransform = false;
List<M> newQueuedClientOps = new LinkedList<M>();
for (M clientOp : queuedClientOps) {
OperationPair<M> pair = transformer.transform(clientOp, serverOp);
newQueuedClientOps.add(pair.clientOp());
serverOp = pair.serverOp();
}
queuedClientOps = newQueuedClientOps;
}
serverOps.add(serverOp);
}
public void clientOp(M clientOp) {
if (!serverOps.isEmpty()) {
List<M> newServerOps = new LinkedList<M>();
for (M serverOp : serverOps) {
OperationPair<M> pair = transformer.transform(clientOp, serverOp);
newServerOps.add(pair.serverOp());
clientOp = pair.clientOp();
}
serverOps = newServerOps;
}
queuedClientOps.add(clientOp);
newClientOpSinceTransform = true;
}
public boolean expectedAck(int resultingRevision) {
if (expectedAckedClientOps == 0) {
return false;
}
Preconditions.checkArgument(resultingRevision == revision - expectedAckedClientOps + 1,
"bad rev %s, current rev %s, expected remaining %s",
resultingRevision, revision, expectedAckedClientOps);
expectedAckedClientOps--;
return true;
}
/**
* @param resultingRevision
* @return true if all unacked ops are now acked
*/
public boolean ackClientOp(int resultingRevision) {
checkRevision(resultingRevision);
Preconditions.checkState(expectedAckedClientOps == 0,
"must call expectedAck, there are %s expectedAckedClientOps", expectedAckedClientOps);
Preconditions.checkState(!unackedClientOps.isEmpty(), "unackedClientOps is empty");
this.revision = resultingRevision;
unackedClientOps.remove(0);
return unackedClientOps.isEmpty();
}
/**
* Pushes the queued client ops into the unacked ops, clearing the queued ops.
* @return see {@link #unackedClientOps()}
*/
public List<M> pushQueuedOpsToUnacked() {
Preconditions.checkState(unackedClientOps.isEmpty(),
"Queue contains unacknowledged operations: %s", unackedClientOps);
unackedClientOps = new LinkedList<M>(transformer.compact(queuedClientOps));
queuedClientOps = new LinkedList<M>();
return unackedClientOps();
}
public boolean hasServerOp() {
return !serverOps.isEmpty();
}
public boolean hasUnacknowledgedClientOps() {
return !unackedClientOps.isEmpty();
}
public int getUnacknowledgedClientOpCount() {
return unackedClientOps.size();
}
public boolean hasQueuedClientOps() {
return !queuedClientOps.isEmpty();
}
public int getQueuedClientOpCount() {
return queuedClientOps.size();
}
public M peekServerOp() {
Preconditions.checkState(hasServerOp(), "No server ops");
return serverOps.get(0);
}
public M removeServerOp() {
Preconditions.checkState(hasServerOp(), "No server ops");
return serverOps.remove(0);
}
public int revision() {
return revision;
}
private void checkRevision(int resultingRevision) {
Preconditions.checkArgument(resultingRevision >= 1, "New revision %s must be >= 1",
resultingRevision);
Preconditions.checkState(this.revision == resultingRevision - 1,
"Revision mismatch: at %s, received %s", this.revision, resultingRevision);
}
@Override
public String toString() {
return "TQ{ " + revision + "\n s:" + serverOps +
"\n exp: " + expectedAckedClientOps +
"\n u:" + unackedClientOps + "\n q:" + queuedClientOps + "\n}";
}
/**
* @return the current queued client ops. Note: the behavior of this list
* after calling mutating methods on the transform queue is undefined.
* This method should be called each time immediately before use.
*/
List<M> queuedClientOps() {
return Collections.unmodifiableList(queuedClientOps);
}
public List<M> ackOpsIfVersionMatches(int newRevision) {
if (newRevision == revision + unackedClientOps.size()) {
List<M> expectedAckingClientOps = unackedClientOps;
expectedAckedClientOps += expectedAckingClientOps.size();
unackedClientOps = new LinkedList<M>();
revision = newRevision;
return expectedAckingClientOps;
}
return null;
}
/**
* @return the current unacked client ops. Note: the behavior of this list
* after calling mutating methods on the transform queue is undefined.
* This method should be called each time immediately before use.
*/
List<M> unackedClientOps() {
return Collections.unmodifiableList(unackedClientOps);
}
}