/**
* Copyright 2008 Google Inc.
*
* 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 org.waveprotocol.wave.concurrencycontrol.channel;
import junit.framework.TestCase;
import org.waveprotocol.wave.common.logging.PrintLogger;
import org.waveprotocol.wave.concurrencycontrol.common.ChannelException;
import org.waveprotocol.wave.concurrencycontrol.common.Recoverable;
import org.waveprotocol.wave.concurrencycontrol.common.ResponseCode;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletDelta;
import org.waveprotocol.wave.model.testing.BasicFactories;
import org.waveprotocol.wave.model.testing.DeltaTestUtil;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* Tests the {@link WaveletDeltaChannelImpl} by mocking the back-end channel.
*
*/
public class WaveletDeltaChannelImplTest extends TestCase {
/**
* A mock delta channel receiver which support setting expectations of method
* calls.
*/
private static class MockReceiver implements WaveletDeltaChannel.Receiver {
private static enum Method { CONNECT, ACK, COMMIT, DELTA, NACK }
private final Queue<Object[]> expectations = new LinkedList<Object[]>();
public void expectConnection(HashedVersion connectVersion, HashedVersion currentVersion) {
expectations.add(new Object[] {Method.CONNECT, connectVersion, currentVersion});
}
public void expectAck(int opsApplied, HashedVersion version) {
expectations.add(new Object[] {Method.ACK, opsApplied, version});
}
public void expectCommit(long version) {
expectations.add(new Object[] {Method.COMMIT, version});
}
public void expectDelta(TransformedWaveletDelta delta) {
expectations.add(new Object[] {Method.DELTA, delta});
}
public void expectNack(String errorString, long version) {
expectations.add(new Object[] {Method.NACK, errorString, version});
}
public void checkExpectationsSatisfied() {
assertTrue(expectations.isEmpty());
}
@Override
public void onConnection(HashedVersion connectVersion, HashedVersion currentVersion) {
Object[] expected = expectations.remove();
assertEquals(expected[0], Method.CONNECT);
assertEquals("incorrect connectVersion", expected[1], connectVersion);
assertEquals("incorrect currentVersion", expected[2], currentVersion);
}
@Override
public void onAck(int opsApplied, HashedVersion version) {
Object[] expected = expectations.remove();
assertEquals(expected[0], Method.ACK);
assertEquals("incorrect opsApplied", expected[1], opsApplied);
assertEquals("incorrect version", expected[2], version);
}
@Override
public void onCommit(long version) {
Object[] expected = expectations.remove();
assertEquals(expected[0], Method.COMMIT);
assertEquals("incorrect version", expected[1], version);
}
@Override
public void onDelta(TransformedWaveletDelta delta) {
Object[] expected = expectations.remove();
assertEquals(expected[0], Method.DELTA);
assertEquals("incorrect delta", expected[1], delta);
}
@Override
public void onNack(ResponseCode responseCode, String errorString, long version) {
Object[] expected = expectations.remove();
assertTrue(responseCode != ResponseCode.OK);
assertEquals(expected[0], Method.NACK);
assertEquals("incorrect error string", expected[1], errorString);
assertEquals("incorrect version", expected[2], version);
}
}
/**
* A mock wavelet channel which allows setting expectations for method calls.
*/
private static class MockWaveletChannel implements WaveletChannel {
private final Queue<WaveletDelta> expectations = new LinkedList<WaveletDelta>();
private final Queue<SubmitCallback> awaitingAck = new LinkedList<SubmitCallback>();
public void expectSubmit(WaveletDelta delta1) {
expectations.add(delta1);
}
public void checkExpectationsSatisfied() {
assertTrue(expectations.isEmpty());
}
@Override
public void submit(WaveletDelta delta, SubmitCallback callback) {
WaveletDelta e = expectations.remove();
assertEquals(e, delta);
awaitingAck.add(callback);
}
public void ackSubmit(int opsApplied, long version, byte[] signature) throws ChannelException {
SubmitCallback nextCallback = awaitingAck.remove();
nextCallback.onSuccess(opsApplied, HashedVersion.of(version, signature), ResponseCode.OK,
null);
}
public void nackSubmit(String reason, ResponseCode error, long version, byte[] signature)
throws ChannelException {
SubmitCallback nextCallback = awaitingAck.remove();
nextCallback.onSuccess(0, HashedVersion.of(version, signature), error, reason);
}
public void failSubmit(String reason) throws ChannelException {
SubmitCallback nextCallback = awaitingAck.remove();
nextCallback.onFailure(reason);
}
@Override
public String debugGetProfilingInfo() {
return null;
}
}
private static final ParticipantId USER_ID = ParticipantId.ofUnsafe("test@example.com");
private static final DeltaTestUtil UTIL = new DeltaTestUtil(USER_ID);
private final static WaveId WAVE_ID = WaveId.of("example.com", "waveid");
private final static WaveletId WAVELET_ID = WaveletId.of("example.com", "waveletid");
private static final ObservableWaveletData.Factory<?> DATA_FACTORY =
BasicFactories.waveletDataImplFactory();
private MockWaveletChannel waveletChannel;
private MockReceiver receiver;
private final PrintLogger logger = new PrintLogger();
private WaveletDeltaChannelImpl deltaChannel; // channel under test
@Override
public void setUp() {
waveletChannel = new MockWaveletChannel();
receiver = new MockReceiver();
deltaChannel = new WaveletDeltaChannelImpl(waveletChannel, logger);
deltaChannel.reset(receiver);
}
/**
* Tests that resetting a new channel does nothing.
*/
public void testResetNewChannel() {
deltaChannel.reset(null);
waveletChannel.checkExpectationsSatisfied();
}
/**
* Tests that sending on an unconnected channel fails.
*/
public void testSendBeforeConnectFails() {
try {
sendDelta(buildDelta(1, 1));
fail("Expected IllegalStateException");
} catch (IllegalStateException expected) {
}
waveletChannel.checkExpectationsSatisfied();
}
/**
* Tests that a channel with no receivers quietly drops messages.
*/
public void testChannelWithNoReceiverDropsMessages() throws ChannelException {
final long initialVersion = 57;
final byte[] signature = sig(1);
final ObservableWaveletData wavelet = buildSnapshot(initialVersion, signature);
final TransformedWaveletDelta delta = buildServerDelta(initialVersion, 3);
deltaChannel.reset(null); // Clear receiver.
connectChannel(wavelet);
receiveUpdateOnConnectedChannel(delta, initialVersion, signature);
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a last committed version sent with the connect message is
* delivered.
*/
public void testConnectReceivesCommittedVersion() throws ChannelException {
final long currentVersion = 57;
final byte[] signature = sig(1);
final long committedVersion = 50;
final byte[] committedSignature = sig(2);
final HashedVersion signedVersion = HashedVersion.of(currentVersion, signature);
final ObservableWaveletData wavelet = buildSnapshot(currentVersion, signature);
receiver.expectConnection(signedVersion, signedVersion);
receiver.expectCommit(committedVersion);
connectChannel(wavelet, committedVersion, committedSignature);
receiver.checkExpectationsSatisfied();
}
/**
* Tests that the last committed version sent with a reconnect initial delta is
* delivered.
*/
public void testReconnectReceivesCommittedVersion() throws ChannelException {
final long currentVersion = 57;
final byte[] signature = sig(1);
final long committedVersion = 50;
final byte[] committedSignature = sig(2);
final HashedVersion signedVersion = HashedVersion.of(currentVersion, signature);
final List<TransformedWaveletDelta> reconnect = buildReconnect(currentVersion, signature);
// Expect connection.
receiver.expectConnection(signedVersion, signedVersion);
receiver.expectCommit(committedVersion);
reconnectChannel(reconnect, committedVersion, committedSignature);
receiver.checkExpectationsSatisfied();
}
/**
* Tests that when the client terminates the channel the receiver
* is not notified.
*/
public void testClientResetTerminatesSilently() throws ChannelException {
checkedConnectChannel(57);
deltaChannel.reset(null);
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a delta sent down the channel is received and the acknowledgment
* delivered.
*/
public void testSubmitDelta() throws ChannelException {
final long currentVersion = 57;
final int ops1 = 7;
final WaveletDelta delta1 = buildDelta(currentVersion, ops1);
final byte[] signature1 = sig(1);
final WaveletDelta delta2 = buildDelta(currentVersion + ops1, 2);
final int opsCommitted = 3;
final String errorMsg2 = "SERVER_ERROR";
checkedConnectChannel(currentVersion);
// Send delta1.
submitDeltaOnConnectedChannel(delta1);
// All ops are acked.
receiver.expectAck(ops1, HashedVersion.of(currentVersion + ops1, signature1));
ackDeltaOnConnectedChannel(currentVersion + ops1, ops1, signature1);
// Send delta2.
submitDeltaOnConnectedChannel(delta2);
// Nack with a randomly injected error, as if something just went wrong
// server-side.
receiver.expectNack(errorMsg2, currentVersion + ops1);
nackDeltaOnConnectedChannel(currentVersion + ops1, signature1, errorMsg2,
ResponseCode.INTERNAL_ERROR);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a delta submitted after a reset has sequence number 1.
*/
public void testSubmitAfterResetRestartsSequence() throws ChannelException {
final long currentVersion = 57;
final int ops1 = 7;
final WaveletDelta delta1 = buildDelta(currentVersion, ops1);
final byte[] signature1 = sig(1);
final WaveletDelta delta2 = buildDelta(currentVersion + ops1, 2);
checkedConnectChannel(currentVersion);
// Send delta1.
submitDeltaOnConnectedChannel(delta1);
// All ops are acked.
receiver.expectAck(ops1, HashedVersion.of(currentVersion + ops1, signature1));
ackDeltaOnConnectedChannel(currentVersion + ops1, ops1, signature1);
deltaChannel.reset(receiver);
checkedReconnectChannel(buildReconnect(currentVersion + ops1, signature1),
currentVersion + ops1, signature1);
// Send delta2.
submitDeltaOnConnectedChannel(delta2);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a delta (with commit version) received from the server is
* delivered.
*/
public void testReceiveDelta() throws ChannelException {
final long initialVersion = 57;
final byte[] commitSig = sig(1);
final TransformedWaveletDelta delta = buildServerDelta(initialVersion, 7);
checkedConnectChannel(initialVersion);
// Receive and deliver delta.
receiver.expectCommit(initialVersion);
receiver.expectDelta(delta);
receiveUpdateOnConnectedChannel(delta, initialVersion, commitSig);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a last-committed-version message with no deltas is correctly
* delivered.
*/
public void testReceiveLastCommittedVersion() throws ChannelException {
final long initialVersion = 57;
final long committedVersion = 50;
final byte[] committedSignature = sig(1);
checkedConnectChannel(initialVersion);
receiver.expectCommit(committedVersion);
receiveUpdateOnConnectedChannel(null, committedVersion, committedSignature);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a submit delta interleaves properly with received deltas when the ACK is
* received in sequence with server deltas.
*/
public void testSynchronizedAckDelta() throws ChannelException {
final long initialVersion = 57;
final int serverOps1 = 7;
checkedConnectChannel(initialVersion);
// Receive server delta.
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, serverOps1);
receiver.expectDelta(delta1);
receiveUpdateOnConnectedChannel(delta1);
// Submit delta.
final long versionAfterServer1 = initialVersion + serverOps1;
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(versionAfterServer1, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Acknowledge all ops immediately.
final long versionAfterClient = versionAfterServer1 + clientOps;
final byte[] ackedSignature = sig(1);
receiver.expectAck(clientOps, HashedVersion.of(versionAfterClient, ackedSignature));
ackDeltaOnConnectedChannel(versionAfterClient, clientOps, ackedSignature);
// Receive a second server delta.
final int serverOps2 = 3;
final TransformedWaveletDelta delta2 = buildServerDelta(versionAfterClient, serverOps2);
receiver.expectDelta(delta2);
receiveUpdateOnConnectedChannel(delta2);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a submit delta interleaves properly with received deltas when the ACK is
* received after a subsequent server delta.
*/
public void testLateAckDelta() throws ChannelException {
final long initialVersion = 57;
final int serverOps1 = 7;
checkedConnectChannel(initialVersion);
// Receive server delta.
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, serverOps1);
receiver.expectDelta(delta1);
receiveUpdateOnConnectedChannel(delta1);
// Submit delta.
final long versionAfterServer1 = initialVersion + serverOps1;
final byte[] sigAfterServer1 = sig(1);
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(versionAfterServer1, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Receive a second server delta, logically after the client ops.
final int serverOps2 = 3;
final long versionAfterClient = versionAfterServer1 + clientOps;
final byte[] sigAfterClient = sig(2);
final TransformedWaveletDelta delta2 = buildServerDelta(versionAfterClient, serverOps2);
// Don't expect the delta yet.
receiveUpdateOnConnectedChannel(delta2);
// Receive commit message for a received delta while there are queued
// messages; the message jumps the queue to be delivered immediately.
receiver.expectCommit(versionAfterServer1);
receiveUpdateOnConnectedChannel(null, versionAfterServer1, sigAfterServer1);
// Receive a commit message for the outstanding submit delta. This
// message is delayed until the ack.
receiveUpdateOnConnectedChannel(null, versionAfterClient, sigAfterClient);
// Receive the ack and expect the pending delta and commit (in the order received).
final byte[] ackedSignature = sig(1);
receiver.expectAck(clientOps, HashedVersion.of(versionAfterClient, ackedSignature));
receiver.expectCommit(versionAfterClient);
receiver.expectDelta(delta2);
ackDeltaOnConnectedChannel(versionAfterClient, clientOps, ackedSignature);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a submit delta interleaves properly with received deltas when the ACK is
* received before a preceding server delta.
*/
public void testEarlyAckDelta() throws ChannelException {
final long initialVersion = 57;
final int serverOps1 = 7;
checkedConnectChannel(initialVersion);
// Submit delta.
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(initialVersion, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Acknowledge the submitted delta against a future version.
final long versionAfterServer1 = initialVersion + serverOps1;
final long versionAfterClient = versionAfterServer1 + clientOps;
final byte[] ackedSignature = sig(1);
// Don't expect ack yet.
ackDeltaOnConnectedChannel(versionAfterClient, clientOps, ackedSignature);
// Receive the server delta and the expect pending ack.
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, serverOps1);
receiver.expectDelta(delta1);
receiver.expectAck(clientOps, HashedVersion.of(versionAfterClient, ackedSignature));
receiveUpdateOnConnectedChannel(delta1);
// Receive a second server delta, logically after the client ops.
final int serverOps2 = 3;
final long versionAfterServer2 = versionAfterClient + serverOps2;
final TransformedWaveletDelta delta2 = buildServerDelta(versionAfterClient, serverOps2);
receiver.expectDelta(delta2);
receiveUpdateOnConnectedChannel(delta2);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that the channel can handle receiving an ack for fewer ops than
* were submitted, as happens when the server transforms some away.
*
* Only the synchronized case is tested.
*/
public void testShrunkAckDelta() throws ChannelException {
final long initialVersion = 57;
final int serverOps1 = 7;
checkedConnectChannel(initialVersion);
// Receive server delta.
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, serverOps1);
receiver.expectDelta(delta1);
receiveUpdateOnConnectedChannel(delta1);
// Submit delta.
final long versionAfterServer1 = initialVersion + serverOps1;
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(versionAfterServer1, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Acknowledge immediately.
final int ackedOps = 0;
final long versionAfterClient = versionAfterServer1 + ackedOps;
final byte[] ackedSignature = sig(1);
receiver.expectAck(ackedOps, HashedVersion.of(versionAfterClient, ackedSignature));
ackDeltaOnConnectedChannel(versionAfterClient, ackedOps, ackedSignature);
// Receive a second server delta.
final int serverOps2 = 3;
final TransformedWaveletDelta delta2 = buildServerDelta(versionAfterClient, serverOps2);
receiver.expectDelta(delta2);
receiveUpdateOnConnectedChannel(delta2);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that a submit delta interleaves properly with received deltas when ops are NACKed
* before the next server delta.
*/
public void testSynchronizedNackDelta() throws ChannelException {
final long initialVersion = 57;
final int serverOps1 = 7;
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, serverOps1);
checkedConnectChannel(initialVersion);
// Receive server delta.
receiver.expectDelta(delta1);
receiveUpdateOnConnectedChannel(delta1);
// Submit delta.
final long versionAfterServer1 = initialVersion + serverOps1;
final byte[] sigAfterServer1 = sig(1);
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(versionAfterServer1, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Nack delta immediately.
final String error = "error";
receiver.expectNack(error, versionAfterServer1);
nackDeltaOnConnectedChannel(versionAfterServer1, sigAfterServer1, error,
ResponseCode.BAD_REQUEST);
// Receive a second server delta.
final int serverOps2 = 3;
final TransformedWaveletDelta delta2 = buildServerDelta(versionAfterServer1, serverOps2);
receiver.expectDelta(delta2);
receiveUpdateOnConnectedChannel(delta2);
// Try another submit.
final long versionAfterServer2 = versionAfterServer1 + serverOps2;
final WaveletDelta clientDelta2 = buildDelta(versionAfterServer2, clientOps);
submitDeltaOnConnectedChannel(clientDelta2);
// Ack.
final long versionAfterClient2 = versionAfterServer2 + clientOps;
final byte[] ackedSignature2 = sig(2);
receiver.expectAck(clientOps, HashedVersion.of(versionAfterClient2, ackedSignature2));
ackDeltaOnConnectedChannel(versionAfterClient2, clientOps, ackedSignature2);
// Close.
closeChannel();
receiver.checkExpectationsSatisfied();
}
/**
* Tests that receiving an acknowledged delta submitted by this client is
* detected as a server-side error.
*/
public void testReflectedSubmittedDeltaAfterAckIsError() throws ChannelException {
final long currentVersion = 57;
final int ops1 = 7;
final byte[] signature1 = sig(1);
final WaveletDelta delta1 = buildDelta(currentVersion, ops1);
checkedConnectChannel(currentVersion);
// Send delta1.
submitDeltaOnConnectedChannel(delta1);
// All ops are acked.
receiver.expectAck(ops1, HashedVersion.of(currentVersion + ops1, signature1));
ackDeltaOnConnectedChannel(currentVersion + ops1, ops1, signature1);
// Receive the delta (erroneously). Expect termination to be reported.
try {
receiveUpdateOnConnectedChannel(buildServerDelta(currentVersion, ops1));
fail("ChannelException expected");
} catch (ChannelException expected) {
}
receiver.checkExpectationsSatisfied();
}
/**
* Tests that receiving an ack for a delta already received by this client
* is detected as a server-side error.
*/
public void testAckForReceivedDeltaIsError() throws ChannelException {
final long currentVersion = 57;
final int ops1 = 7;
final byte[] signature1 = sig(1);
final WaveletDelta delta1 = buildDelta(currentVersion, ops1);
checkedConnectChannel(currentVersion);
// Send delta1.
submitDeltaOnConnectedChannel(delta1);
// Receive the delta (erroneously), but we can't detect it's an error yet.
TransformedWaveletDelta serverDelta1 = buildServerDelta(currentVersion, ops1);
receiver.expectDelta(serverDelta1);
receiveUpdateOnConnectedChannel(serverDelta1);
// Now receive the ack for the delta. Expect failure.
try {
ackDeltaOnConnectedChannel(currentVersion + ops1, ops1, signature1);
fail("ChannelException expected");
} catch (ChannelException expected) {
}
receiver.checkExpectationsSatisfied();
}
/**
* Tests that an ack received for a delta submitted before a channel
* reconnects is dropped.
*/
public void testAckAfterReconnectIgnored() throws ChannelException {
final long initialVersion = 57;
final byte[] initialSignature = sig(4);
checkedConnectChannel(initialVersion);
// Submit delta.
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(initialVersion, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Reset channel.
deltaChannel.reset(receiver);
final List<TransformedWaveletDelta> reconnect =
buildReconnect(initialVersion, initialSignature);
checkedReconnectChannel(reconnect, 0, new byte[0]);
// Acknowledge outstanding submit.
final long versionAfterClient = initialVersion + clientOps;
final byte[] ackedSignature = sig(1);
// Don't expect the ack at the receiver.
ackDeltaOnConnectedChannel(versionAfterClient, clientOps, ackedSignature);
receiver.checkExpectationsSatisfied();
}
public void testNackTooOldIsRecoverable() throws ChannelException {
final long initialVersion = 0;
checkedConnectChannel(initialVersion);
// Submit delta.
final long submitVersion = 0;
final byte[] signature = sig(1);
final int clientOps = 1;
final WaveletDelta clientDelta = buildDelta(submitVersion, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Nack delta immediately with TOO_OLD.
try {
nackDeltaOnConnectedChannel(submitVersion, signature, "too old", ResponseCode.TOO_OLD);
fail("Expected an exception");
} catch (ChannelException e) {
assertEquals(Recoverable.RECOVERABLE, e.getRecoverable());
}
}
/**
* Tests that the delta channel detects a gap in the op stream and
* throws an exception.
*/
public void testMissingDeltaKillsChannel() throws ChannelException {
final long initialVersion = 57;
final byte[] signature = sig(1);
final int ops = 7;
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, ops);
checkedConnectChannel(initialVersion);
// Receive and deliver delta.
receiver.expectCommit(initialVersion);
receiver.expectDelta(delta1);
receiveUpdateOnConnectedChannel(delta1, initialVersion, signature);
// Receive delta with a version number too high.
final TransformedWaveletDelta delta2 = buildServerDelta(initialVersion + ops + 1, 1);
try {
receiveUpdateOnConnectedChannel(delta2, initialVersion, signature);
fail("Expected a ChannelException");
} catch (ChannelException expected) {
}
receiver.checkExpectationsSatisfied();
}
/**
* Tests that the delta channel detects a gap in the op stream even when
* there is an outstanding submission.
*/
public void testMissingDeltaWithLateAckKillsChannel() throws ChannelException {
final long initialVersion = 57;
final int serverOps1 = 7;
checkedConnectChannel(initialVersion);
// Receive server delta.
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, serverOps1);
receiver.expectDelta(delta1);
receiveUpdateOnConnectedChannel(delta1);
// Submit delta.
final long versionAfterServer1 = initialVersion + serverOps1;
final int sigAfterServer1 = 0x11111111;
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(versionAfterServer1, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Receive a second server delta that's after our submission,
// will be queued.
final long versionAfterClient = versionAfterServer1 + clientOps;
final int sigAfterClient = 0x22222222;
final int serverOps2 = 3;
final TransformedWaveletDelta delta2 = buildServerDelta(versionAfterClient, serverOps2);
receiveUpdateOnConnectedChannel(delta2);
// Receive a third server delta that's skipped a version.
final long versionAfterServer2 = versionAfterClient + serverOps2;
final int sigAfterServer2 = 0x44444444;
final TransformedWaveletDelta delta3 = buildServerDelta(versionAfterServer2 + 1, 1);
try {
receiveUpdateOnConnectedChannel(delta3);
fail("Expected a ChannelException");
} catch (ChannelException expected) {
}
receiver.checkExpectationsSatisfied();
}
/**
* Tests that the delta channel detects a gap in the op stream even when there
* is an oustanding delta submission if the ack couldn't possibly account for
* the version gap.
*/
public void testMissingDeltaBeyondLateAckKillsChannel() throws ChannelException {
final long initialVersion = 57;
final int serverOps1 = 7;
checkedConnectChannel(initialVersion);
// Receive server delta.
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, serverOps1);
receiver.expectDelta(delta1);
receiveUpdateOnConnectedChannel(delta1);
// Submit delta.
final long versionAfterServer1 = initialVersion + serverOps1;
final int sigAfterServer1 = 0x11111111;
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(versionAfterServer1, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Receive a second server delta, too far ahead of the client ops.
final long versionAfterClient = versionAfterServer1 + clientOps;
final int sigAfterClient = 0x22222222;
final TransformedWaveletDelta delta2 = buildServerDelta(versionAfterClient + 1, 1);
try {
receiveUpdateOnConnectedChannel(delta2);
fail("Expected a ChannelException");
} catch (ChannelException expected) {
}
receiver.checkExpectationsSatisfied();
}
/**
* Tests that the delta channel detects a gap in the op stream when a received
* ack doesn't account for the version gap.
*/
public void testMissingDeltaBeyondShortAckKillsChannel() throws ChannelException {
final long initialVersion = 57;
final int serverOps1 = 7;
checkedConnectChannel(initialVersion);
// Receive server delta.
final TransformedWaveletDelta delta1 = buildServerDelta(initialVersion, serverOps1);
receiver.expectDelta(delta1);
receiveUpdateOnConnectedChannel(delta1);
// Submit delta.
final long versionAfterServer1 = initialVersion + serverOps1;
final int sigAfterServer1 = 0x11111111;
final int clientOps = 5;
final WaveletDelta clientDelta = buildDelta(versionAfterServer1, clientOps);
submitDeltaOnConnectedChannel(clientDelta);
// Receive a second server delta that initially looks ok.
final long versionAfterClient = versionAfterServer1 + clientOps;
final int sigAfterClient = 0x22222222;
final TransformedWaveletDelta delta2 = buildServerDelta(versionAfterClient, 1);
receiveUpdateOnConnectedChannel(delta2);
// Receive ack for fewer than the number of ops than we
// sent (some were transformed away).
final byte[] ackedSignature = sig(3);
receiver.expectAck(clientOps - 1, HashedVersion.of(versionAfterClient - 1, ackedSignature));
try {
ackDeltaOnConnectedChannel(versionAfterClient - 1, clientOps - 1, ackedSignature);
fail("Expected a ChannelException");
} catch (ChannelException expected) {
}
receiver.checkExpectationsSatisfied();
}
// --- Helper methods --- //
/**
* Connects the channel by sending an initial message with a wavelet snapshot.
* The committed version is synthesized to match the snapshot version.
*
* @param wave initial wavelet snapshot
*/
private void connectChannel(ObservableWaveletData wave)
throws ChannelException {
connectChannel(wave, wave.getVersion(), wave.getHashedVersion().getHistoryHash());
}
/**
* Connects the channel by sending an initial message with a wavelet snapshot.
* The message includes a committed version.
*
* @param snapshot initial wavelet snapshot
* @param committed last committed version at connection (or -1)
* @param commitSignature last committed signature at connection
*/
private void connectChannel(ObservableWaveletData snapshot,
long committed, byte[] commitSignature) throws ChannelException {
HashedVersion commitVersion = HashedVersion.of(committed, commitSignature);
deltaChannel.onWaveletSnapshot(snapshot, commitVersion, null);
}
/**
* Reconnects the channel at the current version by sending an initial message with a
* version and signature.
*
* @param reconnect reconnection message
* @param committed last committed version at reconnection
*/
private void reconnectChannel(List<TransformedWaveletDelta> reconnect, long committed,
byte[] commitSignature) throws ChannelException {
HashedVersion commitVersion = HashedVersion.of(committed, commitSignature);
deltaChannel.onWaveletUpdate(reconnect, commitVersion, null);
}
/**
* Connects the channel and checks expectations.
*
* @param currentVersion version at which to connect.
*/
private void checkedConnectChannel(long currentVersion) throws ChannelException {
byte[] signature = sig(currentVersion);
final HashedVersion signedVersion = HashedVersion.of(currentVersion, sig(currentVersion));
final ObservableWaveletData wavelet = buildSnapshot(currentVersion, signature);
receiver.expectConnection(signedVersion, signedVersion);
receiver.expectCommit(currentVersion);
connectChannel(wavelet);
receiver.checkExpectationsSatisfied();
}
/**
* Reconnects the channel and checks expectations.
*
* @param list reconnection message
* @param committed committed version
* @param commitSignature committed signature
*/
private void checkedReconnectChannel(List<TransformedWaveletDelta> list, long committed,
byte[] commitSignature) throws ChannelException {
HashedVersion signedVersion = HashedVersion.of(
list.get(0).getAppliedAtVersion(), list.get(0).getResultingVersion().getHistoryHash());
receiver.expectConnection(signedVersion, signedVersion);
receiver.expectCommit(committed);
reconnectChannel(list, committed, commitSignature);
}
/**
* Simulates channel termination.
*/
private void closeChannel() {
deltaChannel.reset(null);
}
/** Tracks state for sendDeltaOnConnectedChannel. */
private String sendState = "uninitialized";
/**
* Sends a delta on a connected channel.
* @param delta1 the delta to send
*/
private void submitDeltaOnConnectedChannel(final WaveletDelta delta1) {
waveletChannel.expectSubmit(delta1);
sendState = "initial";
sendDelta(delta1);
assertEquals("sending", sendState);
}
/**
* Sends a delta on a channel. The sendState is set to "sending" when
* the channel takes the delta.
*/
private void sendDelta(final WaveletDelta delta) {
deltaChannel.send(new WaveletDeltaChannel.Transmitter() {
public ClientMessage takeMessage() {
assertEquals("initial", sendState);
sendState = "sending";
return new ClientMessage(delta, false);
}
});
}
/**
* Acknowledges a delta on a connected channel.
*
* @param ackedVersion version to acknowledge
* @param opsApplied number of ops applied
* @param signature acknowledged signature
*/
private void ackDeltaOnConnectedChannel(long ackedVersion, int opsApplied, byte[] signature)
throws ChannelException {
waveletChannel.ackSubmit(opsApplied, ackedVersion, signature);
}
/**
* Negatively acknowledges a delta on a connected channel (possibly acknowledging some ops).
*/
private void nackDeltaOnConnectedChannel(long ackedVersion, byte[] signature, String reason,
ResponseCode error) throws ChannelException {
waveletChannel.nackSubmit(reason, error, ackedVersion, signature);
}
/**
* Receives a delta message on a connected channel.
*
* @param delta1 delta to receive, or null for no delta
*/
private void receiveUpdateOnConnectedChannel(TransformedWaveletDelta delta1)
throws ChannelException {
receiveUpdateOnConnectedChannel(delta1, -1, new byte[0]);
}
/**
* Receives a delta message on a connected channel.
*
* @param delta delta to receive, or null for no delta
* @param committed last committed version, or -1 to omit
*/
private void receiveUpdateOnConnectedChannel(TransformedWaveletDelta delta, long committed,
byte[] commitSignature) throws ChannelException {
HashedVersion commitVersion = null;
List<TransformedWaveletDelta> deltas = CollectionUtils.newArrayList();
if (delta != null) {
deltas.add(delta);
}
if (committed != -1) {
commitVersion = HashedVersion.of(committed, commitSignature);
}
deltaChannel.onWaveletUpdate(deltas, commitVersion, null);
}
/**
* Builds a minimal snapshot message.
*
* @param version wavelet version
* @param signature wavelet signature
*/
private ObservableWaveletData buildSnapshot(final long version, final byte[] signature) {
HashedVersion hv = HashedVersion.of(version, signature);
return DATA_FACTORY.create(
new EmptyWaveletSnapshot(WAVE_ID, WAVELET_ID,
new ParticipantId("creator@gwave.com"), hv, 0L));
}
/**
* Builds a minimal reconnection initial delta.
*
* @param version reconnect version
* @param signature reconnect signature
*/
private List<TransformedWaveletDelta> buildReconnect(long version, byte[] signature) {
return Collections.singletonList(UTIL.makeTransformedDelta(0L, HashedVersion.of(version,
signature), 0));
}
/** Builds a client delta with numOps ops. */
private WaveletDelta buildDelta(long targetVersion, int numOps) {
return UTIL.makeDelta(HashedVersion.unsigned(targetVersion), 123457890L, numOps);
}
/** Builds a server delta with numOps ops. */
private TransformedWaveletDelta buildServerDelta(long initialVersion, int numOps) {
return UTIL.makeTransformedDelta(1234567890L, HashedVersion.unsigned(initialVersion + numOps),
numOps);
}
private static byte[] sig(long v) {
return new byte[] {( byte)v, (byte)v, (byte)v, (byte)v };
}
}