This is an automated email from the ASF dual-hosted git repository.
spmallette pushed a commit to branch tinkergraph-storage
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/tinkergraph-storage by this
push:
new b8b4b3a3f1 Rename ByteBufferBuffer to TinkerByteBuffer and add its
unit tests
b8b4b3a3f1 is described below
commit b8b4b3a3f11a4c9cc5cccbb27d53efc1c71e4895
Author: Stephen Mallette <[email protected]>
AuthorDate: Wed Sep 9 15:08:37 2026 -0400
Rename ByteBufferBuffer to TinkerByteBuffer and add its unit tests
The buffer implementation backing the GraphBinary storage codec had no
direct
tests. Covers the primitive round-trips, the big-endian byte order the
on-disk
format depends on, index and capacity handling, the bulk transfer and nio
views,
and the bounds rejections.
Assisted-by: Claude Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01KgH2VCpRw57sbFg5GoAiVV
---
.../structure/storage/GraphBinaryStorage.java | 30 +--
...ByteBufferBuffer.java => TinkerByteBuffer.java} | 8 +-
.../structure/storage/TinkerByteBufferTest.java | 243 +++++++++++++++++++++
3 files changed, 262 insertions(+), 19 deletions(-)
diff --git
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorage.java
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorage.java
index a5c4707409..0349c4a358 100644
---
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorage.java
+++
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorage.java
@@ -109,7 +109,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
protected byte[] encodeCommit(final long txVersion,
final
Collection<TinkerStorageMutation<TinkerVertex>> changedVertices,
final
Collection<TinkerStorageMutation<TinkerEdge>> changedEdges) throws IOException {
- final ByteBufferBuffer buf = new ByteBufferBuffer();
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
// register the strings introduced by this commit (deletes carry only
an id, no strings)
final List<String> appends = new ArrayList<>();
for (final TinkerStorageMutation<TinkerVertex> m : changedVertices)
@@ -159,7 +159,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
while (edges.hasNext())
registerEdgeStrings(edges.next(), ignored);
- final ByteBufferBuffer dictBuf = new ByteBufferBuffer();
+ final TinkerByteBuffer dictBuf = new TinkerByteBuffer();
writeVarInt(dictBuf, idToKey.size());
for (int id = 0; id < idToKey.size(); id++) {
dictBuf.writeByte(OP_DICT_APPEND);
@@ -182,7 +182,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
* is never buffered whole.
*/
private void writeElementFrame(final DataOutputStream out, final byte op,
final Object element) throws IOException {
- final ByteBufferBuffer buf = new ByteBufferBuffer();
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
writeVarInt(buf, 1);
buf.writeByte(op);
if (op == OP_PUT_VERTEX) writeVertexRecord(buf, (Vertex) element);
@@ -219,7 +219,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
}
}
- private void writeVertexRecord(final ByteBufferBuffer buf, final Vertex v)
throws IOException {
+ private void writeVertexRecord(final TinkerByteBuffer buf, final Vertex v)
throws IOException {
writeScalar(buf, v.id());
final Set<String> labels = v.labels();
writeVarInt(buf, labels.size());
@@ -256,7 +256,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
}
}
- private void writeEdgeRecord(final ByteBufferBuffer buf, final Edge e)
throws IOException {
+ private void writeEdgeRecord(final TinkerByteBuffer buf, final Edge e)
throws IOException {
writeScalar(buf, e.id());
writeVarInt(buf, keyToId.get(e.label()));
writeScalar(buf, e.outVertex().id());
@@ -276,7 +276,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
protected void decodeFrame(final byte[] record,
final Map<Object, DetachedVertex> vertices,
final Map<Object, DetachedEdge> edges) throws
IOException {
- final ByteBufferBuffer buf = new ByteBufferBuffer(record);
+ final TinkerByteBuffer buf = new TinkerByteBuffer(record);
final int entryCount = readVarInt(buf);
for (int i = 0; i < entryCount; i++) {
final byte op = buf.readByte();
@@ -321,7 +321,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
}
}
- private DetachedVertex readVertexRecord(final ByteBufferBuffer buf) throws
IOException {
+ private DetachedVertex readVertexRecord(final TinkerByteBuffer buf) throws
IOException {
final Object id = readScalar(buf);
final DetachedVertex.Builder b = DetachedVertex.build().setId(id);
final int labelCount = readVarInt(buf);
@@ -355,7 +355,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
return b.create();
}
- private DetachedEdge readEdgeRecord(final ByteBufferBuffer buf) throws
IOException {
+ private DetachedEdge readEdgeRecord(final TinkerByteBuffer buf) throws
IOException {
final Object id = readScalar(buf);
final String label = resolveKey(buf);
final Object outVId = readScalar(buf);
@@ -379,7 +379,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
* is a single {@link DataType#UNSPECIFIED_NULL} tag.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
- private void writeScalar(final ByteBufferBuffer buf, final Object value)
throws IOException {
+ private void writeScalar(final TinkerByteBuffer buf, final Object value)
throws IOException {
if (value == null) {
buf.writeByte(DataType.UNSPECIFIED_NULL.getCodeByte());
return;
@@ -390,7 +390,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
}
@SuppressWarnings({"unchecked", "rawtypes"})
- private Object readScalar(final ByteBufferBuffer buf) throws IOException {
+ private Object readScalar(final TinkerByteBuffer buf) throws IOException {
final int code = Byte.toUnsignedInt(buf.readByte());
final DataType dataType = DataType.get(code);
if (dataType == null)
@@ -401,7 +401,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
return serializer.readValue(buf, reader, false);
}
- private static void writeString(final ByteBufferBuffer buf, final String
s) {
+ private static void writeString(final TinkerByteBuffer buf, final String
s) {
final byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
writeVarInt(buf, bytes.length);
buf.writeBytes(bytes);
@@ -412,7 +412,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
* hold is corruption, and is reported as such rather than raised as an
{@code IndexOutOfBoundsException} from the
* backing list.
*/
- private String resolveKey(final ByteBufferBuffer buf) throws IOException {
+ private String resolveKey(final TinkerByteBuffer buf) throws IOException {
final int id = readVarInt(buf);
if (id >= idToKey.size())
throw new IOException(String.format(
@@ -420,7 +420,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
return idToKey.get(id);
}
- private static String readString(final ByteBufferBuffer buf) throws
IOException {
+ private static String readString(final TinkerByteBuffer buf) throws
IOException {
final int length = readVarInt(buf);
// check the declared length against what the frame actually holds
before allocating. The frame itself is
// already bounded against the file by AbstractLogStorage.readFrame,
but a length inside the frame is not,
@@ -439,7 +439,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
* Unsigned LEB128 varint. Counts and dictionary refs are small and
non-negative, so they cost one byte in the
* common case.
*/
- private static void writeVarInt(final ByteBufferBuffer buf, final int
value) {
+ private static void writeVarInt(final TinkerByteBuffer buf, final int
value) {
int v = value;
while ((v & ~0x7F) != 0) {
buf.writeByte((v & 0x7F) | 0x80);
@@ -448,7 +448,7 @@ public final class GraphBinaryStorage extends
AbstractLogStorage {
buf.writeByte(v & 0x7F);
}
- private static int readVarInt(final ByteBufferBuffer buf) throws
IOException {
+ private static int readVarInt(final TinkerByteBuffer buf) throws
IOException {
int result = 0;
int shift = 0;
byte b;
diff --git
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/ByteBufferBuffer.java
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/TinkerByteBuffer.java
similarity index 97%
rename from
tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/ByteBufferBuffer.java
rename to
tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/TinkerByteBuffer.java
index 8872afd404..d32a212a30 100644
---
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/ByteBufferBuffer.java
+++
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/TinkerByteBuffer.java
@@ -32,7 +32,7 @@ import java.util.Arrays;
* as needed on write. This class is not thread-safe; a buffer is used by a
single thread for a single
* serialize/deserialize.
*/
-public final class ByteBufferBuffer implements Buffer {
+public final class TinkerByteBuffer implements Buffer {
private static final int DEFAULT_CAPACITY = 256;
@@ -42,18 +42,18 @@ public final class ByteBufferBuffer implements Buffer {
private int markedWriterIndex = 0;
private int referenceCount = 1;
- public ByteBufferBuffer() {
+ public TinkerByteBuffer() {
this(DEFAULT_CAPACITY);
}
- public ByteBufferBuffer(final int initialCapacity) {
+ public TinkerByteBuffer(final int initialCapacity) {
this.array = new byte[Math.max(initialCapacity, 1)];
}
/**
* Wraps an existing array for reading. The writer index is positioned at
the end of the supplied data.
*/
- public ByteBufferBuffer(final byte[] data) {
+ public TinkerByteBuffer(final byte[] data) {
this.array = data;
this.writerIndex = data.length;
}
diff --git
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/TinkerByteBufferTest.java
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/TinkerByteBufferTest.java
new file mode 100644
index 0000000000..006adba8c8
--- /dev/null
+++
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/TinkerByteBufferTest.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.tinkergraph.structure.storage;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit tests for {@link TinkerByteBuffer}, the heap {@code byte[]}
implementation of the gremlin-core
+ * {@code Buffer} abstraction that backs the GraphBinary storage codec. The
byte order and bounds behaviour asserted
+ * here are relied on by the on-disk format, so a change that breaks them
changes what a store means.
+ */
+public class TinkerByteBufferTest {
+
+ @Test
+ public void shouldRoundTripEveryPrimitive() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+ buf.writeBoolean(true).writeBoolean(false)
+ .writeByte(0x7F).writeShort(-2)
+ .writeInt(Integer.MIN_VALUE).writeLong(Long.MAX_VALUE)
+ .writeFloat(0.5f).writeDouble(-1.25d);
+
+ assertTrue(buf.readBoolean());
+ assertFalse(buf.readBoolean());
+ assertEquals(0x7F, buf.readByte());
+ assertEquals((short) -2, buf.readShort());
+ assertEquals(Integer.MIN_VALUE, buf.readInt());
+ assertEquals(Long.MAX_VALUE, buf.readLong());
+ assertEquals(0.5f, buf.readFloat(), 0.0f);
+ assertEquals(-1.25d, buf.readDouble(), 0.0d);
+ assertEquals(0, buf.readableBytes());
+ }
+
+ @Test
+ public void shouldWriteMultiByteValuesBigEndian() {
+ // the on-disk format is big-endian, matching the Netty-backed buffer
this replaces, so the byte order is
+ // part of the storage contract rather than an implementation detail
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+
buf.writeShort(0x0102).writeInt(0x03040506).writeLong(0x0708090A0B0C0D0EL);
+
+ assertArrayEquals(new byte[] {
+ 0x01, 0x02,
+ 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E
+ }, buf.toWrittenArray());
+ }
+
+ @Test
+ public void shouldGrowBeyondInitialCapacity() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer(1);
+ final byte[] payload = new byte[1000];
+ Arrays.fill(payload, (byte) 7);
+ buf.writeBytes(payload);
+
+ assertTrue("capacity should have grown to hold the payload",
buf.capacity() >= 1000);
+ assertEquals(1000, buf.readableBytes());
+ assertArrayEquals(payload, buf.toWrittenArray());
+ }
+
+ @Test
+ public void shouldWrapAnExistingArrayReadyForReading() {
+ final byte[] data = { 0x00, 0x00, 0x00, 0x2A };
+ final TinkerByteBuffer buf = new TinkerByteBuffer(data);
+
+ assertEquals(4, buf.writerIndex());
+ assertEquals(0, buf.readerIndex());
+ assertEquals(4, buf.readableBytes());
+ assertEquals(42, buf.readInt());
+ }
+
+ @Test
+ public void shouldTrackReaderAndWriterIndexes() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+ buf.writeInt(1).writeInt(2);
+ assertEquals(8, buf.writerIndex());
+ assertEquals(8, buf.readableBytes());
+
+ buf.readInt();
+ assertEquals(4, buf.readerIndex());
+ assertEquals(4, buf.readableBytes());
+
+ buf.readerIndex(0);
+ assertEquals(1, buf.readInt());
+ }
+
+ @Test
+ public void shouldDistinguishReadableFromWrittenBytes() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+ buf.writeInt(1).writeInt(2);
+ buf.readInt();
+
+ // written covers everything from index 0; readable covers only what
is left ahead of the reader
+ assertArrayEquals(new byte[] { 0, 0, 0, 1, 0, 0, 0, 2 },
buf.toWrittenArray());
+ assertArrayEquals(new byte[] { 0, 0, 0, 2 }, buf.toReadableArray());
+ assertEquals("neither view may move the indexes", 4,
buf.readerIndex());
+ }
+
+ @Test
+ public void shouldResetWriterIndexToTheMark() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+ buf.writeInt(1);
+ buf.markWriterIndex();
+ buf.writeInt(2);
+ assertEquals(8, buf.writerIndex());
+
+ buf.resetWriterIndex();
+ assertEquals(4, buf.writerIndex());
+ assertArrayEquals(new byte[] { 0, 0, 0, 1 }, buf.toWrittenArray());
+ }
+
+ @Test
+ public void shouldRejectReadingPastTheWriterIndex() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+ buf.writeByte(1);
+ buf.readByte();
+
+ try {
+ buf.readByte();
+ fail("expected a read past the writer index to be rejected");
+ } catch (IndexOutOfBoundsException expected) {
+ assertTrue(expected.getMessage(),
expected.getMessage().contains("Not enough readable bytes"));
+ }
+ }
+
+ @Test
+ public void shouldRejectReadingMoreBytesThanRemain() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer(new byte[] { 1, 2, 3
});
+ try {
+ buf.readBytes(new byte[4]);
+ fail("expected a bulk read longer than the readable region to be
rejected");
+ } catch (IndexOutOfBoundsException expected) {
+ assertTrue(expected.getMessage(),
expected.getMessage().contains("Not enough readable bytes"));
+ }
+ }
+
+ @Test
+ public void shouldRejectAnOutOfRangeReaderIndex() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+ buf.writeInt(1);
+ try {
+ buf.readerIndex(5);
+ fail("expected a reader index beyond the writer index to be
rejected");
+ } catch (IndexOutOfBoundsException expected) {
+ // expected: the readable region may never extend past what has
been written
+ }
+ }
+
+ @Test
+ public void shouldReadBytesIntoAnArraySlice() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer(new byte[] { 1, 2,
3, 4 });
+ final byte[] dst = new byte[6];
+ buf.readBytes(dst, 1, 4);
+
+ assertArrayEquals(new byte[] { 0, 1, 2, 3, 4, 0 }, dst);
+ assertEquals(0, buf.readableBytes());
+ }
+
+ @Test
+ public void shouldReadAndWriteThroughNioBuffers() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+ buf.writeBytes(ByteBuffer.wrap(new byte[] { 9, 8, 7 }));
+ assertEquals(3, buf.readableBytes());
+
+ final ByteBuffer dst = ByteBuffer.allocate(3);
+ buf.readBytes(dst);
+ assertArrayEquals(new byte[] { 9, 8, 7 }, dst.array());
+ }
+
+ @Test
+ public void shouldReadBytesIntoAnOutputStream() throws Exception {
+ final TinkerByteBuffer buf = new TinkerByteBuffer(new byte[] { 4, 5,
6, 7 });
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ buf.readBytes(out, 3);
+
+ assertArrayEquals(new byte[] { 4, 5, 6 }, out.toByteArray());
+ assertEquals(1, buf.readableBytes());
+ }
+
+ @Test
+ public void shouldCopyAbsoluteBytesWithoutMovingIndexes() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer(new byte[] { 1, 2,
3, 4 });
+ final byte[] dst = new byte[2];
+ buf.getBytes(2, dst);
+
+ assertArrayEquals(new byte[] { 3, 4 }, dst);
+ assertEquals("an absolute read is positional and must not consume", 0,
buf.readerIndex());
+ }
+
+ @Test
+ public void shouldExposeAnNioViewOfTheReadableRegion() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer(new byte[] { 1, 2,
3, 4 });
+ buf.readByte();
+
+ assertEquals(1, buf.nioBufferCount());
+ final ByteBuffer view = buf.nioBuffer();
+ assertArrayEquals(new byte[] { 2, 3, 4 }, view.array());
+ assertEquals("the view is a copy, so consuming it must not move the
buffer", 3, buf.readableBytes());
+ assertArrayEquals(new byte[] { 2, 3 }, buf.nioBuffer(1, 2).array());
+ assertEquals(1, buf.nioBuffers().length);
+ }
+
+ @Test
+ public void shouldCountReferences() {
+ final TinkerByteBuffer buf = new TinkerByteBuffer();
+ assertEquals(1, buf.referenceCount());
+
+ buf.retain();
+ assertEquals(2, buf.referenceCount());
+ assertFalse("still referenced, so release does not report the last
one", buf.release());
+ assertEquals(1, buf.referenceCount());
+ assertTrue("the final release reports that the buffer is done",
buf.release());
+ }
+
+ @Test
+ public void shouldReportAsHeapBacked() {
+ assertFalse(new TinkerByteBuffer().isDirect());
+ }
+}