From 2f96a5430def972d0d138248645663699d1b2866 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:07 -0700 Subject: [PATCH 01/14] Add run_tests.sh harness (JDK 21 + JUnit console, no Maven needed) --- run_tests.sh | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100755 run_tests.sh diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..6e0ff7a --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Compile and run the full test suite without Maven. +# Downloads (once, into gitignored target/): +# - a Temurin JDK (target/jdk/) +# - JUnit console launcher + Mockito and friends (target/deps/) +# Usage: ./run_tests.sh +set -euo pipefail + +cd "$(dirname "$0")" + +JAVA_RELEASE=21 +DEPS_DIR=target/deps +JDK_DIR=target/jdk +CLASSES=target/classes +TEST_CLASSES=target/test-classes + +# --- JDK --------------------------------------------------------------- +find_javac() { + find "$JDK_DIR" -type f -name javac -path '*/bin/*' 2>/dev/null | head -1 +} + +JAVAC="$(find_javac || true)" +if [ -z "${JAVAC:-}" ]; then + case "$(uname -s)" in + Darwin) os=mac ;; + Linux) os=linux ;; + *) echo "unsupported OS: $(uname -s)" >&2; exit 1 ;; + esac + case "$(uname -m)" in + arm64|aarch64) arch=aarch64 ;; + x86_64) arch=x64 ;; + *) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;; + esac + echo "Downloading Temurin $JAVA_RELEASE JDK ($os/$arch) into $JDK_DIR ..." + mkdir -p "$JDK_DIR" + curl -fsSL -o "$JDK_DIR/jdk.tar.gz" \ + "https://api.adoptium.net/v3/binary/latest/$JAVA_RELEASE/ga/$os/$arch/jdk/hotspot/normal/eclipse" + tar -xzf "$JDK_DIR/jdk.tar.gz" -C "$JDK_DIR" + rm "$JDK_DIR/jdk.tar.gz" + JAVAC="$(find_javac)" +fi +[ -n "$JAVAC" ] || { echo "JDK setup failed" >&2; exit 1; } +JAVA="$(dirname "$JAVAC")/java" +"$JAVA" -version + +# --- Test dependencies -------------------------------------------------- +MAVEN_CENTRAL=https://repo1.maven.org/maven2 +deps=( + org/junit/platform/junit-platform-console-standalone/1.10.2/junit-platform-console-standalone-1.10.2.jar + org/mockito/mockito-core/5.11.0/mockito-core-5.11.0.jar + net/bytebuddy/byte-buddy/1.14.12/byte-buddy-1.14.12.jar + net/bytebuddy/byte-buddy-agent/1.14.12/byte-buddy-agent-1.14.12.jar + org/objenesis/objenesis/3.3/objenesis-3.3.jar +) +mkdir -p "$DEPS_DIR" +for dep in "${deps[@]}"; do + jar="$DEPS_DIR/$(basename "$dep")" + if [ ! -f "$jar" ]; then + echo "Downloading $(basename "$dep") ..." + curl -fsSL -o "$jar" "$MAVEN_CENTRAL/$dep" + fi +done +JUNIT_CONSOLE="$DEPS_DIR/junit-platform-console-standalone-1.10.2.jar" +DEP_CP="$(printf '%s:' "$DEPS_DIR"/*.jar)" +DEP_CP="${DEP_CP%:}" + +# --- Compile ------------------------------------------------------------ +rm -rf "$CLASSES" "$TEST_CLASSES" +mkdir -p "$CLASSES" "$TEST_CLASSES" +echo "Compiling main sources ..." +find src/main -name '*.java' -print0 | xargs -0 "$JAVAC" --release "$JAVA_RELEASE" \ + -d "$CLASSES" +echo "Compiling test sources ..." +find src/test -name '*.java' -print0 | xargs -0 "$JAVAC" --release "$JAVA_RELEASE" \ + -nowarn -cp "$CLASSES:$DEP_CP" -d "$TEST_CLASSES" + +# --- Run ---------------------------------------------------------------- +echo "Running tests ..." +"$JAVA" -jar "$JUNIT_CONSOLE" execute \ + -cp "$CLASSES:$TEST_CLASSES:$DEP_CP" \ + --scan-classpath \ + --fail-if-no-tests \ + --disable-ansi-colors From 51ae2f3b8ca952ca611b2ac1e2a4a28fcf8b415f Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:08 -0700 Subject: [PATCH 02/14] Golden characterization tests for wire and file formats --- .../elimelt/pmqueue/core/FileFormatTest.java | 167 ++++++++++++++++++ .../pmqueue/message/WireFormatTest.java | 158 +++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 src/test/java/io/github/elimelt/pmqueue/core/FileFormatTest.java create mode 100644 src/test/java/io/github/elimelt/pmqueue/message/WireFormatTest.java diff --git a/src/test/java/io/github/elimelt/pmqueue/core/FileFormatTest.java b/src/test/java/io/github/elimelt/pmqueue/core/FileFormatTest.java new file mode 100644 index 0000000..1b19439 --- /dev/null +++ b/src/test/java/io/github/elimelt/pmqueue/core/FileFormatTest.java @@ -0,0 +1,167 @@ +package io.github.elimelt.pmqueue.core; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import io.github.elimelt.pmqueue.message.Message; +import io.github.elimelt.pmqueue.message.MessageSerializer; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; +import java.util.zip.CRC32; + +/** + * Characterization tests that pin the exact on-disk queue file layout + * produced by {@link PersistentMessageQueue}, as documented in its class + * Javadoc and file-format comment: + * + *
+ * Queue header (24 bytes), all fields big-endian (java.nio.ByteBuffer's default order):
+ *   offset 0  (8 bytes): front offset (long)
+ *   offset 8  (8 bytes): rear offset (long)
+ *   offset 16 (8 bytes): reserved
+ *
+ * Each message block:
+ *   offset +0 (4 bytes): message size (int, big-endian)
+ *   offset +4 (4 bytes): CRC32 checksum of the serialized message (int, big-endian)
+ *   offset +8 (n bytes): serialized message (see MessageSerializer / WireFormatTest)
+ * 
+ * + *

+ * These tests write a real queue file with {@link PersistentMessageQueue}, + * close it, then read the raw bytes back with {@link RandomAccessFile} to + * assert the header/block structure exactly. Later refactors of the queue + * internals must keep producing byte-identical files. + */ +class FileFormatTest { + + private static final int QUEUE_HEADER_SIZE = 24; + private static final int BLOCK_HEADER_SIZE = 8; + + @TempDir + Path tempDir; + + @Test + @DisplayName("Constants documented in the class Javadoc have not drifted") + void headerConstantsAreUnchanged() { + assertEquals(QUEUE_HEADER_SIZE, PersistentMessageQueue.QUEUE_HEADER_SIZE); + assertEquals(BLOCK_HEADER_SIZE, PersistentMessageQueue.BLOCK_HEADER_SIZE); + } + + @Test + @DisplayName("A freshly created, empty queue file is exactly 24 bytes: front=24, rear=24, reserved=0") + void newEmptyQueueFileHeaderLayout() throws IOException { + File file = tempDir.resolve("empty.queue").toFile(); + + PersistentMessageQueue queue = new PersistentMessageQueue(file.getPath()); + queue.close(); + + assertEquals(QUEUE_HEADER_SIZE, file.length(), + "A brand-new, empty queue file must be exactly QUEUE_HEADER_SIZE bytes"); + + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + raf.seek(0); + long front = raf.readLong(); + long rear = raf.readLong(); + long reserved = raf.readLong(); + + assertEquals(QUEUE_HEADER_SIZE, front, "Front offset must start at the header size"); + assertEquals(QUEUE_HEADER_SIZE, rear, "Rear offset must start at the header size"); + assertEquals(0L, reserved, "Reserved header bytes must be untouched/zero"); + } + } + + @Test + @DisplayName("Offering one message writes a block header (size, CRC32) then the serialized message right after the queue header") + void singleMessageBlockLayout() throws IOException { + File file = tempDir.resolve("single.queue").toFile(); + byte[] data = "hello".getBytes(); + int type = 5; + Message message = new Message(data, type); + + // Capture the exact wire bytes PersistentMessageQueue will embed. The + // message is immutable (fixed timestamp), so re-serializing it here + // yields byte-identical output to what offer() serializes internally. + byte[] expectedSerialized = MessageSerializer.serialize(message); + CRC32 crc = new CRC32(); + crc.update(expectedSerialized); + int expectedChecksum = (int) crc.getValue(); + + PersistentMessageQueue queue = new PersistentMessageQueue(file.getPath()); + queue.offer(message); + queue.close(); + + long expectedRear = QUEUE_HEADER_SIZE + BLOCK_HEADER_SIZE + expectedSerialized.length; + + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + raf.seek(0); + long front = raf.readLong(); + long rear = raf.readLong(); + long reserved = raf.readLong(); + + assertEquals(QUEUE_HEADER_SIZE, front, "Front offset unchanged: nothing has been polled"); + assertEquals(expectedRear, rear, "Rear offset must advance by block header + serialized message size"); + assertEquals(0L, reserved); + + raf.seek(QUEUE_HEADER_SIZE); + int messageSize = raf.readInt(); + int checksum = raf.readInt(); + + assertEquals(expectedSerialized.length, messageSize, + "Block header message-size field must equal the serialized message length"); + assertEquals(expectedChecksum, checksum, + "Block header checksum field must equal CRC32 of the serialized message bytes"); + + byte[] actualSerialized = new byte[expectedSerialized.length]; + raf.readFully(actualSerialized); + assertArrayEquals(expectedSerialized, actualSerialized, + "Bytes right after the block header must be exactly the serialized message"); + } + + // Note: PersistentMessageQueue.offer() pre-grows the file with headroom + // (see the file.setLength() sizing in offer()) rather than truncating to + // exactly the bytes written, so total file length is not pinned here - + // only the header and block layout within it are. + assertTrue(file.length() >= QUEUE_HEADER_SIZE + BLOCK_HEADER_SIZE + expectedSerialized.length, + "File must be at least large enough to hold the header and the one block written"); + } + + @Test + @DisplayName("After polling the only message, front offset catches up to rear offset; underlying block bytes are left in place") + void frontOffsetAdvancesAfterPollWithoutErasingData() throws IOException { + File file = tempDir.resolve("polled.queue").toFile(); + Message message = new Message("bye".getBytes(), 3); + byte[] expectedSerialized = MessageSerializer.serialize(message); + + PersistentMessageQueue queue = new PersistentMessageQueue(file.getPath()); + queue.offer(message); + queue.poll(); + queue.close(); + + long expectedOffset = QUEUE_HEADER_SIZE + BLOCK_HEADER_SIZE + expectedSerialized.length; + + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + raf.seek(0); + long front = raf.readLong(); + long rear = raf.readLong(); + + assertEquals(expectedOffset, front, "Front offset must catch up to rear after draining the queue"); + assertEquals(expectedOffset, rear); + assertTrue(front == rear, "Queue file header must show an empty queue after poll"); + + // Block bytes are not erased by poll(); they remain on disk past the + // (now equal) front/rear offsets. + raf.seek(QUEUE_HEADER_SIZE); + int messageSize = raf.readInt(); + assertEquals(expectedSerialized.length, messageSize, + "poll() must not erase the block header/data still physically on disk"); + } + } +} diff --git a/src/test/java/io/github/elimelt/pmqueue/message/WireFormatTest.java b/src/test/java/io/github/elimelt/pmqueue/message/WireFormatTest.java new file mode 100644 index 0000000..c577776 --- /dev/null +++ b/src/test/java/io/github/elimelt/pmqueue/message/WireFormatTest.java @@ -0,0 +1,158 @@ +package io.github.elimelt.pmqueue.message; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Characterization tests that pin the exact wire format produced by + * {@link MessageSerializer#serialize(Message)}. + * + *

+ * Current layout (16-byte header + data). All multi-byte fields are written + * in the JVM's native byte order, because {@link MessageSerializer} writes + * them with {@code sun.misc.Unsafe}: + * + *

+ * offset 0  (8 bytes): timestamp (long)
+ * offset 8  (4 bytes): message type (int)
+ * offset 12 (4 bytes): data length (int)
+ * offset 16 (n bytes): message data
+ * 
+ * + *

+ * These tests exist so later refactors of {@link MessageSerializer} cannot + * silently change this on-wire layout; they must keep passing byte-for-byte + * against unmodified source. + */ +class WireFormatTest { + + private static final int HEADER_SIZE = 16; + private static final int TIMESTAMP_OFFSET = 0; + private static final int TYPE_OFFSET = 8; + private static final int LENGTH_OFFSET = 12; + private static final int DATA_OFFSET = 16; + + // MessageSerializer writes header fields via Unsafe using native byte + // order. On the x86-64/ARM64 hosts this project runs on, that is + // little-endian; pin that assumption explicitly so a change of host + // architecture (rather than a code change) is what would break this test. + private static final ByteOrder WIRE_ORDER = ByteOrder.LITTLE_ENDIAN; + + @Test + @DisplayName("Precondition: host native byte order is little-endian") + void nativeOrderIsLittleEndian() { + assertEquals(ByteOrder.LITTLE_ENDIAN, ByteOrder.nativeOrder(), + "MessageSerializer relies on Unsafe's native-order writes; " + + "these fixed-offset assertions assume a little-endian host"); + } + + @Test + @DisplayName("Header fields sit at fixed offsets: type@8, length@12, data@16") + void headerFieldsAtFixedOffsets() throws IOException { + byte[] data = { 0x41, 0x42, 0x43, 0x44 }; // "ABCD" + int type = 0x12345678; + Message message = new Message(data, type); + + byte[] wire = MessageSerializer.serialize(message); + + assertEquals(HEADER_SIZE + data.length, wire.length, + "Wire size must be header (16) + data length"); + + ByteBuffer buf = ByteBuffer.wrap(wire).order(WIRE_ORDER); + + assertEquals(message.getTimestamp(), buf.getLong(TIMESTAMP_OFFSET), + "Timestamp bytes at offset 0 must decode to the message's timestamp"); + assertEquals(type, buf.getInt(TYPE_OFFSET), "Message type must be at offset 8"); + assertEquals(data.length, buf.getInt(LENGTH_OFFSET), "Data length must be at offset 12"); + + byte[] decodedData = new byte[data.length]; + buf.position(DATA_OFFSET); + buf.get(decodedData); + assertArrayEquals(data, decodedData, "Data must start at offset 16"); + } + + @Test + @DisplayName("Empty data produces exactly a 16-byte header-only wire encoding") + void emptyDataProducesHeaderOnlyWire() throws IOException { + Message message = new Message(new byte[0], 7); + + byte[] wire = MessageSerializer.serialize(message); + + assertEquals(HEADER_SIZE, wire.length); + ByteBuffer buf = ByteBuffer.wrap(wire).order(WIRE_ORDER); + assertEquals(7, buf.getInt(TYPE_OFFSET)); + assertEquals(0, buf.getInt(LENGTH_OFFSET)); + } + + @Test + @DisplayName("Message type is written as raw 4-byte little-endian bits, including negative values") + void negativeMessageTypeBitsPreserved() throws IOException { + Message message = new Message(new byte[] { 9 }, -1); + + byte[] wire = MessageSerializer.serialize(message); + + assertArrayEquals( + new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF }, + new byte[] { wire[8], wire[9], wire[10], wire[11] }, + "type == -1 must serialize as four 0xFF bytes at offset 8"); + } + + @Test + @DisplayName("Timestamp occupies bytes 0-7; masking them leaves a fully deterministic remainder") + void timestampBytesPositionAndPlausibility() throws IOException { + long before = System.currentTimeMillis(); + Message message = new Message(new byte[] { 'x' }, 1); + long after = System.currentTimeMillis(); + + byte[] wire = MessageSerializer.serialize(message); + ByteBuffer buf = ByteBuffer.wrap(wire).order(WIRE_ORDER); + long timestampFromWire = buf.getLong(TIMESTAMP_OFFSET); + + assertTrue(timestampFromWire >= before && timestampFromWire <= after, + "Timestamp bytes at offset 0 must decode to a plausible current-time-millis value"); + assertEquals(message.getTimestamp(), timestampFromWire); + + byte[] maskedWire = wire.clone(); + for (int i = TIMESTAMP_OFFSET; i < TYPE_OFFSET; i++) { + maskedWire[i] = 0; + } + + byte[] expectedMasked = { + 0, 0, 0, 0, 0, 0, 0, 0, // masked timestamp (offset 0-7) + 1, 0, 0, 0, // type = 1, little-endian (offset 8-11) + 1, 0, 0, 0, // length = 1, little-endian (offset 12-15) + 'x' // data (offset 16) + }; + assertArrayEquals(expectedMasked, maskedWire, + "With the timestamp masked, the rest of the wire encoding is fully deterministic"); + } + + @Test + @DisplayName("deserialize() reads timestamp/type/length/data from the documented fixed offsets") + void deserializeReadsFixedOffsets() throws IOException { + byte[] data = "payload".getBytes(); + int type = 42; + long timestamp = 1_700_000_000_000L; + + ByteBuffer buf = ByteBuffer.allocate(HEADER_SIZE + data.length).order(WIRE_ORDER); + buf.putLong(TIMESTAMP_OFFSET, timestamp); + buf.putInt(TYPE_OFFSET, type); + buf.putInt(LENGTH_OFFSET, data.length); + buf.position(DATA_OFFSET); + buf.put(data); + + Message decoded = MessageSerializer.deserialize(buf.array()); + + assertEquals(timestamp, decoded.getTimestamp()); + assertEquals(type, decoded.getMessageType()); + assertArrayEquals(data, decoded.getData()); + } +} From 330fe262c9efc0fb19ecc5436b3949c988e0bcb2 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:08 -0700 Subject: [PATCH 03/14] Per-instance config in PersistentMessageQueue --- .../pmqueue/core/PersistentMessageQueue.java | 66 ++++++--- .../pmqueue/core/MultiInstanceConfigTest.java | 129 ++++++++++++++++++ 2 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 src/test/java/io/github/elimelt/pmqueue/core/MultiInstanceConfigTest.java diff --git a/src/main/java/io/github/elimelt/pmqueue/core/PersistentMessageQueue.java b/src/main/java/io/github/elimelt/pmqueue/core/PersistentMessageQueue.java index 641a697..467948b 100644 --- a/src/main/java/io/github/elimelt/pmqueue/core/PersistentMessageQueue.java +++ b/src/main/java/io/github/elimelt/pmqueue/core/PersistentMessageQueue.java @@ -6,6 +6,8 @@ import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.zip.CRC32; import io.github.elimelt.pmqueue.MessageQueue; @@ -111,14 +113,16 @@ public class PersistentMessageQueue implements MessageQueue { */ public static final int PAGE_SIZE = 4096; - // default configuration - private static boolean debug = false; - private static boolean shouldChecksum = true; - private static long maxFileSize = 1024L * 1024L * 1024L; // 1GB - private static int initialFileSize = QUEUE_HEADER_SIZE; - private static int defaultBufferSize = (1024 * 1024 / PAGE_SIZE) * PAGE_SIZE; - private static int maxBufferSize = (8 * 1024 * 1024 / PAGE_SIZE) * PAGE_SIZE; - private static int batchThreshold = 64; + private static final Logger LOGGER = Logger.getLogger(PersistentMessageQueue.class.getName()); + + // per-instance configuration + private final boolean debug; + private final boolean shouldChecksum; + private final long maxFileSize; + private final int initialFileSize; + private final int defaultBufferSize; + private final int maxBufferSize; + private final int batchThreshold; // instance variables private final ByteBuffer writeBatchBuffer; @@ -143,13 +147,13 @@ public class PersistentMessageQueue implements MessageQueue { */ public PersistentMessageQueue(QueueConfig config) throws IOException { // configure - debug = config.isDebugEnabled(); - shouldChecksum = config.isChecksumEnabled(); - maxFileSize = config.getMaxFileSize(); - initialFileSize = config.getInitialFileSize(); - defaultBufferSize = alignToPageSize(config.getDefaultBufferSize()); - maxBufferSize = alignToPageSize(config.getMaxBufferSize()); - batchThreshold = config.getBatchThreshold(); + this.debug = config.isDebugEnabled(); + this.shouldChecksum = config.isChecksumEnabled(); + this.maxFileSize = config.getMaxFileSize(); + this.initialFileSize = config.getInitialFileSize(); + this.defaultBufferSize = alignToPageSize(config.getDefaultBufferSize()); + this.maxBufferSize = alignToPageSize(config.getMaxBufferSize()); + this.batchThreshold = config.getBatchThreshold(); // init queue File f = new File(config.getFilePath()); @@ -163,10 +167,27 @@ public PersistentMessageQueue(QueueConfig config) throws IOException { this.checksumCalculator = shouldChecksum ? new CRC32() : null; - if (isNew) { - initializeNewFile(); - } else { - loadMetadata(); + debug("Opening queue file=%s isNew=%b checksum=%b maxFileSize=%d", f.getPath(), isNew, shouldChecksum, + maxFileSize); + + try { + if (isNew) { + initializeNewFile(); + } else { + loadMetadata(); + } + } catch (IOException | RuntimeException e) { + try { + channel.close(); + } catch (IOException closeException) { + e.addSuppressed(closeException); + } + try { + file.close(); + } catch (IOException closeException) { + e.addSuppressed(closeException); + } + throw e; } } @@ -360,6 +381,7 @@ public Message poll() throws IOException { frontOffset += BLOCK_HEADER_SIZE + messageSize; saveMetadata(); + debug("Polled message of size %d, new frontOffset=%d", messageSize, frontOffset); return message; } finally { lock.unlock(); @@ -406,6 +428,7 @@ public void close() throws IOException { */ public void flushBatch() throws IOException { if (batchSize > 0) { + debug("Flushing batch of %d messages at offset %d", batchSize, batchStartOffset); writeBatchBuffer.flip(); channel.write(writeBatchBuffer, batchStartOffset); writeBatchBuffer.clear(); @@ -466,10 +489,9 @@ private void initializeNewFile() throws IOException { saveMetadata(); } - @SuppressWarnings("unused") private void debug(String format, Object... args) { - if (debug) { - System.out.printf("[DEBUG] " + format + "%n", args); + if (debug && LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(String.format(format, args)); } } } \ No newline at end of file diff --git a/src/test/java/io/github/elimelt/pmqueue/core/MultiInstanceConfigTest.java b/src/test/java/io/github/elimelt/pmqueue/core/MultiInstanceConfigTest.java new file mode 100644 index 0000000..4c2160b --- /dev/null +++ b/src/test/java/io/github/elimelt/pmqueue/core/MultiInstanceConfigTest.java @@ -0,0 +1,129 @@ +package io.github.elimelt.pmqueue.core; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import io.github.elimelt.pmqueue.QueueConfig; +import io.github.elimelt.pmqueue.message.Message; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Regression tests for per-instance configuration in + * {@link PersistentMessageQueue}. + * + *

+ * Before this fix, the queue's configuration (checksum enablement, buffer + * sizes, batch threshold, etc.) was stored in {@code static} fields, so + * creating a second queue instance with different settings clobbered the + * configuration of every other open instance. In particular, opening a + * checksum-enabled queue while a checksum-disabled queue was already open + * would flip the shared {@code shouldChecksum} flag to {@code true} for + * both, while the checksum-disabled instance still had a {@code null} + * {@code checksumCalculator}, causing a {@link NullPointerException} on the + * next {@code offer}/{@code poll}. + */ +class MultiInstanceConfigTest { + + @TempDir + Path tempDir; + + @Test + @DisplayName("Concurrently open queues with different checksum settings offer/poll correctly") + void queuesWithDifferentChecksumSettingsDoNotInterfere() throws IOException { + QueueConfig noChecksumConfig = new QueueConfig.Builder() + .filePath(tempDir.resolve("no-checksum.dat").toString()) + .checksumEnabled(false) + .build(); + + try (PersistentMessageQueue noChecksumQueue = new PersistentMessageQueue(noChecksumConfig)) { + // Use the checksum-disabled queue before a second, differently + // configured instance exists. + Message first = new Message("no-checksum-before".getBytes(), 1); + assertTrue(noChecksumQueue.offer(first)); + + QueueConfig checksumConfig = new QueueConfig.Builder() + .filePath(tempDir.resolve("checksum.dat").toString()) + .checksumEnabled(true) + .build(); + + try (PersistentMessageQueue checksumQueue = new PersistentMessageQueue(checksumConfig)) { + // Opening the second (checksum-enabled) queue must not change the + // behavior of the first (checksum-disabled) queue: this offer/poll + // pair NPEs before the per-instance-config fix. + Message afterOpen = new Message("no-checksum-after".getBytes(), 2); + assertTrue(noChecksumQueue.offer(afterOpen)); + + Message polledFirst = noChecksumQueue.poll(); + assertNotNull(polledFirst); + assertArrayEquals(first.getData(), polledFirst.getData()); + assertEquals(first.getMessageType(), polledFirst.getMessageType()); + + Message polledAfterOpen = noChecksumQueue.poll(); + assertNotNull(polledAfterOpen); + assertArrayEquals(afterOpen.getData(), polledAfterOpen.getData()); + assertEquals(afterOpen.getMessageType(), polledAfterOpen.getMessageType()); + assertTrue(noChecksumQueue.isEmpty()); + + // The checksum-enabled queue must independently validate its own + // messages via CRC32. + Message checksummed = new Message("checksummed-payload".getBytes(), 3); + assertTrue(checksumQueue.offer(checksummed)); + + Message polledChecksummed = checksumQueue.poll(); + assertNotNull(polledChecksummed); + assertArrayEquals(checksummed.getData(), polledChecksummed.getData()); + assertEquals(checksummed.getMessageType(), polledChecksummed.getMessageType()); + assertTrue(checksumQueue.isEmpty()); + } + } + } + + @Test + @DisplayName("Concurrently open queues with different batch thresholds and buffer sizes keep independent config") + void queuesWithDifferentBatchAndBufferConfigDoNotInterfere() throws IOException { + QueueConfig smallBatchConfig = new QueueConfig.Builder() + .filePath(tempDir.resolve("small-batch.dat").toString()) + .batchThreshold(1) + .build(); + + QueueConfig largeBatchConfig = new QueueConfig.Builder() + .filePath(tempDir.resolve("large-batch.dat").toString()) + .batchThreshold(50) + .build(); + + try (PersistentMessageQueue smallBatchQueue = new PersistentMessageQueue(smallBatchConfig); + PersistentMessageQueue largeBatchQueue = new PersistentMessageQueue(largeBatchConfig)) { + + // Interleave operations across both instances; if batchThreshold were + // still shared static state, opening largeBatchQueue after + // smallBatchQueue would overwrite smallBatchQueue's threshold. + for (int i = 0; i < 5; i++) { + assertTrue(smallBatchQueue.offer(new Message(("small-" + i).getBytes(), i))); + assertTrue(largeBatchQueue.offer(new Message(("large-" + i).getBytes(), i))); + } + + for (int i = 0; i < 5; i++) { + Message smallMsg = smallBatchQueue.poll(); + assertNotNull(smallMsg); + assertArrayEquals(("small-" + i).getBytes(), smallMsg.getData()); + + Message largeMsg = largeBatchQueue.poll(); + assertNotNull(largeMsg); + assertArrayEquals(("large-" + i).getBytes(), largeMsg.getData()); + } + + assertTrue(smallBatchQueue.isEmpty()); + assertTrue(largeBatchQueue.isEmpty()); + assertFalse(smallBatchQueue == largeBatchQueue); + } + } +} From fe816b4fa7042fa8f6cebba60e263f245c17b710 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:09 -0700 Subject: [PATCH 04/14] Remove Unsafe and dead Serializable path from Message, add equals --- .../elimelt/pmqueue/message/Message.java | 110 +++++------------- .../elimelt/pmqueue/message/MessageTest.java | 82 +++++++++++++ 2 files changed, 108 insertions(+), 84 deletions(-) diff --git a/src/main/java/io/github/elimelt/pmqueue/message/Message.java b/src/main/java/io/github/elimelt/pmqueue/message/Message.java index eac5463..6bf428d 100644 --- a/src/main/java/io/github/elimelt/pmqueue/message/Message.java +++ b/src/main/java/io/github/elimelt/pmqueue/message/Message.java @@ -1,20 +1,11 @@ package io.github.elimelt.pmqueue.message; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; import java.lang.ref.SoftReference; -import sun.misc.Unsafe; -import java.lang.reflect.Field; +import java.util.Arrays; /** * 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: @@ -27,16 +18,12 @@ *

* This class implements optimizations including: *

* * @see MessageSerializer */ -@SuppressWarnings("deprecation") -public class Message implements Serializable { - private static final long serialVersionUID = 1L; +public class Message { /** * Soft reference to cache the hash code for this message. @@ -59,38 +46,6 @@ public class Message implements Serializable { */ private final int length; - /** - * The Unsafe instance for direct memory access. - */ - private static final Unsafe unsafe; - /** - * The offset of the data field. - */ - @SuppressWarnings("unused") - private static final long dataOffset; - /** - * The offset of the timestamp field. - */ - private static final long timestampOffset; - /** - * The offset of the messageType field. - */ - private static final long messageTypeOffset; - - static { - try { - Field f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - unsafe = (Unsafe) f.get(null); - - dataOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("data")); - timestampOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("timestamp")); - messageTypeOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("messageType")); - } catch (Exception e) { - throw new Error(e); - } - } - /** * Creates a new Message with the specified data and message type. * The message's timestamp is automatically set to the current system time. @@ -104,12 +59,8 @@ public Message(byte[] data, int messageType) { if (data == null) { throw new NullPointerException("Message data cannot be null"); } - int dataLength = data.length; - this.data = new byte[dataLength]; - unsafe.copyMemory(data, Unsafe.ARRAY_BYTE_BASE_OFFSET, - this.data, Unsafe.ARRAY_BYTE_BASE_OFFSET, - dataLength); - this.length = dataLength; + this.data = data.clone(); + this.length = this.data.length; this.timestamp = System.currentTimeMillis(); this.messageType = messageType; } @@ -121,14 +72,7 @@ public Message(byte[] data, int messageType) { * @return a copy of the message data as a byte array */ public byte[] getData() { - if (data == null) { - return null; - } - byte[] copy = new byte[length]; - unsafe.copyMemory(data, Unsafe.ARRAY_BYTE_BASE_OFFSET, - copy, Unsafe.ARRAY_BYTE_BASE_OFFSET, - length); - return copy; + return Arrays.copyOf(data, length); } /** @@ -137,7 +81,7 @@ public byte[] getData() { * @return the message creation timestamp as milliseconds since epoch */ public long getTimestamp() { - return unsafe.getLong(this, timestampOffset); + return timestamp; } /** @@ -146,7 +90,7 @@ public long getTimestamp() { * @return the integer message type */ public int getMessageType() { - return unsafe.getInt(this, messageTypeOffset); + return messageType; } /** @@ -179,27 +123,25 @@ public int hashCode() { } /** - * Custom serialization implementation for better performance. - * Writes the message fields directly to the output stream. + * Compares this message to another object for equality. + * Two messages are equal if they have the same data, timestamp, and message + * type, i.e. the same fields used to compute {@link #hashCode()}. * - * @param out the output stream to write to - * @throws IOException if an I/O error occurs + * @param obj the object to compare against + * @return true if the given object is a Message with the same data, + * timestamp, and message type */ - private void writeObject(ObjectOutputStream out) throws IOException { - out.writeLong(unsafe.getLong(this, timestampOffset)); - out.writeInt(unsafe.getInt(this, messageTypeOffset)); - out.writeInt(length); - out.write(data, 0, length); - } - - /** - * Disabled default deserialization. - * Use {@link MessageSerializer} instead for proper deserialization. - * - * @param in the input stream to read from - * @throws IOException always, to prevent default deserialization - */ - private void readObject(ObjectInputStream in) throws IOException { - throw new IOException("Use MessageSerializer instead"); + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Message)) { + return false; + } + Message other = (Message) obj; + return timestamp == other.timestamp + && messageType == other.messageType + && Arrays.equals(data, other.data); } -} \ No newline at end of file +} diff --git a/src/test/java/io/github/elimelt/pmqueue/message/MessageTest.java b/src/test/java/io/github/elimelt/pmqueue/message/MessageTest.java index 80cc2ec..930b4a7 100644 --- a/src/test/java/io/github/elimelt/pmqueue/message/MessageTest.java +++ b/src/test/java/io/github/elimelt/pmqueue/message/MessageTest.java @@ -56,4 +56,86 @@ void constructorShouldCreateDefensiveCopy() { void constructorShouldRejectNullData() { assertThrows(NullPointerException.class, () -> new Message(null, 1)); } + + @Test + @DisplayName("equals should return true for messages with the same data, timestamp, and type") + void equalsShouldReturnTrueForEqualMessages() { + byte[] data = "test data".getBytes(); + Message message1 = new Message(data, 1); + Message message2 = new Message(data, 1); + + // constructed back-to-back with the same data and type; only differ if + // they happen to straddle a system-clock millisecond tick + assertEquals(message1, message2); + } + + @Test + @DisplayName("equals should return true for the same instance") + void equalsShouldReturnTrueForSameInstance() { + Message message = new Message("test data".getBytes(), 1); + + assertEquals(message, message); + } + + @Test + @DisplayName("equals should return false for messages with different data") + void equalsShouldReturnFalseForDifferentData() { + Message message1 = new Message("test data".getBytes(), 1); + Message message2 = new Message("other data".getBytes(), 1); + + assertNotEquals(message1, message2); + } + + @Test + @DisplayName("equals should return false for messages with different message types") + void equalsShouldReturnFalseForDifferentMessageType() { + byte[] data = "test data".getBytes(); + Message message1 = new Message(data, 1); + Message message2 = new Message(data, 2); + + assertNotEquals(message1, message2); + } + + @Test + @DisplayName("equals should return false for messages with different timestamps") + void equalsShouldReturnFalseForDifferentTimestamps() throws InterruptedException { + byte[] data = "test data".getBytes(); + Message message1 = new Message(data, 1); + Thread.sleep(5); + Message message2 = new Message(data, 1); + + assertNotEquals(message1.getTimestamp(), message2.getTimestamp()); + assertNotEquals(message1, message2); + } + + @Test + @DisplayName("equals should return false when compared to null or a different type") + void equalsShouldReturnFalseForNullOrDifferentType() { + Message message = new Message("test data".getBytes(), 1); + + assertNotEquals(null, message); + assertNotEquals("not a message", message); + } + + @Test + @DisplayName("hashCode should be consistent with equals for equal messages") + void hashCodeShouldBeConsistentForEqualMessages() { + byte[] data = "test data".getBytes(); + Message message1 = new Message(data, 1); + Message message2 = new Message(data, 1); + + assertEquals(message1, message2); + assertEquals(message1.hashCode(), message2.hashCode()); + } + + @Test + @DisplayName("hashCode should be stable across repeated calls") + void hashCodeShouldBeStableAcrossCalls() { + Message message = new Message("test data".getBytes(), 1); + + int firstCall = message.hashCode(); + int secondCall = message.hashCode(); + + assertEquals(firstCall, secondCall); + } } \ No newline at end of file From f4fec41f4b4a12790e4e95893e59045a12f8d1fd Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:09 -0700 Subject: [PATCH 05/14] MessageSerializer via ByteBuffer, Unsafe removed --- .../pmqueue/message/MessageSerializer.java | 69 +++++++------------ 1 file changed, 24 insertions(+), 45 deletions(-) diff --git a/src/main/java/io/github/elimelt/pmqueue/message/MessageSerializer.java b/src/main/java/io/github/elimelt/pmqueue/message/MessageSerializer.java index 102880d..f094146 100644 --- a/src/main/java/io/github/elimelt/pmqueue/message/MessageSerializer.java +++ b/src/main/java/io/github/elimelt/pmqueue/message/MessageSerializer.java @@ -1,15 +1,12 @@ package io.github.elimelt.pmqueue.message; import java.io.IOException; -import java.nio.Buffer; import java.nio.ByteBuffer; - -import sun.misc.Unsafe; -import java.lang.reflect.Field; +import java.nio.ByteOrder; /** - * A high-performance serializer for {@link Message} objects using direct memory - * operations. + * A high-performance serializer for {@link Message} objects using + * {@link ByteBuffer} operations. * This class provides methods to convert {@link Message} objects to and from * byte arrays * with minimal overhead and maximum performance. @@ -24,10 +21,13 @@ * * *

+ * All multi-byte fields are written and read in the JVM's native byte order, + * matching the on-wire layout this class has always produced. + * + *

* Performance optimizations include: *

    *
  • Thread-local {@link ByteBuffer} reuse to minimize allocation - *
  • Direct memory operations using {@link sun.misc.Unsafe} *
  • Buffer size doubling strategy for growing buffers *
* @@ -35,7 +35,6 @@ * Note: This class is not intended for external use and * should only be used by the {@link Message} class's serialization mechanism. */ -@SuppressWarnings("deprecation") public class MessageSerializer { private static final int HEADER_SIZE = 16; @@ -43,23 +42,7 @@ private MessageSerializer() { } private static final ThreadLocal threadLocalBuffer = ThreadLocal - .withInitial(() -> ByteBuffer.allocateDirect(4096)); - - private static final Unsafe unsafe; - private static final long addressOffset; - - static { - try { - Field f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - unsafe = (Unsafe) f.get(null); - - Field addressField = Buffer.class.getDeclaredField("address"); - addressOffset = unsafe.objectFieldOffset(addressField); - } catch (Exception e) { - throw new Error(e); - } - } + .withInitial(() -> ByteBuffer.allocateDirect(4096).order(ByteOrder.nativeOrder())); /** * Serializes a {@link Message} object into a byte array. @@ -86,24 +69,20 @@ public static byte[] serialize(Message message) throws IOException { ByteBuffer buffer = threadLocalBuffer.get(); if (buffer.capacity() < totalLength) { - buffer = ByteBuffer.allocateDirect(Math.max(totalLength, buffer.capacity() * 2)); + buffer = ByteBuffer.allocateDirect(Math.max(totalLength, buffer.capacity() * 2)) + .order(ByteOrder.nativeOrder()); threadLocalBuffer.set(buffer); } 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); + buffer.putLong(message.getTimestamp()); + buffer.putInt(message.getMessageType()); + buffer.putInt(data.length); + buffer.put(data); byte[] result = new byte[totalLength]; - unsafe.copyMemory(null, bufferAddress, - result, Unsafe.ARRAY_BYTE_BASE_OFFSET, - totalLength); + buffer.flip(); + buffer.get(result); return result; } @@ -116,7 +95,7 @@ public static byte[] serialize(Message message) throws IOException { *

* 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 + * extracted from the serialized format using a {@link ByteBuffer} view for * optimal performance. * * @param bytes the byte array containing the serialized message @@ -129,18 +108,18 @@ public static Message deserialize(byte[] bytes) throws IOException { throw new IOException("Invalid message: too short"); } - 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); + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.nativeOrder()); + long timestamp = buffer.getLong(0); + int type = buffer.getInt(8); + int length = buffer.getInt(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); + buffer.position(HEADER_SIZE); + buffer.get(data); return new Message(data, type) { @Override @@ -149,4 +128,4 @@ public long getTimestamp() { } }; } -} \ No newline at end of file +} From c21b493e43ead0855e1c0538a0b1384e3b401633 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:09 -0700 Subject: [PATCH 06/14] Interrupt handling and close() convention --- .../github/elimelt/pmqueue/MessageQueue.java | 8 ++++++++ .../consumer/DefaultMessageConsumer.java | 3 +++ .../producer/DefaultMessageProducer.java | 6 ++---- .../consumer/DefaultMessageConsumerTest.java | 20 +++++++++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/elimelt/pmqueue/MessageQueue.java b/src/main/java/io/github/elimelt/pmqueue/MessageQueue.java index 41221c5..ad35e8f 100644 --- a/src/main/java/io/github/elimelt/pmqueue/MessageQueue.java +++ b/src/main/java/io/github/elimelt/pmqueue/MessageQueue.java @@ -47,4 +47,12 @@ public interface MessageQueue extends AutoCloseable { * @return true if the queue is empty, false otherwise */ boolean isEmpty(); + + /** + * Closes the queue, releasing any resources it holds. + * + * @throws IOException if an I/O error occurs while closing + */ + @Override + void close() throws IOException; } \ No newline at end of file diff --git a/src/main/java/io/github/elimelt/pmqueue/consumer/DefaultMessageConsumer.java b/src/main/java/io/github/elimelt/pmqueue/consumer/DefaultMessageConsumer.java index 35dd37f..c7de2f0 100644 --- a/src/main/java/io/github/elimelt/pmqueue/consumer/DefaultMessageConsumer.java +++ b/src/main/java/io/github/elimelt/pmqueue/consumer/DefaultMessageConsumer.java @@ -99,6 +99,9 @@ private void consumeMessages() { } else { Thread.sleep(100); // Prevent tight loop } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; } catch (Exception e) { logger.warning("Error polling message: " + e.getMessage()); } diff --git a/src/main/java/io/github/elimelt/pmqueue/producer/DefaultMessageProducer.java b/src/main/java/io/github/elimelt/pmqueue/producer/DefaultMessageProducer.java index a6dd712..ee3c872 100644 --- a/src/main/java/io/github/elimelt/pmqueue/producer/DefaultMessageProducer.java +++ b/src/main/java/io/github/elimelt/pmqueue/producer/DefaultMessageProducer.java @@ -42,9 +42,7 @@ public void send(byte[] data, int messageType) throws IOException { } @Override - public void close() throws Exception { - if (queue instanceof AutoCloseable) { - ((AutoCloseable) queue).close(); - } + public void close() throws IOException { + queue.close(); } } \ No newline at end of file diff --git a/src/test/java/io/github/elimelt/pmqueue/consumer/DefaultMessageConsumerTest.java b/src/test/java/io/github/elimelt/pmqueue/consumer/DefaultMessageConsumerTest.java index 7417725..f6dcc7a 100644 --- a/src/test/java/io/github/elimelt/pmqueue/consumer/DefaultMessageConsumerTest.java +++ b/src/test/java/io/github/elimelt/pmqueue/consumer/DefaultMessageConsumerTest.java @@ -261,4 +261,24 @@ void shouldRespectRetryDelay() throws Exception { assertTrue(duration >= DEFAULT_RETRY_DELAY * (DEFAULT_MAX_RETRIES - 1), "Retry delay should be respected"); } + + @Test + @DisplayName("Consumer should exit loop and restore interrupt status when sleep is interrupted") + void shouldExitLoopAndRestoreInterruptOnSleepInterruption() throws Exception { + CountDownLatch pollLatch = new CountDownLatch(1); + when(mockQueue.poll()).thenAnswer(invocation -> { + pollLatch.countDown(); + return null; + }); + + consumer.start(); + assertTrue(pollLatch.await(1, TimeUnit.SECONDS)); + + Thread consumerThread = consumer.getConsumerThread(); + consumerThread.interrupt(); + consumerThread.join(1000); + + assertFalse(consumerThread.isAlive(), "Consumer thread should exit the loop after interruption"); + assertTrue(consumerThread.isInterrupted(), "Interrupt status should be restored on the thread"); + } } \ No newline at end of file From 4caadb4206432c516e553165165c8932df518899 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:10 -0700 Subject: [PATCH 07/14] QueuePreset is the single source of preset values --- .../github/elimelt/pmqueue/QueueFactory.java | 214 ++++-------------- .../elimelt/pmqueue/QueueFactoryTest.java | 82 ++++++- 2 files changed, 117 insertions(+), 179 deletions(-) diff --git a/src/main/java/io/github/elimelt/pmqueue/QueueFactory.java b/src/main/java/io/github/elimelt/pmqueue/QueueFactory.java index 482c9b8..c4b8b7c 100644 --- a/src/main/java/io/github/elimelt/pmqueue/QueueFactory.java +++ b/src/main/java/io/github/elimelt/pmqueue/QueueFactory.java @@ -1,7 +1,6 @@ package io.github.elimelt.pmqueue; import java.io.IOException; -import java.util.logging.Logger; import io.github.elimelt.pmqueue.core.PersistentMessageQueue; @@ -20,21 +19,19 @@ * *

{@code
  * String filePath = "path/to/queue.dat";
- * MessageQueue queue = new QueueFactory.CustomQueueBuilder()
- *         .withFilePath(filePath)
- *         .withDebugEnabled(true)
- *         .withChecksumEnabled(true)
- *         .withMaxFileSize(1024 * 1024 * 1024)
- *         .withDefaultBufferSize(1024 * 1024)
- *         .withMaxBufferSize(8 * 1024 * 1024)
- *         .withBatchThreshold(64)
+ * QueueConfig config = new QueueConfig.Builder()
+ *         .filePath(filePath)
+ *         .debugEnabled(true)
+ *         .checksumEnabled(true)
+ *         .maxFileSize(1024 * 1024 * 1024)
+ *         .defaultBufferSize(1024 * 1024)
+ *         .maxBufferSize(8 * 1024 * 1024)
+ *         .batchThreshold(64)
  *         .build();
+ * MessageQueue queue = new PersistentMessageQueue(config);
  * }
*/ public class QueueFactory { - @SuppressWarnings("unused") - private static final Logger logger = Logger.getLogger(QueueFactory.class.getName()); - // prevent instantiation private QueueFactory() { } @@ -72,14 +69,7 @@ public static MessageQueue createQueue(String filePath) throws IOException { * @throws IOException if an I/O error occurs */ public static MessageQueue createHighThroughputQueue(String filePath) throws IOException { - return new PersistentMessageQueue( - new QueueConfig.Builder() - .filePath(filePath) - .defaultBufferSize(4 * 1024 * 1024) // 4MB default buffer - .maxBufferSize(16 * 1024 * 1024) // 16MB max buffer - .batchThreshold(256) // Larger batch size - .checksumEnabled(false) // Disable checksums for performance - .build()); + return QueuePreset.HIGH_THROUGHPUT.createQueue(filePath); } /** @@ -100,15 +90,7 @@ public static MessageQueue createHighThroughputQueue(String filePath) throws IOE * @throws IOException if an I/O error occurs */ public static MessageQueue createDurableQueue(String filePath) throws IOException { - return new PersistentMessageQueue( - new QueueConfig.Builder() - .filePath(filePath) - .defaultBufferSize(1024 * 1024) // 1MB default buffer - .maxBufferSize(4 * 1024 * 1024) // 4MB max buffer - .batchThreshold(32) // Smaller batch size - .checksumEnabled(true) // Enable checksums - .debugEnabled(true) // Enable debug logging - .build()); + return QueuePreset.DURABLE.createQueue(filePath); } /** @@ -127,14 +109,7 @@ public static MessageQueue createDurableQueue(String filePath) throws IOExceptio * @throws IOException if an I/O error occurs */ public static MessageQueue createLargeMessageQueue(String filePath) throws IOException { - return new PersistentMessageQueue( - new QueueConfig.Builder() - .filePath(filePath) - .defaultBufferSize(16 * 1024 * 1024) // 16MB default buffer - .maxBufferSize(32 * 1024 * 1024) // 32MB max buffer - .maxFileSize(10L * 1024L * 1024L * 1024L) // 10GB max file size - .batchThreshold(16) // Smaller batch size for large messages - .build()); + return QueuePreset.LARGE_MESSAGE.createQueue(filePath); } /** @@ -153,14 +128,7 @@ public static MessageQueue createLargeMessageQueue(String filePath) throws IOExc * @throws IOException if an I/O error occurs */ public static MessageQueue createLowMemoryQueue(String filePath) throws IOException { - return new PersistentMessageQueue( - new QueueConfig.Builder() - .filePath(filePath) - .defaultBufferSize(256 * 1024) // 256KB default buffer - .maxBufferSize(1024 * 1024) // 1MB max buffer - .batchThreshold(16) // Small batch size - .maxFileSize(1024L * 1024L * 1024L) // 1GB max file size - .build()); + return QueuePreset.LOW_MEMORY.createQueue(filePath); } /** @@ -182,127 +150,7 @@ public static MessageQueue createLowMemoryQueue(String filePath) throws IOExcept * @throws IOException if an I/O error occurs */ public static MessageQueue createDebugQueue(String filePath) throws IOException { - return new PersistentMessageQueue( - new QueueConfig.Builder() - .filePath(filePath) - .debugEnabled(true) - .checksumEnabled(true) - .defaultBufferSize(1024 * 1024) // 1MB default buffer - .maxBufferSize(2 * 1024 * 1024) // 2MB max buffer - .batchThreshold(32) - .build()); - } - - /** - * Creates a queue with a custom configuration. - * Features configurable buffer sizes, batch threshold, and checksum - * verification. - *

- * Configuration Defaults: - *

    - *
  • Default buffer size: 1MB
  • - *
  • Max buffer size: 8MB
  • - *
  • Batch threshold: 64
  • - *
  • Checksums: Enabled
  • - *
  • Debug logging: Enabled
  • - *
- */ - public static class CustomQueueBuilder { - private final QueueConfig.Builder builder; - - /** - * Creates a new CustomQueueBuilder. - */ - public CustomQueueBuilder() { - this.builder = new QueueConfig.Builder(); - } - - /** - * Sets the file path for the queue. - * - * @param filePath the file path for the queue - * @return this - */ - public CustomQueueBuilder withFilePath(String filePath) { - builder.filePath(filePath); - return this; - } - - /** - * Enables or disables debug mode. - * - * @param enabled true to enable debug logging - * @return this - */ - public CustomQueueBuilder withDebugEnabled(boolean enabled) { - builder.debugEnabled(enabled); - return this; - } - - /** - * Enables or disables checksums. - * - * @param enabled true to enable checksum verification - * @return this - */ - public CustomQueueBuilder withChecksumEnabled(boolean enabled) { - builder.checksumEnabled(enabled); - return this; - } - - /** - * Sets the maximum file size for the queue. - * - * @param maxFileSize the maximum file size - * @return this - */ - public CustomQueueBuilder withMaxFileSize(long maxFileSize) { - builder.maxFileSize(maxFileSize); - return this; - } - - /** - * Sets the initial file size for the queue. - * - * @param initialFileSize the initial file size - * @return this - */ - public CustomQueueBuilder withDefaultBufferSize(int initialFileSize) { - builder.defaultBufferSize(initialFileSize); - return this; - } - - /** - * Sets the maximum buffer size for the queue. - * - * @param size the maximum buffer size - * @return this - */ - public CustomQueueBuilder withMaxBufferSize(int size) { - builder.maxBufferSize(size); - return this; - } - - /** - * Sets the batch threshold for the queue. - * - * @param threshold the batch threshold - * @return this - */ - public CustomQueueBuilder withBatchThreshold(int threshold) { - builder.batchThreshold(threshold); - return this; - } - - /** - * Builds the MessageQueue instance. - * - * @return MessageQueue instance - * @throws IOException if an I/O error occurs - */ - public MessageQueue build() throws IOException { - return new PersistentMessageQueue(builder.build()); - } + return QueuePreset.DEBUG.createQueue(filePath); } /** @@ -341,7 +189,7 @@ void configure(QueueConfig.Builder builder) { } }, /** - * Configures the queue with large message settings. + * Configures the queue with low memory settings. */ LOW_MEMORY { /** @@ -351,8 +199,40 @@ void configure(QueueConfig.Builder builder) { void configure(QueueConfig.Builder builder) { builder.defaultBufferSize(256 * 1024) .maxBufferSize(1024 * 1024) + .batchThreshold(16) + .maxFileSize(1024L * 1024L * 1024L); + } + }, + /** + * Configures the queue with large message settings. + */ + LARGE_MESSAGE { + /** + * Configures the queue with large message settings. + */ + @Override + void configure(QueueConfig.Builder builder) { + builder.defaultBufferSize(16 * 1024 * 1024) + .maxBufferSize(32 * 1024 * 1024) + .maxFileSize(10L * 1024L * 1024L * 1024L) .batchThreshold(16); } + }, + /** + * Configures the queue with debug settings. + */ + DEBUG { + /** + * Configures the queue with debug settings. + */ + @Override + void configure(QueueConfig.Builder builder) { + builder.debugEnabled(true) + .checksumEnabled(true) + .defaultBufferSize(1024 * 1024) + .maxBufferSize(2 * 1024 * 1024) + .batchThreshold(32); + } }; abstract void configure(QueueConfig.Builder builder); diff --git a/src/test/java/io/github/elimelt/pmqueue/QueueFactoryTest.java b/src/test/java/io/github/elimelt/pmqueue/QueueFactoryTest.java index bfb4aa9..1109930 100644 --- a/src/test/java/io/github/elimelt/pmqueue/QueueFactoryTest.java +++ b/src/test/java/io/github/elimelt/pmqueue/QueueFactoryTest.java @@ -159,20 +159,78 @@ void debugQueueCreation() throws Exception { } @Test - @DisplayName("Custom queue builder should create queue with specified settings") - void customQueueBuilderCreation() throws Exception { - String filePath = getTestFilePath("custom"); - MessageQueue queue = new QueueFactory.CustomQueueBuilder() - .withFilePath(filePath) - .withDefaultBufferSize(2 * 1024 * 1024) - .withMaxBufferSize(4 * 1024 * 1024) - .withBatchThreshold(64) - .withChecksumEnabled(true) - .withDebugEnabled(true) - .build(); + @DisplayName("Factory methods should be equivalent to delegating to their preset") + void factoryMethodsMatchPresetValues() throws Exception { + // Each createXxxQueue(String) method now delegates directly to a + // QueuePreset. Verify the preset's configure() still produces the exact + // QueueConfig values the original hand-written factory methods used, so + // the presets remain the single source of truth without value drift. + assertPresetConfig(QueueFactory.QueuePreset.HIGH_THROUGHPUT, config -> { + assertEquals(4 * 1024 * 1024, config.getDefaultBufferSize()); + assertEquals(16 * 1024 * 1024, config.getMaxBufferSize()); + assertEquals(256, config.getBatchThreshold()); + assertFalse(config.isChecksumEnabled()); + }); + + assertPresetConfig(QueueFactory.QueuePreset.DURABLE, config -> { + assertEquals(1024 * 1024, config.getDefaultBufferSize()); + assertEquals(4 * 1024 * 1024, config.getMaxBufferSize()); + assertEquals(32, config.getBatchThreshold()); + assertTrue(config.isChecksumEnabled()); + assertTrue(config.isDebugEnabled()); + }); + + assertPresetConfig(QueueFactory.QueuePreset.LOW_MEMORY, config -> { + assertEquals(256 * 1024, config.getDefaultBufferSize()); + assertEquals(1024 * 1024, config.getMaxBufferSize()); + assertEquals(16, config.getBatchThreshold()); + assertEquals(1024L * 1024L * 1024L, config.getMaxFileSize()); + }); + + assertPresetConfig(QueueFactory.QueuePreset.LARGE_MESSAGE, config -> { + assertEquals(16 * 1024 * 1024, config.getDefaultBufferSize()); + assertEquals(32 * 1024 * 1024, config.getMaxBufferSize()); + assertEquals(10L * 1024L * 1024L * 1024L, config.getMaxFileSize()); + assertEquals(16, config.getBatchThreshold()); + }); + + assertPresetConfig(QueueFactory.QueuePreset.DEBUG, config -> { + assertTrue(config.isDebugEnabled()); + assertTrue(config.isChecksumEnabled()); + assertEquals(1024 * 1024, config.getDefaultBufferSize()); + assertEquals(2 * 1024 * 1024, config.getMaxBufferSize()); + assertEquals(32, config.getBatchThreshold()); + }); + // Also confirm the queues built by the factory methods and by the + // presets both actually work end to end (open, read/write, close). + assertQueueWorks(QueueFactory.createHighThroughputQueue(getTestFilePath("factory-high-throughput"))); + assertQueueWorks(QueueFactory.QueuePreset.HIGH_THROUGHPUT.createQueue(getTestFilePath("preset-high-throughput"))); + + assertQueueWorks(QueueFactory.createDurableQueue(getTestFilePath("factory-durable"))); + assertQueueWorks(QueueFactory.QueuePreset.DURABLE.createQueue(getTestFilePath("preset-durable"))); + + assertQueueWorks(QueueFactory.createLowMemoryQueue(getTestFilePath("factory-low-memory"))); + assertQueueWorks(QueueFactory.QueuePreset.LOW_MEMORY.createQueue(getTestFilePath("preset-low-memory"))); + + assertQueueWorks(QueueFactory.createLargeMessageQueue(getTestFilePath("factory-large-message"))); + assertQueueWorks(QueueFactory.QueuePreset.LARGE_MESSAGE.createQueue(getTestFilePath("preset-large-message"))); + + assertQueueWorks(QueueFactory.createDebugQueue(getTestFilePath("factory-debug"))); + assertQueueWorks(QueueFactory.QueuePreset.DEBUG.createQueue(getTestFilePath("preset-debug"))); + } + + private void assertPresetConfig(QueueFactory.QueuePreset preset, java.util.function.Consumer assertions) + throws IOException { + QueueConfig.Builder builder = new QueueConfig.Builder().filePath(getTestFilePath("config-" + preset.name())); + preset.configure(builder); + assertions.accept(builder.build()); + } + + private void assertQueueWorks(MessageQueue queue) throws Exception { assertNotNull(queue); - assertTrue(new File(filePath).exists()); + assertTrue(queue.offer(new Message("test".getBytes(), 1))); + assertNotNull(queue.poll()); queue.close(); } From 9b7880aa59df227b34598b0ffec3ff7d5e617700 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:10 -0700 Subject: [PATCH 08/14] QueueConfig owns QUEUE_HEADER_SIZE; filePath(null) fails fast --- .../io/github/elimelt/pmqueue/QueueConfig.java | 15 ++++++++++----- .../pmqueue/core/PersistentMessageQueue.java | 4 +++- .../{QueueConfig.java => QueueConfigTest.java} | 10 ++++------ 3 files changed, 17 insertions(+), 12 deletions(-) rename src/test/java/io/github/elimelt/pmqueue/{QueueConfig.java => QueueConfigTest.java} (96%) diff --git a/src/main/java/io/github/elimelt/pmqueue/QueueConfig.java b/src/main/java/io/github/elimelt/pmqueue/QueueConfig.java index ad1eb6b..7d12b96 100644 --- a/src/main/java/io/github/elimelt/pmqueue/QueueConfig.java +++ b/src/main/java/io/github/elimelt/pmqueue/QueueConfig.java @@ -1,6 +1,6 @@ package io.github.elimelt.pmqueue; -import io.github.elimelt.pmqueue.core.PersistentMessageQueue; +import java.util.Objects; /** * Configuration for a message queue. @@ -22,6 +22,11 @@ * } */ public class QueueConfig { + /** + * The size of the queue file header in bytes. + */ + public static final int QUEUE_HEADER_SIZE = 24; + private final String filePath; private final boolean debugEnabled; private final boolean checksumEnabled; @@ -52,7 +57,7 @@ public static class Builder { private boolean debugEnabled = false; private boolean checksumEnabled = true; private long maxFileSize = 1024L * 1024L * 1024L; // 1GB - private int initialFileSize = PersistentMessageQueue.QUEUE_HEADER_SIZE; + private int initialFileSize = QUEUE_HEADER_SIZE; private int defaultBufferSize = 1024 * 1024; // 1MB private int maxBufferSize = 8 * 1024 * 1024; // 8MB private int batchThreshold = 64; @@ -70,7 +75,7 @@ public Builder() { * @return this */ public Builder filePath(String filePath) { - this.filePath = filePath; + this.filePath = Objects.requireNonNull(filePath, "filePath must not be null"); return this; } @@ -161,8 +166,8 @@ public QueueConfig build() { if (maxBufferSize < defaultBufferSize) { throw new IllegalArgumentException("maxBufferSize must be >= defaultBufferSize"); } - if (initialFileSize < PersistentMessageQueue.QUEUE_HEADER_SIZE) { - throw new IllegalArgumentException("initialFileSize must be >= " + PersistentMessageQueue.QUEUE_HEADER_SIZE); + if (initialFileSize < QUEUE_HEADER_SIZE) { + throw new IllegalArgumentException("initialFileSize must be >= " + QUEUE_HEADER_SIZE); } if (batchThreshold <= 0) { throw new IllegalArgumentException("batchThreshold must be > 0"); diff --git a/src/main/java/io/github/elimelt/pmqueue/core/PersistentMessageQueue.java b/src/main/java/io/github/elimelt/pmqueue/core/PersistentMessageQueue.java index 467948b..26b9141 100644 --- a/src/main/java/io/github/elimelt/pmqueue/core/PersistentMessageQueue.java +++ b/src/main/java/io/github/elimelt/pmqueue/core/PersistentMessageQueue.java @@ -100,8 +100,10 @@ public class PersistentMessageQueue implements MessageQueue { /** * The size of the queue header in bytes. + * + * @see QueueConfig#QUEUE_HEADER_SIZE */ - public static final int QUEUE_HEADER_SIZE = 24; + public static final int QUEUE_HEADER_SIZE = QueueConfig.QUEUE_HEADER_SIZE; /** * The size of the block header in bytes. diff --git a/src/test/java/io/github/elimelt/pmqueue/QueueConfig.java b/src/test/java/io/github/elimelt/pmqueue/QueueConfigTest.java similarity index 96% rename from src/test/java/io/github/elimelt/pmqueue/QueueConfig.java rename to src/test/java/io/github/elimelt/pmqueue/QueueConfigTest.java index b35438e..c04ff97 100644 --- a/src/test/java/io/github/elimelt/pmqueue/QueueConfig.java +++ b/src/test/java/io/github/elimelt/pmqueue/QueueConfigTest.java @@ -118,13 +118,11 @@ void configShouldBeImmutable() { } @Test - @DisplayName("Builder should handle null file path") - void builderShouldHandleNullFilePath() { - QueueConfig config = new QueueConfig.Builder() - .filePath(null) - .build(); + @DisplayName("Builder should reject null file path") + void builderShouldRejectNullFilePath() { + QueueConfig.Builder builder = new QueueConfig.Builder(); - assertNull(config.getFilePath()); + assertThrows(NullPointerException.class, () -> builder.filePath(null)); } @Test From 9ec566075719f4364a571b78295acbe3e6b25b76 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:11 -0700 Subject: [PATCH 09/14] CI on Temurin JDK 21; wire publish credentials --- .github/workflows/publish.yml | 7 ++++--- .github/workflows/test.yml | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9d6aa1f..e585924 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,13 +14,14 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '21' distribution: 'temurin' cache: maven - + server-id: github + - name: Build with Maven run: mvn -B package --file pom.xml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 90db424..289dcc4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '21' distribution: 'temurin' cache: maven From 4890de7e1e7e873df32cf3b1565251a4d581cdb3 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:11 -0700 Subject: [PATCH 10/14] README quickstart and build instructions --- README.md | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f2da31..610a284 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,41 @@ `pmqueue` is a simple persistent message queue written in Java (no dependencies). -## Usage +## Quickstart -Just read the [docs](https://elimelt.com/pmqueue/) +```java +import io.github.elimelt.pmqueue.MessageQueue; +import io.github.elimelt.pmqueue.QueueFactory; +import io.github.elimelt.pmqueue.message.Message; + +try (MessageQueue queue = QueueFactory.createQueue("path/to/queue.dat")) { + queue.offer(new Message("Hello, World!".getBytes(), 1)); + Message message = queue.poll(); + System.out.println(new String(message.getData())); +} +``` + +`offer`, `poll`, and `close` throw `IOException`. `QueueFactory` also provides +`createHighThroughputQueue`, `createDurableQueue`, `createLargeMessageQueue`, +`createLowMemoryQueue`, and `createDebugQueue`, each taking a file path. + +## Build and test + +Requires JDK 21+. + +``` +./run_tests.sh +``` + +This downloads a JDK and test dependencies into `target/` on first run, then +compiles and runs the test suite. No Maven install needed. + +Alternatively, with Maven installed: + +``` +mvn test +``` + +## Docs + +Full docs: https://elimelt.com/pmqueue/ From 5630cf04b8fe9b7061cb22130dc170c80ac979ef Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:11 -0700 Subject: [PATCH 11/14] Pom targets Java 21, dead build config removed --- pom.xml | 62 ++++----------------------------------------------------- 1 file changed, 4 insertions(+), 58 deletions(-) diff --git a/pom.xml b/pom.xml index d80bde4..d556f3f 100644 --- a/pom.xml +++ b/pom.xml @@ -40,10 +40,9 @@ ${project.version} UTF-8 - 21 - 21 - 5.9.2 - 5.3.1 + 21 + 5.10.2 + 5.11.0 @@ -78,14 +77,6 @@ ${mockito.version} test - - - - org.mockito - mockito-junit-jupiter - ${mockito.version} - test - @@ -102,18 +93,9 @@ maven-compiler-plugin 3.11.0 - 22 - - --enable-preview - - 21 - 21 + 21 - maven-surefire-plugin 3.1.2 @@ -123,7 +105,6 @@ classes 4 - --enable-preview @@ -171,39 +152,4 @@ https://maven.pkg.github.com/elimelt/pmqueue - - - - release - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.1.0 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - ossrh - https://s01.oss.sonatype.org/ - true - - - - - - \ No newline at end of file From 9110427caa86b2de09ec44fe52073238e1007c07 Mon Sep 17 00:00:00 2001 From: Elijah Melton Date: Fri, 14 Aug 2026 22:59:12 -0700 Subject: [PATCH 12/14] Regenerate docs from current source; javadoc-based script --- docs/META-INF/MANIFEST.MF | 4 - docs/allclasses-index.html | 48 +- docs/allpackages-index.html | 24 +- docs/constant-values.html | 45 +- docs/copy.svg | 33 - docs/help-doc.html | 65 +- docs/index-all.html | 116 +-- docs/index.html | 27 +- .../github/elimelt/pmqueue/MessageQueue.html | 102 +- .../elimelt/pmqueue/QueueConfig.Builder.html | 68 +- .../github/elimelt/pmqueue/QueueConfig.html | 110 ++- .../QueueFactory.CustomQueueBuilder.html | 332 ------- .../pmqueue/QueueFactory.QueuePreset.html | 148 +-- .../github/elimelt/pmqueue/QueueFactory.html | 136 ++- .../pmqueue/class-use/MessageQueue.html | 182 ---- .../class-use/QueueConfig.Builder.html | 124 --- .../pmqueue/class-use/QueueConfig.html | 107 --- .../QueueFactory.CustomQueueBuilder.html | 119 --- .../class-use/QueueFactory.QueuePreset.html | 95 -- .../pmqueue/class-use/QueueFactory.html | 62 -- .../consumer/DefaultMessageConsumer.html | 78 +- .../pmqueue/consumer/MessageConsumer.html | 62 +- .../class-use/DefaultMessageConsumer.html | 62 -- .../consumer/class-use/MessageConsumer.html | 89 -- .../pmqueue/consumer/package-summary.html | 45 +- .../pmqueue/consumer/package-tree.html | 32 +- .../elimelt/pmqueue/consumer/package-use.html | 86 -- .../pmqueue/core/PersistentMessageQueue.html | 109 +-- .../class-use/PersistentMessageQueue.html | 62 -- .../elimelt/pmqueue/core/package-summary.html | 35 +- .../elimelt/pmqueue/core/package-tree.html | 30 +- .../elimelt/pmqueue/core/package-use.html | 62 -- .../pmqueue/impl/AutoFlushingQueue.html | 282 ------ .../elimelt/pmqueue/impl/ValidatingQueue.html | 278 ------ .../impl/class-use/AutoFlushingQueue.html | 62 -- .../impl/class-use/ValidatingQueue.html | 62 -- .../elimelt/pmqueue/impl/package-summary.html | 121 --- .../elimelt/pmqueue/impl/package-tree.html | 77 -- .../elimelt/pmqueue/impl/package-use.html | 62 -- .../elimelt/pmqueue/message/Message.html | 127 ++- .../pmqueue/message/MessageSerializer.html | 87 +- .../pmqueue/message/class-use/Message.html | 180 ---- .../message/class-use/MessageSerializer.html | 62 -- .../pmqueue/message/package-summary.html | 39 +- .../elimelt/pmqueue/message/package-tree.html | 32 +- .../elimelt/pmqueue/message/package-use.html | 135 --- .../elimelt/pmqueue/package-summary.html | 55 +- .../github/elimelt/pmqueue/package-tree.html | 41 +- .../github/elimelt/pmqueue/package-use.html | 151 --- .../producer/DefaultMessageProducer.html | 76 +- .../pmqueue/producer/MessageProducer.html | 66 +- .../class-use/DefaultMessageProducer.html | 62 -- .../producer/class-use/MessageProducer.html | 89 -- .../pmqueue/producer/package-summary.html | 45 +- .../pmqueue/producer/package-tree.html | 32 +- .../elimelt/pmqueue/producer/package-use.html | 86 -- docs/jquery-ui.overrides.css | 35 + docs/legal/ASSEMBLY_EXCEPTION | 6 +- docs/legal/jquery.md | 50 +- docs/legal/jqueryUI.md | 4 +- docs/link.svg | 31 - docs/member-search-index.js | 2 +- docs/overview-summary.html | 7 +- docs/overview-tree.html | 43 +- docs/script-dir/jquery-3.6.1.min.js | 2 - docs/script-dir/jquery-3.7.1.min.js | 2 + docs/script-dir/jquery-ui.min.css | 8 +- docs/script-dir/jquery-ui.min.js | 8 +- docs/script.js | 143 +-- docs/search-page.js | 284 ------ docs/search.html | 77 -- docs/search.js | 612 +++++------- docs/serialized-form.html | 143 --- docs/stylesheet.css | 869 +++++------------- docs/tag-search-index.js | 2 +- docs/type-search-index.js | 2 +- update_javadoc.sh | 57 +- 77 files changed, 1387 insertions(+), 6008 deletions(-) delete mode 100644 docs/META-INF/MANIFEST.MF delete mode 100644 docs/copy.svg delete mode 100644 docs/io/github/elimelt/pmqueue/QueueFactory.CustomQueueBuilder.html delete mode 100644 docs/io/github/elimelt/pmqueue/class-use/MessageQueue.html delete mode 100644 docs/io/github/elimelt/pmqueue/class-use/QueueConfig.Builder.html delete mode 100644 docs/io/github/elimelt/pmqueue/class-use/QueueConfig.html delete mode 100644 docs/io/github/elimelt/pmqueue/class-use/QueueFactory.CustomQueueBuilder.html delete mode 100644 docs/io/github/elimelt/pmqueue/class-use/QueueFactory.QueuePreset.html delete mode 100644 docs/io/github/elimelt/pmqueue/class-use/QueueFactory.html delete mode 100644 docs/io/github/elimelt/pmqueue/consumer/class-use/DefaultMessageConsumer.html delete mode 100644 docs/io/github/elimelt/pmqueue/consumer/class-use/MessageConsumer.html delete mode 100644 docs/io/github/elimelt/pmqueue/consumer/package-use.html delete mode 100644 docs/io/github/elimelt/pmqueue/core/class-use/PersistentMessageQueue.html delete mode 100644 docs/io/github/elimelt/pmqueue/core/package-use.html delete mode 100644 docs/io/github/elimelt/pmqueue/impl/AutoFlushingQueue.html delete mode 100644 docs/io/github/elimelt/pmqueue/impl/ValidatingQueue.html delete mode 100644 docs/io/github/elimelt/pmqueue/impl/class-use/AutoFlushingQueue.html delete mode 100644 docs/io/github/elimelt/pmqueue/impl/class-use/ValidatingQueue.html delete mode 100644 docs/io/github/elimelt/pmqueue/impl/package-summary.html delete mode 100644 docs/io/github/elimelt/pmqueue/impl/package-tree.html delete mode 100644 docs/io/github/elimelt/pmqueue/impl/package-use.html delete mode 100644 docs/io/github/elimelt/pmqueue/message/class-use/Message.html delete mode 100644 docs/io/github/elimelt/pmqueue/message/class-use/MessageSerializer.html delete mode 100644 docs/io/github/elimelt/pmqueue/message/package-use.html delete mode 100644 docs/io/github/elimelt/pmqueue/package-use.html delete mode 100644 docs/io/github/elimelt/pmqueue/producer/class-use/DefaultMessageProducer.html delete mode 100644 docs/io/github/elimelt/pmqueue/producer/class-use/MessageProducer.html delete mode 100644 docs/io/github/elimelt/pmqueue/producer/package-use.html create mode 100644 docs/jquery-ui.overrides.css delete mode 100644 docs/link.svg delete mode 100644 docs/script-dir/jquery-3.6.1.min.js create mode 100644 docs/script-dir/jquery-3.7.1.min.js delete mode 100644 docs/search-page.js delete mode 100644 docs/search.html delete mode 100644 docs/serialized-form.html diff --git a/docs/META-INF/MANIFEST.MF b/docs/META-INF/MANIFEST.MF deleted file mode 100644 index 2fe49f6..0000000 --- a/docs/META-INF/MANIFEST.MF +++ /dev/null @@ -1,4 +0,0 @@ -Manifest-Version: 1.0 -Created-By: Maven Javadoc Plugin 3.5.0 -Build-Jdk-Spec: 21 - diff --git a/docs/allclasses-index.html b/docs/allclasses-index.html index f90dfba..6e9ff9b 100644 --- a/docs/allclasses-index.html +++ b/docs/allclasses-index.html @@ -1,21 +1,25 @@ - -All Classes and Interfaces (pmqueue 1.0-SNAPSHOT API) + +All Classes and Interfaces - - + + - + -