com.google.collide.clientlibs.invalidation.DropRecoveringInvalidationController.java Source code

Java tutorial

Introduction

Here is the source code for com.google.collide.clientlibs.invalidation.DropRecoveringInvalidationController.java

Source

// 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));
            }
        });
    }
}