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 379deede9256845a4fc9fadedae2cf6c2a18da79
Author: Stephen Mallette <[email protected]>
AuthorDate: Wed Aug 19 16:34:36 2026 +0000

    Test TinkerStorage durability, lifecycle, and dictionary crash window
    
    Add TCK cases for repeated compaction cycles, multiple open/close sessions,
    and concurrent commits with distinct keys. Add a crash-consistency test for 
the
    compaction crash window where a dead dictionary key forces the surviving 
log to
    diverge from the new snapshot's dictionary — guarding the decision to 
preserve
    dictionary numbering across compaction rather than renumber.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 .../AbstractTinkerStorageConformanceTest.java      | 112 +++++++++++++++++++++
 .../storage/StorageCrashConsistencyTest.java       |  43 ++++++++
 2 files changed, 155 insertions(+)

diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/AbstractTinkerStorageConformanceTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/AbstractTinkerStorageConformanceTest.java
index f5ec5033f4..34afede360 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/AbstractTinkerStorageConformanceTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/AbstractTinkerStorageConformanceTest.java
@@ -490,6 +490,118 @@ public abstract class 
AbstractTinkerStorageConformanceTest {
         }
     }
 
+    @Test
+    public void shouldSurviveRepeatedCompactionCycles() {
+        TinkerStorageGraph graph = open();
+        try {
+            graph.addVertex(T.id, 1, "a", 1);
+            graph.tx().commit();
+            graph.compact();
+            graph.addVertex(T.id, 2, "b", 2);
+            graph.tx().commit();
+            graph.compact();
+            graph.addVertex(T.id, 3, "c", 3);
+            graph.tx().commit();
+            graph.compact();
+        } finally {
+            graph.close();
+        }
+        graph = open();
+        try {
+            assertEquals(3, countOf(graph.vertices()));
+            assertEquals(Integer.valueOf(1), 
graph.vertices(1).next().value("a"));
+            assertEquals(Integer.valueOf(2), 
graph.vertices(2).next().value("b"));
+            assertEquals(Integer.valueOf(3), 
graph.vertices(3).next().value("c"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void shouldSurviveMultipleOpenCloseSessions() {
+        TinkerStorageGraph graph = open();
+        try {
+            graph.addVertex(T.id, 1, "n", "one");
+            graph.tx().commit();
+        } finally {
+            graph.close();
+        }
+        graph = open();
+        try {
+            graph.addVertex(T.id, 2, "n", "two");
+            graph.tx().commit();
+        } finally {
+            graph.close();
+        }
+        graph = open();
+        try {
+            graph.addVertex(T.id, 3, "n", "three");
+            graph.tx().commit();
+            graph.compact();
+        } finally {
+            graph.close();
+        }
+        graph = open();
+        try {
+            assertEquals(3, countOf(graph.vertices()));
+            assertEquals("one", graph.vertices(1).next().value("n"));
+            assertEquals("two", graph.vertices(2).next().value("n"));
+            assertEquals("three", graph.vertices(3).next().value("n"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void shouldRoundTripConcurrentCommitsWithDistinctKeys() throws 
Exception {
+        // concurrent commits that each introduce a distinct property key 
stress dictionary growth under the
+        // commit-write lock; on reopen every distinct key must resolve
+        final int threads = 8;
+        final int perThread = 25;
+        final TinkerStorageGraph writeGraph = open();
+        try {
+            final ExecutorService pool = Executors.newFixedThreadPool(threads);
+            final CountDownLatch start = new CountDownLatch(1);
+            final List<Future<?>> futures = new ArrayList<>();
+            for (int t = 0; t < threads; t++) {
+                final int threadId = t;
+                futures.add(pool.submit(() -> {
+                    start.await();
+                    for (int i = 0; i < perThread; i++) {
+                        final int id = threadId * perThread + i;
+                        writeGraph.addVertex(T.id, id, "k_" + threadId + "_" + 
i, id);
+                        writeGraph.tx().commit();
+                    }
+                    return null;
+                }));
+            }
+            start.countDown();
+            for (final Future<?> f : futures)
+                f.get(60, TimeUnit.SECONDS);
+            pool.shutdown();
+            assertTrue(pool.awaitTermination(60, TimeUnit.SECONDS));
+        } finally {
+            writeGraph.close();
+        }
+        final TinkerStorageGraph reopened = open();
+        try {
+            final int expected = threads * perThread;
+            assertEquals(expected, countOf(reopened.vertices()));
+            for (int t = 0; t < threads; t++) {
+                for (int i = 0; i < perThread; i++) {
+                    final int id = threadId(t, i, perThread);
+                    assertEquals(Integer.valueOf(id), 
reopened.vertices(id).next().value("k_" + t + "_" + i));
+                }
+            }
+        } finally {
+            reopened.close();
+        }
+    }
+
+    private static int threadId(final int t, final int i, final int perThread) 
{
+        return t * perThread + i;
+    }
+
     private static long countOf(final Iterator<?> it) {
         long count = 0;
         while (it.hasNext()) {
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 8f3c475f13..4bbb3854ce 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
@@ -22,6 +22,7 @@ import org.apache.commons.configuration2.BaseConfiguration;
 import org.apache.commons.configuration2.Configuration;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.T;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerStorageGraph;
 import org.junit.Before;
@@ -169,6 +170,48 @@ public class StorageCrashConsistencyTest {
         assertReopensTo(1, 2);
     }
 
+    @Test
+    public void 
shouldRecoverCrashWindowWithADeadKeyForcingDictionaryDivergence() throws 
Exception {
+        // Guards the preserve-dictionary-numbering decision. A key ("alpha") 
is deleted from the live graph after it
+        // is in the dictionary, then a new key ("gamma") is added. Preserving 
numbering keeps alpha's id forever, so
+        // the surviving log's gamma ref still matches the new snapshot's 
dictionary. Renumbering on compaction would
+        // instead drop the now-dead alpha and shift gamma to a lower id, so 
the log's higher-numbered gamma ref would
+        // no longer resolve. A single-key (or no-delete) state cannot tell 
the two apart.
+        final TinkerStorageGraph g = open();
+        g.addVertex(T.id, 1, "alpha", 1);
+        g.tx().commit();
+        g.addVertex(T.id, 2, "beta", 2);
+        g.tx().commit();
+        g.compact(); // snapshot holds alpha and beta in the dictionary at 
stable ids
+
+        g.vertices(1).next().remove(); // alpha becomes a dead key: retained 
only under preserve-numbering
+        g.tx().commit();
+        // re-write the surviving vertex: its record now carries a bare 
reference to the pre-existing key "beta"
+        // (not re-appended) plus a new key "gamma". If compaction renumbered, 
"beta"'s id would shift and this bare
+        // reference would resolve to the wrong key on replay.
+        g.vertices(2).next().property("gamma", "g");
+        g.tx().commit();
+        final byte[] logNotYetDeleted = Files.readAllBytes(logFile.toPath());
+        g.compact(); // new full-dictionary snapshot (preserved numbering), 
then log truncated
+        final byte[] newSnapshot = Files.readAllBytes(snapshotFile.toPath());
+        g.close();
+
+        // reconstruct the crash window: new snapshot in place, old log not 
yet deleted
+        Files.write(snapshotFile.toPath(), newSnapshot);
+        Files.write(logFile.toPath(), logNotYetDeleted);
+
+        final TinkerStorageGraph reopened = open();
+        try {
+            assertEquals(1, countOf(reopened.vertices())); // only the 
surviving vertex 2
+            assertEquals(0, countOf(reopened.vertices(1))); // alpha's vertex 
was deleted
+            final Vertex v = reopened.vertices(2).next();
+            assertEquals(Integer.valueOf(2), v.value("beta"));
+            assertEquals("g", v.value("gamma"));
+        } finally {
+            reopened.close();
+        }
+    }
+
     private static long countOf(final Iterator<?> it) {
         long count = 0;
         while (it.hasNext()) {

Reply via email to