diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e69de29..90db424 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + + - name: Run tests + run: mvn -B test + + - name: Build with Maven + run: mvn -B package --file pom.xml \ No newline at end of file diff --git a/src/main/java/io/github/elimelt/pmqueue/Message.java b/src/main/java/io/github/elimelt/pmqueue/Message.java index 43df085..bfdacbd 100644 --- a/src/main/java/io/github/elimelt/pmqueue/Message.java +++ b/src/main/java/io/github/elimelt/pmqueue/Message.java @@ -5,107 +5,168 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.ref.SoftReference; -import java.util.Arrays; import sun.misc.Unsafe; import java.lang.reflect.Field; +/** + * A high-performance, immutable message container optimized for memory + * efficiency and fast access. + * This class uses direct memory operations via {@link sun.misc.Unsafe} for + * improved performance + * and implements custom serialization for better control over the serialization + * process. + * + *
+ * The message contains: + *
+ * This class implements optimizations including: + *
+ * The serialization format consists of: + *
+ * Performance optimizations include: + *
+ * Note: This class is not intended for external use and
+ * should only be used by the {@link Message} class's serialization mechanism.
+ */
@SuppressWarnings("deprecation")
class MessageSerializer {
- private static final int HEADER_SIZE = 16; // 8 bytes timestamp + 4 bytes type + 4 bytes length
+ private static final int HEADER_SIZE = 16;
- // Thread-local ByteBuffer for reuse
- private static final ThreadLocal
+ * This method uses thread-local direct {@link ByteBuffer}s to optimize
+ * performance and minimize garbage collection pressure. The buffer size
+ * automatically grows if needed.
+ *
+ * @param message the Message object to serialize
+ * @return a byte array containing the serialized message
+ * @throws IOException if the message is null or cannot be serialized
+ * @throws OutOfMemoryError if unable to allocate required buffer space
+ */
+ public static byte[] serialize(Message message) throws IOException {
+ if (message == null) {
+ throw new IOException("Message is null");
+ }
+
+ byte[] data = message.getData();
+ int totalLength = HEADER_SIZE + data.length;
- Field addressField = Buffer.class.getDeclaredField("address");
- addressOffset = unsafe.objectFieldOffset(addressField);
- } catch (Exception e) {
- throw new Error(e);
- }
+ ByteBuffer buffer = threadLocalBuffer.get();
+ if (buffer.capacity() < totalLength) {
+ buffer = ByteBuffer.allocateDirect(Math.max(totalLength, buffer.capacity() * 2));
+ threadLocalBuffer.set(buffer);
}
- public static byte[] serialize(Message message) throws IOException {
- if (message == null) {
- throw new IOException("Message is null");
- }
-
- byte[] data = message.getData();
- int totalLength = HEADER_SIZE + data.length;
-
- // Get thread-local buffer or allocate new if needed
- ByteBuffer buffer = threadLocalBuffer.get();
- if (buffer.capacity() < totalLength) {
- buffer = ByteBuffer.allocateDirect(Math.max(totalLength, buffer.capacity() * 2));
- threadLocalBuffer.set(buffer);
- }
-
- buffer.clear();
- long bufferAddress = unsafe.getLong(buffer, addressOffset);
-
- // Direct memory writes
- unsafe.putLong(bufferAddress, message.getTimestamp());
- unsafe.putInt(bufferAddress + 8, message.getMessageType());
- unsafe.putInt(bufferAddress + 12, data.length);
- unsafe.copyMemory(data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
- null, bufferAddress + HEADER_SIZE,
- data.length);
-
- // Create result array and copy data
- byte[] result = new byte[totalLength];
- unsafe.copyMemory(null, bufferAddress,
- result, Unsafe.ARRAY_BYTE_BASE_OFFSET,
- totalLength);
-
- return result;
+ buffer.clear();
+ long bufferAddress = unsafe.getLong(buffer, addressOffset);
+
+ unsafe.putLong(bufferAddress, message.getTimestamp());
+ unsafe.putInt(bufferAddress + 8, message.getMessageType());
+ unsafe.putInt(bufferAddress + 12, data.length);
+ unsafe.copyMemory(data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
+ null, bufferAddress + HEADER_SIZE,
+ data.length);
+
+ byte[] result = new byte[totalLength];
+ unsafe.copyMemory(null, bufferAddress,
+ result, Unsafe.ARRAY_BYTE_BASE_OFFSET,
+ totalLength);
+
+ return result;
+ }
+
+ /**
+ * Deserializes a byte array into a {@link Message} object.
+ * The byte array must contain data in the format produced by
+ * {@link #serialize}.
+ *
+ *
+ * This method creates a new Message object with the original timestamp
+ * preserved through anonymous subclassing. The message type and data are
+ * extracted from the serialized format using direct memory operations for
+ * optimal performance.
+ *
+ * @param bytes the byte array containing the serialized message
+ * @return a new Message object with the deserialized data
+ * @throws IOException if the byte array is too short, contains invalid length
+ * information, or is otherwise malformed
+ */
+ public static Message deserialize(byte[] bytes) throws IOException {
+ if (bytes.length < HEADER_SIZE) {
+ throw new IOException("Invalid message: too short");
}
- public static Message deserialize(byte[] bytes) throws IOException {
- if (bytes.length < HEADER_SIZE) {
- throw new IOException("Invalid message: too short");
- }
-
- // Direct memory reads using Unsafe
- long timestamp = unsafe.getLong(bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET);
- int type = unsafe.getInt(bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET + 8);
- int length = unsafe.getInt(bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET + 12);
-
- if (length < 0 || length > bytes.length - HEADER_SIZE) {
- throw new IOException("Invalid message length");
- }
-
- // Create data array and copy directly
- byte[] data = new byte[length];
- unsafe.copyMemory(bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET + HEADER_SIZE,
- data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
- length);
-
- return new Message(data, type) {
- @Override
- public long getTimestamp() {
- return timestamp;
- }
- };
+ long timestamp = unsafe.getLong(bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET);
+ int type = unsafe.getInt(bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET + 8);
+ int length = unsafe.getInt(bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET + 12);
+
+ if (length < 0 || length > bytes.length - HEADER_SIZE) {
+ throw new IOException("Invalid message length");
}
+
+ byte[] data = new byte[length];
+ unsafe.copyMemory(bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET + HEADER_SIZE,
+ data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
+ length);
+
+ return new Message(data, type) {
+ @Override
+ public long getTimestamp() {
+ return timestamp;
+ }
+ };
+ }
}
\ No newline at end of file
diff --git a/src/main/java/io/github/elimelt/pmqueue/PersistentMessageQueue.java b/src/main/java/io/github/elimelt/pmqueue/PersistentMessageQueue.java
index adeda55..646b380 100644
--- a/src/main/java/io/github/elimelt/pmqueue/PersistentMessageQueue.java
+++ b/src/main/java/io/github/elimelt/pmqueue/PersistentMessageQueue.java
@@ -1,4 +1,5 @@
package io.github.elimelt.pmqueue;
+
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
@@ -8,259 +9,396 @@
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.CRC32;
+/**
+ * A high-performance, persistent queue implementation for storing messages on
+ * disk.
+ * This queue provides durability guarantees while maintaining efficient read
+ * and write
+ * operations through various optimizations including write batching and direct
+ * I/O.
+ *
+ *
+ * The queue stores messages in a single file with the following structure:
+ *
+ * Key features:
+ *
+ * Example usage:
+ * {@code
+ * // Create a new queue
+ * try (PersistentMessageQueue queue = new PersistentMessageQueue("messages.queue")) {
+ * // Create and offer a message
+ * Message msg1 = new Message("Hello".getBytes(), 1);
+ * queue.offer(msg1);
+ *
+ * // Poll a message from the queue
+ * Message received = queue.poll();
+ * if (received != null) {
+ * System.out.println(new String(received.getData()));
+ * }
+ * }
+ * }
+ *
+ *
+ * Performance considerations:
+ *
+ * If the file doesn't exist, it will be created with initial metadata.
+ * If it exists, the queue metadata will be loaded and validated.
+ *
+ * @param filename the path to the queue file
+ * @throws IOException if the file cannot be created/opened or if the
+ * existing file is corrupted
+ * @throws SecurityException if the application doesn't have required file
+ * permissions
+ */
+ public PersistentMessageQueue(String filename) throws IOException {
+ File f = new File(filename);
+ boolean isNew = !f.exists();
+ this.file = new RandomAccessFile(f, "rw");
+ this.channel = file.getChannel();
+
+ this.messageBuffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE);
+ this.writeBatchBuffer = ByteBuffer.allocateDirect(MAX_BUFFER_SIZE);
+ this.lock = new ReentrantLock(true);
+ this.checksumCalculator = new CRC32();
+
+ if (isNew) {
+ initializeNewFile();
+ } else {
+ loadMetadata();
}
-
- public boolean offer(Message message) throws IOException {
- if (message == null)
- throw new NullPointerException("Message cannot be null");
-
- byte[] serialized = MessageSerializer.serialize(message);
- int totalSize = BLOCK_HEADER_SIZE + serialized.length;
-
- lock.lock();
- try {
- if (rearOffset + totalSize > MAX_FILE_SIZE)
- return false;
-
- // Ensure file has enough space
- long requiredLength = rearOffset + totalSize;
- if (requiredLength > file.length()) {
- long newSize = Math.min(MAX_FILE_SIZE,
- Math.max(file.length() * 2, requiredLength + DEFAULT_BUFFER_SIZE));
- file.setLength(newSize);
- }
-
- checksumCalculator.reset();
- checksumCalculator.update(serialized);
- int checksum = (int) checksumCalculator.getValue();
-
- // Start new batch if needed
- if (batchSize == 0) {
- batchStartOffset = rearOffset;
- }
-
- // Add to batch if message is small enough
- if (serialized.length < DEFAULT_BUFFER_SIZE / 4 &&
- totalSize <= MAX_BUFFER_SIZE - writeBatchBuffer.position() &&
- batchSize < BATCH_THRESHOLD) {
-
- writeBatchBuffer.putInt(serialized.length);
- writeBatchBuffer.putInt(checksum);
- writeBatchBuffer.put(serialized);
- batchSize++;
-
- rearOffset += totalSize;
-
- // Flush if batch is full
- if (batchSize >= BATCH_THRESHOLD ||
- writeBatchBuffer.position() >= writeBatchBuffer.capacity() / 2) {
- flushBatch();
- }
- } else {
- // Flush any existing batch first
- if (batchSize > 0) {
- flushBatch();
- }
-
- // Handle large message directly
- if (serialized.length + BLOCK_HEADER_SIZE > messageBuffer.capacity()) {
- messageBuffer = ByteBuffer.allocateDirect(
- Math.min(MAX_BUFFER_SIZE, serialized.length + BLOCK_HEADER_SIZE));
- }
-
- messageBuffer.clear();
- messageBuffer.putInt(serialized.length);
- messageBuffer.putInt(checksum);
- messageBuffer.put(serialized);
- messageBuffer.flip();
-
- channel.write(messageBuffer, rearOffset);
- rearOffset += totalSize;
- saveMetadata();
- }
-
- return true;
- } finally {
- lock.unlock();
+ }
+
+ /**
+ * Offers a message to the queue.
+ *
+ *
+ * Messages smaller than 256KB are batched together for better performance.
+ * Larger messages are written directly to disk. If the queue file would exceed
+ * its maximum size (1GB), the message is rejected.
+ *
+ *
+ * Example:
+ * {@code
+ * Message msg = new Message("Important data".getBytes(), 1);
+ * boolean success = queue.offer(msg);
+ * if (!success) {
+ * System.err.println("Queue is full");
+ * }
+ * }
+ *
+ * @param message the message to add to the queue
+ * @return true if the message was added, false if the queue is full
+ * @throws IOException if an I/O error occurs
+ * @throws NullPointerException if message is null
+ */
+ public boolean offer(Message message) throws IOException {
+ if (message == null)
+ throw new NullPointerException("Message cannot be null");
+
+ byte[] serialized = MessageSerializer.serialize(message);
+ int totalSize = BLOCK_HEADER_SIZE + serialized.length;
+
+ lock.lock();
+ try {
+ if (rearOffset + totalSize > MAX_FILE_SIZE)
+ return false;
+
+ long requiredLength = rearOffset + totalSize;
+ if (requiredLength > file.length()) {
+ long newSize = Math.min(MAX_FILE_SIZE,
+ Math.max(file.length() * 2, requiredLength + DEFAULT_BUFFER_SIZE));
+ file.setLength(newSize);
+ }
+
+ checksumCalculator.reset();
+ checksumCalculator.update(serialized);
+ int checksum = (int) checksumCalculator.getValue();
+
+ if (batchSize == 0) {
+ batchStartOffset = rearOffset;
+ }
+
+ if (serialized.length < DEFAULT_BUFFER_SIZE / 4 &&
+ totalSize <= MAX_BUFFER_SIZE - writeBatchBuffer.position() &&
+ batchSize < BATCH_THRESHOLD) {
+
+ writeBatchBuffer.putInt(serialized.length);
+ writeBatchBuffer.putInt(checksum);
+ writeBatchBuffer.put(serialized);
+ batchSize++;
+
+ rearOffset += totalSize;
+
+ if (batchSize >= BATCH_THRESHOLD ||
+ writeBatchBuffer.position() >= writeBatchBuffer.capacity() / 2) {
+ flushBatch();
}
- }
-
- private void flushBatch() throws IOException {
+ } else {
if (batchSize > 0) {
- writeBatchBuffer.flip();
- channel.write(writeBatchBuffer, batchStartOffset);
- writeBatchBuffer.clear();
- batchSize = 0;
- saveMetadata();
+ flushBatch();
}
- }
- public Message poll() throws IOException {
- lock.lock();
- try {
- if (isEmpty())
- return null;
-
- // Flush any pending writes before reading
- if (batchSize > 0) {
- flushBatch();
- }
-
- // Read message header
- ByteBuffer headerBuffer = ByteBuffer.allocate(BLOCK_HEADER_SIZE);
- int bytesRead = channel.read(headerBuffer, frontOffset);
- if (bytesRead != BLOCK_HEADER_SIZE) {
- throw new IOException("Failed to read message header");
- }
- headerBuffer.flip();
-
- int messageSize = headerBuffer.getInt();
- int storedChecksum = headerBuffer.getInt();
-
- if (messageSize <= 0 || frontOffset + BLOCK_HEADER_SIZE + messageSize > file.length()) {
- throw new IOException(String.format(
- "Corrupted queue: invalid block size %d at offset %d (file length: %d)",
- messageSize, frontOffset, file.length()));
- }
-
- // Read message data
- ByteBuffer dataBuffer = ByteBuffer.allocate(messageSize);
- bytesRead = channel.read(dataBuffer, frontOffset + BLOCK_HEADER_SIZE);
- if (bytesRead != messageSize) {
- throw new IOException("Failed to read message data");
- }
- dataBuffer.flip();
-
- byte[] data = new byte[messageSize];
- dataBuffer.get(data);
-
- checksumCalculator.reset();
- checksumCalculator.update(data);
- int calculatedChecksum = (int) checksumCalculator.getValue();
-
- if (storedChecksum != calculatedChecksum) {
- throw new IOException(String.format(
- "Corrupted message: checksum mismatch at offset %d. Expected: %d, Got: %d",
- frontOffset, storedChecksum, calculatedChecksum));
- }
-
- Message message = MessageSerializer.deserialize(data);
- frontOffset += BLOCK_HEADER_SIZE + messageSize;
- saveMetadata();
-
- return message;
- } finally {
- lock.unlock();
+ if (serialized.length + BLOCK_HEADER_SIZE > messageBuffer.capacity()) {
+ messageBuffer = ByteBuffer.allocateDirect(
+ Math.min(MAX_BUFFER_SIZE, serialized.length + BLOCK_HEADER_SIZE));
}
- }
- private void loadMetadata() throws IOException {
- if (file.length() < QUEUE_HEADER_SIZE) {
- throw new IOException("File too small to contain valid header");
- }
+ messageBuffer.clear();
+ messageBuffer.putInt(serialized.length);
+ messageBuffer.putInt(checksum);
+ messageBuffer.put(serialized);
+ messageBuffer.flip();
- ByteBuffer buffer = ByteBuffer.allocate(QUEUE_HEADER_SIZE);
- int bytesRead = channel.read(buffer, 0);
- if (bytesRead != QUEUE_HEADER_SIZE) {
- throw new IOException("Failed to read queue metadata");
- }
- buffer.flip();
-
- frontOffset = buffer.getLong();
- rearOffset = buffer.getLong();
+ channel.write(messageBuffer, rearOffset);
+ rearOffset += totalSize;
+ saveMetadata();
+ }
- if (frontOffset < QUEUE_HEADER_SIZE || rearOffset < QUEUE_HEADER_SIZE ||
- frontOffset > file.length() || rearOffset > file.length() ||
- frontOffset > rearOffset) {
- throw new IOException("Corrupted queue metadata");
- }
+ return true;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Retrieves and removes the head of the queue, or returns null if the queue is
+ * empty.
+ *
+ *
+ * This operation ensures data integrity by validating the CRC32 checksum of the
+ * message before returning it.
+ *
+ *
+ * Example:
+ * {@code
+ * while (true) {
+ * Message msg = queue.poll();
+ * if (msg == null) {
+ * // Queue is empty
+ * break;
+ * }
+ * processMessage(msg);
+ * }
+ * }
+ *
+ * @return the head message of the queue, or null if the queue is empty
+ * @throws IOException if an I/O error occurs or if the message data is
+ * corrupted
+ */
+ public Message poll() throws IOException {
+ lock.lock();
+ try {
+ if (isEmpty())
+ return null;
+
+ if (batchSize > 0) {
+ flushBatch();
+ }
+
+ ByteBuffer headerBuffer = ByteBuffer.allocate(BLOCK_HEADER_SIZE);
+ int bytesRead = channel.read(headerBuffer, frontOffset);
+ if (bytesRead != BLOCK_HEADER_SIZE) {
+ throw new IOException("Failed to read message header");
+ }
+ headerBuffer.flip();
+
+ int messageSize = headerBuffer.getInt();
+ int storedChecksum = headerBuffer.getInt();
+
+ if (messageSize <= 0 || frontOffset + BLOCK_HEADER_SIZE + messageSize > file.length()) {
+ throw new IOException(String.format(
+ "Corrupted queue: invalid block size %d at offset %d (file length: %d)",
+ messageSize, frontOffset, file.length()));
+ }
+
+ ByteBuffer dataBuffer = ByteBuffer.allocate(messageSize);
+ bytesRead = channel.read(dataBuffer, frontOffset + BLOCK_HEADER_SIZE);
+ if (bytesRead != messageSize) {
+ throw new IOException("Failed to read message data");
+ }
+ dataBuffer.flip();
+
+ byte[] data = new byte[messageSize];
+ dataBuffer.get(data);
+
+ checksumCalculator.reset();
+ checksumCalculator.update(data);
+ int calculatedChecksum = (int) checksumCalculator.getValue();
+
+ if (storedChecksum != calculatedChecksum) {
+ throw new IOException(String.format(
+ "Corrupted message: checksum mismatch at offset %d. Expected: %d, Got: %d",
+ frontOffset, storedChecksum, calculatedChecksum));
+ }
+
+ Message message = MessageSerializer.deserialize(data);
+ frontOffset += BLOCK_HEADER_SIZE + messageSize;
+ saveMetadata();
+
+ return message;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Checks if the queue is empty.
+ *
+ * @return true if the queue contains no messages, false otherwise
+ */
+ public boolean isEmpty() {
+ return frontOffset >= rearOffset && batchSize == 0;
+ }
+
+ /**
+ * Closes the queue, ensuring all pending writes are flushed to disk.
+ *
+ *
+ * This method should be called when the queue is no longer needed to
+ * ensure proper resource cleanup. It's recommended to use try-with-resources
+ * to ensure the queue is properly closed.
+ *
+ * @throws IOException if an I/O error occurs while closing
+ */
+ @Override
+ public void close() throws IOException {
+ lock.lock();
+ try {
+ flushBatch();
+ saveMetadata();
+ channel.force(true);
+ channel.close();
+ file.close();
+ } finally {
+ lock.unlock();
}
+ }
+
+ private void flushBatch() throws IOException {
+ if (batchSize > 0) {
+ writeBatchBuffer.flip();
+ channel.write(writeBatchBuffer, batchStartOffset);
+ writeBatchBuffer.clear();
+ batchSize = 0;
+ saveMetadata();
+ }
+ }
- private void saveMetadata() throws IOException {
- ByteBuffer buffer = ByteBuffer.allocate(QUEUE_HEADER_SIZE);
- buffer.putLong(frontOffset);
- buffer.putLong(rearOffset);
- buffer.flip();
- channel.write(buffer, 0);
- channel.force(true);
+ private void loadMetadata() throws IOException {
+ if (file.length() < QUEUE_HEADER_SIZE) {
+ throw new IOException("File too small to contain valid header");
}
- public boolean isEmpty() {
- return frontOffset >= rearOffset && batchSize == 0;
+ ByteBuffer buffer = ByteBuffer.allocate(QUEUE_HEADER_SIZE);
+ int bytesRead = channel.read(buffer, 0);
+ if (bytesRead != QUEUE_HEADER_SIZE) {
+ throw new IOException("Failed to read queue metadata");
}
- @Override
- public void close() throws IOException {
- lock.lock();
- try {
- flushBatch();
- saveMetadata();
- channel.force(true);
- channel.close();
- file.close();
- } finally {
- lock.unlock();
- }
+ buffer.flip();
+
+ frontOffset = buffer.getLong();
+ rearOffset = buffer.getLong();
+
+ if (frontOffset < QUEUE_HEADER_SIZE || rearOffset < QUEUE_HEADER_SIZE ||
+ frontOffset > file.length() || rearOffset > file.length() ||
+ frontOffset > rearOffset) {
+ throw new IOException("Corrupted queue metadata");
}
+ }
- private void initializeNewFile() throws IOException {
- file.setLength(INITIAL_FILE_SIZE);
- frontOffset = QUEUE_HEADER_SIZE;
- rearOffset = QUEUE_HEADER_SIZE;
- saveMetadata();
+ private void saveMetadata() throws IOException {
+ // check for closed file
+ if (channel == null || !channel.isOpen() || file == null || !file.getFD().valid() ||
+ file.getChannel() == null) {
+ throw new IOException("Queue file is closed");
}
- private void debug(String format, Object... args) {
- if (DEBUG) {
- System.out.printf("[DEBUG] " + format + "%n", args);
- }
+ // check for corruption before writing
+ if (frontOffset < QUEUE_HEADER_SIZE || rearOffset < QUEUE_HEADER_SIZE ||
+ frontOffset > file.length() || rearOffset > file.length() ||
+ frontOffset > rearOffset) {
+ throw new IOException("Corrupted queue metadata");
+ }
+ ByteBuffer buffer = ByteBuffer.allocate(QUEUE_HEADER_SIZE);
+ buffer.putLong(frontOffset);
+ buffer.putLong(rearOffset);
+ buffer.flip();
+
+ channel.write(buffer, 0);
+ channel.force(true);
+ }
+
+ private void initializeNewFile() throws IOException {
+ file.setLength(INITIAL_FILE_SIZE);
+ frontOffset = QUEUE_HEADER_SIZE;
+ rearOffset = QUEUE_HEADER_SIZE;
+ saveMetadata();
+ }
+
+ @SuppressWarnings("unused")
+ private void debug(String format, Object... args) {
+ if (DEBUG) {
+ System.out.printf("[DEBUG] " + format + "%n", args);
}
+ }
}
\ No newline at end of file
diff --git a/src/test/java/io/github/elimelt/pmqueue/MessageSerializerTest.java b/src/test/java/io/github/elimelt/pmqueue/MessageSerializerTest.java
index 81a4176..4465db3 100644
--- a/src/test/java/io/github/elimelt/pmqueue/MessageSerializerTest.java
+++ b/src/test/java/io/github/elimelt/pmqueue/MessageSerializerTest.java
@@ -12,59 +12,58 @@
class MessageSerializerTest {
- private static Stream
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
public class PersistentMessageQueue implements Closeable {
- private static final boolean DEBUG = false;
- // size params
- private static final int QUEUE_HEADER_SIZE = 24;
- private static final int BLOCK_HEADER_SIZE = 8;
- private static final long MAX_FILE_SIZE = 1024L * 1024L * 1024L;
- private static final int INITIAL_FILE_SIZE = QUEUE_HEADER_SIZE;
- private static final int PAGE_SIZE = 4096;
- private static final int DEFAULT_BUFFER_SIZE = (1024 * 1024 / PAGE_SIZE) * PAGE_SIZE; // 1MB aligned
- private static final int MAX_BUFFER_SIZE = (8 * 1024 * 1024 / PAGE_SIZE) * PAGE_SIZE; // 8MB aligned
-
- // write batching
- private static final int BATCH_THRESHOLD = 64;
- private final ByteBuffer writeBatchBuffer;
- private int batchSize = 0;
- private long batchStartOffset;
-
- // store
- private final FileChannel channel;
- private final RandomAccessFile file;
- private ByteBuffer messageBuffer;
-
- // offsets
- private volatile long frontOffset;
- private volatile long rearOffset;
-
- // utils
- private final ReentrantLock lock;
- private final CRC32 checksumCalculator;
-
- public PersistentMessageQueue(String filename) throws IOException {
- File f = new File(filename);
- boolean isNew = !f.exists();
- this.file = new RandomAccessFile(f, "rw");
- this.channel = file.getChannel();
-
- // Use direct buffers for better I/O performance
- this.messageBuffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE);
- this.writeBatchBuffer = ByteBuffer.allocateDirect(MAX_BUFFER_SIZE);
-
- this.lock = new ReentrantLock(true); // Fair lock for predictable ordering
- this.checksumCalculator = new CRC32();
-
- if (isNew) {
- initializeNewFile();
- } else {
- loadMetadata();
- }
+ private static final boolean DEBUG = false;
+ private static final int QUEUE_HEADER_SIZE = 24;
+ private static final int BLOCK_HEADER_SIZE = 8;
+ private static final long MAX_FILE_SIZE = 1024L * 1024L * 1024L;
+ private static final int INITIAL_FILE_SIZE = QUEUE_HEADER_SIZE;
+ private static final int PAGE_SIZE = 4096;
+ private static final int DEFAULT_BUFFER_SIZE = (1024 * 1024 / PAGE_SIZE) * PAGE_SIZE;
+ private static final int MAX_BUFFER_SIZE = (8 * 1024 * 1024 / PAGE_SIZE) * PAGE_SIZE;
+ private static final int BATCH_THRESHOLD = 64;
+
+ private final ByteBuffer writeBatchBuffer;
+ private int batchSize = 0;
+ private long batchStartOffset;
+ private final FileChannel channel;
+ private final RandomAccessFile file;
+ private ByteBuffer messageBuffer;
+ private volatile long frontOffset;
+ private volatile long rearOffset;
+ private final ReentrantLock lock;
+ private final CRC32 checksumCalculator;
+
+ /**
+ * Creates a new persistent message queue or opens an existing one.
+ *
+ *