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

com.crankuptheamps.client.BlockPublishStore 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 com.crankuptheamps.client.BlockPublishStore.Block;
import com.crankuptheamps.client.exception.CommandException;
import com.crankuptheamps.client.exception.TimedOutException;
import com.crankuptheamps.client.fields.Field;
import com.crankuptheamps.client.fields.IntegerField;
import java.io.IOException;
import java.lang.Long;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
import java.util.zip.CRC32;

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

/**
 * BlockPublishStore is a base class for publish {@link Store} implementations.
 * As messages are stored, space is allocated from a pre-created flat buffer. As
 * messages are discarded, space in that buffer is marked "free" for future
 * store operations. If messages are stored faster than they are published, the buffer
 * is re-sized to create more capacity.
 */
public class BlockPublishStore implements Store 
{
    // This field represents the number of blocks per reallocation. It determines
    // how many blocks are allocated when resizing the buffer to create more
    // capacity
    private int _blocksPerRealloc = 10000;
    // Keeps track of the next sequence number to be used when storing messages
    private volatile long _nextSequence = 1;
    // A ReentrantLock instance used for synchronization purposes
    private final Lock lock = new ReentrantLock();
    // A Condition object associated with the lock used to signal when blocks are
    // free for storage
    private final Condition _blocksFree = lock.newCondition();
    // A Condition object associated with the lock used to signal when messages are
    // ready for processing
    private final Condition _messageReady = lock.newCondition();
    // boolean flag that indicates whether the buffer is currently being resized
    private volatile boolean _resizing = false;
    protected PublishStoreResizeHandler _resizeHandler = null;

    /**
     * A simple wrapper object around a byte array that allows a sub-range to be
     * specified using its offset and length properties.
     */
    public static class ByteSequence {
        public byte[] array;
        public long offset, len;

        // This constructor initializes a 'ByteSequence' with null array and zero offset
        // and length
        public ByteSequence()
        {
            this.array = null;
            this.offset = this.len = 0;
        }

        /**
         * Initializes a new instance of the 'ByteSequence' class with specified array,
         * offset, and length values.
         * 
         * @param array  The byte array.
         * @param offset The offset within the byte array.
         * @param len    The length of the byte sequence.
         */
        public ByteSequence(byte[] array, long offset, long len)
        {
            this.array = array;
            this.offset = offset;
            this.len = len;
        }
    }

    /**
     * Interface which is used to hold the BlockPublishStore
     * buffer data.
     */
    public interface Buffer extends AutoCloseable
    {
        // Returns the size of the buffer
        public long getSize() throws IOException;

        // Sets the size of the buffer
        public void setSize(long newSize) throws IOException;

        // Returns the current position in the buffer
        public long getPosition() throws IOException;

        // Sets the current position in the buffer
        public void setPosition(long position) throws IOException;

        // Writes a byte to the buffer
        public void putByte(byte b) throws IOException;

        // Reads a byte from the buffer
        public byte getByte() throws IOException;

        // Writes an integer to the buffer
        public void putInt(int i) throws IOException;

        // Writes an integer to a specified position in the buffer
        public void putInt(long position, int i) throws IOException;

        // Reads an integer from the buffer
        public int getInt() throws IOException;

        // Reads an integer from a specified position in the buffer
        public int getInt(long position) throws IOException;

        // Writes a long to the buffer
        public void putLong(long l) throws IOException;

        // Writes a long to a specified position in the buffer
        public void putLong(long position, long l) throws IOException;

        // Reads a long from the buffer
        public long getLong() throws IOException;

        // Writes a ByteSequence to the buffer
        public void putBytes(ByteSequence bytes) throws IOException;

        // Reads a ByteSequence from the buffer
        public void getBytes(ByteSequence outBytes) throws IOException;

        // Sets a range of bytes to zero in the buffer
        public void zero(long offset, int length) throws IOException;

        // Writes a byte array to the buffer
        public void putBytes(byte[] buffer, long offset, long length) throws IOException;

        // Reads data into a Field object from the buffer
        public void getBytes(Field outField, int length) throws IOException;
    }

    // This nested class represents a block in the buffer. It is used for managing
    // the storage of messages
    static class Block {
        // An integer representing the index of the block
        public int index;
        // A reference to the next block in the chain
        public Block nextInChain;
        // A reference to the next block in the list
        public Block nextInList;
        // A long representing the sequence number associated with the block
        public long sequenceNumber;
        // A constant integer indicating the size of a block
        public static final int SIZE = 2048; // finely honed
        // A constant integer representing the size of the block header
        public static final int BLOCK_HEADER_SIZE = 32;
        // A constant integer representing the size of block data
        public static final int BLOCK_DATA_SIZE = Block.SIZE - BLOCK_HEADER_SIZE;
        // A constant integer representing the chain header size
        public static final int CHAIN_HEADER = 32;
        // Position of the CRC value
        public static final int BLOCK_CRC_POSITION = 16;
        // Position of the ready flag, non-zero is ready
        public static final int BLOCK_READY_POSITION = 24;

        // Initializes a Block with the specified index
        public Block(int index) { this.index = index; }

        // Returns the position of the block within the buffer
        public long getPosition() { return (long) SIZE * index; }

        // Provides a string representation of the block, including its index and
        // sequence number
        public String toString()
        {
            return String.format("block %d sequenceNumber %d", index, sequenceNumber);
        }
    }

    // A buffer acts as a random access file -- mmap, memory only, and file
    // implementations exist.
    protected Buffer _buffer;

    // _freeList is the list of free blocks in the file. _usedList is the list of
    // the used head blocks. Used non-head blocks are only kept alive by "chain"
    // references from the head block. Used list is ordered where the start is the
    // LEAST recently used.
    volatile Block _freeList = null;
    volatile Block _usedList = null;
    volatile Block _endOfUsedList = null;

    // The metadata format is currently:
    // int size - unused
    // int nextInChain - set to the int of the version number, such as 4000100 for
    // 4.0.1.0
    // long sequence - set to the index of the last discarded
    Block _metadataBlock = null;

    // Save this so we're not GCing stuff.
    CRC32 _crc = null;

    // We use this _argument thing since Java doesn't have output parameters, but we
    // sort of need them.
    ByteSequence _argument = new ByteSequence();

    // We use this to hold everything we read from the buffer for replay
    Message _message;

    // Determines if discardUpTo will throw
    protected boolean _errorOnPublishGap = false;

    // When a message spans multiple blocks we need to reassemble it
    // into one place before replaying. We use this buffer to do so,
    // allocating on demand.
    byte[] _readBuffer;

    // Write assembly area
    ArrayStoreBuffer _sb = null;

    protected static final int AMPS_MIN_PUB_STORE_DISCARDED_VERSION = 4000100;
    protected static final int METADATA_VERSION_LOCATION = 4;
    protected static final int METADATA_LAST_DISCARDED_LOCATION = 8;

    /**
     * Protected constructor for BlockPublishStore.
     * This constructor initializes the BlockPublishStore with a specified buffer
     * and blocksPerRealloc
     *
     * @param buffer           An instance of the Buffer interface representing the
     *                         data buffer.
     * @param blocksPerRealloc An integer representing the number of blocks per
     *                         reallocation.
     * @param isAFile          A boolean indicating whether this store represents a
     *                         file.
     */
    protected BlockPublishStore(Buffer buffer, int blocksPerRealloc, boolean isAFile)
    {
        this._blocksPerRealloc = blocksPerRealloc;
        this._buffer = buffer;
        if (isAFile)
        {
            this._crc = new CRC32();
            this._sb = new ArrayStoreBuffer();
            try {
                this._sb.setSize(Block.SIZE);
            } catch (IOException e)
            {
                // If there's an IOException, set _sb to null
                this._sb = null;
            }
        }
    }

    /**
     * Protected constructor for BlockPublishStore.
     * This constructor is a convenience method that calls the first constructor
     * with isAFile set to false
     *
     * @param buffer           An instance of the Buffer interface representing the
     *                         data buffer.
     * @param blocksPerRealloc An integer representing the number of blocks per
     *                         reallocation.
     */
    protected BlockPublishStore(Buffer buffer, int blocksPerRealloc)
    {
        this(buffer, blocksPerRealloc, false);
    }

    /**
     * Protected constructor for BlockPublishStore.
     * This constructor is a convenience method that calls the second constructor
     * with default blocksPerRealloc (10000) and isAFile (false).
     * @param buffer An instance of the Buffer interface representing the data
     *               buffer.
     */
    protected BlockPublishStore(Buffer buffer)
    {
        this(buffer, 10000, false);
    }

    /**
     * Stores a message in the BlockPublishStore.
     * @param m The message to be stored.
     * @throws StoreException If an error occurs during message storage.
     */
    public void store(Message m) throws StoreException
    {
        store(m, true);
    }

    /**
     * Closes the BlockPublishStore.
     *
     * @throws Exception If an error occurs during the closing operation.
     */
    public void close() throws Exception
    {
        _buffer.close();
    }

    /**
     * Flattens a chain of blocks and moves them to the free list for reuse.
     * @param toRemove The Block object to be removed and flattened to the free
     *                 list.
     */
    private void flattenToFreeList(Block toRemove)
    {
        Block currentInChain = toRemove;
        lock.lock();
        try
        {
            if (toRemove == _usedList) {
                _usedList = _usedList.nextInList;
            } else {
                Block prev = _usedList;
                for (Block b = prev.nextInList; b != null; b = b.nextInList) {
                    if (b == toRemove) {
                        prev.nextInList = b.nextInList;
                        break;
                    }
                    prev = prev.nextInList;
                }
            }
            while (currentInChain != null)
            {
                Block nextInChain = currentInChain.nextInChain;
                try {
                    _buffer.zero(currentInChain.getPosition(),
                            Block.BLOCK_HEADER_SIZE);
                } catch (IOException ex) {
                    // Going to ignore this, since we here because of an error
                }
                currentInChain.sequenceNumber = 0;
                currentInChain.nextInChain = null;
                // and, flatten this chain onto the zeroed out list.
                currentInChain.nextInList = _freeList;
                _freeList = currentInChain;
                currentInChain = nextInChain;
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Protected utility method for storing messages in the BlockPublishStore.
     * @param m              The message to be stored.
     * @param assignSequence A boolean flag indicating whether to assign a sequence
     *                       number to the message.
     * @throws StoreException If an error occurs during message storage.
     */
    protected void store(Message m, boolean assignSequence)
            throws StoreException
    {
        // Get the command type of the message (e.g., Publish, SOWDelete)
        int operation = m.getCommand();
        // Initialize the 'flag' to -1 (a default value)
        int flag = -1;
        // Initialize 'data' as null; it will hold the message data
        Field data = null;
        // Check the type of operation and set 'data' and 'flag' accordingly
        if (operation == Message.Command.Publish ||
                operation == Message.Command.DeltaPublish ||
                (operation == Message.Command.SOWDelete && !m.isDataNull()))
        {
            data = m.getDataRaw();
            if (operation == Message.Command.SOWDelete)
                flag = Store.SOWDeleteByData;
        } else if (!m.isFilterNull()) {
            data = m.getFilterRaw();
            flag = Store.SOWDeleteByFilter;
        } else if (!m.isSowKeysNull()) {
            data = m.getSowKeysRaw();
            flag = Store.SOWDeleteByKeys;
        } else if (!m.isBookmarkNull()) {
            if (m.isOptionsNull() || !m.getOptions().contains("cancel")) {
                data = m.getBookmarkRaw();
                flag = Store.SOWDeleteByBookmark;
            } else {
                data = m.getBookmarkRaw();
                flag = Store.SOWDeleteByBookmarkCancel;
            }
        } else {
            // Throw an exception if the message has no valid data, filter, sow keys, or
            // bookmark
            throw new StoreException("Cannot store a Message with no data, filter, sow keys, or bookmark.");
        }
        // Declare Block objects for current, next, and the first block
        Block current, next, first = null;
        // Get fields related to message properties
        Field cmdId = m.getCommandIdRaw();
        long commandIdRemaining = cmdId == null ? 0 : cmdId.length;
        Field correlationId = m.getCorrelationIdRaw();
        long correlationIdRemaining = correlationId == null ? 0 : correlationId.length;
        Field expiration = m.getExpirationRaw();
        long expirationRemaining = expiration == null ? 0 : expiration.length;
        Field sowKey = m.getSowKeyRaw();
        long sowKeyRemaining = sowKey == null ? 0 : sowKey.length;
        Field topic = m.getTopicRaw();
        long topicRemaining = topic.length;
        long dataRemaining = data.length;
        // Nothing to write if this is a Unknown command, which is a Queue ack
        // Calculate the total remaining bytes to write
        long totalRemaining = ((operation == Message.Command.Unknown) ? 0
                : (Block.CHAIN_HEADER + commandIdRemaining +
                        correlationIdRemaining + expirationRemaining +
                        sowKeyRemaining + topicRemaining + dataRemaining));
        // Calculate the length of the last block
        int lastBlockLength = ((operation == Message.Command.Unknown) ? 0
                : ((int) (totalRemaining % Block.BLOCK_DATA_SIZE)));
        // Calculate the number of blocks to write
        int blocksToWrite = ((operation == Message.Command.Unknown) ? 0
                : ((int) ((totalRemaining / Block.BLOCK_DATA_SIZE) +
                        (lastBlockLength > 0 ? 1 : 0))));

        // Initialize CRC value
        long crcVal = 0;
        try {
            // First block gets sequence assigned
            // Assign a sequence number to the first block if required
            long currentSequence = (assignSequence ? 0 : m.getSequence());
            first = next = get(assignSequence, currentSequence);
            // Check if there is space to allocate a block; throw an exception if not
            if (first == null)
            {
                throw new StoreException("The store is full, and no blocks can be allocated.");
            }
            // Assign the sequence number to the message if required
            if (assignSequence)
                m.setSequence(first.sequenceNumber);
            boolean loopComplete = false;

            // Loop through the blocks to write
            while (blocksToWrite > 0)
            {
                loopComplete = false;
                current = next;
                int bytesRemainingInBlock = Block.BLOCK_DATA_SIZE;
                // Allocate a new block if more than one block needs to be written
                if (blocksToWrite > 1)
                {
                    next = get(false, 0);
                    if (next == null)
                    {
                        // If no additional block can be allocated, flatten the list and throw an
                        // exception
                        flattenToFreeList(first);
                        throw new StoreException("The store is full, and no additional blocks can be allocated.");
                    }
                } else
                {
                    next = null;
                }

                current.nextInChain = next;

                // Write block information into the buffer
                Buffer buffer = _sb == null ? _buffer : _sb;
                lock.lock();
                try {
                    long start = _sb == null ? current.getPosition() : 0;
                    buffer.setPosition(start);
                    if (current == first)
                    {
                        buffer.putInt((int) totalRemaining);
                    } else
                    {
                        buffer.putInt(next == null ? lastBlockLength : Block.BLOCK_DATA_SIZE);
                    }
                    buffer.putInt(next == null ? current.index : next.index);
                    buffer.putLong(current == first ? m.getSequence() : 0);
                    // Calculate CRC value if needed (CRC is disabled when not writing to disk)
                    if (_crc != null)
                    {
                        _crc.reset();
                        if (cmdId != null && commandIdRemaining > 0)
                            _crc.update(cmdId.buffer,
                                    (int) cmdId.position,
                                    (int) commandIdRemaining);
                        if (correlationId != null && correlationIdRemaining > 0)
                            _crc.update(correlationId.buffer,
                                    (int) correlationId.position,
                                    (int) correlationIdRemaining);
                        if (expiration != null && expirationRemaining > 0)
                            _crc.update(expiration.buffer,
                                    (int) expiration.position,
                                    (int) expirationRemaining);
                        if (sowKey != null && sowKeyRemaining > 0)
                            _crc.update(sowKey.buffer,
                                    (int) sowKey.position,
                                    (int) sowKeyRemaining);
                        if (topicRemaining > 0)
                            _crc.update(topic.buffer,
                                    (int) topic.position,
                                    (int) topicRemaining);
                        if (dataRemaining > 0)
                            _crc.update(data.buffer,
                                    (int) data.position,
                                    (int) dataRemaining);
                        crcVal = _crc.getValue();
                    }
                    buffer.putLong(crcVal);

                    // Write data, topic, and other content
                    buffer.setPosition(start + Block.BLOCK_HEADER_SIZE);
                    if (current == first)
                    {
                        buffer.putInt((int) operation);
                        buffer.putInt((int) commandIdRemaining);
                        buffer.putInt((int) correlationIdRemaining);
                        buffer.putInt((int) expirationRemaining);
                        buffer.putInt((int) sowKeyRemaining);
                        buffer.putInt((int) topicRemaining);
                        buffer.putInt(flag);
                        buffer.putInt((int) m.getAckTypeOutgoing());
                        bytesRemainingInBlock -= Block.CHAIN_HEADER;
                    }
                    // Write remaining fields into the block
                    long bytesToWrite = commandIdRemaining <= bytesRemainingInBlock ? commandIdRemaining
                            : bytesRemainingInBlock;
                    if (bytesToWrite > 0)
                    {
                        Field rawField = m.getCommandIdRaw();
                        buffer.putBytes(rawField.buffer,
                                rawField.position + rawField.length -
                                        commandIdRemaining,
                                bytesToWrite);
                        bytesRemainingInBlock -= bytesToWrite;
                        commandIdRemaining -= bytesToWrite;
                    }
                    bytesToWrite = correlationIdRemaining <= bytesRemainingInBlock ? correlationIdRemaining
                            : bytesRemainingInBlock;
                    if (bytesToWrite > 0)
                    {
                        Field rawField = m.getCorrelationIdRaw();
                        buffer.putBytes(rawField.buffer,
                                rawField.position + rawField.length -
                                        correlationIdRemaining,
                                bytesToWrite);
                        bytesRemainingInBlock -= bytesToWrite;
                        correlationIdRemaining -= bytesToWrite;
                    }
                    bytesToWrite = expirationRemaining <= bytesRemainingInBlock ? expirationRemaining
                            : bytesRemainingInBlock;
                    if (bytesToWrite > 0)
                    {
                        Field rawField = m.getExpirationRaw();
                        buffer.putBytes(rawField.buffer,
                                rawField.position + rawField.length -
                                        expirationRemaining,
                                bytesToWrite);
                        bytesRemainingInBlock -= bytesToWrite;
                        expirationRemaining -= bytesToWrite;
                    }
                    bytesToWrite = sowKeyRemaining <= bytesRemainingInBlock ? sowKeyRemaining : bytesRemainingInBlock;
                    if (bytesToWrite > 0)
                    {
                        Field rawField = m.getSowKeyRaw();
                        buffer.putBytes(rawField.buffer,
                                rawField.position + rawField.length -
                                        sowKeyRemaining,
                                bytesToWrite);
                        bytesRemainingInBlock -= bytesToWrite;
                        sowKeyRemaining -= bytesToWrite;
                    }
                    bytesToWrite = topicRemaining <= bytesRemainingInBlock ? topicRemaining : bytesRemainingInBlock;
                    if (bytesToWrite > 0)
                    {
                        Field rawField = m.getTopicRaw();
                        buffer.putBytes(rawField.buffer,
                                rawField.position + rawField.length -
                                        topicRemaining,
                                bytesToWrite);
                        bytesRemainingInBlock -= bytesToWrite;
                        topicRemaining -= bytesToWrite;
                    }
                    bytesToWrite = dataRemaining <= bytesRemainingInBlock ? dataRemaining : bytesRemainingInBlock;
                    if (bytesToWrite > 0)
                    {
                        buffer.putBytes(data.buffer,
                                data.position + data.length -
                                        dataRemaining,
                                bytesToWrite);
                        dataRemaining -= bytesToWrite;
                    }
                    // If we've used sb as an assembly area, here's where we actually write to the
                    // file.
                    if (_sb != null)
                    {
                        _buffer.setPosition(current.getPosition());
                        _argument.array = _sb.getBuffer();
                        _argument.offset = 0;
                        _argument.len = _sb.getPosition();
                        _buffer.putBytes(_argument);
                    }
                    loopComplete = true;
                } finally {
                    lock.unlock();
                    if (!loopComplete) {
                        // If the loop is not completed, flatten the list and set 'first' to null
                        flattenToFreeList(first);
                        first = null;
                    }
                }
                --blocksToWrite;
            }
            // Now, there should be no data or topic left to write.
            assert (dataRemaining == 0 && topicRemaining == 0);
            // Lock and signal that the message is ready
            lock.lock();
            try // Done, signal the ready flag
            {
                _buffer.putInt(first.getPosition() + Block.BLOCK_READY_POSITION,
                        1);
                _messageReady.signalAll();
            } finally {
                lock.unlock();
            }
        } catch (IOException e) {
            // If an IOException occurs, flatten the list and throw a StoreException
            if (first != null)flattenToFreeList(first);
            throw new StoreException(e);
        }
    }

    /**
     * This method is responsible for managing the discarding of blocks in a store
     * based on the specified index. It handles locking, updating metadata, and
     * organizing the blocks for efficient reuse.
     */
    public void discardUpTo(long index) throws StoreException
    {
        // If we're empty or already past this value, just return
        if (index == 0 || _usedList == null ||
                index <= _metadataBlock.sequenceNumber)
        {
            lock.lock();
            try {
                if (_errorOnPublishGap && index < _metadataBlock.sequenceNumber
                    && index > 0) {
                    throw new PublishGapException("Connection to current server could cause a message gap as server is only up to " + index + " but Store is already to " + _metadataBlock.sequenceNumber);
                }
                // If the last persisted index is less than the specified index, update it
                if (_getLastPersisted() < index) {
                    _metadataBlock.sequenceNumber = index;
                    // Position plus size of two ints
                    // Update the metadata with the new sequence number
                    _buffer.putLong(_metadataBlock.getPosition() +
                            METADATA_LAST_DISCARDED_LOCATION,
                            _metadataBlock.sequenceNumber);
                }
                if (_nextSequence <= index)
                    _nextSequence = index + 1;
            } catch (IOException ex) {
                throw new StoreException("Error saving last discarded: " + ex,
                        ex);
            } finally {
                lock.unlock();
            }
            return;
        }

        // Set the metadata block's sequence number to the specified index
        _metadataBlock.sequenceNumber = index;
        // First, find the beginning of where we should discard
        Block almostFreeList = detach(index);

        // We no longer need to keep a lock. The only funny thing about our state is
        // that there are "missing" entries -- some on _usedList, some on _freeList, and
        // some on our (local) almostFreeList, about to be freed. We could be fancy
        // and enqueue them for freeing, not going to do that.

        // For each one of these guys on our list, flatten onto the
        // new "zeroedOutList" and remember the last entry. We'll use
        // that to efficiently prepend, below.
        Block zeroedOutList = null;
        Block lastOnTheList = almostFreeList;
        try
        {
            // Clear out the muck in the byte buffer
            Block current = almostFreeList;

            while (current != null) {
                Block next = current.nextInList;

                Block currentInChain = current;
                while (currentInChain != null) {
                    Block nextInChain = currentInChain.nextInChain;
                    _buffer.zero(currentInChain.getPosition(),
                            Block.BLOCK_HEADER_SIZE);
                    currentInChain.sequenceNumber = 0;
                    currentInChain.nextInChain = null;
                    // And, flatten this chain onto the zeroed out list.
                    currentInChain.nextInList = zeroedOutList;
                    zeroedOutList = currentInChain;
                    currentInChain = nextInChain;
                }
                current = next;
            }
        } catch (IOException ioex)
        {
            throw new StoreException(ioex);
        }

        // now zeroedOutList is flat and free. We can just prepend it to the free list.
        lock.lock();
        try
        {
            _metadataBlock.sequenceNumber = index;
            try {
                // Position plus size of two ints
                // Update the metadata with the new sequence number
                _buffer.putLong(_metadataBlock.getPosition() +
                        METADATA_LAST_DISCARDED_LOCATION,
                        _metadataBlock.sequenceNumber);
            } catch (IOException ex) {
                throw new StoreException("Error saving last discarded: " + ex, ex);
            }
            if (lastOnTheList != null) {
                lastOnTheList.nextInList = _freeList;
                _freeList = zeroedOutList;
            }
            // Signal that blocks are free
            _blocksFree.signalAll();
        }
        finally {
            lock.unlock();
        }
    }

    /**
     * Gets the last persisted sequence number.
     *
     * @return The last persisted sequence number.
     * @throws StoreException If an error occurs while retrieving the last persisted
     *                        sequence number.
     */
    public long getLastPersisted() throws StoreException {
        lock.lock();
        try {
            return _getLastPersisted();
        } finally {
            lock.unlock();
        }
    }

    // Lock should already be held. Gets the last persisted sequence number
    private long _getLastPersisted() throws StoreException {
        if (_metadataBlock.sequenceNumber != 0) {
            return _metadataBlock.sequenceNumber;
        }
        if (_nextSequence != 1) {
            long minSeq = _getLowestUnpersisted();
            long maxSeq = _getHighestUnpersisted();
            if (minSeq != -1)
                _metadataBlock.sequenceNumber = minSeq - 1;
            if (maxSeq != -1 && _nextSequence <= maxSeq)
                _nextSequence = maxSeq + 1;
            if (_nextSequence < _metadataBlock.sequenceNumber)
                _metadataBlock.sequenceNumber = _nextSequence - 1;
        } else {
            _metadataBlock.sequenceNumber = System.currentTimeMillis() * 1000000;
            _nextSequence = _metadataBlock.sequenceNumber + 1;
        }
        try {
            // Position plus size of two ints
            _buffer.putLong(_metadataBlock.getPosition() +
                    METADATA_LAST_DISCARDED_LOCATION,
                    _metadataBlock.sequenceNumber);
        } catch (IOException ex) {
            throw new StoreException("Error saving last discarded: " + ex, ex);
        }
        return _metadataBlock.sequenceNumber;
    }

    /**
     * Gets the lowest unpersisted sequence number.
     *
     * @return The lowest unpersisted sequence number or -1 if no sequence numbers
     *         are unpersisted.
     */
    public long getLowestUnpersisted() {
        lock.lock();
        try {
            return _getLowestUnpersisted();
        } finally {
            lock.unlock();
        }
    }

    // Lock should already be held. Gets the lowest unpersisted sequence number
    private long _getLowestUnpersisted() {
        if (_usedList != null) {
            return _usedList.sequenceNumber;
        }
        return -1;
    }

    /**
     * Gets the highest unpersisted sequence number.
     *
     * @return The highest unpersisted sequence number or -1 if no sequence numbers
     *         are unpersisted.
     */
    public long getHighestUnpersisted() {
        lock.lock();
        try {
            return _getHighestUnpersisted();
        } finally {
            lock.unlock();
        }
    }

    // Lock should already be held. Gets the highest unpersisted sequence number
    private long _getHighestUnpersisted() {
        if (_endOfUsedList != null) {
            return _endOfUsedList.sequenceNumber;
        }
        return -1;
    }

    /**
     * Sets the message to be stored.
     *
     * @param m The message to be stored.
     */
    public void setMessage(Message m) {
        _message = m;
    }

    /**
     * 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 _errorOnPublishGap;
    }

    /**
     * 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)
    {
        _errorOnPublishGap = errorOnGap;
    }

    // Detaches blocks from the list up to a specified index
    // The detach method is used to remove blocks from a list (_usedList) up to a
    // specified index. It iterates through the list, identifying blocks that should
    // be discarded based on their sequenceNumber compared to the given index. The
    // method then detaches these blocks from the list, updates the list of used
    // blocks, and returns the detached blocks that should be discarded. This
    // process ensures efficient management of blocks in the list for storage
    // purposes.
    private Block detach(long index) {
        // Initialize variables to keep track of different lists of blocks
        Block keepList = null, endOfDiscardList = null, discardList = null;
        // Acquire a lock to ensure thread safety while detaching blocks
        lock.lock();
        try {
            // Advance through the list, find the first one to keep.
            // We keep everything after that point, and discard everything up
            // to that point.
            // Start with the current used list
            keepList = _usedList;
            // Iterate through the list of blocks (usedList)
            while (keepList != null && keepList.sequenceNumber <= index) {
                // If a block should be discarded, add it to the discard list
                if (discardList == null) {
                    discardList = keepList;
                }
                // Update the last block to discard
                endOfDiscardList = keepList;
                // Move to the next block in the list
                keepList = keepList.nextInList;
            }
            // Detach the remainder of the list by breaking the link to the discarded
            // blocks, and make it the new usedList.
            if (endOfDiscardList != null) {
                endOfDiscardList.nextInList = null;
            }
            // Update the used list to contain only the blocks that should be kept
            _usedList = keepList;
            // If no blocks remain in the used list, update the end of the used list
            if (keepList == null) {
                _endOfUsedList = null;
            }
        } finally {
            // Release the lock to allow other threads to access the list
            lock.unlock();
        }
        // Return the detached blocks that should be discarded
        return discardList;
    }

    /**
     * Replays a single message onto a store replayer.
     * 
     * @param b        The first block of the message.
     * @param replayer The replayer to play this message on.
     * @return Success returns true, false if not ready or failure.
     * @throws IOException           If an I/O error occurs.
     * @throws DisconnectedException If a disconnection occurs.
     */
    private void replayOnto(Block b, StoreReplayer replayer)
            throws IOException, DisconnectedException {
        // Lock is already held
        // Clear fields we use
        // If message isn't set by a Client, then it must not matter what type
        if (_message == null)
            _message = new JSONMessage(StandardCharsets.UTF_8.newEncoder(),
                    StandardCharsets.UTF_8.newDecoder());
        _message.reset();
        // Get the position of the block in the buffer
        long bPosition = b.getPosition();
        _buffer.setPosition(bPosition);
        // Get the total length of the message (excluding headers)
        int totalLength = _buffer.getInt() - Block.CHAIN_HEADER;

        // Can occur if replaying during disconnect or for queue ack messages
        if (totalLength <= 0) {
            return; // this can occur when replaying during a disconnect scenario
        }
        // Don't care about the next in chain -- we already read that
        _buffer.getInt();
        _message.setSequence(_buffer.getLong());
        _buffer.getLong(); // Skip CRC until ready
        // Wait until the message is ready for replay (block is marked as ready)
        while (_buffer.getInt(bPosition + Block.BLOCK_READY_POSITION) == 0) {
            // An interrupted exception is ignored to recheck
            try {
                _messageReady.await(1000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }
        }
        // Set the buffer position to read the CRC value
        _buffer.setPosition(bPosition + Block.BLOCK_CRC_POSITION);
        long crcVal = _buffer.getLong();

        // Grab the operation, lengths, flag, and ack type from the block header
        _buffer.setPosition(bPosition + Block.BLOCK_HEADER_SIZE);
        int operation = _buffer.getInt();
        if (operation == Message.Command.Unknown)
            return;
        _message.setCommand(operation);
        int commandIdLength = _buffer.getInt();
        int correlationIdLength = _buffer.getInt();
        int expirationLength = _buffer.getInt();
        int sowKeyLength = _buffer.getInt();
        int topicLength = _buffer.getInt();
        int flag = _buffer.getInt();
        _message.setAckType(_buffer.getInt());
        // Set the buffer position to read message data
        _buffer.setPosition(bPosition + Block.BLOCK_HEADER_SIZE +
                Block.CHAIN_HEADER);
        byte[] array = null;
        int offset = 0;
        if (b.nextInChain == null) {
            // Everything fits into one block. replay directly from the block memory.
            _argument.len = totalLength;
            _buffer.getBytes(_argument);
            array = _argument.array;
            offset = (int) _argument.offset;
        } else {
            // Discontinuous data; we'll do this the hard way.
            // Allocate enough space to hold it contiguously. then, copy the pieces
            // in place, then call the replayer.
            if (_readBuffer == null || _readBuffer.length < totalLength) {
                _readBuffer = new byte[totalLength];
            }
            int position = 0;
            for (Block part = b; part != null; part = part.nextInChain) {
                int maxReadable = Block.BLOCK_DATA_SIZE;
                if (b != part) {
                    _buffer.setPosition(part.getPosition() + Block.BLOCK_HEADER_SIZE);
                } else // read chain and extended metadata
                {
                    maxReadable -= Block.CHAIN_HEADER;
                }

                int readLen = totalLength - position > maxReadable ? maxReadable : totalLength - position;
                _argument.len = readLen;
                try {
                    _buffer.getBytes(_argument);
                } catch (Exception e) {
                    throw new IOException("Corrupted message found.", e);
                }

                // Copy the data to the read buffer
                System.arraycopy(_argument.array, (int) _argument.offset, _readBuffer, position, readLen);
                position += readLen;
            }
            // Ensure that the total length matches the position (data integrity check)
            if (position != totalLength) {
                throw new IOException(String.format("Corrupted message found.  Expected %d bytes, read %d bytes.",
                        totalLength, position));
            }
            array = _readBuffer;
            offset = 0;
        }
        if (_crc != null) {
            _crc.reset();
            try {
                _crc.update(array, offset, totalLength);
            } catch (Exception e) {
                throw new IOException("Corrupted message found.", e);
            }
            if (_crc.getValue() != crcVal) {
                StringBuilder sb = new StringBuilder();
                for (Block part = b; part != null; part = part.nextInChain) {
                    sb.append(part);
                    sb.append('\n');
                }
                String message = String
                        .format("Corrupted message found.  Block index %d, sequence %d, topic length %d, expected CRC %d, actual CRC %d, Block list: %s",
                                b.index, b.sequenceNumber, topicLength,
                                crcVal, _crc.getValue(),
                                sb.toString());
                throw new IOException(message);
            }
        }
        if (commandIdLength > 0) {
            _message.setCommandId(array, offset, commandIdLength);
            totalLength -= commandIdLength;
            offset += commandIdLength;
        }
        if (correlationIdLength > 0) {
            _message.getCorrelationIdRaw().set(array, offset, correlationIdLength);
            totalLength -= correlationIdLength;
            offset += correlationIdLength;
        }
        if (expirationLength > 0) {
            _message.getExpirationRaw().set(array, offset, expirationLength);
            totalLength -= expirationLength;
            offset += expirationLength;
        }
        if (sowKeyLength > 0) {
            _message.setSowKey(array, offset, sowKeyLength);
            totalLength -= sowKeyLength;
            offset += sowKeyLength;
        }
        if (topicLength > 0) {
            _message.setTopic(array, offset, topicLength);
            totalLength -= topicLength;
            offset += topicLength;
        }
        // Set various message fields based on the operation and flag
        if (operation == Message.Command.Publish ||
                operation == Message.Command.DeltaPublish) {
            _message.setData(array, offset, totalLength);
            replayer.execute(_message);
        } else if (operation == Message.Command.SOWDelete) {
            switch (flag) {
                case Store.SOWDeleteByData: {
                    _message.setData(array, offset, totalLength);
                    replayer.execute(_message);
                }
                    break;
                case Store.SOWDeleteByFilter: {
                    _message.setFilter(array, offset, totalLength);
                    replayer.execute(_message);
                }
                    break;
                case Store.SOWDeleteByKeys: {
                    _message.setSowKeys(array, offset, totalLength);
                    replayer.execute(_message);
                }
                    break;
                case Store.SOWDeleteByBookmark: {
                    _message.setBookmark(array, offset, totalLength);
                    replayer.execute(_message);
                }
                    break;
                case Store.SOWDeleteByBookmarkCancel: {
                    _message.setBookmark(array, offset, totalLength).setOptions("cancel");
                    replayer.execute(_message);
                }
                    break;
                default: {
                    String message = String.format(
                            "SOWDelete message with invalid flag found.  Block index %d, sequence %d, topic length %d, expected data length %d, operation %d, flag %d",
                            b.index, b.sequenceNumber, topicLength,
                            totalLength, operation, flag);
                    throw new IOException(message);
                }
                // break;
            }
        } else {
            String message = String.format(
                    "Message with invalid operation found.  Block index %d, sequence %d, topic length %d, expected data length %d, operation %d, flag %d",
                    b.index, b.sequenceNumber, topicLength,
                    totalLength, operation, flag);
            throw new IOException(message);
        }
    }

    /**
     * Replays stored messages onto a store replayer. It processes each block in the
     * list of used blocks, replaying them onto the provided replayer.
     *
     * @param replayer The replayer to play messages on.
     * @throws StoreException        If an exception occurs during replay, the
     *                               method will throw a StoreException, and all
     *                               following blocks will be ignored.
     * @throws DisconnectedException If a disconnection occurs during replay.
     */
    public void replay(StoreReplayer replayer) throws StoreException, DisconnectedException {
        lock.lock();
        try {
            // If there are no used blocks, there's nothing to replay
            if (_usedList == null)
                return;
            boolean corrupted = false;
            StoreException ex = null;
            Block lastGood = null;
            Block nextInList = null;
            // for each used block
            for (Block b = _usedList; b != null; b = nextInList) {
                nextInList = b.nextInList;
                if (!corrupted) {
                    try {
                        if (_buffer.getInt(b.getPosition() +
                                Block.BLOCK_READY_POSITION) == 0) {
                            // Replay needs to stop here
                            break;
                        }
                        replayOnto(b, replayer);
                        lastGood = b;
                    } catch (IOException ioex) {
                        corrupted = true;
                        _endOfUsedList = lastGood == null ? _usedList : lastGood;
                        ex = new StoreException("Exception during replay, ignoring all following blocks", ioex);
                    }
                }
                // Can't just do an else here, need to deal with first corrupted block
                if (corrupted) // We're corrupt, let's clean up bad blocks
                {
                    Block currentInChain = b;
                    while (currentInChain != null) {
                        Block nextInChain = currentInChain.nextInChain;
                        try {
                            _buffer.zero(currentInChain.getPosition(),
                                    Block.BLOCK_HEADER_SIZE);
                        } catch (IOException e) {
                        } // ignore
                        currentInChain.sequenceNumber = 0;
                        currentInChain.nextInChain = null;
                        // and, flatten this chain onto the _freeList
                        currentInChain.nextInList = _freeList;
                        _freeList = currentInChain;
                        currentInChain = nextInChain;
                    }
                }
            }
            if (corrupted)
            {
                throw ex;
            }
        }
        finally {
            // If there are no used blocks, there's nothing to replay
            lock.unlock();
        }
    }

    /**
     * Replays a single message from the store, given a specific sequence index,
     * onto a store replayer.
     * If the specified index is less than or equal to the current sequence number,
     * the method will create a message with the correct sequence number and return 
     * true. Otherwise, it searches for the block corresponding to the provided index 
     * in the list of used blocks, replays the message, and returns true if successful.
     *
     * @param replayer The replayer to play the message on.
     * @param index    The sequence index of the message to replay.
     * @return True if the message was successfully replayed or created; false
     *         otherwise.
     * @throws StoreException        If an exception occurs during replay, the
     *                               method will throw a StoreException.
     * @throws DisconnectedException If a disconnection occurs during replay.
     */
    public boolean replaySingle(StoreReplayer replayer, long index)
            throws StoreException, DisconnectedException {
        lock.lock();
        try {
            // If the specified index is less than or equal to the current sequence number,
            // create a message with the correct sequence number and return true.
            if (index <= _metadataBlock.sequenceNumber) {
                return true;
            }

            // Find the first one that equals this index.
            for(Block b = this._usedList; b != null; b = b.nextInList)
            {
                // Continue to check sequenceNumber which can get reset due to
                // an ack or if store() failed for some reason.
                while (b.sequenceNumber == index &&
                    (_buffer.getInt(b.getPosition() + Block.BLOCK_READY_POSITION) == 0))
                {
                    // An interrupted exception is ignored to recheck
                    try {
                        _messageReady.await(1000, TimeUnit.MILLISECONDS);
                    }
                    catch (InterruptedException e) { }
                }
                if (b.sequenceNumber == index)
                {
                    _buffer.setPosition(b.getPosition());
                    if (_buffer.getInt() <= 0)
                        return false;
                    // Replay the message onto the replayer
                    replayOnto(b, replayer);
                    return true;
                }
                else if (b.sequenceNumber > index) // too far
                {
                    break;
                }
            }
            return false;
        } catch (IOException ioex) {
            throw new StoreException(ioex);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Returns the count of unpersisted messages in the store. This count represents
     * the number of messages that have been received but not yet persisted.
     *
     * @return The count of unpersisted messages.
     */
    public long unpersistedCount() {
        lock.lock();
        try {
            if (_usedList == null)
                return 0;
            // Calculate the count of unpersisted messages based on the difference between
            // the sequence numbers of the last and first used blocks, inclusive.
            return _endOfUsedList.sequenceNumber - _usedList.sequenceNumber + 1;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Finds or creates a Block object with the specified index from a provided map
     * of Blocks. If a Block with the given index already exists in the map, it is
     * returned. Otherwise, a new Block is created, added to the map, and returned.
     *
     * @param index     The index of the Block to find or create.
     * @param allBlocks A map containing Block objects where the index is used as
     *                  the key.
     * @return The Block object with the specified index.
     */
    Block findOrCreate(int index, Map allBlocks) {
        if (allBlocks.containsKey(index)) {
            return allBlocks.get(index);
        } else {
            Block b = new Block((int) index);
            allBlocks.put(index, b);
            return b;
        }
    }

    /**
     * Recovers the store's state during initialization. This method parses the
     * store's buffer, reconstructs the used and free lists of blocks, and updates
     * the metadata block accordingly.
     *
     * @throws StoreException If an error occurs during store recovery.
     */
    protected void recover() throws StoreException {
        lock.lock();
        try {
            _usedList = _freeList = null;
            // Start here and work the buffer.
            // Create data structures to hold block information during recovery.
            HashMap allBlocks = new HashMap(); // // A map to store block objects by
                                                                               // file index
            TreeMap heads = new TreeMap(); // A map to store block objects by sequence number
            try {
                long end = _buffer.getSize();
                if (end == 0)
                    // If the buffer is empty, return as there's nothing to recover
                    return;
                long minSeq = 0;
                long maxSeq = 0;
                // Iterate through the store's buffer to recover block informa
                for (long offset = 0; offset < end; offset += Block.SIZE) {
                    // Calculate the block index based on the offset
                    int index = (int) (offset / Block.SIZE);
                    // Set the buffer's read position to the current offset
                    _buffer.setPosition(offset);
                    // Read the block size
                    int size = _buffer.getInt();
                    int nextIndex = _buffer.getInt();
                    long sequence = _buffer.getLong();
                    _buffer.getLong(); // ignore crcVal
                    int ready = _buffer.getInt();

                    // All empty blocks are free.
                    // The first block will be the metadata block
                    if (index == 0) {
                        // Create a new metadata block
                        _metadataBlock = new Block(index);
                        _metadataBlock.nextInChain = null;
                        _metadataBlock.nextInList = null;
                        _metadataBlock.sequenceNumber = sequence;
                        // Update the next sequence number
                        _nextSequence = sequence + 1;
                        // Update the minimum sequence number
                        minSeq = _nextSequence;
                        continue;
                    }
                    // Find or create a block based on the index
                    Block b = findOrCreate(index, allBlocks);

                    b.nextInChain = ((size == 0 || nextIndex == index) ? null : findOrCreate(nextIndex, allBlocks));

                    // All empty blocks are free. That's the rule.
                    if (size == 0) {
                        // Add the block to the free list
                        b.nextInList = _freeList;
                        _freeList = b;
                    } else if (sequence != 0) { // Only head blocks have their sequence number set.
                        if (ready == 0) // Block wasn't completed, add to free list
                        {
                            Block currentInChain = b;
                            while (currentInChain != null) {
                                Block nextInChain = currentInChain.nextInChain;
                                _buffer.zero(currentInChain.getPosition(),
                                        Block.BLOCK_HEADER_SIZE);
                                currentInChain.sequenceNumber = 0;
                                currentInChain.nextInChain = null;
                                // and, flatten this chain onto the free list
                                currentInChain.nextInList = _freeList;
                                _freeList = currentInChain;
                                currentInChain = nextInChain;
                            }
                        } else {
                            b.sequenceNumber = sequence;
                            if (sequence < minSeq)
                                minSeq = sequence;
                            if (sequence > maxSeq)
                                maxSeq = sequence;
                            heads.put(sequence, b);
                        }
                    }
                    // else, Belongs to part of a "chain"
                }
                // Update metadata block with the correct sequence number.
                if (_metadataBlock.sequenceNumber < (minSeq - 1))
                    _metadataBlock.sequenceNumber = minSeq - 1;
                if (_nextSequence <= maxSeq)
                    _nextSequence = maxSeq + 1;
                // Add head blocks to the used list in ascending order of sequence numbers.
                for (Entry e : heads.entrySet()) {
                    Block b = e.getValue();
                    b.nextInList = null;
                    if (_endOfUsedList != null) {
                        _endOfUsedList.nextInList = b;
                        _endOfUsedList = b;
                    } else {
                        _usedList = _endOfUsedList = b;
                    }
                }
            } catch (IOException ioex) {
                throw new StoreException(ioex);
            }
        } finally {
            // Release the lock
            lock.unlock();
        }
    }

    /**
     * Ensures that the free list is not empty by potentially growing it. This
     * method acquires the lock, checks if the free list is empty, and if so,
     * attempts to grow the free list by adding more blocks to it.
     *
     * @throws StoreException If an error occurs during the process of growing the
     *                        free list.
     */
    protected void growFreeListIfEmpty() throws StoreException {
        lock.lock();
        try {
            _growFreeListIfEmpty();
        } finally {
            lock.unlock();
        }
    }

    /**
     * The internal method that actually grows the free list if it's empty. This
     * method is called while the lock is already acquired. It checks if the free
     * list is empty, and if so, it attempts to add more blocks to it, ensuring that
     * the free list is not empty.
     *
     * @throws StoreException If an error occurs during the process of growing the
     *                        free list.
     */
    private void _growFreeListIfEmpty() throws StoreException {
        // Lock must already be acquired.
        // Wait while `_resizing` is true.
        while (_resizing) {
            // An interrupted exception is ignored to recheck the time
            try {
                _blocksFree.await(1000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException ex) {
            }
        }
        // If `_freeList` is not empty, return early
        if (_freeList != null)
            return;
        try {
            // Get the current size of the buffer
            long oldSize = _buffer.getSize();
            // Calculate the new size of the buffer by adding more blocks
            long newSize = oldSize + ((long) Block.SIZE * (long) _blocksPerRealloc);
            // Set `_resizing` to true to indicate resizing is in progress
            _resizing = true;
            try {
                // Wait until the buffer's size is updated to the new size
                while (_buffer.getSize() == oldSize) {
                    boolean changeSize = true;
                    // Check if there's a `_resizeHandler`, and if so, call it to potentially change
                    // the size
                    if (_resizeHandler != null) {
                        // Unlock the lock temporarily to invoke the resize handler
                        lock.unlock();
                        try {
                            changeSize = _resizeHandler.invoke(this, newSize);
                        } finally {
                            // Reacquire the lock after calling the handler
                            lock.lock();
                        }
                    }
                    // During unlock time, something may have freed some space.
                    if (_freeList != null)
                        return;
                    // Unless resize handler denied us, increase size
                    if (changeSize) {
                        if (newSize > Integer.MAX_VALUE) {
                            // If this message is changed, be sure to update testPubStoreMaxException
                            throw new StoreException("Maximum size for publish Store exceeded. Cannot grow beyond Integer.MAX_VALUE");
                        }
                        _buffer.setSize(newSize);
                    }
                }
            } finally {
                // Set `_resizing` back to false once resizing is done
                _resizing = false;
            }
            // If the buffer's size is still less than the new size, throw an exception
            if (_buffer.getSize() < newSize)
                throw new StoreException("Publish store could not resize, possibly due to resize handler.");
            // Initialize the index (`idx`) to the last block's index in the old buffer size
            int idx = (int) (oldSize / Block.SIZE);
            // If `idx` is 0, create and initialize the metadata block
            if (idx == 0) {
                _metadataBlock = new Block(idx);
                _metadataBlock.nextInChain = null;
                _metadataBlock.nextInList = null;
                _metadataBlock.sequenceNumber = 0;
                long metadataPosition = _metadataBlock.getPosition();
                _buffer.zero(metadataPosition, Block.SIZE);
                _buffer.setPosition(metadataPosition +
                        METADATA_VERSION_LOCATION);
                // Default to lowest version where first block is last discarded
                int version;
                try {
                    version = Client.getVersionAsInt(Client.getVersion());
                } catch (CommandException ex) {
                    // An exception will occur for develop branch
                    version = AMPS_MIN_PUB_STORE_DISCARDED_VERSION;
                }
                _buffer.putInt(version);
                _buffer.setPosition(metadataPosition +
                        METADATA_LAST_DISCARDED_LOCATION);
                _buffer.putLong(0);
                ++idx;
            }
            // Create and initialize new blocks and add them to the free list
            while (idx < (int) (newSize / Block.SIZE)) {
                Block b = new Block(idx);
                _buffer.zero(b.getPosition(), Block.SIZE);
                b.nextInChain = null;
                b.nextInList = _freeList;
                _freeList = b;
                ++idx;
            }
            // Signal all waiting threads that new blocks are available
            _blocksFree.signalAll();
        } catch (IOException e) {
            // Catch any IOException and wrap it in a StoreException before rethrowing
            throw new StoreException(e);
        }
    }

    /**
     * Flushes the publish store.
     * This method blocks until all messages have been successfully flushed or until
     * a timeout occurs.
     *
     * @throws TimedOutException If the flush operation times out before all
     *                           messages are flushed.
     */
    public void flush() throws TimedOutException {
        // Acquire a lock to enter a critical section
        lock.lock();
        try {
            if (_usedList == null)
                // If the usedList is empty (no messages), return immediately
                return;
            // Get the sequence number of the last used message
            long current = _endOfUsedList.sequenceNumber;
            // Continue to wait while there are messages in the usedList and the current
            // message's sequence number is greater than or equal to the last used message's
            // sequence number
            while (_usedList != null && current >= _usedList.sequenceNumber)
            {
                try {
                    // Wait for a signal (notification) that indicates the store has been flushed
                    _blocksFree.await();
                    // Handle an InterruptedException if it occurs (ignored in this code)
                } catch (InterruptedException e) { }
            }
        } 
        finally {
            // Release the lock when done with the critical section
            lock.unlock(); }
    }

    /**
     * Flushes the publish store, waiting until all messages in the store are
     * flushed or until a timeout occurs. This method allows specifying a timeout
     * for the flush operation.
     *
     * @param timeout The maximum time to wait for the flush operation, in
     *                milliseconds.
     * @throws TimedOutException If the flush operation times out before all
     *                           messages are flushed.
     */
    public void flush(long timeout) throws TimedOutException {
        lock.lock();
        try {
            // Check if the _usedList is null (no items to flush), and if so, return
            // immediately
            if (_usedList == null)
                return;
            // Get the sequence number of the last item in the _usedList
            long current = _endOfUsedList.sequenceNumber;
            // Record the current time in milliseconds
            long start = System.currentTimeMillis();
            long end;
            // Continue looping until either the _usedList is empty or the current time
            // exceeds the timeout
            while (_usedList != null && current >= _usedList.sequenceNumber) {
                // Get the current time again
                end = System.currentTimeMillis();
                // If a positive timeout is specified and the elapsed time exceeds the timeout,
                // throw a TimedOutException
                if (timeout > 0 && (end - start) >= timeout)
                    throw new TimedOutException("Timed out waiting to flush publish store.");
                try {
                    // Calculate the remaining time until the timeout and await the notification of
                    // a change in _blocksFree
                    long remaining = timeout - (end - start);
                    // An interrupted exception is ignored to recheck
                    _blocksFree.await(remaining, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                }
            }
        } finally {
            // Release the lock to allow other threads to access the critical section
            lock.unlock();
        }
    }

    /**
     * Sets a resize handler for the publish store. The resize handler is called
     * when the store needs to be resized.
     *
     * @param handler The resize handler to set.
     */
    public void setResizeHandler(PublishStoreResizeHandler handler) {
        _resizeHandler = handler;
    }

    /********* PRIVATE METHODS *************/

    /**
     * Gets a block from the free list, optionally assigning a sequence number to
     * it. If a sequence number is not assigned, the provided sequence value is
     * used.
     *
     * @param assignSequence Indicates whether to assign a new sequence number to
     *                       the block.
     * @param sequence       The sequence number to assign to the block if not
     *                       assigning a new one.
     * @return The retrieved block from the free list.
     * @throws StoreException If an error occurs while getting the block.
     */
    private Block get(boolean assignSequence, long sequence)
            throws StoreException {
        // Declare a Block variable and initialize it to null
        Block b = null;
        lock.lock();
        try {
            // Ensure the free list is populated with blocks
            _growFreeListIfEmpty();
            // Assign the first block from the free list to variable 'b'
            b = _freeList;
            // Check if a block was successfully obtained from the free list
            if (b != null) {
                // Update the free list to exclude the retrieved block
                _freeList = b.nextInList;
                // Remove any reference to the next block in the retrieved block
                b.nextInList = null;
                // Check if a new sequence number should be assigned.
                // Make sure we start at a good value
                if (assignSequence) {
                    // Make sure we start at a good value
                    if (_nextSequence <= 1)
                        // If the sequence is not initialized, retrieve it.
                        _getLastPersisted();
                    // Assign a new sequence number to the block
                    b.sequenceNumber = _nextSequence++;
                    // Check if a specific sequence number was provided
                } else if (sequence != 0) {
                    // Assign the provided sequence number to the block
                    b.sequenceNumber = sequence;
                }
                if (assignSequence || sequence != 0) {
                    if (_endOfUsedList != null) {
                        // Attach the block to the end of the used list
                        _endOfUsedList.nextInList = b;
                        // Update the end of the used list to the new block
                        _endOfUsedList = b;
                    } else {
                        // _endOfUsedList is null if _usedList is null, as well.
                        // If the used list is empty, initialize it with the retrieved block
                        _endOfUsedList = _usedList = b;
                    }
                }
            }
        } finally {
            // Release the lock to allow other threads to access the method
            lock.unlock();
        }
        // Return the retrieved block (or null if no block was available)
        return b;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy