// 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.clientlibs.invalidation;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.eq;
import com.google.collide.clientlibs.invalidation.InvalidationManager.Recoverer;
import com.google.collide.clientlibs.invalidation.InvalidationManager.Recoverer.Callback;
import com.google.collide.clientlibs.invalidation.InvalidationRegistrar.Listener;
import com.google.collide.clientlibs.invalidation.InvalidationRegistrar.Listener.AsyncProcessingHandle;
import com.google.collide.dto.RecoverFromDroppedTangoInvalidationResponse.RecoveredPayload;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.invalidations.InvalidationObjectId;
import com.google.collide.shared.invalidations.InvalidationObjectId.VersioningRequirement;
import com.google.collide.shared.invalidations.InvalidationUtils.InvalidationObjectPrefix;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.MockTimer;
import com.google.common.collect.Lists;
import junit.framework.TestCase;
import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.easymock.IMocksControl;
import java.util.List;
/**
* Tests for {@link DropRecoveringInvalidationController}.
*/
public class DropRecoveringInvalidationControllerTest extends TestCase {
private static class StubRecoveredPayload implements RecoveredPayload {
private final long version;
private final String payload;
public StubRecoveredPayload(long version, String payload) {
this.version = version;
this.payload = payload;
}
@Override
public String getPayload() {
return payload;
}
@Override
public int getPayloadVersion() {
return (int) version;
}
}
private static final InvalidationObjectId<?> obj = new InvalidationObjectId<Void>(
InvalidationObjectPrefix.FILE_TREE_MUTATION, "12345", VersioningRequirement.PAYLOADS);
/**
* The index corresponds to the version. Since there was never an invalidation to get to version
* 0, all invalidations will start at 1 for consistency.
*/
private static final List<String> PAYLOADS = Lists.newArrayList("a", "b", "c", "d", "e", "f", "g");
private IMocksControl strictControl;
private Listener listener;
private Recoverer recoverer;
private MockTimer.Factory timerFactory;
private DropRecoveringInvalidationControllerFactory controllerFactory;
public void testNormal() {
makeListenerExpectInvalidations(1, PAYLOADS.size());
strictControl.replay();
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
controller.setNextExpectedVersion(1);
for (int i = 1; i < PAYLOADS.size(); i++) {
controller.handleInvalidated(PAYLOADS.get(i), i, false);
}
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
public void testNullPayloadNotCountedAsDropped() {
listener.onInvalidated(
eq(obj.getName()), eq(1L), eq((String) null), anyObject(AsyncProcessingHandle.class));
strictControl.replay();
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
controller.setNextExpectedVersion(1);
controller.handleInvalidated(null, 1, true);
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
/**
* Test the object-bootstrap sequence: we subscribe to Tango and then do an XHR to fetch the full
* version of the object. Before we get the XHR response, we could have gotten invalidations.
*/
public void testBootstrapInOrder() {
strictControl.replay();
// Client instantiated/registered Tango listener and performs XHR
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
// While XHR is on the wire, we get a Tango invalidation
triggerInvalidated(controller, 1, 2, false);
// Ensure those invalidations didn't reach the listener
strictControl.verify();
strictControl.reset();
makeListenerExpectInvalidations(2, PAYLOADS.size());
strictControl.replay();
// We got XHR response, its payload start at version 2
controller.setNextExpectedVersion(2);
for (int i = 2; i < PAYLOADS.size(); i++) {
controller.handleInvalidated(PAYLOADS.get(i), i, false);
}
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
/**
* Like {@link #testBootstrapInOrder()} except the there is overlap between the received payloads
* before we got the XHR response and the payloads in the XHR response.
*/
public void testBootstrapOverlap() {
strictControl.replay();
// Client instantiated/registered Tango listener and performs XHR
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
// While XHR is on the wire, we get a Tango invalidation
triggerInvalidated(controller, 1, 2, false);
// Ensure those invalidations didn't reach the listener
strictControl.verify();
strictControl.reset();
makeListenerExpectInvalidations(1, PAYLOADS.size());
strictControl.replay();
// We got XHR response, its payloads start at version 0
controller.setNextExpectedVersion(1);
for (int i = 2; i < PAYLOADS.size(); i++) {
controller.handleInvalidated(PAYLOADS.get(i), i, false);
}
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
public void testSquelchedImmediatelyAfterBootstrap() {
makeRecovererExpectRecover(1, 3);
makeListenerExpectInvalidations(1, PAYLOADS.size());
strictControl.replay();
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
controller.setNextExpectedVersion(1);
// We were expecting v1 but got v2, recovery should kick off and get through v3 (defined above)
triggerInvalidated(controller, 2, PAYLOADS.size(), false);
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
public void testSquelchedDuringOperation() {
makeListenerExpectInvalidations(1, 3);
makeRecovererExpectRecover(3, 3);
makeListenerExpectInvalidations(3, PAYLOADS.size());
strictControl.replay();
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
controller.setNextExpectedVersion(1);
// Invalidate v1 and v2, then v4 - end
triggerInvalidated(controller, 1, 3, false);
triggerInvalidated(controller, 4, PAYLOADS.size(), false);
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
public void testDroppedPayloadsImmediatelyAfterBootstrap() {
makeRecovererExpectRecover(1, 1);
makeListenerExpectInvalidations(1, 2);
makeRecovererExpectRecover(2, 1);
makeListenerExpectInvalidations(2, PAYLOADS.size());
strictControl.replay();
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
controller.setNextExpectedVersion(1);
triggerInvalidated(controller, 1, 3, true);
triggerInvalidated(controller, 3, PAYLOADS.size(), false);
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
public void testDroppedPayloadsDuringOperation() {
makeListenerExpectInvalidations(1, 4);
makeRecovererExpectRecover(4, 1);
makeListenerExpectInvalidations(4, 5);
makeRecovererExpectRecover(5, 1);
makeListenerExpectInvalidations(5, PAYLOADS.size());
strictControl.replay();
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
controller.setNextExpectedVersion(1);
triggerInvalidated(controller, 1, 4, false);
triggerInvalidated(controller, 4, 6, true);
triggerInvalidated(controller, 6, PAYLOADS.size(), false);
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
public void testOutageOfAllDroppedPayloads() {
for (int i = 1; i < PAYLOADS.size(); i++) {
makeRecovererExpectRecover(i, 1);
makeListenerExpectInvalidations(i, i + 1);
}
strictControl.replay();
DropRecoveringInvalidationController controller =
controllerFactory.create(obj, listener, recoverer);
controller.setNextExpectedVersion(1);
triggerInvalidated(controller, 1, PAYLOADS.size(), true);
timerFactory.tickToFireAllTimers();
strictControl.verify();
}
@Override
protected void setUp() throws Exception {
strictControl = EasyMock.createStrictControl();
recoverer = strictControl.createMock(Recoverer.class);
listener = strictControl.createMock(Listener.class);
timerFactory = new MockTimer.Factory();
controllerFactory = new DropRecoveringInvalidationControllerFactory(
new InvalidationLogger(false, false), timerFactory);
}
/**
* @param end exclusive
*/
private void makeListenerExpectInvalidations(int begin, int end) {
for (int i = begin; i < end; i++) {
listener.onInvalidated(eq(obj.getName()), eq((long) i), eq(PAYLOADS.get(i)),
anyObject(AsyncProcessingHandle.class));
}
}
/**
* @param end exclusive
* @param dropPayloads TODO:
*/
private void triggerInvalidated(
DropRecoveringInvalidationController controller, int begin, int end, boolean dropPayloads) {
for (int i = begin; i < end; i++) {
controller.handleInvalidated(dropPayloads ? null : PAYLOADS.get(i), i, !dropPayloads);
}
}
private void makeRecovererExpectRecover(final int nextExpectedVersion, final int payloadCount) {
recoverer.recoverPayloads(
EasyMock.eq(obj), EasyMock.eq(nextExpectedVersion - 1), EasyMock.anyObject(Callback.class));
EasyMock.expectLastCall().andAnswer(new IAnswer<Void>() {
@Override
public Void answer() throws Throwable {
Callback callback = (Callback) EasyMock.getCurrentArguments()[2];
JsonArray<RecoveredPayload> recoveredPayloads = JsonCollections.createArray();
for (int i = nextExpectedVersion; i < nextExpectedVersion + payloadCount; i++) {
recoveredPayloads.add(new StubRecoveredPayload(i, PAYLOADS.get(i)));
}
callback.onPayloadsRecovered(recoveredPayloads, nextExpectedVersion);
return null;
}
});
}
}