From 4b736aef9e88114e8f5708e3469c1c0819b0e410 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 8 Sep 2026 07:01:20 +0100 Subject: [PATCH] Preserve Java 8 buffer descriptors in persisted Map I/O Use Buffer-typed cursor calls so modern source/target-8 compilation does not link Map header I/O against Java 9+ ByteBuffer methods. Include the global-state flip required by reopening, exposed by exercising the packaged artifact on Java 8. Extract the compatibility changes identified in #588 and add create/reopen/recover/reopen coverage. The Java-21-built baseline fails on Java 8; the corrected packaged JAR passes, as does full Java 21 verification. --- .../hash/impl/VanillaChronicleHash.java | 4 +- .../chronicle/map/ChronicleMapBuilder.java | 17 +++++--- .../chronicle/map/PersistedMapHeaderTest.java | 43 +++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 src/test/java/net/openhft/chronicle/map/PersistedMapHeaderTest.java diff --git a/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java b/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java index b201d0526..06f871b9b 100644 --- a/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java +++ b/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java @@ -37,6 +37,7 @@ import java.io.RandomAccessFile; import java.lang.ref.WeakReference; import java.lang.reflect.Type; +import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; @@ -425,7 +426,8 @@ public final void initBeforeMapping(@NotNull final File file, throw throwRecoveryOrReturnIOException(file, "truncated", recover); } } - globalMutableStateBuffer.flip(); + //! Keep the Java 8 Buffer descriptor when this reopening path is compiled on newer JDKs. + ((Buffer) globalMutableStateBuffer).flip(); //noinspection unchecked globalMutableState.bytesStore(BytesStore.wrap(globalMutableStateBuffer), 0, globalMutableState.maxSize()); } diff --git a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java index 0102436ad..0ee34f625 100644 --- a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java +++ b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java @@ -41,6 +41,7 @@ import java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; +import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.HashMap; @@ -356,7 +357,8 @@ private static void writeNotComplete(@NotNull final FileChannel fileChannel, final int headerSize) throws IOException { //noinspection PointlessBitwiseExpression headerBuffer.putInt(SIZE_WORD_OFFSET, NOT_COMPLETE | DATA | headerSize); - headerBuffer.clear().position(SIZE_WORD_OFFSET).limit(SIZE_WORD_OFFSET + 4); + //! Buffer views preserve Java 8 call descriptors when compiling on newer JDKs. + ((Buffer) headerBuffer).clear().position(SIZE_WORD_OFFSET).limit(SIZE_WORD_OFFSET + 4); writeFully(fileChannel, SIZE_WORD_OFFSET, headerBuffer); } @@ -388,11 +390,12 @@ private static ByteBuffer writeHeader(@NotNull final FileChannel fileChan headerBuffer.putInt(SIZE_WORD_OFFSET, NOT_COMPLETE | DATA | headerSize); // Write the size-prefixed blob to the file - headerBuffer.position(0); - headerBuffer.limit(headerLimit); + final Buffer headerView = headerBuffer; + headerView.position(0); + headerView.limit(headerLimit); writeFully(fileChannel, 0, headerBuffer); - headerBuffer.position(SELF_BOOTSTRAPPING_HEADER_OFFSET); + headerView.position(SELF_BOOTSTRAPPING_HEADER_OFFSET); return headerBuffer; } @@ -406,7 +409,7 @@ private static void commitChronicleMapReady(@NotNull final VanillaChronicleHash //noinspection PointlessBitwiseExpression headerBuffer.putInt(SIZE_WORD_OFFSET, READY | DATA | headerSize); - headerBuffer.clear().position(SIZE_WORD_OFFSET).limit(SIZE_WORD_OFFSET + 4); + ((Buffer) headerBuffer).clear().position(SIZE_WORD_OFFSET).limit(SIZE_WORD_OFFSET + 4); writeFully(fileChannel, SIZE_WORD_OFFSET, headerBuffer); } @@ -446,7 +449,7 @@ private static ByteBuffer readSelfBootstrappingHeader(@NotNull final File file, throw new IOException("file=" + file + ": sizeWord is not ready: " + sizeWord); } } - headerBuffer.position(SELF_BOOTSTRAPPING_HEADER_OFFSET); + ((Buffer) headerBuffer).position(SELF_BOOTSTRAPPING_HEADER_OFFSET); return headerBuffer; } @@ -1789,7 +1792,7 @@ private int waitUntilReady(@NotNull final RandomAccessFile raf, for (int attempt = 0; attempt < attempts; attempt++) { if (raf.length() >= SELF_BOOTSTRAPPING_HEADER_OFFSET) { - sizeWordBuffer.clear(); + ((Buffer) sizeWordBuffer).clear(); readFully(fileChannel, SIZE_WORD_OFFSET, sizeWordBuffer); if (sizeWordBuffer.remaining() == 0) { int sizeWord = sizeWordBuffer.getInt(0); diff --git a/src/test/java/net/openhft/chronicle/map/PersistedMapHeaderTest.java b/src/test/java/net/openhft/chronicle/map/PersistedMapHeaderTest.java new file mode 100644 index 000000000..9eb3f6d26 --- /dev/null +++ b/src/test/java/net/openhft/chronicle/map/PersistedMapHeaderTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.chronicle.map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class PersistedMapHeaderTest { + @Rule + public final TemporaryFolder temporaryFolder = TemporaryFolder.builder().assureDeletion().build(); + + @Test + public void createReopenAndRecoverPreserveEntries() throws IOException { + File file = temporaryFolder.newFile("header.map"); + ChronicleMapBuilder builder = ChronicleMap.of(Long.class, Long.class).entries(10); + + try (ChronicleMap created = builder.createPersistedTo(file)) { + created.put(1L, 42L); + assertEquals(Long.valueOf(42L), created.get(1L)); + } + try (ChronicleMap reopened = builder.createPersistedTo(file)) { + assertEquals(Long.valueOf(42L), reopened.get(1L)); + reopened.put(2L, 43L); + } + try (ChronicleMap recovered = builder.recoverPersistedTo(file, true)) { + assertEquals(2, recovered.size()); + assertEquals(Long.valueOf(42L), recovered.get(1L)); + assertEquals(Long.valueOf(43L), recovered.get(2L)); + } + try (ChronicleMap reopened = builder.createPersistedTo(file)) { + assertEquals(2, reopened.size()); + assertEquals(Long.valueOf(42L), reopened.get(1L)); + assertEquals(Long.valueOf(43L), reopened.get(2L)); + } + } +}