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
commit dc72ba579a1245ee774a81924e9a9e792d7aa79a Author: Stephen Mallette <[email protected]> AuthorDate: Wed Aug 19 14:31:18 2026 +0000 Encode TinkerStorageGraph elements as dictionary-referenced components Replace whole-DetachedVertex/DetachedEdge GraphBinary serialization with a component codec: element ids and property values go through GraphBinary's scalar serializers (a one-byte DataType tag + raw value), while labels, property keys, and meta-property keys are dictionary-encoded to small integer refs. New keys are emitted as OP_DICT_APPEND records within the same frame, before their first use. Vertex-property ids are auto-generated and no longer persisted (regenerated on load); element and edge ids are preserved. Compaction preserves dictionary numbering and writes the whole dictionary as a self-contained snapshot header, and dict-append decode is idempotent, so a log surviving the compaction crash window still resolves its refs. This removes the old whole-object codec (no dead code) and drops per-element key/label repetition and the per-property envelope, shrinking the on-disk footprint. Assisted-by: Claude Code:claude-opus-4-8 --- .../structure/storage/GraphBinaryStorage.java | 371 ++++++++++++++++++--- .../structure/storage/GraphBinaryStorageTest.java | 14 +- .../storage/StorageCrashConsistencyTest.java | 15 +- 3 files changed, 334 insertions(+), 66 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 a5f3fd1c5c..1266dee705 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 @@ -19,34 +19,51 @@ package org.apache.tinkerpop.gremlin.tinkergraph.structure.storage; import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Property; import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.VertexProperty; +import org.apache.tinkerpop.gremlin.structure.io.binary.DataType; import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader; import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter; +import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer; import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry; import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge; -import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedFactory; +import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedProperty; import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex; +import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertexProperty; import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph; import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerEdge; import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerVertex; import java.io.DataOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Set; /** * The GraphBinary {@link TinkerStorage} codec on top of {@link AbstractLogStorage}. The base owns the durable * log-structured machinery (file layout, {@code VERSION} marker, CRC framing, replay fold, {@link SyncMode} - * durability, crash-safe and threshold compaction); this class supplies only how an element is encoded and decoded, - * using the GraphBinary serializers ({@link GraphBinaryWriter}/{@link GraphBinaryReader}). + * durability, crash-safe and threshold compaction); this class supplies only how an element is encoded and decoded. * <p/> - * Known limitation (write amplification): a commit records each changed element in full — a single property change on - * a large element rewrites the whole element to the log. Elements are typically small and automatic compaction bounds - * the resulting log growth, so this is accepted rather than mitigated with per-property deltas, which would complicate - * the {@link TinkerStorageMutation} contract and the replay fold. The snapshot, by contrast, is streamed one element - * at a time so compaction never holds a second full copy of the graph in heap. + * Rather than serialize a whole {@code DetachedVertex}/{@code DetachedEdge} (which repeats every property key and + * label as a full string on every element and wraps each property in a {@code VertexProperty} envelope), this codec + * writes element <em>components</em> directly: element ids and property values go through GraphBinary's scalar + * serializers (a one-byte {@link DataType} tag plus the raw value), while labels, property keys, and meta-property + * keys are dictionary-encoded to small integer refs. The dictionary is a single dense namespace built by first + * appearance; new entries are emitted as {@code OP_DICT_APPEND} records inside the same frame, before the entries + * that reference them, so a torn trailing frame drops a ref and its user atomically. + * <p/> + * Vertex-property ids are auto-generated and, by default, not persisted (they are regenerated on load); element and + * edge ids are always preserved. The snapshot streams one element per frame, so compaction never holds a second full + * copy of the graph in heap. */ public final class GraphBinaryStorage extends AbstractLogStorage { @@ -54,71 +71,230 @@ public final class GraphBinaryStorage extends AbstractLogStorage { private static final byte OP_DEL_VERTEX = 2; private static final byte OP_PUT_EDGE = 3; private static final byte OP_DEL_EDGE = 4; + private static final byte OP_DICT_APPEND = 5; - private final GraphBinaryWriter writer = new GraphBinaryWriter(TypeSerializerRegistry.INSTANCE); - private final GraphBinaryReader reader = new GraphBinaryReader(TypeSerializerRegistry.INSTANCE); + private final TypeSerializerRegistry registry = TypeSerializerRegistry.INSTANCE; + private final GraphBinaryWriter writer = new GraphBinaryWriter(registry); + private final GraphBinaryReader reader = new GraphBinaryReader(registry); /** - * Serialize a commit record: txVersion, entry count, then each entry as an op byte followed by either the - * serialized element (put) or the serialized id (delete). + * Shared string dictionary for labels, property keys, and meta-property keys. Dense ids assigned by first + * appearance; grows monotonically within a write session and is rebuilt fresh on compaction and on replay. */ + private final Map<String, Integer> keyToId = new HashMap<>(); + private final List<String> idToKey = new ArrayList<>(); + + @Override + protected void beginReplay() { + keyToId.clear(); + idToKey.clear(); + } + + // --------------------------------------------------------------------------------------------- encode + @Override protected byte[] encodeCommit(final long txVersion, final Collection<TinkerStorageMutation<TinkerVertex>> changedVertices, final Collection<TinkerStorageMutation<TinkerEdge>> changedEdges) throws IOException { - final ByteBufferBuffer buffer = new ByteBufferBuffer(); - buffer.writeLong(txVersion); - buffer.writeInt(changedVertices.size() + changedEdges.size()); + final ByteBufferBuffer buf = new ByteBufferBuffer(); + // 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) + if (!m.isDeleted()) registerVertexStrings(m.element(), appends); + for (final TinkerStorageMutation<TinkerEdge> m : changedEdges) + if (!m.isDeleted()) registerEdgeStrings(m.element(), appends); + + writeVarInt(buf, appends.size() + changedVertices.size() + changedEdges.size()); + // dictionary appends first, so every ref below resolves during the fold + for (final String s : appends) { + buf.writeByte(OP_DICT_APPEND); + writeVarInt(buf, keyToId.get(s)); + writeString(buf, s); + } for (final TinkerStorageMutation<TinkerVertex> m : changedVertices) { if (m.isDeleted()) { - buffer.writeByte(OP_DEL_VERTEX); - writer.write(m.id(), buffer); + buf.writeByte(OP_DEL_VERTEX); + writeScalar(buf, m.id()); } else { - buffer.writeByte(OP_PUT_VERTEX); - // detach to a stable form independent of the transactional element - writer.write(DetachedFactory.detach(m.element(), true), buffer); + buf.writeByte(OP_PUT_VERTEX); + writeVertexRecord(buf, m.element()); } } for (final TinkerStorageMutation<TinkerEdge> m : changedEdges) { if (m.isDeleted()) { - buffer.writeByte(OP_DEL_EDGE); - writer.write(m.id(), buffer); + buf.writeByte(OP_DEL_EDGE); + writeScalar(buf, m.id()); } else { - buffer.writeByte(OP_PUT_EDGE); - writer.write(DetachedFactory.detach(m.element(), true), buffer); + buf.writeByte(OP_PUT_EDGE); + writeEdgeRecord(buf, m.element()); + } + } + return buf.toWrittenArray(); + } + + @Override + protected void writeSnapshot(final AbstractTinkerGraph graph, final DataOutputStream out) throws IOException { + // Preserve the existing dictionary numbering rather than renumbering: the compaction crash window can leave + // the new snapshot in place with the old log not yet truncated, and that log's refs use the current + // numbering. Register any not-yet-seen live strings (this only extends the dictionary, never renumbers), then + // emit the whole dictionary as a self-contained header frame so a snapshot-only replay resolves every ref. + final List<String> ignored = new ArrayList<>(); + Iterator<Vertex> vertices = graph.vertices(); + while (vertices.hasNext()) + registerVertexStrings(vertices.next(), ignored); + Iterator<Edge> edges = graph.edges(); + while (edges.hasNext()) + registerEdgeStrings(edges.next(), ignored); + + final ByteBufferBuffer dictBuf = new ByteBufferBuffer(); + writeVarInt(dictBuf, idToKey.size()); + for (int id = 0; id < idToKey.size(); id++) { + dictBuf.writeByte(OP_DICT_APPEND); + writeVarInt(dictBuf, id); + writeString(dictBuf, idToKey.get(id)); + } + writeFrame(out, dictBuf.toWrittenArray()); + + // one element per frame; all keys are already in the dictionary header, so no per-frame appends + vertices = graph.vertices(); + while (vertices.hasNext()) + writeElementFrame(out, OP_PUT_VERTEX, vertices.next()); + edges = graph.edges(); + while (edges.hasNext()) + writeElementFrame(out, OP_PUT_EDGE, edges.next()); + } + + /** + * Write one element as its own single-entry put frame. One element is held in memory at a time, so a large graph + * is never buffered whole. + */ + private void writeElementFrame(final DataOutputStream out, final byte op, final Object element) throws IOException { + final ByteBufferBuffer buf = new ByteBufferBuffer(); + writeVarInt(buf, 1); + buf.writeByte(op); + if (op == OP_PUT_VERTEX) writeVertexRecord(buf, (Vertex) element); + else writeEdgeRecord(buf, (Edge) element); + writeFrame(out, buf.toWrittenArray()); + } + + private void registerVertexStrings(final Vertex v, final List<String> appends) { + for (final String label : v.labels()) + register(label, appends); + final Iterator<VertexProperty<Object>> vps = v.properties(); + while (vps.hasNext()) { + final VertexProperty<Object> vp = vps.next(); + register(vp.key(), appends); + final Iterator<Property<Object>> metas = vp.properties(); + while (metas.hasNext()) + register(metas.next().key(), appends); + } + } + + private void registerEdgeStrings(final Edge e, final List<String> appends) { + register(e.label(), appends); + final Iterator<Property<Object>> props = e.properties(); + while (props.hasNext()) + register(props.next().key(), appends); + } + + private void register(final String s, final List<String> appends) { + if (!keyToId.containsKey(s)) { + final int id = idToKey.size(); + keyToId.put(s, id); + idToKey.add(s); + appends.add(s); + } + } + + private void writeVertexRecord(final ByteBufferBuffer buf, final Vertex v) throws IOException { + writeScalar(buf, v.id()); + final Set<String> labels = v.labels(); + writeVarInt(buf, labels.size()); + for (final String label : labels) + writeVarInt(buf, keyToId.get(label)); + + // group vertex properties by key so multi-properties (list/set) round-trip + final Map<String, List<VertexProperty<Object>>> groups = new LinkedHashMap<>(); + final Iterator<VertexProperty<Object>> vps = v.properties(); + while (vps.hasNext()) { + final VertexProperty<Object> vp = vps.next(); + groups.computeIfAbsent(vp.key(), k -> new ArrayList<>()).add(vp); + } + writeVarInt(buf, groups.size()); + for (final Map.Entry<String, List<VertexProperty<Object>>> group : groups.entrySet()) { + writeVarInt(buf, keyToId.get(group.getKey())); + final List<VertexProperty<Object>> values = group.getValue(); + writeVarInt(buf, values.size()); + for (final VertexProperty<Object> vp : values) { + writeScalar(buf, vp.value()); + final List<Property<Object>> metas = new ArrayList<>(); + vp.properties().forEachRemaining(metas::add); + writeVarInt(buf, metas.size()); + for (final Property<Object> meta : metas) { + writeVarInt(buf, keyToId.get(meta.key())); + writeScalar(buf, meta.value()); + } } } - return buffer.toWrittenArray(); } + private void writeEdgeRecord(final ByteBufferBuffer buf, final Edge e) throws IOException { + writeScalar(buf, e.id()); + writeVarInt(buf, keyToId.get(e.label())); + writeScalar(buf, e.outVertex().id()); + writeScalar(buf, e.inVertex().id()); + final List<Property<Object>> props = new ArrayList<>(); + e.properties().forEachRemaining(props::add); + writeVarInt(buf, props.size()); + for (final Property<Object> p : props) { + writeVarInt(buf, keyToId.get(p.key())); + writeScalar(buf, p.value()); + } + } + + // --------------------------------------------------------------------------------------------- decode + @Override protected void decodeFrame(final byte[] record, final Map<Object, DetachedVertex> vertices, final Map<Object, DetachedEdge> edges) throws IOException { - final ByteBufferBuffer buffer = new ByteBufferBuffer(record); - buffer.readLong(); // txVersion, retained for diagnostics/future use - final int entryCount = buffer.readInt(); + final ByteBufferBuffer buf = new ByteBufferBuffer(record); + final int entryCount = readVarInt(buf); for (int i = 0; i < entryCount; i++) { - final byte op = buffer.readByte(); + final byte op = buf.readByte(); switch (op) { + case OP_DICT_APPEND: { + final int id = readVarInt(buf); + final String s = readString(buf); + // idempotent: a snapshot header defines the whole dictionary, and a log surviving the compaction + // crash window may re-append entries the snapshot already established. Re-appending an existing + // id with the same string is a no-op; a mismatch or a gap is corruption. + if (id < idToKey.size()) { + if (!idToKey.get(id).equals(s)) + throw new IOException(String.format("Corrupt storage: dictionary id %d redefined ('%s' vs '%s')", id, idToKey.get(id), s)); + } else if (id == idToKey.size()) { + idToKey.add(s); + } else { + throw new IOException(String.format("Corrupt storage: dictionary append gap (got %d, expected <= %d)", id, idToKey.size())); + } + break; + } case OP_PUT_VERTEX: { - final Vertex v = reader.read(buffer); - vertices.put(v.id(), (DetachedVertex) v); + final DetachedVertex v = readVertexRecord(buf); + vertices.put(v.id(), v); break; } case OP_DEL_VERTEX: { - final Object id = reader.read(buffer); - vertices.remove(id); + vertices.remove(readScalar(buf)); break; } case OP_PUT_EDGE: { - final Edge e = reader.read(buffer); - edges.put(e.id(), (DetachedEdge) e); + final DetachedEdge e = readEdgeRecord(buf); + edges.put(e.id(), e); break; } case OP_DEL_EDGE: { - final Object id = reader.read(buffer); - edges.remove(id); + edges.remove(readScalar(buf)); break; } default: @@ -127,29 +303,114 @@ public final class GraphBinaryStorage extends AbstractLogStorage { } } + private DetachedVertex readVertexRecord(final ByteBufferBuffer buf) throws IOException { + final Object id = readScalar(buf); + final DetachedVertex.Builder b = DetachedVertex.build().setId(id); + final int labelCount = readVarInt(buf); + if (labelCount == 1) { + b.setLabel(idToKey.get(readVarInt(buf))); + } else if (labelCount > 1) { + final Set<String> labels = new LinkedHashSet<>(); + for (int i = 0; i < labelCount; i++) + labels.add(idToKey.get(readVarInt(buf))); + b.setLabels(labels); + } + final int keyGroupCount = readVarInt(buf); + for (int g = 0; g < keyGroupCount; g++) { + final String key = idToKey.get(readVarInt(buf)); + final int valueCount = readVarInt(buf); + for (int j = 0; j < valueCount; j++) { + final Object value = readScalar(buf); + final DetachedVertexProperty.Builder vpb = DetachedVertexProperty.build().setLabel(key).setValue(value); + final int metaCount = readVarInt(buf); + for (int m = 0; m < metaCount; m++) { + final String metaKey = idToKey.get(readVarInt(buf)); + final Object metaValue = readScalar(buf); + vpb.addProperty(new DetachedProperty<>(metaKey, metaValue)); + } + b.addProperty(vpb.create()); + } + } + return b.create(); + } + + private DetachedEdge readEdgeRecord(final ByteBufferBuffer buf) throws IOException { + final Object id = readScalar(buf); + final String label = idToKey.get(readVarInt(buf)); + final Object outVId = readScalar(buf); + final Object inVId = readScalar(buf); + final DetachedEdge.Builder b = DetachedEdge.build().setId(id).setLabel(label) + .setOutV(DetachedVertex.build().setId(outVId).create()) + .setInV(DetachedVertex.build().setId(inVId).create()); + final int propCount = readVarInt(buf); + for (int i = 0; i < propCount; i++) { + final String key = idToKey.get(readVarInt(buf)); + final Object value = readScalar(buf); + b.addProperty(new DetachedProperty<>(key, value)); + } + return b.create(); + } + + // --------------------------------------------------------------------------------------------- primitives + /** - * Stream the current committed state as one framed put record per vertex and per edge, so peak memory is bounded - * to a single element rather than materializing the whole graph as one byte array. + * Write a value as a one-byte {@link DataType} tag followed by the raw value (no value-flag byte). A {@code null} + * is a single {@link DataType#UNSPECIFIED_NULL} tag. */ - @Override - protected void writeSnapshot(final AbstractTinkerGraph graph, final DataOutputStream out) throws IOException { - final Iterator<Vertex> vertexIterator = graph.vertices(); - while (vertexIterator.hasNext()) - writeElementFrame(out, OP_PUT_VERTEX, vertexIterator.next()); - final Iterator<Edge> edgeIterator = graph.edges(); - while (edgeIterator.hasNext()) - writeElementFrame(out, OP_PUT_EDGE, edgeIterator.next()); + @SuppressWarnings({"unchecked", "rawtypes"}) + private void writeScalar(final ByteBufferBuffer buf, final Object value) throws IOException { + if (value == null) { + buf.writeByte(DataType.UNSPECIFIED_NULL.getCodeByte()); + return; + } + final TypeSerializer serializer = registry.getSerializer(value.getClass()); + buf.writeByte(serializer.getDataType().getCodeByte()); + serializer.writeValue(value, buf, writer, false); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object readScalar(final ByteBufferBuffer buf) throws IOException { + final DataType dataType = DataType.get(Byte.toUnsignedInt(buf.readByte())); + if (dataType == DataType.UNSPECIFIED_NULL) + return null; + final TypeSerializer serializer = registry.getSerializer(dataType); + return serializer.readValue(buf, reader, false); + } + + private static void writeString(final ByteBufferBuffer buf, final String s) { + final byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + writeVarInt(buf, bytes.length); + buf.writeBytes(bytes); + } + + private static String readString(final ByteBufferBuffer buf) { + final byte[] bytes = new byte[readVarInt(buf)]; + buf.readBytes(bytes); + return new String(bytes, StandardCharsets.UTF_8); } /** - * Encode a single element as a one-entry put record and write it as a framed record to {@code out}. + * Unsigned LEB128 varint. Counts and dictionary refs are small and non-negative, so they cost one byte in the + * common case. */ - private void writeElementFrame(final DataOutputStream out, final byte op, final Object element) throws IOException { - final ByteBufferBuffer buffer = new ByteBufferBuffer(); - buffer.writeLong(0L); // snapshot records have no single tx version - buffer.writeInt(1); - buffer.writeByte(op); - writer.write(DetachedFactory.detach(element, true), buffer); - writeFrame(out, buffer.toWrittenArray()); + private static void writeVarInt(final ByteBufferBuffer buf, final int value) { + int v = value; + while ((v & ~0x7F) != 0) { + buf.writeByte((v & 0x7F) | 0x80); + v >>>= 7; + } + buf.writeByte(v & 0x7F); + } + + private static int readVarInt(final ByteBufferBuffer buf) { + int result = 0; + int shift = 0; + byte b; + do { + b = buf.readByte(); + result |= (b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + return result; } } diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorageTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorageTest.java index c113511176..cbde701a22 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorageTest.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorageTest.java @@ -127,10 +127,10 @@ public class GraphBinaryStorageTest extends AbstractTinkerStorageConformanceTest graph.tx().commit(); graph.compact(); - // the snapshot must be written as one framed record per element (3 vertices + 2 edges = 5), rather than a - // single whole-graph frame, so compaction never buffers the entire graph in one array + // the snapshot must be streamed: a dictionary header frame plus one framed record per element (3 vertices + + // 2 edges = 5), rather than a single whole-graph frame, so compaction never buffers the entire graph at once final File snapshotFile = new File(location, GraphBinaryStorage.SNAPSHOT_FILE); - assertEquals(5, countFrames(snapshotFile)); + assertEquals(1 + 5, countFrames(snapshotFile)); graph.close(); // and the streamed snapshot must reopen to exactly the same graph @@ -145,9 +145,9 @@ public class GraphBinaryStorageTest extends AbstractTinkerStorageConformanceTest @Test public void shouldStreamSnapshotFrameByFrameAtScale() { // Bounded-memory proxy for the streaming snapshot path: rather than measure heap (flaky, JVM-dependent), assert - // the observable streaming property holds at scale — a large graph is written as exactly one frame per element, - // never one whole-graph frame — and round-trips intact. This is the property that keeps compaction from - // materializing a second full copy of the graph in memory; it is not a hard OOM assertion. + // the observable streaming property holds at scale — a large graph is written as a dictionary header frame plus + // one frame per element, never one whole-graph frame — and round-trips intact. This is the property that keeps + // compaction from materializing a second full copy of the graph in memory; it is not a hard OOM assertion. final int vertexCount = 500; final int edgeCount = 499; final TinkerStorageGraph graph = open(); @@ -161,7 +161,7 @@ public class GraphBinaryStorageTest extends AbstractTinkerStorageConformanceTest graph.compact(); try { - assertEquals(vertexCount + edgeCount, countFrames(new File(location, GraphBinaryStorage.SNAPSHOT_FILE))); + assertEquals(1 + vertexCount + edgeCount, countFrames(new File(location, GraphBinaryStorage.SNAPSHOT_FILE))); } catch (Exception ex) { throw new RuntimeException(ex); } diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/StorageCrashConsistencyTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/StorageCrashConsistencyTest.java index 614cfdb0ba..8f3c475f13 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/StorageCrashConsistencyTest.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/StorageCrashConsistencyTest.java @@ -123,10 +123,17 @@ public class StorageCrashConsistencyTest { @Test public void shouldRecoverDurableCommitWithNoSnapshot() throws Exception { - // WAL guarantee: a commit whose frame was durably written to the log, with no compaction having run, must - // recover on reopen even though no snapshot exists. - captureBuildingBlocks(); - Files.write(logFile.toPath(), logV2); + // WAL guarantee: a commit durably written to the log with no compaction (no snapshot) recovers on reopen. On + // a fresh store the dictionary starts empty, so the commit frame is self-contained (it carries its own + // dictionary appends), which is exactly the real "log only, never compacted" case. + final TinkerStorageGraph g = open(); + g.addVertex(T.id, 2, "value", 2); + g.tx().commit(); + g.tx().close(); + final byte[] selfContainedLog = Files.readAllBytes(logFile.toPath()); + g.close(); // compaction on close would fold the log away; the raw log was captured above + resetFiles(); + Files.write(logFile.toPath(), selfContainedLog); assertReopensTo(2); }
