All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.crankuptheamps.client.HybridPublishStore Maven / Gradle / Ivy

////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2024 60East Technologies Inc., All Rights Reserved.
//
// This computer software is owned by 60East Technologies Inc. and is
// protected by U.S. copyright laws and other laws and by international
// treaties.  This computer software is furnished by 60East Technologies
// Inc. pursuant to a written license agreement and may be used, copied,
// transmitted, and stored only in accordance with the terms of such
// license agreement and with the inclusion of the above copyright notice.
// This computer software or any other copies thereof may not be provided
// or otherwise made available to any other person.
//
// U.S. Government Restricted Rights.  This computer software: (a) was
// developed at private expense and is in all respects the proprietary
// information of 60East Technologies Inc.; (b) was not developed with
// government funds; (c) is a trade secret of 60East Technologies Inc.
// for all purposes of the Freedom of Information Act; and (d) is a
// commercial item and thus, pursuant to Section 12.212 of the Federal
// Acquisition Regulations (FAR) and DFAR Supplement Section 227.7202,
// Government's use, duplication or disclosure of the computer software
// is subject to the restrictions set forth by 60East Technologies Inc..
//
////////////////////////////////////////////////////////////////////////////

package com.crankuptheamps.client;

import java.io.IOException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

import com.crankuptheamps.client.exception.DisconnectedException;
import com.crankuptheamps.client.exception.StoreException;
import com.crankuptheamps.client.exception.TimedOutException;

/**
 * PublishStore that stores first in memory, and swaps excess out to disk.
 *
 */
public class HybridPublishStore implements Store {
    // Two storage components: one in-memory, one on disk
    protected PublishStore _fileStore;
    protected MemoryPublishStore _memoryStore;
    // Maximum capacity for in-memory storage
    protected int _cap;
    // File path for disk storage
    protected String _path;
    // Threshold for swapping data to disk
    protected int _lowWatermark = 0;
    // Flag to control swapping behavior
    private boolean _holdSwapping = false;
    // Lock for synchronization
    private final Lock _lock = new ReentrantLock();
    // Condition variable for signaling when swapping occurs
    private final Condition _swapping = _lock.newCondition();

    // Inner class for replaying messages during swapping
    private static class SwappingOutReplayer implements Store.StoreReplayer {
        HybridPublishStore _store;
        int _entries, _errorCount;
        long _lastIndex;
        StoreException _exception = null;
        private SwappingOutReplayer(HybridPublishStore store_, int entries_)
        {
            _store = store_;
            _entries = entries_;
        }
        public int getErrors() throws StoreException
        {
            if (_exception != null) {
                throw _exception;
            }
            return _errorCount;
        }

        public long lastIndex() {
            return _lastIndex;
        }

        public void execute(Message message)
        {
            if(_entries > 0 && _errorCount == 0)
            {
                try {
                    // Sequence numbers are assigned by memory store
                    _store._fileStore.store(message, false);
                    _lastIndex = message.getSequence();
                } catch (StoreException e) {
                    ++_errorCount;
                    _exception = e;
                }
                --_entries;
            }
        }
    }

    /**
     * Constructor for the HybridPublishStore.
     *
     * @param path The path for the disk-based storage.
     * @param cap  The maximum number of messages that can be stored in memory. It
     *             is an integer that defines how many messages the in-memory storage
     *             component of the HybridPublishStore can hold before swapping excess
     *             data out to disk.
     * @throws StoreException Thrown when an operation fails with details of the
     *                        failure.
     */
    public HybridPublishStore(String path, int cap) throws StoreException {
        // Initialize the file store with the provided path
        _fileStore = new PublishStore(path);
        // Need to create with room for 1 extra to hold metadata
        _memoryStore = new MemoryPublishStore(cap+1);
        _cap = cap;
        _lowWatermark = (int) (_cap * 0.5);
        // Store the provided path
        _path = path;
        // Synchronize the last persisted values if file store is empty
        if (_fileStore.getLastPersisted() % 1000000 == 0) {
            _fileStore.discardUpTo(_memoryStore.getLastPersisted());
        }
    }

    /**
     * Sets the low watermark; once we start swapping out to disk, we
     * keep going until the number of entries in memory is lower than this.
     * This method allows changing the low watermark value while ensuring it
     * doesn't exceed the maximum capacity.
     * 
     * @param lowWatermark_ The number of entries to serve as a low watermark.
     */
    public void setLowWatermark(int lowWatermark_) {
        _lock.lock();
        try {
            // Update the low watermark if it's less than the maximum capacity
            if (lowWatermark_ < _cap) {
                _lowWatermark = lowWatermark_;
            }
        } finally {
            _lock.unlock();
        }
    }

    /**
     * Returns the lowest sequence number currently in the store.
     */
    public long getLowestUnpersisted() {
        // Get the lowest unpersisted sequence number from the disk store
        long count = _fileStore.getLowestUnpersisted();
        // If no unpersisted sequence number is found in the disk store,
        // get it from the memory store
        if (count == -1)
            return _memoryStore.getLowestUnpersisted();
        // Return the lowest unpersisted sequence number found
        return count;
    }

    /**
     * Discards data from both the disk store and memory store up to the specified
     * index.
     * 
     * @see com.crankuptheamps.client.Store#discardUpTo(long)
     * 
     * @param index The index up to which data should be discarded.
     * 
     * @throws StoreException Thrown when an operation fails with details of the
     *                        failure.
     */
    public void discardUpTo(long index) throws StoreException {
        _lock.lock();
        try {
            // Wait while swapping is in progress to ensure consistency
            while (_holdSwapping) {
                try {
                    _swapping.await(10, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                }
            }
            // Set a flag to prevent further swapping while discarding
            _holdSwapping = true;
        } finally {
            _lock.unlock();
        }
        try {
            // If the specified index is greater than or equal to the lowest unpersisted
            // index in memory, discard data from the memory store
            if (index >= _memoryStore.getLowestUnpersisted())
                _memoryStore.discardUpTo(index);
            // Discard data from the disk store up to the specified index
            _fileStore.discardUpTo(index);
        } finally {
            // Signal that the discarding process is complete
            _signalLock();
        }
    }

    /**
     * Private method to signal that swapping or discarding is complete.
     */
    private void _signalLock() {
        _lock.lock();
        try {
            // Reset the swapping flag and signal waiting threads
            _holdSwapping = false;
            _swapping.signalAll();
        } finally {
            _lock.unlock();
        }
    }

    /**
     * Replays messages using the provided replayer, ensuring no swapping occurs
     * during the replay process.
     * 
     * @see com.crankuptheamps.client.Store#replay(com.crankuptheamps.client.Store.StoreReplayer)
     * 
     * @param replayer The replayer used to replay messages.
     * @throws StoreException        Thrown when an operation fails with details of
     *                               the failure.
     * @throws DisconnectedException Thrown when the store is disconnected.
     */
    public void replay(StoreReplayer replayer) throws StoreException, DisconnectedException {
        // Have to make sure that no swapping happens or messages could get moved
        // from memory to file between file replay and memory replay
        _lock.lock();
        try {
            // Wait while swapping is in progress to ensure consistency
            while (_holdSwapping) {
                try {
                    _swapping.await(10, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                }
            }
            // Set a flag to prevent further swapping during replay
            _holdSwapping = true;
        } finally {
            _lock.unlock();
        }
        try {
            // First replay everything on disk, then replay everything in memory.
            _fileStore.replay(replayer);
            _memoryStore.replay(replayer);
        } finally {
            // Signal that the replay process is complete
            _signalLock();
        }
    }

    /**
     * Replays a single message using the provided replayer and index.
     * 
     * @see com.crankuptheamps.client.Store#replaySingle(com.crankuptheamps.client.Store.StoreReplayer,
     *      long)
     * @param replayer The replayer used to replay the message.
     * @param index    The index of the message to replay.
     * @return true if the message was successfully replayed, false otherwise.
     * @throws StoreException        Thrown when an operation fails with details of
     *                               the failure.
     * @throws DisconnectedException Thrown when the store is disconnected.
     */
    public boolean replaySingle(StoreReplayer replayer, long index)
            throws StoreException, DisconnectedException {
        // Determine whether to replay from memory or disk based on the index
        if (index < _memoryStore.getLowestUnpersisted()) {
            return _fileStore.replaySingle(replayer, index);
        } else {
            return _memoryStore.replaySingle(replayer, index);
        }
    }

    /**
     * Gets the count of unpersisted messages in both the disk store and memory
     * store
     * 
     * @see com.crankuptheamps.client.Store#unpersistedCount()
     * 
     * @return The total count of unpersisted messages.
     */
    public long unpersistedCount() {
        // Sum the counts of unpersisted messages from both stores
        return _fileStore.unpersistedCount() + _memoryStore.unpersistedCount();
    }

    /**
     * Stores a message in the hybrid store, swapping to disk if memory usage
     * exceeds capacity.
     *
     * @param message The message to store.
     * @throws StoreException Thrown when an operation fails with details of the
     *                        failure.
     */
    public void store(Message message) throws StoreException {
        SwappingOutReplayer replayer = null;
        _lock.lock();
        try {
            // Wait while swapping is in progress to ensure consistency
            while (_holdSwapping) {
                try {
                    _swapping.await(10, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                }
            }
            long memUnpersistedCount = _memoryStore.unpersistedCount();
            if (memUnpersistedCount >= _cap) {
                _holdSwapping = true;
                // Swap some messages from memory to disk
                replayer = new SwappingOutReplayer(this, (int) memUnpersistedCount - _lowWatermark);
            }
        } finally {
            _lock.unlock();
        }
        try {
            if (replayer != null) {
                _memoryStore.replay(replayer);
                // If there are no errors during replay, discard the replayed messages from
                // memory
                if (replayer.getErrors() == 0) {
                    _memoryStore.discardUpTo(replayer.lastIndex());
                }
            }
            // Store the incoming message in memory
            _memoryStore.store(message);
        } catch (DisconnectedException e) {
            // This "cannot" happen, as our replayer does not throw DisconnectedException
            assert false;
        } finally {
            _lock.lock();
            if (replayer != null)
                _holdSwapping = false;
            _lock.unlock();
        }
    }

    /**
     * Sets a message in both the memory store and the disk store.
     *
     * @param m The message to set.
     */
    public void setMessage(Message m) {
        _memoryStore.setMessage(m);
        _fileStore.setMessage(m.copy());
    }

    /**
     * Gets the last persisted sequence number from the file store.
     * The file store value is always considered correct.
     *
     * @return The last persisted sequence number.
     * @throws StoreException Thrown when an operation fails with details of the
     *                        failure.
     */
    public long getLastPersisted() throws StoreException {
        // File store value will always be correct. It is updated on every
        // call to discardUpTo and initialized with the same values as the
        // memory store. The memory store value could be too high because
        // it has discardUpTo called after swapping to file store.
        return _fileStore.getLastPersisted();
    }

    /**
     * Flushes the memory store and, if necessary, the file store.
     *
     * @throws TimedOutException Thrown if the flush operation times out.
     */
    public void flush() throws TimedOutException {
        // Flush memory first
        _memoryStore.flush();
        // Flush file store in case it needs flushing after swapping
        _fileStore.flush();
    }

    /**
     * Flushes the memory store and, if necessary, the file store with a specified
     * timeout.
     *
     * @param timeout The maximum time to wait for the flush operation to complete.
     * @throws TimedOutException Thrown if the flush operation times out.
     */
    public void flush(long timeout) throws TimedOutException {
        long start = System.currentTimeMillis();
        // Flush memory store first
        _memoryStore.flush(timeout);
        // Calculate remaining time for file store flushing, if needed
        long remaining = timeout - (System.currentTimeMillis() - start);
        if (remaining > 0)
            _fileStore.flush(remaining);
        else
            throw new TimedOutException("Timed out waiting to flush publish store.");
    }

    /**
     * Sets a resize handler for both the file store and the memory store.
     *
     * @param handler The resize handler to set.
     */
    public void setResizeHandler(PublishStoreResizeHandler handler) {
        _fileStore.setResizeHandler(handler);
        _memoryStore.setResizeHandler(handler);
    }

    /**
     * Closes both the memory store and the file store.
     *
     * @throws Exception Thrown if an error occurs during closure.
     */
    public void close() throws Exception {
        _memoryStore.close();
        _fileStore.close();
    }

    /**
     * Get whether the Store will throw a PublishGapException from discardUpTo if the
     * sequence number being discarded is less then the current last persisted.
     * @return If true, an exception will be thrown during logon if a gap could be
     * created. If false, logon is allowed to proceed.
     */
    public boolean getErrorOnPublishGap()
    {
        return _fileStore.getErrorOnPublishGap();
    }

    /**
     * Set whether the Store should throw a PublishGapException from discardUpTo if the
     * sequence number being discarded is less then the current last persisted. This can
     * occur if the client fails over to a server that has not been synchronously
     * replicated to by its previous server, which lead to the new server having a gap
     * in messages from this client.
     * @param errorOnGap If true, an exception will be thrown during logon if a gap
     * could be created. If false, allow the logon to proceed.
     */
    public void setErrorOnPublishGap(boolean errorOnGap)
    {
        // Don't set this on the memory store, as it always has only the most recent items
        // _memoryStore.setErrorOnPublishGap(errorOnGap);
        _fileStore.setErrorOnPublishGap(errorOnGap);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy