// 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 com.google.collide.clientlibs.invalidation.InvalidationManager.Recoverer;
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.InvalidationUtils;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.Reorderer;
import com.google.collide.shared.util.Timer;
import com.google.collide.shared.util.Reorderer.ItemSink;
import com.google.collide.shared.util.Reorderer.TimeoutCallback;
import com.google.collide.shared.util.Timer.Factory;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.user.client.Random;
/**
* A controller for ensuring all (even dropped payload) invalidations are given to the
* {@link Listener} in-order. This will queue out-of-order invalidations (if prior invalidations
* were dropped), perform out-of-band recovery, and replay invalidations in-order to the listener.
*
*/
class DropRecoveringInvalidationController {
private static final int ERROR_RETRY_DELAY_MS = 15000;
private final InvalidationLogger logger;
private final Factory timerFactory;
private final InvalidationObjectId<?> objectId;
private final Recoverer recoverer;
/** Reorderer of invalidations, storing the payload. */
private final Reorderer<String> invalidationReorderer;
private boolean isRecovering;
private class ReordererSink implements ItemSink<String> {
private final Listener listener;
private boolean isListenerProcessingAsync;
private final AsyncProcessingHandle asyncProcessingHandle = new AsyncProcessingHandle() {
@Override
public void startedAsyncProcessing() {
isListenerProcessingAsync = true;
}
@Override
public void finishedAsyncProcessing() {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
isListenerProcessingAsync = false;
dispatchQueuedPayloads();
}
});
}
};
private final JsonArray<String> queuedPayloads = JsonCollections.createArray();
private int firstQueuedPayloadVersion;
ReordererSink(Listener listener) {
this.listener = listener;
}
@Override
public void onItem(String payload, int version) {
if (!isListenerProcessingAsync) {
listener.onInvalidated(objectId.getName(), version, payload, asyncProcessingHandle);
} else {
if (queuedPayloads.isEmpty()) {
firstQueuedPayloadVersion = version;
}
queuedPayloads.add(payload);
}
}
void handleSetNextExpectedVersion(int nextExpectedVersion) {
while (!queuedPayloads.isEmpty() && firstQueuedPayloadVersion < nextExpectedVersion) {
queuedPayloads.remove(0);
firstQueuedPayloadVersion++;
}
}
private void dispatchQueuedPayloads() {
while (!queuedPayloads.isEmpty() && !isListenerProcessingAsync) {
listener.onInvalidated(objectId.getName(), firstQueuedPayloadVersion++,
queuedPayloads.remove(0), asyncProcessingHandle);
}
}
}
private final ReordererSink reorderedItemSink;
private final TimeoutCallback outOfOrderCallback = new TimeoutCallback() {
@Override
public void onTimeout(int lastVersionDispatched) {
logger.fine(
"Entering recovery for object %s, last version %s, due to out-of-order.", objectId,
lastVersionDispatched);
recover();
}
};
DropRecoveringInvalidationController(InvalidationLogger logger, InvalidationObjectId<?> objectId,
InvalidationRegistrar.Listener listener, Recoverer recoverer,
Timer.Factory timerFactory) {
this.logger = logger;
this.objectId = objectId;
this.recoverer = recoverer;
this.timerFactory = timerFactory;
reorderedItemSink = new ReordererSink(listener);
/*
* We don't know the actual version to expect at this point, so we will just queue until our
* client calls through with a version to expect
*/
// TimeoutMs is very small since tango will not deliver out-of-order items.
invalidationReorderer =
Reorderer.create(0, reorderedItemSink, 1, outOfOrderCallback, timerFactory);
invalidationReorderer.queueUntilSkipToVersionIsCalled();
// Disable timeout until we know the next expected version
invalidationReorderer.setTimeoutEnabled(false);
}
void cleanup() {
invalidationReorderer.cleanup();
}
void setNextExpectedVersion(int nextExpectedVersion) {
reorderedItemSink.handleSetNextExpectedVersion(nextExpectedVersion);
invalidationReorderer.skipToVersion(nextExpectedVersion);
invalidationReorderer.setTimeoutEnabled(true);
}
/**
* @param version the version of the payload, or
* {@link InvalidationUtils#UNKNOWN_VERSION}
*/
void handleInvalidated(String payload, long version, boolean isEmptyPayload) {
if (version > Integer.MAX_VALUE) {
// 1 invalidation per sec would take 24k days to exhaust the positive integer range
throw new IllegalStateException("Version space exhausted (int on client)");
}
// Check if we dropped a payload or msised an invalidation
if (version == InvalidationUtils.UNKNOWN_VERSION || (payload == null && !isEmptyPayload)) {
logger.fine("Entering recovery due to unknown version or missing payload."
+ " Object id: %s, Version: %s, Payload(%s): %s", objectId, version, isEmptyPayload,
payload);
recover();
return;
}
invalidationReorderer.acceptItem(payload, (int) version);
}
void recover() {
if (isRecovering) {
return;
}
isRecovering = true;
int currentClientVersion = invalidationReorderer.getNextExpectedVersion() - 1;
// Disable timeout, as it would trigger a recover
invalidationReorderer.setTimeoutEnabled(false);
// Perform XHR, feed results into reorderer, get callbacks
recoverer.recoverPayloads(objectId, currentClientVersion, new Recoverer.Callback() {
@Override
public void onPayloadsRecovered(JsonArray<RecoveredPayload> payloads, int currentVersion) {
for (int i = 0; i < payloads.size(); i++) {
RecoveredPayload payload = payloads.get(i);
invalidationReorderer.acceptItem(payload.getPayload(), payload.getPayloadVersion());
}
invalidationReorderer.skipToVersion(currentVersion + 1);
isRecovering = false;
invalidationReorderer.setTimeoutEnabled(true);
}
@Override
public void onError() {
// Consider ourselves to be in the "retrying" state during this delay
timerFactory.createTimer(new Runnable() {
@Override
public void run() {
isRecovering = false;
// This will end up retrying
invalidationReorderer.setTimeoutEnabled(true);
}
}).schedule(ERROR_RETRY_DELAY_MS + Random.nextInt(ERROR_RETRY_DELAY_MS / 10));
}
});
}
}