// 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;
import com.google.collide.client.bootstrap.BootstrapSession;
import com.google.collide.client.collaboration.cc.RevisionProvider;
import com.google.collide.client.communication.FrontendApi.ApiCallback;
import com.google.collide.client.communication.FrontendApi.RequestResponseApi;
import com.google.collide.client.util.logging.Log;
import com.google.collide.dto.RecoverFromMissedDocOps;
import com.google.collide.dto.RecoverFromMissedDocOpsResponse;
import com.google.collide.dto.ServerError.FailureReason;
import com.google.collide.dto.ServerToClientDocOp;
import com.google.collide.dto.client.DtoClientImpls.ClientToServerDocOpImpl;
import com.google.collide.dto.client.DtoClientImpls.RecoverFromMissedDocOpsImpl;
import com.google.collide.dto.client.DtoClientImpls.ServerToClientDocOpImpl;
import com.google.collide.json.client.JsoArray;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.ErrorCallback;
import elemental.util.Timer;
/**
* A class that performs the XHR to recover missed doc ops and funnels the results into the right
* components.
*/
class DocOpRecoverer {
private static final int RECOVERY_MAX_RETRIES = 5;
private static final int RECOVERY_RETRY_DELAY_MS = 5000;
private final String fileEditSessionKey;
private final RequestResponseApi<RecoverFromMissedDocOps, RecoverFromMissedDocOpsResponse>
recoverFrontendApi;
private final DocOpReceiver docOpReceiver;
private final LastClientToServerDocOpProvider lastSentDocOpProvider;
private final RevisionProvider revisionProvider;
private boolean isRecovering;
DocOpRecoverer(String fileEditSessionKey, RequestResponseApi<
RecoverFromMissedDocOps, RecoverFromMissedDocOpsResponse> recoverFrontendApi,
DocOpReceiver docOpReceiver, LastClientToServerDocOpProvider lastSentDocOpProvider,
RevisionProvider revisionProvider) {
this.fileEditSessionKey = fileEditSessionKey;
this.recoverFrontendApi = recoverFrontendApi;
this.docOpReceiver = docOpReceiver;
this.lastSentDocOpProvider = lastSentDocOpProvider;
this.revisionProvider = revisionProvider;
}
/**
* Attempts to recover after missed doc ops.
*/
void recover(ErrorCallback errorCallback) {
recover(errorCallback, 0);
}
private void recover(final ErrorCallback errorCallback, final int retryCount) {
if (isRecovering) {
return;
}
isRecovering = true;
Log.info(getClass(), "Recovering from disconnection");
// 1) Gather potentially unacked doc ops
final ClientToServerDocOpImpl lastSentMsg =
lastSentDocOpProvider.getLastClientToServerDocOpMsg();
/*
* 2) Pause processing of incoming doc ops and queue them instead. This allows us, in the
* future, to apply the queued doc ops after recovery (the recovery response may not have
* contained some of the queued doc ops depending on the order the XHR and doc ops being
* processed by the server.)
*/
docOpReceiver.pause();
// 3) Perform recovery XHR
/*
* If we had unacked doc ops, we must use their intended version since that
* is the version of the document to which the unacked doc ops apply
* cleanly. If there aren't any unacked doc ops, we can use the latest
* version of the document that we have. (These can differ if we received
* doc ops while still waiting for our ack.)
*
* The unacked doc ops' intended version will always be less than or equal
* to the latest version we have received. When applying the returned doc
* ops from the document history, we will skip those that have already been
* applied.
*/
int revision = lastSentMsg != null ? lastSentMsg.getCcRevision() : revisionProvider.revision();
RecoverFromMissedDocOpsImpl recoveryDto =
RecoverFromMissedDocOpsImpl.make()
.setClientId(BootstrapSession.getBootstrapSession().getActiveClientId())
.setCurrentCcRevision(revision)
.setFileEditSessionKey(fileEditSessionKey);
if (lastSentMsg != null) {
recoveryDto.setDocOps2((JsoArray<String>) lastSentMsg.getDocOps2());
}
recoverFrontendApi.send(recoveryDto,
new ApiCallback<RecoverFromMissedDocOpsResponse>() {
@Override
public void onMessageReceived(RecoverFromMissedDocOpsResponse message) {
// 4) Process the doc ops while I was disconnected (which will include our ack)
JsonArray<ServerToClientDocOp> recoveredServerDocOps = message.getDocOps();
for (int i = 0; i < recoveredServerDocOps.size(); i++) {
ServerToClientDocOp serverDocOp = recoveredServerDocOps.get(i);
if (serverDocOp.getAppliedCcRevision() > revisionProvider.revision()) {
docOpReceiver.simulateOrderedDocOpReceived((ServerToClientDocOpImpl) serverDocOp,
true);
}
}
// 5) Process queued doc ops while I was recovering
JsonArray<ServerToClientDocOp> queuedServerDocOps =
docOpReceiver.getOrderedQueuedServerToClientDocOps();
for (int i = 0; i < queuedServerDocOps.size(); i++) {
ServerToClientDocOp serverDocOp = queuedServerDocOps.get(i);
if (serverDocOp.getAppliedCcRevision() > revisionProvider.revision()) {
docOpReceiver.simulateOrderedDocOpReceived((ServerToClientDocOpImpl) serverDocOp,
true);
}
}
/*
* 6) Back to normal! At this point, any unacked doc ops will have
* been acked. Any queued doc ops are scheduled to be sent. We clear
* the last client-to-server-doc-op. We can also resume the doc op
* receiver now since our document is at the version that they will
* be targetting.
*/
lastSentDocOpProvider.clearLastClientToServerDocOpMsg(lastSentMsg);
docOpReceiver.resume(revisionProvider.revision() + 1);
Log.info(getClass(), "Recovered successfully");
handleRecoverFinished();
}
@Override
public void onFail(FailureReason reason) {
if (retryCount < RECOVERY_MAX_RETRIES) {
new Timer() {
@Override
public void run() {
recover(errorCallback, retryCount + 1);
}
}.schedule(RECOVERY_RETRY_DELAY_MS);
} else {
Log.info(getClass(), "Could not recover");
errorCallback.onError();
handleRecoverFinished();
}
}
});
}
private void handleRecoverFinished() {
isRecovering = false;
}
}