/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.internal.cache;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.CacheWriterException;
import org.apache.geode.cache.EntryNotFoundException;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.RegionDestroyedException;
import org.apache.geode.cache.TimeoutException;
import org.apache.geode.cache.wan.GatewayQueueEvent;
import org.apache.geode.internal.cache.execute.BucketMovedException;
import org.apache.geode.internal.cache.persistence.query.mock.ByteComparator;
import org.apache.geode.internal.cache.versions.RegionVersionVector;
import org.apache.geode.internal.cache.versions.VersionSource;
import org.apache.geode.internal.cache.wan.AbstractGatewaySenderEventProcessor;
import org.apache.geode.internal.cache.wan.GatewaySenderEventCallbackDispatcher;
import org.apache.geode.internal.cache.wan.GatewaySenderEventImpl;
import org.apache.geode.internal.cache.wan.parallel.BucketRegionQueueUnavailableException;
import org.apache.geode.internal.cache.wan.parallel.ConcurrentParallelGatewaySenderQueue;
import org.apache.geode.internal.concurrent.Atomics;
import org.apache.geode.internal.offheap.OffHeapClearRequired;
import org.apache.geode.internal.offheap.annotations.Released;
import org.apache.geode.internal.statistics.StatisticsClock;
import org.apache.geode.logging.internal.log4j.api.LogService;

public class BucketRegionQueue extends AbstractBucketRegionQueue {

  private static final Logger logger = LogService.getLogger();

  /**
   * The <code>Map</code> mapping the regionName->key to the queue key. This index allows fast
   * updating of entries in the queue for conflation. This is necesaary for Colocated regions and if
   * any of the regions use same key for data.
   */
  private final Map indexes;

  /**
   * A transient deque, but should be treated like as a fifo queue to maintain the eventSeqNum of
   * the events that are to be sent to remote site. It is cleared when the queue is cleared.
   */
  private final BlockingDeque<Object> eventSeqNumDeque = new LinkedBlockingDeque<>();

  private final List<Object> markAsDuplicate = new ArrayList<>();

  private long lastKeyRecovered;

  private final AtomicLong latestQueuedKey = new AtomicLong();

  private final AtomicLong latestAcknowledgedKey = new AtomicLong();

  public BucketRegionQueue(String regionName, RegionAttributes attrs, LocalRegion parentRegion,
      InternalCache cache, InternalRegionArguments internalRegionArgs,
      StatisticsClock statisticsClock) {
    super(regionName, attrs, parentRegion, cache, internalRegionArgs, statisticsClock);
    keySet();
    indexes = new ConcurrentHashMap<>();
  }

  @Override
  protected void cleanUpDestroyedTokensAndMarkGIIComplete(
      InitialImageOperation.GIIStatus giiStatus) {
    // Load events from temp queued events
    loadEventsFromTempQueue();

    // Initialize the eventSeqNumQueue
    initializeEventSeqNumQueue();

    // Clean up destroyed tokens
    super.cleanUpDestroyedTokensAndMarkGIIComplete(giiStatus);
  }

  private void initializeEventSeqNumQueue() {
    getInitializationLock().writeLock().lock();
    try {
      if (!keySet().isEmpty()) {
        if (getPartitionedRegion().getColocatedWith() == null) {
          List<EventID> keys = new ArrayList<>(keySet());
          Collections.sort(keys, (o1, o2) -> {
            int compareMem =
                new ByteComparator().compare(o1.getMembershipID(), o2.getMembershipID());
            if (compareMem == 1) {
              return 1;
            } else if (compareMem == -1) {
              return -1;
            } else {
              if (o1.getThreadID() > o2.getThreadID()) {
                return 1;
              } else if (o1.getThreadID() < o2.getThreadID()) {
                return -1;
              } else {
                return o1.getSequenceID() < o2.getSequenceID() ? -1
                    : o1.getSequenceID() == o2.getSequenceID() ? 0 : 1;
              }
            }
          });
          for (EventID eventID : keys) {
            eventSeqNumDeque.addLast(eventID);
          }
        } else {
          TreeSet<Long> sortedKeys = new TreeSet<>(keySet());
          // although the empty check for this.keySet() is done above,
          // do the same for sortedKeys as well because the keySet() might have become
          // empty since the above check was made (keys might have been destroyed through
          // BatchRemoval)
          // fix for #49679 NoSuchElementException thrown from BucketRegionQueue.initialize
          if (!sortedKeys.isEmpty()) {
            for (Long key : sortedKeys) {
              eventSeqNumDeque.addLast(key);
            }
            lastKeyRecovered = sortedKeys.last();
            if (getEventSeqNum() != null) {
              Atomics.setIfGreater(getEventSeqNum(), lastKeyRecovered);
            }
          }
        }

        if (logger.isDebugEnabled()) {
          logger.debug(
              "For bucket:{}, total keys recovered are:{} last key recovered is:{} and the seqNo is:{}",
              getId(), eventSeqNumDeque.size(), lastKeyRecovered, getEventSeqNum());
        }
      }
      initialized = true;
      // Now, the bucket is initialized. Destroy the failedBatchRemovalKeys.
      destroyFailedBatchRemovalMessageKeys();
    } finally {
      notifyEventProcessor();
      getInitializationLock().writeLock().unlock();
    }
  }

  /**
   * When the GII was going, some of BatchRemoval messages (especially for events destroyed due to
   * conflation) might have failed since the event might not be found in the BucketRegionQueue.
   * Those messages are added to failedBatchRemovalMessageKeys map in ParallelQueueRemovalMessage.
   * Events found in the map need to be destroyed once GII is done. Fix for #47431. This method is
   * invoked deliberately after this.initialized is set to true to fix defect #50316.
   */
  private void destroyFailedBatchRemovalMessageKeys() {
    final boolean isDebugEnabled = logger.isDebugEnabled();

    Iterator<Object> itr = getFailedBatchRemovalMessageKeys().iterator();
    while (itr.hasNext()) {
      // at this point, failedBatchRemovalMessageKeys contains messages failed during bucket
      // initialization only. Iterate over failedBatchRemovalMessageKeys and clear it completely.
      Object key = itr.next();
      itr.remove();
      if (isDebugEnabled) {
        logger.debug("key from failedBatchRemovalMessageKeys is: {}", key);
      }
      if (containsKey(key)) {
        try {
          // The destroyKey method is called with forceBasicDestroy set to true since containsKey
          // can be true even though get on the key returns null. That happens when the
          // ParallelQueueRemovalMessage destroys the entry first. In that case, when this method is
          // invoked, the raw value is the DESTROYED token. This was causing the
          // EntryNotFoundException to be thrown by basicDestroy. The forceBasicDestroy boolean set
          // to true forces the super.basicDestroy call to be made instead of the
          // EntryNotFoundException to be thrown.
          destroyKey(key, true);
          if (isDebugEnabled) {
            logger.debug("Destroyed {} from bucket: {}", key, getId());
          }
        } catch (ForceReattemptException fe) {
          if (isDebugEnabled) {
            logger.debug("Bucket: {} moved to other member", getId());
          }
        }
      }
    }
    setFailedBatchRemovalMessageKeysClearedFlag(true);
  }

  @Override
  public void beforeAcquiringPrimaryState() {
    PartitionedRegion region = getPartitionedRegion();

    if (region != null && region.getParallelGatewaySender() != null) {
      AbstractGatewaySenderEventProcessor ep =
          region.getParallelGatewaySender().getEventProcessor();

      if (ep != null && !(ep.getDispatcher() instanceof GatewaySenderEventCallbackDispatcher)) {
        if (isReceivedGatewaySenderStoppedMessage()) {
          setReceivedGatewaySenderStoppedMessage(false);
          return;
        }
        BucketAdvisor parent = getParentAdvisor(getBucketAdvisor());
        if (parent.getHasBecomePrimary()) {
          return;
        }
      }
    }
    markAsDuplicate.addAll(eventSeqNumDeque);
  }

  @Override
  public void closeEntries() {
    OffHeapClearRequired.doWithOffHeapClear(BucketRegionQueue.super::closeEntries);
    indexes.clear();
    eventSeqNumDeque.clear();
    markAsDuplicate.clear();
  }

  @Override
  public Set<VersionSource> clearEntries(final RegionVersionVector rvv) {
    final AtomicReference<Set<VersionSource>> result = new AtomicReference<>();
    OffHeapClearRequired.doWithOffHeapClear(
        () -> result.set(BucketRegionQueue.super.clearEntries(rvv)));
    eventSeqNumDeque.clear();
    markAsDuplicate.clear();
    return result.get();
  }


  @Override
  public void forceSerialized(EntryEventImpl event) {
    // NOOP since we want the value in the region queue to stay in object form.
  }

  @Override
  protected void clearQueues() {
    getInitializationLock().writeLock().lock();
    try {
      indexes.clear();
      eventSeqNumDeque.clear();
      markAsDuplicate.clear();
    } finally {
      getInitializationLock().writeLock().unlock();
    }
  }

  @Override
  public boolean virtualPut(EntryEventImpl event, boolean ifNew, boolean ifOld,
      Object expectedOldValue, boolean requireOldValue, long lastModified,
      boolean overwriteDestroyed, boolean invokeCallbacks, boolean throwConcurrentModification)
      throws TimeoutException, CacheWriterException {
    try {
      boolean success = super.virtualPut(event, ifNew, ifOld, expectedOldValue, requireOldValue,
          lastModified, overwriteDestroyed, invokeCallbacks, throwConcurrentModification);

      if (success) {
        if (getPartitionedRegion().getColocatedWith() == null) {
          return success;
        }

        if (getPartitionedRegion().isConflationEnabled() && getBucketAdvisor().isPrimary()) {
          Object object = event.getNewValue();
          Long key = (Long) event.getKey();
          if (object instanceof Conflatable) {
            if (logger.isDebugEnabled()) {
              logger.debug("Key :{} , Object : {} is conflatable", key, object);
            }
            // TODO: TO optimize by destroying on primary and secondary separately
            // in case of conflation
            conflateOldEntry((Conflatable) object, key);
          } else {
            if (logger.isDebugEnabled()) {
              logger.debug("Object : {} is not conflatable", object);
            }
          }
        }
      } else {
        GatewaySenderEventImpl.release(event.getRawNewValue());
      }
      return success;
    } finally {
      GatewaySenderEventImpl.release(event.getRawOldValue());
    }
  }

  private void conflateOldEntry(Conflatable object, Long tailKey) {
    PartitionedRegion region = getPartitionedRegion();
    Conflatable conflatableObject = object;
    if (region.isConflationEnabled() && conflatableObject.shouldBeConflated()) {
      Object keyToConflate = conflatableObject.getKeyToConflate();
      String rName = object.getRegionToConflate();
      if (logger.isDebugEnabled()) {
        logger.debug(" The region name is : {}", rName);
      }
      Map latestIndexesForRegion = (Map) indexes.get(rName);
      if (latestIndexesForRegion == null) {
        latestIndexesForRegion = new ConcurrentHashMap();
        indexes.put(rName, latestIndexesForRegion);
      }
      Long previousTailKey = (Long) latestIndexesForRegion.put(keyToConflate, tailKey);
      if (previousTailKey != null) {
        if (logger.isDebugEnabled()) {
          logger.debug("{}: Conflating {} at queue index={} and previousTailKey={} ", this, object,
              tailKey, previousTailKey);
        }
        AbstractGatewaySenderEventProcessor ep =
            region.getParallelGatewaySender().getEventProcessor();
        if (ep == null) {
          return;
        }
        ConcurrentParallelGatewaySenderQueue queue =
            (ConcurrentParallelGatewaySenderQueue) ep.getQueue();
        // Give the actual conflation work to another thread.
        // ParallelGatewaySenderQueue takes care of maintaining a thread pool.
        queue.conflateEvent(conflatableObject, getId(), previousTailKey);
      } else {
        region.getParallelGatewaySender().getStatistics().incConflationIndexesMapSize();
      }
    } else {
      if (logger.isDebugEnabled()) {
        logger.debug("{}: Not conflating {}", this, object);
      }
    }
  }

  // No need to synchronize because it is called from a synchronized method
  protected boolean removeIndex(Long qkey) {
    // Determine whether conflation is enabled for this queue and object
    boolean entryFound;
    Object o = getNoLRU(qkey, true, false, false);
    if (o == null) {
      entryFound = false;
    } else {
      entryFound = true;
      if (o instanceof Conflatable) {
        Conflatable object = (Conflatable) o;
        if (object.shouldBeConflated()) {
          // Otherwise, remove the index from the indexes map.
          String rName = object.getRegionToConflate();
          Object key = object.getKeyToConflate();
          Map latestIndexesForRegion = (Map) indexes.get(rName);
          if (latestIndexesForRegion != null) {
            // Remove the index if appropriate. Verify the qKey is actually the one being referenced
            // in the index. If it isn't, then another event has been received for the real key. In
            // that case, don't remove the index since it has already been overwritten.
            if (latestIndexesForRegion.get(key) == qkey) {
              Long index = (Long) latestIndexesForRegion.remove(key);
              if (index != null) {
                getPartitionedRegion().getParallelGatewaySender().getStatistics()
                    .decConflationIndexesMapSize();
                if (logger.isDebugEnabled()) {
                  logger.debug("{}: Removed index {} for {}", this, index, object);
                }
              }
            }
          }
        }
      }
    }
    return entryFound;
  }

  public void basicDestroy(final EntryEventImpl event, final boolean cacheWrite,
      Object expectedOldValue, boolean forceBasicDestroy)
      throws EntryNotFoundException, CacheWriterException, TimeoutException {
    boolean indexEntryFound = true;
    if (getPartitionedRegion().isConflationEnabled()) {
      indexEntryFound = containsKey(event.getKey()) && removeIndex((Long) event.getKey());
    }
    try {
      if (indexEntryFound || forceBasicDestroy) {
        super.basicDestroy(event, cacheWrite, expectedOldValue);
      } else {
        throw new EntryNotFoundException(event.getKey().toString());
      }
    } finally {
      GatewaySenderEventImpl.release(event.getRawOldValue());
    }

    // Primary buckets should already remove the key while peeking
    if (!getBucketAdvisor().isPrimary()) {
      if (logger.isDebugEnabled()) {
        logger.debug(" removing the key {} from eventSeqNumQueue", event.getKey());
      }
      eventSeqNumDeque.remove(event.getKey());
    }
  }

  /**
   * Does a get that gets the value without fault values in from disk.
   */
  private Object optimalGet(Object k) {
    // Get the object at that key (to remove the index).
    Object object = null;
    try {
      object = getValueInVMOrDiskWithoutFaultIn(k);
      if (object != null && object instanceof CachedDeserializable) {
        object = ((CachedDeserializable) object).getDeserializedValue(this, getRegionEntry(k));
      }
    } catch (EntryNotFoundException ok) {
      // just return null;
    }
    if (object == Token.TOMBSTONE) {
      object = null;
    }
    return object; // OFFHEAP: ok since callers are careful to do destroys on region queue after
                   // finished with peeked object.
  }

  public Object peek() {
    Object key = null;
    Object object = null;
    // doing peek in initializationLock because during region destroy, the clearQueues
    // clears the eventSeqNumQueue and can cause data inconsistency (defect #48984)
    getInitializationLock().readLock().lock();
    try {
      if (getPartitionedRegion().isDestroyed()) {
        throw new BucketRegionQueueUnavailableException();
      }
      key = eventSeqNumDeque.peekFirst();
      if (key != null) {
        boolean setDuplicate = markAsDuplicate.remove(key);

        object = optimalGet(key);
        if (object != null) {
          if (setDuplicate) {
            if (logger.isDebugEnabled()) {
              logger.debug("BucketRegionQueue: mark event {} as possible duplicate due to" +
                  " change of primary bucket.", object);
            }
            ((GatewaySenderEventImpl) object).setPossibleDuplicate(true);
          }
        } else if (!getPartitionedRegion().isConflationEnabled()) {
          if (logger.isDebugEnabled()) {
            logger.debug(
                "The value against key {} in the bucket region queue with id {} is NULL for the GatewaySender {}",
                key, getId(), getPartitionedRegion().getParallelGatewaySender());
          }
        }
        // In case of conflation and a race where bucket recovers
        // key-value from other bucket while put has come to this bucket.
        // if (object != null) {
        // ParallelGatewaySenderQueue queue =
        // (ParallelGatewaySenderQueue)getPartitionedRegion()
        // .getParallelGatewaySender().getQueues().toArray(new
        // RegionQueue[1])[0];
        // //queue.addToPeekedKeys(key);
        // }
        eventSeqNumDeque.remove(key);
      }
      return object; // OFFHEAP: ok since callers are careful to do destroys on
                     // region queue after finished with peeked object.
    } finally {
      getInitializationLock().readLock().unlock();
    }
  }

  /**
   * This method returns a list of objects that fulfill the matchingPredicate.
   * If a matching object also fulfills the endPredicate then the method
   * stops looking for more matching objects.
   */
  public List<Object> getElementsMatching(Predicate matchingPredicate, Predicate endPredicate) {
    getInitializationLock().readLock().lock();
    try {
      if (getPartitionedRegion().isDestroyed()) {
        throw new BucketRegionQueueUnavailableException();
      }
      List<Object> elementsMatching = new ArrayList<>();
      for (final Object key : eventSeqNumDeque) {
        Object object = optimalGet(key);
        if (object == null) {
          continue;
        }

        if (matchingPredicate.test(object)) {
          elementsMatching.add(object);
          eventSeqNumDeque.remove(key);
          if (endPredicate.test(object)) {
            break;
          }
        }
      }
      return elementsMatching;
    } finally {
      getInitializationLock().readLock().unlock();
    }
  }

  @Override
  protected void addToEventQueue(Object key, boolean didPut, EntryEventImpl event) {
    if (didPut) {
      if (initialized) {
        eventSeqNumDeque.addLast(key);
        updateLargestQueuedKey((Long) key);
      }
      if (logger.isDebugEnabled()) {
        if (event != null) {
          logger.debug("Put successfully in the queue : {} was initialized: {}",
              event.getRawNewValue(), initialized);
        }
      }
    }
    if (getBucketAdvisor().isPrimary()) {
      incQueueSize(1);
    } else {
      incSecondaryQueueSize(1);
    }
  }

  public void pushKeyIntoQueue(Object key) {
    eventSeqNumDeque.addFirst(key);
  }

  private void updateLargestQueuedKey(Long key) {
    Atomics.setIfGreater(latestQueuedKey, key);
  }

  private void setLatestAcknowledgedKey(Long key) {
    latestAcknowledgedKey.set(key);
  }

  public long getLatestQueuedKey() {
    return latestQueuedKey.get();
  }

  public boolean waitUntilFlushed(long latestQueuedKey, long timeout, TimeUnit unit)
      throws InterruptedException {
    long then = System.currentTimeMillis();
    if (logger.isDebugEnabled()) {
      logger.debug("BucketRegionQueue: waitUntilFlushed bucket=" + getId() + "; latestQueuedKey="
          + latestQueuedKey + "; timeout=" + timeout + "; unit=" + unit);
    }
    boolean result = false;
    // Wait until latestAcknowledgedKey > latestQueuedKey or the queue is empty
    if (initialized) {
      long nanosRemaining = unit.toNanos(timeout);
      long endTime = System.nanoTime() + nanosRemaining;
      while (nanosRemaining > 0) {
        try {
          if (latestAcknowledgedKey.get() > latestQueuedKey || isEmpty()) {
            result = true;
            break;
          }
        } catch (RegionDestroyedException e) {
          if (isBucketDestroyed()) {
            getCancelCriterion().checkCancelInProgress(e);
            throw new BucketMovedException(getFullPath());
          }
        }
        Thread.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(nanosRemaining) + 1, 100));
        nanosRemaining = endTime - System.nanoTime();
      }
    }
    if (logger.isDebugEnabled()) {
      logger.debug("BucketRegionQueue: waitUntilFlushed completed bucket=" + getId() + "; duration="
          + (System.currentTimeMillis() - then) + "; result=" + result);
    }
    return result;
  }

  /**
   * It removes the first key from the queue.
   *
   * @return Returns the key for which value was destroyed.
   */
  public Object remove() throws ForceReattemptException {
    Object key = eventSeqNumDeque.removeFirst();
    if (key != null) {
      destroyKey(key);
    }
    return key;
  }

  /**
   * It removes the first key from the queue.
   *
   * @return Returns the value.
   */
  public Object take() throws InterruptedException, ForceReattemptException {
    throw new UnsupportedOperationException();
    // Currently has no callers.
    // To support this callers need to call release on the returned GatewaySenderEventImpl.
    // Object key = this.eventSeqNumQueue.remove();
    // Object object = null;
    // if (key != null) {
    // object = PartitionRegionHelper
    // .getLocalPrimaryData(getPartitionedRegion()).get(key);
    // /**
    // * TODO: For the time being this is same as peek. To do a batch peek we
    // * need to remove the head key. We will destroy the key once the event is
    // * delivered to the GatewayReceiver.
    // */
    // destroyKey(key);
    // }
    // return object;
  }

  /**
   * Overriding this method from AbstractBucketRegionQueue in order to remove the event from
   * eventSeqNumQueue if EntryNotFoundException is encountered during basicDestroy. This change is
   * done during selective merge from r41425 from gemfire701X_maint.
   */
  @Override
  public void destroyKey(Object key) throws ForceReattemptException {
    destroyKey(key, false);
  }

  private void destroyKey(Object key, boolean forceBasicDestroy) throws ForceReattemptException {
    if (logger.isDebugEnabled()) {
      logger.debug(" destroying primary key {}", key);
    }
    @Released
    EntryEventImpl event = newDestroyEntryEvent(key, null);
    try {
      event.setEventId(new EventID(cache.getInternalDistributedSystem()));
      event.setRegion(this);
      basicDestroy(event, true, null, forceBasicDestroy);
      setLatestAcknowledgedKey((Long) key);
      checkReadiness();
    } catch (EntryNotFoundException enf) {
      if (getPartitionedRegion().isDestroyed()) {
        getPartitionedRegion().checkReadiness();
        if (isBucketDestroyed()) {
          throw new ForceReattemptException("Bucket moved",
              new RegionDestroyedException(
                  "Region has been destroyed",
                  getPartitionedRegion().getFullPath()));
        }
      }
      throw enf;
    } catch (RegionDestroyedException rde) {
      getPartitionedRegion().checkReadiness();
      if (isBucketDestroyed()) {
        throw new ForceReattemptException("Bucket moved while destroying key " + key, rde);
      }
    } finally {
      event.release();
    }

    notifyEntriesRemoved();
  }

  @Override
  public EntryEventImpl newDestroyEntryEvent(Object key, Object aCallbackArgument) {
    return getPartitionedRegion().newDestroyEntryEvent(key, aCallbackArgument);
  }

  public boolean isReadyForPeek() {
    return !getPartitionedRegion().isDestroyed() && !isEmpty()
        && !eventSeqNumDeque.isEmpty() && getBucketAdvisor().isPrimary();
  }

  public List<Object> getHelperQueueList() {
    getInitializationLock().readLock().lock();
    try {
      if (getPartitionedRegion().isDestroyed()) {
        throw new BucketRegionQueueUnavailableException();
      }
      return eventSeqNumDeque.stream()
          .map(this::optimalGet)
          .filter(o -> o instanceof GatewayQueueEvent)
          .collect(Collectors.toList());

    } finally {
      getInitializationLock().readLock().unlock();
    }
  }

  public void setAsPossibleDuplicate(Object key) {
    Object object = optimalGet(key);
    if (object != null) {
      ((GatewaySenderEventImpl) object).setPossibleDuplicate(true);
    }
  }

  public boolean checkIfQueueContainsKey(Object key) {
    return eventSeqNumDeque.contains(key);
  }

  BucketAdvisor getParentAdvisor(BucketAdvisor advisor) {
    BucketAdvisor parent = advisor.getParentAdvisor();
    while (parent != null) {
      advisor = parent;
      parent = advisor.getParentAdvisor();
    }
    return advisor;

  }
}
