// 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.shared.util; import com.google.collide.json.shared.JsonIntegerMap; import com.google.collide.shared.util.Timer.Factory; /** * A utility class that accepts out-of-order versioned items and delivers them to an * {@link ItemSink} in-order. */ public class Reorderer<T> { public interface ItemSink<T> { void onItem(T item, int version); } public interface TimeoutCallback { void onTimeout(int lastVersionDispatched); } /** * @param firstVersionToExpect the first version to expect * @param timeoutMs the amount of time to wait before triggering the {@code timeoutCallback} for * dropped items */ public static <T> Reorderer<T> create(int firstVersionToExpect, ItemSink<T> itemSink, int timeoutMs, TimeoutCallback timeoutCallback, Factory timerFactory) { return new Reorderer<T>( firstVersionToExpect, itemSink, timeoutMs, timeoutCallback, timerFactory); } private boolean isQueueingUntilSkipToVersionCalled; private int nextExpectedVersion; private final ItemSink<T> itemSink; private JsonIntegerMap<T> itemsByVersion = JsonCollections.createIntegerMap(); private boolean isTimeoutEnabled = true; private final int timeoutMs; private final TimeoutCallback timeoutCallback; private final Timer timeoutTimer; private final Runnable timeoutTimerRunnable = new Runnable() { @Override public void run() { timeoutCallback.onTimeout(nextExpectedVersion - 1); } }; private Reorderer(int firstVersionToExpect, ItemSink<T> itemSink, int timeoutMs, TimeoutCallback timeoutCallback, Timer.Factory timerFactory) { this.itemSink = itemSink; this.timeoutMs = timeoutMs; this.timeoutCallback = timeoutCallback; this.nextExpectedVersion = firstVersionToExpect; timeoutTimer = timerFactory.createTimer(timeoutTimerRunnable); } public void cleanup() { setTimeoutEnabled(false); } public int getNextExpectedVersion() { return nextExpectedVersion; } /** * Enables the timeout feature. * * <p> * If there are out-of-order items queued, this will immediately start the timeout timer. */ public void setTimeoutEnabled(boolean isTimeoutEnabled) { this.isTimeoutEnabled = isTimeoutEnabled; if (isTimeoutEnabled) { scheduleTimeoutIfEnabledAndNecessary(); } else { cancelTimeout(); } } /** * Allows the client to skip ahead to a version. For example, if a client fills in the gap * out-of-band, this should be called afterward to begin reordering at the latest version. */ public void skipToVersion(int nextVersionToExpect) { this.nextExpectedVersion = nextVersionToExpect; isQueueingUntilSkipToVersionCalled = false; // Cancel any pending timer (we'll re-set one later if necessary) cancelTimeout(); // Remove items for old versions that we no longer care about removeOldVersions(nextExpectedVersion - 1); // See if we have any items at the new and following versions dispatchQueuedStartingAtNextExpectedVersion(); scheduleTimeoutIfEnabledAndNecessary(); } /* * This is purposefully not a setXxx since canceling the queueing requires dispatching queued * version, etc. and that's not a code path we're going to use immediately, so punting. */ public void queueUntilSkipToVersionIsCalled() { isQueueingUntilSkipToVersionCalled = true; } private void removeOldVersions(final int maxVersionToRemove) { // Simulate a set from a map final JsonIntegerMap<Void> oldVersionsToRemove = JsonCollections.createIntegerMap(); itemsByVersion.iterate(new JsonIntegerMap.IterationCallback<T>() { @Override public void onIteration(int version, T val) { if (version <= maxVersionToRemove) { // Can't remove in-place, so queue for removal oldVersionsToRemove.put(version, null); } } }); oldVersionsToRemove.iterate(new JsonIntegerMap.IterationCallback<Void>() { @Override public void onIteration(int version, Void val) { itemsByVersion.erase(version); } }); } public void acceptItem(T item, int version) { if (version < nextExpectedVersion) { // Ignore, we've already passed this version onto our client return; } final boolean hadPreviouslyStoredItems = !itemsByVersion.isEmpty(); itemsByVersion.put(version, item); if (isQueueingUntilSkipToVersionCalled) { // The item is stored, exit return; } if (version == nextExpectedVersion) { cancelTimeout(); dispatchQueuedStartingAtNextExpectedVersion(); /* * E.g. previous to this call, nextExpectedVersion=3, we have queued 4, 5, 7. We move to * nextExpectedVersion=6 and are now waiting on it to come in, so schedule a timeout. */ scheduleTimeoutIfEnabledAndNecessary(); } else { if (!hadPreviouslyStoredItems) { /* * This is the first time we've missed the item at nextExpectedVersion. We wouldn't always * want to do this when we receive a future item because that would keep resetting the * timeout for the nextExpectedVersion item. * * For example, imagine we are waiting for v2 (assume it got dropped indefinitely). When we * get v3, itemsByVersion will be empty, so we schedule a timeout. If/when we get v4, v5, * v6, ..., we don't want to keep resetting the timeout for each item that comes in -- we'd * never actually timeout in a busy session! */ scheduleTimeoutIfEnabled(); } } } /** * Dispatches to the {@link ItemSink}. This should be the ONLY place that dispatches given the * subtle pause behavior that is possible. */ private void dispatchQueuedStartingAtNextExpectedVersion() { for (; itemsByVersion.hasKey(nextExpectedVersion); nextExpectedVersion++) { T item = itemsByVersion.get(nextExpectedVersion); itemsByVersion.erase(nextExpectedVersion); itemSink.onItem(item, nextExpectedVersion); } } private void scheduleTimeoutIfEnabledAndNecessary() { if (!itemsByVersion.isEmpty()) { scheduleTimeoutIfEnabled(); } } private void scheduleTimeoutIfEnabled() { if (isTimeoutEnabled) { timeoutTimer.schedule(timeoutMs); } } private void cancelTimeout() { timeoutTimer.cancel(); } }