Package org.apache.hedwig.server.persistence

Source Code of org.apache.hedwig.server.persistence.ReadAheadCache$ScanRequestWrapper

/**
* 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.hedwig.server.persistence;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.bookkeeper.util.MathUtils;
import org.apache.bookkeeper.util.OrderedSafeExecutor;
import org.apache.bookkeeper.util.SafeRunnable;
import org.apache.hedwig.exceptions.PubSubException;
import org.apache.hedwig.exceptions.PubSubException.ServerNotResponsibleForTopicException;
import org.apache.hedwig.protocol.PubSubProtocol;
import org.apache.hedwig.protocol.PubSubProtocol.Message;
import org.apache.hedwig.protocol.PubSubProtocol.MessageSeqId;
import org.apache.hedwig.protoextensions.MessageIdUtils;
import org.apache.hedwig.server.common.ServerConfiguration;
import org.apache.hedwig.server.jmx.HedwigJMXService;
import org.apache.hedwig.server.jmx.HedwigMBeanInfo;
import org.apache.hedwig.server.jmx.HedwigMBeanRegistry;
import org.apache.hedwig.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.protobuf.ByteString;

public class ReadAheadCache implements PersistenceManager, HedwigJMXService {

    static Logger logger = LoggerFactory.getLogger(ReadAheadCache.class);

    protected interface CacheRequest {
        public void performRequest();
    }

    /**
     * The underlying persistence manager that will be used for persistence and
     * scanning below the cache
     */
    protected PersistenceManagerWithRangeScan realPersistenceManager;

    /**
     * The structure for the cache
     */
    protected ConcurrentMap<CacheKey, CacheValue> cache =
        new ConcurrentHashMap<CacheKey, CacheValue>();

    /**
     * We also want to track the entries in seq-id order so that we can clean up
     * entries after the last subscriber
     */
    protected ConcurrentMap<ByteString, SortedSet<Long>> orderedIndexOnSeqId =
        new ConcurrentHashMap<ByteString, SortedSet<Long>>();

    /**
     * Partition Cache into Serveral Segments for simplify synchronization.
     * Each segment maintains its time index and segment size.
     */
    static class CacheSegment {

        /**
         * We want to keep track of when entries were added in the cache, so that we
         * can remove them in a FIFO fashion
         */
        protected SortedMap<Long, Set<CacheKey>> timeIndexOfAddition = new TreeMap<Long, Set<CacheKey>>();

        /**
         * We maintain an estimate of the current size of each cache segment,
         * so that the thread know when to evict entries from cache segment.
         */
        protected AtomicLong presentSegmentSize = new AtomicLong(0);

    }

    /**
     * We maintain an estimate of the current size of the cache, so that we know
     * when to evict entries.
     */
    protected AtomicLong presentCacheSize = new AtomicLong(0);

    /**
     * Num pending requests.
     */
    protected AtomicInteger numPendingRequests = new AtomicInteger(0);

    /**
     * Cache segment for different threads
     */
    protected final ThreadLocal<CacheSegment> cacheSegment =
        new ThreadLocal<CacheSegment>() {
            @Override
            protected CacheSegment initialValue() {
                return new CacheSegment();
            }
        };

    /**
     * One instance of a callback that we will pass to the underlying
     * persistence manager when asking it to persist messages
     */
    protected PersistCallback persistCallbackInstance = new PersistCallback();

    /**
     * 2 kinds of exceptions that we will use to signal error from readahead
     */
    protected NoSuchSeqIdException noSuchSeqIdExceptionInstance = new NoSuchSeqIdException();
    protected ReadAheadException readAheadExceptionInstance = new ReadAheadException();

    protected ServerConfiguration cfg;
    // Boolean indicating if this thread should continue running. This is used
    // when we want to stop the thread during a PubSubServer shutdown.
    protected volatile boolean keepRunning = true;

    protected final OrderedSafeExecutor cacheWorkers;
    protected final int numCacheWorkers;
    protected volatile long maxSegmentSize;
    protected volatile long cacheEntryTTL;

    // JMX Beans
    ReadAheadCacheBean jmxCacheBean = null;

    /**
     * Constructor. Starts the cache maintainer thread
     *
     * @param realPersistenceManager
     */
    public ReadAheadCache(PersistenceManagerWithRangeScan realPersistenceManager, ServerConfiguration cfg) {
        this.realPersistenceManager = realPersistenceManager;
        this.cfg = cfg;
        numCacheWorkers = cfg.getNumReadAheadCacheThreads();
        cacheWorkers = new OrderedSafeExecutor(numCacheWorkers);
        reloadConf(cfg);
    }

    /**
     * Reload configuration
     *
     * @param conf
     *          Server configuration object
     */
    protected void reloadConf(ServerConfiguration cfg) {
        maxSegmentSize = cfg.getMaximumCacheSize() / numCacheWorkers;
        cacheEntryTTL = cfg.getCacheEntryTTL();
    }

    public ReadAheadCache start() {
        return this;
    }

    /**
     * ========================================================================
     * Methods of {@link PersistenceManager} that we will pass straight down to
     * the real persistence manager.
     */

    @Override
    public long getSeqIdAfterSkipping(ByteString topic, long seqId, int skipAmount) {
        return realPersistenceManager.getSeqIdAfterSkipping(topic, seqId, skipAmount);
    }

    @Override
    public MessageSeqId getCurrentSeqIdForTopic(ByteString topic) throws ServerNotResponsibleForTopicException {
        return realPersistenceManager.getCurrentSeqIdForTopic(topic);
    }

    /**
     * ========================================================================
     * Other methods of {@link PersistenceManager} that the cache needs to take
     * some action on.
     *
     * 1. Persist: We pass it through to the real persistence manager but insert
     * our callback on the return path
     *
     */
    @Override
    public void persistMessage(PersistRequest request) {
        // make a new PersistRequest object so that we can insert our own
        // callback in the middle. Assign the original request as the context
        // for the callback.

        PersistRequest newRequest = new PersistRequest(request.getTopic(), request.getMessage(),
                persistCallbackInstance, request);
        realPersistenceManager.persistMessage(newRequest);
    }

    /**
     * The callback that we insert on the persist request return path. The
     * callback simply forms a {@link PersistResponse} object and inserts it in
     * the request queue to be handled serially by the cache maintainer thread.
     *
     */
    public class PersistCallback implements Callback<PubSubProtocol.MessageSeqId> {

        /**
         * In case there is a failure in persisting, just pass it to the
         * original callback
         */
        @Override
        public void operationFailed(Object ctx, PubSubException exception) {
            PersistRequest originalRequest = (PersistRequest) ctx;
            Callback<PubSubProtocol.MessageSeqId> originalCallback = originalRequest.getCallback();
            Object originalContext = originalRequest.getCtx();
            originalCallback.operationFailed(originalContext, exception);
        }

        /**
         * When the persist finishes, we first notify the original callback of
         * success, and then opportunistically treat the message as if it just
         * came in through a scan
         */
        @Override
        public void operationFinished(Object ctx, PubSubProtocol.MessageSeqId resultOfOperation) {
            PersistRequest originalRequest = (PersistRequest) ctx;

            // Lets call the original callback first so that the publisher can
            // hear success
            originalRequest.getCallback().operationFinished(originalRequest.getCtx(), resultOfOperation);

            // Original message that was persisted didn't have the local seq-id.
            // Lets add that in
            Message messageWithLocalSeqId = MessageIdUtils.mergeLocalSeqId(originalRequest.getMessage(),
                                            resultOfOperation.getLocalComponent());

            // Now enqueue a request to add this newly persisted message to our
            // cache
            CacheKey cacheKey = new CacheKey(originalRequest.getTopic(), resultOfOperation.getLocalComponent());

            enqueueWithoutFailureByTopic(cacheKey.getTopic(),
                    new ScanResponse(cacheKey, messageWithLocalSeqId));
        }

    }

    protected void enqueueWithoutFailureByTopic(ByteString topic, final CacheRequest obj) {
        if (!keepRunning) {
            return;
        }
        try {
            numPendingRequests.incrementAndGet();
            cacheWorkers.submitOrdered(topic, new SafeRunnable() {
                @Override
                public void safeRun() {
                    numPendingRequests.decrementAndGet();
                    obj.performRequest();
                }
            });
        } catch (RejectedExecutionException ree) {
            logger.error("Failed to submit cache request for topic " + topic.toStringUtf8() + " : ", ree);
        }
    }

    /**
     * Another method from {@link PersistenceManager}.
     *
     * 2. Scan - Since the scan needs to touch the cache, we will just enqueue
     * the scan request and let the cache maintainer thread handle it.
     */
    @Override
    public void scanSingleMessage(ScanRequest request) {
        // Let the scan requests be serialized through the queue
        enqueueWithoutFailureByTopic(request.getTopic(),
                new ScanRequestWrapper(request));
    }

    /**
     * Another method from {@link PersistenceManager}.
     *
     * 3. Enqueue the request so that the cache maintainer thread can delete all
     * message-ids older than the one specified
     */
    @Override
    public void deliveredUntil(ByteString topic, Long seqId) {
        enqueueWithoutFailureByTopic(topic, new DeliveredUntil(topic, seqId));
    }

    /**
     * Another method from {@link PersistenceManager}.
     *
     * Since this is a cache layer on top of an underlying persistence manager,
     * we can just call the consumedUntil method there. The messages older than
     * the latest one passed here won't be accessed anymore so they should just
     * get aged out of the cache eventually. For now, there is no need to
     * proactively remove those entries from the cache.
     */
    @Override
    public void consumedUntil(ByteString topic, Long seqId) {
        realPersistenceManager.consumedUntil(topic, seqId);
    }

    @Override
    public void setMessageBound(ByteString topic, Integer bound) {
        realPersistenceManager.setMessageBound(topic, bound);
    }

    @Override
    public void clearMessageBound(ByteString topic) {
        realPersistenceManager.clearMessageBound(topic);
    }

    @Override
    public void consumeToBound(ByteString topic) {
        realPersistenceManager.consumeToBound(topic);
    }

    /**
     * Stop the readahead cache.
     */
    @Override
    public void stop() {
        try {
            keepRunning = false;
            cacheWorkers.shutdown();
        } catch (Exception e) {
            logger.warn("Failed to shut down cache workers : ", e);
        }
    }

    /**
     * The readahead policy is simple: We check if an entry already exists for
     * the message being requested. If an entry exists, it means that either
     * that message is already in the cache, or a read for that message is
     * outstanding. In that case, we look a little ahead (by readAheadCount/2)
     * and issue a range read of readAheadCount/2 messages. The idea is to
     * ensure that the next readAheadCount messages are always available.
     *
     * @return the range scan that should be issued for read ahead
     */
    protected RangeScanRequest doReadAhead(ScanRequest request) {
        ByteString topic = request.getTopic();
        Long seqId = request.getStartSeqId();

        int readAheadCount = cfg.getReadAheadCount();
        // To prevent us from getting screwed by bad configuration
        readAheadCount = Math.max(1, readAheadCount);

        RangeScanRequest readAheadRequest = doReadAheadStartingFrom(topic, seqId, readAheadCount);

        if (readAheadRequest != null) {
            return readAheadRequest;
        }

        // start key was already there in the cache so no readahead happened,
        // lets look a little beyond
        seqId = realPersistenceManager.getSeqIdAfterSkipping(topic, seqId, readAheadCount / 2);

        readAheadRequest = doReadAheadStartingFrom(topic, seqId, readAheadCount / 2);

        return readAheadRequest;
    }

    /**
     * This method just checks if the provided seq-id already exists in the
     * cache. If not, a range read of the specified amount is issued.
     *
     * @param topic
     * @param seqId
     * @param readAheadCount
     * @return The range read that should be issued
     */
    protected RangeScanRequest doReadAheadStartingFrom(ByteString topic, long seqId, int readAheadCount) {

        long startSeqId = seqId;
        Queue<CacheKey> installedStubs = new LinkedList<CacheKey>();

        int i = 0;

        for (; i < readAheadCount; i++) {
            CacheKey cacheKey = new CacheKey(topic, seqId);

            // Even if a stub exists, it means that a scan for that is
            // outstanding
            if (cache.containsKey(cacheKey)) {
                break;
            }
            CacheValue cacheValue = new CacheValue();
            if (null != cache.putIfAbsent(cacheKey, cacheValue)) {
                logger.warn("It is unexpected that more than one threads are adding message to cache key {}"
                            +" at the same time.", cacheKey);
            }

            logger.debug("Adding cache stub for: {}", cacheKey);
            installedStubs.add(cacheKey);

            seqId = realPersistenceManager.getSeqIdAfterSkipping(topic, seqId, 1);
        }

        // so how many did we decide to readahead
        if (i == 0) {
            // no readahead, hence return false
            return null;
        }

        long readAheadSizeLimit = cfg.getReadAheadSizeBytes();
        ReadAheadScanCallback callback = new ReadAheadScanCallback(installedStubs, topic);
        RangeScanRequest rangeScanRequest = new RangeScanRequest(topic, startSeqId, i, readAheadSizeLimit, callback,
                null);

        return rangeScanRequest;

    }

    /**
     * This is the callback that is used for the range scans.
     */
    protected class ReadAheadScanCallback implements ScanCallback {
        Queue<CacheKey> installedStubs;
        ByteString topic;

        /**
         * Constructor
         *
         * @param installedStubs
         *            The list of stubs that were installed for this range scan
         * @param topic
         */
        public ReadAheadScanCallback(Queue<CacheKey> installedStubs, ByteString topic) {
            this.installedStubs = installedStubs;
            this.topic = topic;
        }

        @Override
        public void messageScanned(Object ctx, Message message) {

            // Any message we read is potentially useful for us, so lets first
            // enqueue it
            CacheKey cacheKey = new CacheKey(topic, message.getMsgId().getLocalComponent());
            enqueueWithoutFailureByTopic(topic, new ScanResponse(cacheKey, message));

            // Now lets see if this message is the one we were expecting
            CacheKey expectedKey = installedStubs.peek();

            if (expectedKey == null) {
                // Was not expecting any more messages to come in, but they came
                // in so we will keep them
                return;
            }

            if (expectedKey.equals(cacheKey)) {
                // what we got is what we expected, dequeue it so we get the
                // next expected one
                installedStubs.poll();
                return;
            }

            // If reached here, what we scanned was not what we were expecting.
            // This means that we have wrong stubs installed in the cache. We
            // should remove them, so that whoever is waiting on them can retry.
            // This shouldn't be happening usually
            logger.warn("Unexpected message seq-id: " + message.getMsgId().getLocalComponent() + " on topic: "
                        + topic.toStringUtf8() + " from readahead scan, was expecting seq-id: " + expectedKey.seqId
                        + " topic: " + expectedKey.topic.toStringUtf8() + " installedStubs: " + installedStubs);
            enqueueDeleteOfRemainingStubs(noSuchSeqIdExceptionInstance);

        }

        @Override
        public void scanFailed(Object ctx, Exception exception) {
            enqueueDeleteOfRemainingStubs(exception);
        }

        @Override
        public void scanFinished(Object ctx, ReasonForFinish reason) {
            // If the scan finished because no more messages are present, its ok
            // to leave the stubs in place because they will get filled in as
            // new publishes happen. However, if the scan finished due to some
            // other reason, e.g., read ahead size limit was reached, we want to
            // delete the stubs, so that when the time comes, we can schedule
            // another readahead request.
            if (reason != ReasonForFinish.NO_MORE_MESSAGES) {
                enqueueDeleteOfRemainingStubs(readAheadExceptionInstance);
            }
        }

        private void enqueueDeleteOfRemainingStubs(Exception reason) {
            CacheKey installedStub;
            while ((installedStub = installedStubs.poll()) != null) {
                enqueueWithoutFailureByTopic(installedStub.getTopic(),
                        new ExceptionOnCacheKey(installedStub, reason));
            }
        }
    }

    protected static class HashSetCacheKeyFactory implements Factory<Set<CacheKey>> {
        protected final static HashSetCacheKeyFactory instance = new HashSetCacheKeyFactory();

        @Override
        public Set<CacheKey> newInstance() {
            return new HashSet<CacheKey>();
        }
    }

    protected static class TreeSetLongFactory implements Factory<SortedSet<Long>> {
        protected final static TreeSetLongFactory instance = new TreeSetLongFactory();

        @Override
        public SortedSet<Long> newInstance() {
            return new TreeSet<Long>();
        }
    }

    /**
     * For adding the message to the cache, we do some bookeeping such as the
     * total size of cache, order in which entries were added etc. If the size
     * of the cache has exceeded our budget, old entries are collected.
     *
     * @param cacheKey
     * @param message
     */
    protected void addMessageToCache(final CacheKey cacheKey,
                                     final Message message, final long currTime) {
        logger.debug("Adding msg {} to readahead cache", cacheKey);

        CacheValue cacheValue;
        if ((cacheValue = cache.get(cacheKey)) == null) {
            cacheValue = new CacheValue();
            CacheValue oldValue = cache.putIfAbsent(cacheKey, cacheValue);
            if (null != oldValue) {
                logger.warn("Weird! Should not have two threads adding message to cache key {} at the same time.",
                            cacheKey);
                cacheValue = oldValue;
            }
        }

        CacheSegment segment = cacheSegment.get();
        if (cacheValue.isStub()) { // update cache size only when cache value is a stub
            int size = message.getBody().size();

            // update the cache size
            segment.presentSegmentSize.addAndGet(size);
            presentCacheSize.addAndGet(size);
        }

        synchronized (cacheValue) {
            // finally add the message to the cache
            cacheValue.setMessageAndInvokeCallbacks(message, currTime);
        }

        // maintain the index of seq-id
        // no lock since threads are partitioned by topics
        MapMethods.addToMultiMap(orderedIndexOnSeqId, cacheKey.getTopic(),
                                 cacheKey.getSeqId(), TreeSetLongFactory.instance);

        // maintain the time index of addition
        MapMethods.addToMultiMap(segment.timeIndexOfAddition, currTime,
                                 cacheKey, HashSetCacheKeyFactory.instance);

        collectOldOrExpiredCacheEntries(segment);
    }

    protected void removeMessageFromCache(final CacheKey cacheKey, Exception exception,
                                          final boolean maintainTimeIndex,
                                          final boolean maintainSeqIdIndex) {
        CacheValue cacheValue = cache.remove(cacheKey);

        if (cacheValue == null) {
            return;
        }

        CacheSegment segment = cacheSegment.get();

        long timeOfAddition = 0;
        synchronized (cacheValue) {
            if (cacheValue.isStub()) {
                cacheValue.setErrorAndInvokeCallbacks(exception);
                // Stubs are not present in the indexes, so don't need to maintain
                // indexes here
                return;
            }

            int size = 0 - cacheValue.getMessage().getBody().size();
            presentCacheSize.addAndGet(size);
            segment.presentSegmentSize.addAndGet(size);
            timeOfAddition = cacheValue.getTimeOfAddition();
        }

        if (maintainSeqIdIndex) {
            MapMethods.removeFromMultiMap(orderedIndexOnSeqId, cacheKey.getTopic(),
                                          cacheKey.getSeqId());
        }
        if (maintainTimeIndex) {
            MapMethods.removeFromMultiMap(segment.timeIndexOfAddition,
                                          timeOfAddition, cacheKey);
        }
    }

    /**
     * Collection of old entries is simple. Just collect in insert-time order,
     * oldest to newest.
     */
    protected void collectOldOrExpiredCacheEntries(CacheSegment segment) {
        if (cacheEntryTTL > 0) {
            // clear expired entries
            while (!segment.timeIndexOfAddition.isEmpty()) {
                Long earliestTime = segment.timeIndexOfAddition.firstKey();
                if (MathUtils.now() - earliestTime < cacheEntryTTL) {
                    break;
                }
                collectCacheEntriesAtTimestamp(segment, earliestTime);
            }
        }

        while (segment.presentSegmentSize.get() > maxSegmentSize &&
               !segment.timeIndexOfAddition.isEmpty()) {
            Long earliestTime = segment.timeIndexOfAddition.firstKey();
            collectCacheEntriesAtTimestamp(segment, earliestTime);
        }
    }

    private void collectCacheEntriesAtTimestamp(CacheSegment segment, long timestamp) {
        Set<CacheKey> oldCacheEntries = segment.timeIndexOfAddition.get(timestamp);

        // Note: only concrete cache entries, and not stubs are in the time
        // index. Hence there can be no callbacks pending on these cache
        // entries. Hence safe to remove them directly.
        for (Iterator<CacheKey> iter = oldCacheEntries.iterator(); iter.hasNext();) {
            final CacheKey cacheKey = iter.next();

            logger.debug("Removing {} from cache because it's the oldest.", cacheKey);
            removeMessageFromCache(cacheKey, readAheadExceptionInstance, //
                                   // maintainTimeIndex=
                                   false,
                                   // maintainSeqIdIndex=
                                   true);
        }

        segment.timeIndexOfAddition.remove(timestamp);
    }

    /**
     * ========================================================================
     * The rest is just simple wrapper classes.
     *
     */

    protected class ExceptionOnCacheKey implements CacheRequest {
        CacheKey cacheKey;
        Exception exception;

        public ExceptionOnCacheKey(CacheKey cacheKey, Exception exception) {
            this.cacheKey = cacheKey;
            this.exception = exception;
        }

        /**
         * If for some reason, an outstanding read on a cache stub fails,
         * exception for that key is enqueued by the
         * {@link ReadAheadScanCallback}. To handle this, we simply send error
         * on the callbacks registered for that stub, and delete the entry from
         * the cache
         */
        @Override
        public void performRequest() {
            removeMessageFromCache(cacheKey, exception,
                                   // maintainTimeIndex=
                                   true,
                                   // maintainSeqIdIndex=
                                   true);
        }

    }

    @SuppressWarnings("serial")
    protected static class NoSuchSeqIdException extends Exception {

        public NoSuchSeqIdException() {
            super("No such seq-id");
        }
    }

    @SuppressWarnings("serial")
    protected static class ReadAheadException extends Exception {
        public ReadAheadException() {
            super("Readahead failed");
        }
    }

    public class CancelScanRequestOp implements CacheRequest {

        final CancelScanRequest request;

        public CancelScanRequestOp(CancelScanRequest request) {
            this.request = request;
        }

        @Override
        public void performRequest() {
            // cancel scan request
            cancelScanRequest(request.getScanRequest());
        }

        void cancelScanRequest(ScanRequest request) {
            if (null == request) {
                // nothing to cancel
                return;
            }

            CacheKey cacheKey = new CacheKey(request.getTopic(), request.getStartSeqId());
            CacheValue cacheValue = cache.get(cacheKey);
            if (null == cacheValue) {
                // cache value is evicted
                // so it's callback would be called, we don't need to worry about
                // cancel it. since it was treated as executed.
                return;
            }
            cacheValue.removeCallback(request.getCallback(), request.getCtx());
        }
    }

    public void cancelScanRequest(ByteString topic, CancelScanRequest request) {
        enqueueWithoutFailureByTopic(topic, new CancelScanRequestOp(request));
    }

    protected class ScanResponse implements CacheRequest {
        CacheKey cacheKey;
        Message message;

        public ScanResponse(CacheKey cacheKey, Message message) {
            this.cacheKey = cacheKey;
            this.message = message;
        }

        @Override
        public void performRequest() {
            addMessageToCache(cacheKey, message, MathUtils.now());
        }

    }

    protected class DeliveredUntil implements CacheRequest {
        ByteString topic;
        Long seqId;

        public DeliveredUntil(ByteString topic, Long seqId) {
            this.topic = topic;
            this.seqId = seqId;
        }

        @Override
        public void performRequest() {
            SortedSet<Long> orderedSeqIds = orderedIndexOnSeqId.get(topic);
            if (orderedSeqIds == null) {
                return;
            }

            // focus on the set of messages with seq-ids <= the one that
            // has been delivered until
            SortedSet<Long> headSet = orderedSeqIds.headSet(seqId + 1);

            for (Iterator<Long> iter = headSet.iterator(); iter.hasNext();) {
                Long seqId = iter.next();
                CacheKey cacheKey = new CacheKey(topic, seqId);

                logger.debug("Removing {} from cache because every subscriber has moved past",
                    cacheKey);

                removeMessageFromCache(cacheKey, readAheadExceptionInstance, //
                                       // maintainTimeIndex=
                                       true,
                                       // maintainSeqIdIndex=
                                       false);
                iter.remove();
            }

            if (orderedSeqIds.isEmpty()) {
                orderedIndexOnSeqId.remove(topic);
            }
        }
    }

    protected class ScanRequestWrapper implements CacheRequest {
        ScanRequest request;

        public ScanRequestWrapper(ScanRequest request) {
            this.request = request;
        }

        /**
         * To handle a scan request, we first try to do readahead (which might
         * cause a range read to be issued to the underlying persistence
         * manager). The readahead will put a stub in the cache, if the message
         * is not already present in the cache. The scan callback that is part
         * of the scan request is added to this stub, and will be called later
         * when the message arrives as a result of the range scan issued to the
         * underlying persistence manager.
         */

        @Override
        public void performRequest() {

            RangeScanRequest readAheadRequest = doReadAhead(request);

            // Read ahead must have installed at least a stub for us, so this
            // can't be null
            CacheKey cacheKey = new CacheKey(request.getTopic(), request.getStartSeqId());
            CacheValue cacheValue = cache.get(cacheKey);
            if (null == cacheValue) {
                logger.error("Cache key {} is removed after installing stub when scanning.", cacheKey);
                // reissue the request
                scanSingleMessage(request);
                return;
            }

            synchronized (cacheValue) {
                // Add our callback to the stub. If the cache value was already a
                // concrete message, the callback will be called right away
                cacheValue.addCallback(request.getCallback(), request.getCtx());
            }

            if (readAheadRequest != null) {
                realPersistenceManager.scanMessages(readAheadRequest);
            }
        }
    }

    @Override
    public void registerJMX(HedwigMBeanInfo parent) {
        try {
            jmxCacheBean = new ReadAheadCacheBean(this);
            HedwigMBeanRegistry.getInstance().register(jmxCacheBean, parent);
        } catch (Exception e) {
            logger.warn("Failed to register readahead cache with JMX", e);
            jmxCacheBean = null;
        }
    }

    @Override
    public void unregisterJMX() {
        try {
            if (jmxCacheBean != null) {
                HedwigMBeanRegistry.getInstance().unregister(jmxCacheBean);
            }
        } catch (Exception e) {
            logger.warn("Failed to unregister readahead cache with JMX", e);
        }
    }
}
TOP

Related Classes of org.apache.hedwig.server.persistence.ReadAheadCache$ScanRequestWrapper

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.