This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit fb5ffe15d610d215b008b6a3c9127ef0b0a767b3
Merge: a95d2e442a d0346fd28c
Author: Stephen Mallette <[email protected]>
AuthorDate: Wed Oct 21 12:15:31 2026 +0000

    Merge branch '3.8-dev'

 CHANGELOG.asciidoc                                 |  2 +
 .../structure/TinkerTransactionalIndex.java        | 15 +++++-
 .../structure/TinkerStorageGraphTest.java          | 53 ++++++++++++++++++++++
 3 files changed, 69 insertions(+), 1 deletion(-)

diff --cc 
tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerStorageGraphTest.java
index fa5ea114be,0000000000..12f9f85796
mode 100644,000000..100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerStorageGraphTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerStorageGraphTest.java
@@@ -1,2691 -1,0 +1,2744 @@@
 +/*
 + * 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;
 +
 +import java.io.File;
 +import java.io.UncheckedIOException;
 +import java.lang.reflect.Field;
 +import java.nio.file.Files;
 +import java.util.ArrayList;
 +import java.util.Collection;
 +import java.util.Collections;
 +import java.util.HashSet;
 +import java.util.Iterator;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.Set;
 +import java.util.concurrent.ConcurrentHashMap;
 +import java.util.concurrent.CountDownLatch;
 +import java.util.concurrent.TimeUnit;
 +import java.util.concurrent.atomic.AtomicBoolean;
 +import java.util.concurrent.atomic.AtomicLong;
 +import java.util.concurrent.atomic.AtomicReference;
 +import java.util.stream.Collectors;
 +import java.util.stream.IntStream;
 +import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
++import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
 +import org.apache.tinkerpop.gremlin.structure.Direction;
 +import org.apache.tinkerpop.gremlin.structure.Edge;
 +import org.apache.tinkerpop.gremlin.structure.Property;
 +import org.apache.tinkerpop.gremlin.structure.T;
 +import org.apache.tinkerpop.gremlin.structure.Vertex;
 +import org.apache.tinkerpop.gremlin.structure.service.Service;
 +import 
org.apache.tinkerpop.gremlin.tinkergraph.services.TinkerServiceRegistry;
 +import 
org.apache.tinkerpop.gremlin.tinkergraph.structure.storage.TinkerStorage;
 +import 
org.apache.tinkerpop.gremlin.tinkergraph.structure.storage.TinkerStorageMutation;
 +import org.apache.tinkerpop.gremlin.structure.VertexProperty;
 +import org.apache.commons.configuration2.BaseConfiguration;
 +import org.apache.commons.configuration2.Configuration;
 +import org.apache.tinkerpop.gremlin.structure.util.TransactionException;
 +import org.junit.Rule;
 +import org.junit.Test;
 +import org.junit.rules.TemporaryFolder;
 +
 +import static org.junit.Assert.assertEquals;
 +import static org.junit.Assert.assertFalse;
 +import static org.junit.Assert.assertNotEquals;
 +import static org.junit.Assert.assertNotNull;
 +import static org.junit.Assert.assertNull;
 +import static org.junit.Assert.assertTrue;
 +import static org.junit.Assert.fail;
 +import static org.mockito.ArgumentMatchers.any;
 +import static org.mockito.Mockito.doThrow;
 +import static org.mockito.Mockito.spy;
 +
 +public class TinkerStorageGraphTest {
 +
 +    @Rule
 +    public TemporaryFolder tempFolder = new TemporaryFolder();
 +
 +    @Test
 +    public void 
shouldFailACommitThatWaitedOnTheStorageLockWhileTheGraphWasCleared() throws 
Exception {
 +        final Configuration conf = new BaseConfiguration();
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE, 
"graphbinary");
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE_DIRECTORY, 
tempFolder.newFolder().getAbsolutePath());
 +        TinkerStorageGraph graph = TinkerStorageGraph.open(conf);
 +        graph.addVertex(T.id, 1);
 +        graph.tx().commit();
 +
 +        // hold the commit lock so the other thread's commit passes its 
conflict checks and then waits on it
 +        final TinkerStorageGraph g = graph;
 +        final AtomicReference<Throwable> otherCommit = new 
AtomicReference<>();
 +        graph.storageCommitLock.lock();
 +        final Thread other = new Thread(() -> {
 +            g.vertices(1).next().property("name", "late");
 +            try {
 +                g.tx().commit();
 +            } catch (Throwable t) {
 +                otherCommit.set(t);
 +            }
 +        });
 +        try {
 +            other.start();
 +            // bounded, so a thread that fails before it reaches the lock 
fails the test instead of hanging the build
 +            final long deadline = System.nanoTime() + 
TimeUnit.SECONDS.toNanos(10);
 +            while (!graph.storageCommitLock.hasQueuedThreads() && 
other.isAlive() && System.nanoTime() < deadline)
 +                Thread.sleep(1);
 +            assertTrue("the other commit should be waiting on the commit 
lock: " + otherCommit.get(),
 +                    graph.storageCommitLock.hasQueuedThreads());
 +            graph.clear();
 +        } finally {
 +            graph.storageCommitLock.unlock();
 +        }
 +        other.join(10_000);
 +
 +        assertTrue("the waiting commit should conflict: " + otherCommit.get(),
 +                otherCommit.get() instanceof TransactionException);
 +        graph.close();
 +        graph = TinkerStorageGraph.open(conf);
 +        assertFalse(graph.vertices().hasNext());
 +        graph.close();
 +    }
 +
 +    @Test
 +    public void 
shouldKeepAVertexAddedAfterClearWhenAnOlderTransactionOnItsIdConflicts() throws 
Exception {
 +        assertVertexAddedAfterClearSurvivesTheOlderTransaction(true);
 +    }
 +
 +    @Test
 +    public void 
shouldKeepAVertexAddedAfterClearWhenAnOlderTransactionOnItsIdRollsBack() throws 
Exception {
 +        assertVertexAddedAfterClearSurvivesTheOlderTransaction(false);
 +    }
 +
 +    /**
 +     * A transaction on another thread changes vertex 1 and is still open 
when the graph is cleared and vertex 1 is added
 +     * again and committed. Ending that older transaction must leave the new 
vertex 1 in place, in memory and on disk.
 +     */
 +    private void assertVertexAddedAfterClearSurvivesTheOlderTransaction(final 
boolean commit) throws Exception {
 +        final Configuration conf = new BaseConfiguration();
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE, 
"graphbinary");
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE_DIRECTORY, 
tempFolder.newFolder().getAbsolutePath());
 +        TinkerStorageGraph graph = TinkerStorageGraph.open(conf);
 +        graph.addVertex(T.id, 1, "name", "old");
 +        graph.tx().commit();
 +
 +        final TinkerStorageGraph g = graph;
 +        final CountDownLatch changed = new CountDownLatch(1);
 +        final CountDownLatch end = new CountDownLatch(1);
 +        final AtomicReference<Throwable> older = new AtomicReference<>();
 +        final Thread other = new Thread(() -> {
 +            try {
 +                g.vertices(1).next().property("name", "older");
 +                changed.countDown();
 +                end.await();
 +                if (commit)
 +                    g.tx().commit();
 +                else
 +                    g.tx().rollback();
 +            } catch (Throwable t) {
 +                older.set(t);
 +            }
 +        });
 +        other.start();
 +        assertTrue("the other transaction should change vertex 1", 
changed.await(10, TimeUnit.SECONDS));
 +
 +        graph.clear();
 +        graph.addVertex(T.id, 1, "name", "new");
 +        graph.tx().commit();
 +        end.countDown();
 +        other.join(10_000);
 +        assertFalse("the other transaction should have ended", 
other.isAlive());
 +        if (commit)
 +            assertTrue("the older commit should conflict: " + older.get(), 
older.get() instanceof TransactionException);
 +        else
 +            assertNull(older.get());
 +
 +        assertEquals("new", graph.vertices(1).next().value("name"));
 +        graph.close();
 +        graph = TinkerStorageGraph.open(conf);
 +        assertEquals("new", graph.vertices(1).next().value("name"));
 +        graph.close();
 +    }
 +
 +    @Test
 +    public void shouldCloseTheServicesAndTheEngineWhenTheOpenFails() throws 
Exception {
 +        final Configuration conf = new BaseConfiguration();
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE, 
ReplayFailingStorage.class.getName());
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE_DIRECTORY, 
tempFolder.newFolder().getAbsolutePath());
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_SERVICE, 
CloseRecordingServiceFactory.class.getName());
 +        CloseRecordingServiceFactory.closed.set(false);
 +        ReplayFailingStorage.closed.set(false);
 +        try {
 +            TinkerStorageGraph.open(conf).close();
 +            fail("the replay failure should fail the open");
 +        } catch (IllegalStateException expected) {
 +            assertEquals("replay failure", expected.getMessage());
 +        }
 +        assertTrue("the configured service should be closed", 
CloseRecordingServiceFactory.closed.get());
 +        assertTrue("the storage engine should be closed", 
ReplayFailingStorage.closed.get());
 +    }
 +
 +    /**
 +     * A storage engine whose replay fails, recording whether it is closed. 
Static because it is instantiated by name.
 +     */
 +    public static final class ReplayFailingStorage implements TinkerStorage {
 +        static final AtomicBoolean closed = new AtomicBoolean();
 +
 +        @Override
 +        public void open(final AbstractTinkerGraph graph, final Configuration 
config) { }
 +
 +        @Override
 +        public void replay(final AbstractTinkerGraph graph) {
 +            throw new IllegalStateException("replay failure");
 +        }
 +
 +        @Override
 +        public void persist(final long txVersion, final 
Collection<TinkerStorageMutation<TinkerVertex>> changedVertices,
 +                            final 
Collection<TinkerStorageMutation<TinkerEdge>> changedEdges) { }
 +
 +        @Override
 +        public void flush() { }
 +
 +        @Override
 +        public void compact(final AbstractTinkerGraph graph) { }
 +
 +        @Override
 +        public void clear(final AbstractTinkerGraph graph) { }
 +
 +        @Override
 +        public void failStop(final Throwable cause) { }
 +
 +        @Override
 +        public void close() {
 +            closed.set(true);
 +        }
 +    }
 +
 +    /**
 +     * A service factory that records whether it is closed. Static because it 
is instantiated by name.
 +     */
 +    public static final class CloseRecordingServiceFactory extends 
TinkerServiceRegistry.TinkerServiceFactory<Object, Object> {
 +        static final AtomicBoolean closed = new AtomicBoolean();
 +
 +        public CloseRecordingServiceFactory(final AbstractTinkerGraph graph) {
 +            super(graph, "close.recording");
 +        }
 +
 +        @Override
 +        public Set<Service.Type> getSupportedTypes() {
 +            return Collections.singleton(Service.Type.Start);
 +        }
 +
 +        @Override
 +        public Service<Object, Object> createService(final boolean isStart, 
final Map params) {
 +            throw new UnsupportedOperationException();
 +        }
 +
 +        @Override
 +        public void close() {
 +            closed.set(true);
 +        }
 +    }
 +
 +    @Test
 +    public void 
shouldStopAcceptingCommitsWhenTheEngineFailsToClearWithoutStoppingItself() 
throws Exception {
 +        final Configuration conf = new BaseConfiguration();
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE, 
"graphbinary");
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE_DIRECTORY, 
tempFolder.newFolder().getAbsolutePath());
 +        TinkerStorageGraph graph = TinkerStorageGraph.open(conf);
 +        graph.addVertex(T.id, 1);
 +        graph.tx().commit();
 +
 +        // an engine whose clear() throws without putting itself into the 
failed state
 +        final TinkerStorage storage = spy(graph.storage);
 +        doThrow(new IllegalStateException("clear 
failure")).when(storage).clear(any());
 +        graph.storage = storage;
 +        try {
 +            graph.clear();
 +            fail("the failure emptying the store should be reported");
 +        } catch (IllegalStateException expected) {
 +            assertEquals("clear failure", expected.getMessage());
 +        }
 +        assertTrue(graph.storage.isFailed());
 +        assertFalse(graph.vertices().hasNext());
 +
 +        graph.addVertex(T.id, 2);
 +        try {
 +            graph.tx().commit();
 +            fail("a commit after the failed clear should be refused");
 +        } catch (UncheckedIOException expected) {
 +            // the storage stopped accepting commits
 +        }
 +        graph.close();
 +
 +        // the store was never emptied, so the cleared data returns, but 
nothing was appended to it
 +        graph = TinkerStorageGraph.open(conf);
 +        assertEquals(Collections.singleton(1), elementIds(graph, false));
 +        graph.close();
 +    }
 +
 +    @Test
 +    public void 
shouldAgreeInMemoryAndOnDiskAboutAVertexAddedWhileTheGraphIsCleared() throws 
Exception {
 +        assertElementsAddedWhileClearingAgreeInMemoryAndOnDisk(false);
 +    }
 +
 +    @Test
 +    public void 
shouldAgreeInMemoryAndOnDiskAboutAnEdgeAddedWhileTheGraphIsCleared() throws 
Exception {
 +        assertElementsAddedWhileClearingAgreeInMemoryAndOnDisk(true);
 +    }
 +
 +    /**
 +     * Another thread adds vertex 5 (and, with {@code withEdge}, vertex 6 and 
edge 100 between them) while clear() is
 +     * emptying the element map, and then commits. Whether that commit 
conflicts or succeeds, memory, the store as a
 +     * crash would leave it, and the store after a clean close must all show 
the same elements.
 +     */
 +    private void assertElementsAddedWhileClearingAgreeInMemoryAndOnDisk(final 
boolean withEdge) throws Exception {
 +        final File directory = tempFolder.newFolder();
 +        final Configuration conf = new BaseConfiguration();
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE, 
"graphbinary");
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE_DIRECTORY, 
directory.getAbsolutePath());
 +        TinkerStorageGraph graph = TinkerStorageGraph.open(conf);
 +        final Vertex v1 = graph.addVertex(T.id, 1);
 +        v1.addEdge("knows", graph.addVertex(T.id, 2), T.id, 99);
 +        graph.tx().commit();
 +
 +        // pause clear() inside its pass over the element map, after it has 
started emptying it
 +        final PausingMap<Object> paused = new PausingMap<>();
 +        final Field field = 
TinkerStorageGraph.class.getDeclaredField(withEdge ? "edges" : "vertices");
 +        field.setAccessible(true);
 +        paused.putAll((Map<Object, Object>) field.get(graph));
 +        field.set(graph, paused);
 +
 +        final TinkerStorageGraph g = graph;
 +        final AtomicReference<Throwable> addCommit = new AtomicReference<>();
 +        final Thread other = new Thread(() -> {
 +            try {
 +                if (!paused.paused.await(10, TimeUnit.SECONDS))
 +                    throw new AssertionError("clear() did not reach the 
element map");
 +                final Vertex v5 = g.addVertex(T.id, 5);
 +                if (withEdge) v5.addEdge("knows", g.addVertex(T.id, 6), T.id, 
100);
 +                paused.resume.countDown();
 +                g.tx().commit();
 +            } catch (Throwable t) {
 +                addCommit.set(t);
 +            } finally {
 +                paused.resume.countDown();
 +            }
 +        });
 +        other.start();
 +        graph.clear();
 +        other.join(10_000);
 +        assertFalse("the other thread should have finished", other.isAlive());
 +        final Throwable failure = addCommit.get();
 +        assertTrue("the commit should succeed or conflict: " + failure,
 +                null == failure || failure instanceof TransactionException);
 +
 +        final Set<Object> inMemory = elementIds(graph, withEdge);
 +        if (null == failure)
 +            assertTrue(inMemory.contains(withEdge ? 100 : 5));
 +        else
 +            assertTrue(inMemory.isEmpty());
 +
 +        // the store as a crash right now would leave it, read from a copy 
since the graph still holds the directory
 +        final File crashed = tempFolder.newFolder();
 +        for (final File file : directory.listFiles()) {
 +            if (!file.getName().equals("LOCK"))
 +                Files.copy(file.toPath(), new File(crashed, 
file.getName()).toPath());
 +        }
 +        graph.close();
 +        final Configuration crashedConf = new BaseConfiguration();
 +        crashedConf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE, 
"graphbinary");
 +        
crashedConf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE_DIRECTORY, 
crashed.getAbsolutePath());
 +        graph = TinkerStorageGraph.open(crashedConf);
 +        assertEquals("after a crash", inMemory, elementIds(graph, withEdge));
 +        graph.close();
 +
 +        graph = TinkerStorageGraph.open(conf);
 +        assertEquals("after a clean close", inMemory, elementIds(graph, 
withEdge));
 +        graph.close();
 +    }
 +
 +    private static Set<Object> elementIds(final TinkerStorageGraph graph, 
final boolean edges) {
 +        final Set<Object> ids = new HashSet<>();
 +        (edges ? graph.edges() : graph.vertices()).forEachRemaining(e -> 
ids.add(e.id()));
 +        return ids;
 +    }
 +
 +    /**
 +     * Pauses the first removal from the map, which is how clear() empties 
it, until another thread lets it go on.
 +     */
 +    private static class PausingMap<V> extends ConcurrentHashMap<Object, V> {
 +        final CountDownLatch paused = new CountDownLatch(1);
 +        final CountDownLatch resume = new CountDownLatch(1);
 +        private final AtomicBoolean first = new AtomicBoolean(true);
 +
 +        @Override
 +        public boolean remove(final Object key, final Object value) {
 +            pauseOnce();
 +            return super.remove(key, value);
 +        }
 +
 +        @Override
 +        public void clear() {
 +            pauseOnce();
 +            super.clear();
 +        }
 +
 +        private void pauseOnce() {
 +            if (!first.getAndSet(false)) return;
 +            paused.countDown();
 +            try {
 +                resume.await(10, TimeUnit.SECONDS);
 +            } catch (InterruptedException ex) {
 +                Thread.currentThread().interrupt();
 +            }
 +        }
 +    }
 +
 +    @Test
 +    @SuppressWarnings("unchecked")
 +    public void shouldReportACommitThatFailsAfterItsStorageWriteAsDurable() 
throws Exception {
 +        final Configuration conf = new BaseConfiguration();
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE, 
"graphbinary");
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE_DIRECTORY, 
tempFolder.newFolder().getAbsolutePath());
 +        TinkerStorageGraph graph = TinkerStorageGraph.open(conf);
 +        graph.createIndex("name", Vertex.class);
 +        graph.addVertex(T.id, 1, "name", "a");
 +        graph.tx().commit();
 +
 +        // the index commit runs once the transaction is durable, so a 
failure there comes after the write
 +        final TinkerTransactionalIndex<TinkerVertex> index = 
spy((TinkerTransactionalIndex<TinkerVertex>) graph.vertexIndex);
 +        doThrow(new IllegalStateException("index 
failure")).when(index).commit(any());
 +        graph.vertexIndex = index;
 +
 +        graph.addVertex(T.id, 2, "name", "b");
 +        try {
 +            graph.tx().commit();
 +            fail("the failure applying the transaction should be reported");
 +        } catch (TransactionException expected) {
 +            assertTrue(expected.getMessage(), 
expected.getMessage().contains("will be present when the graph is reopened"));
 +        }
 +        assertTrue(graph.storage.isFailed());
 +        assertFalse("the failure came before any of the transaction was 
applied", graph.vertices(2).hasNext());
 +
 +        graph.addVertex(T.id, 3);
 +        try {
 +            graph.tx().commit();
 +            fail("no further commits are accepted until the graph is 
reopened");
 +        } catch (UncheckedIOException expected) {
 +            assertTrue(expected.getMessage(), 
expected.getMessage().contains("stopped accepting commits"));
 +        }
 +        graph.close();
 +
 +        graph = TinkerStorageGraph.open(conf);
 +        assertTrue("the durable transaction is present after the reopen", 
graph.vertices(2).hasNext());
 +        assertFalse(graph.vertices(3).hasNext());
 +        graph.close();
 +    }
 +
 +    @Test
 +    public void shouldReportACommitThatFailsPartwayThroughItsApplyAsDurable() 
throws Exception {
 +        final Configuration conf = new BaseConfiguration();
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE, 
"graphbinary");
 +        conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_STORAGE_DIRECTORY, 
tempFolder.newFolder().getAbsolutePath());
 +        TinkerStorageGraph graph = TinkerStorageGraph.open(conf);
 +        graph.createIndex("name", Vertex.class);
 +        graph.createIndex("weight", Edge.class);
 +        graph.addVertex(T.id, 1, "name", "a");
 +        graph.tx().commit();
 +
 +        // the vertex index is committed first, so a failure in the edge 
index comes after part of the apply
 +        final TinkerTransactionalIndex<TinkerEdge> index = 
spy((TinkerTransactionalIndex<TinkerEdge>) graph.edgeIndex);
 +        doThrow(new IllegalStateException("index 
failure")).when(index).commit(any());
 +        graph.edgeIndex = index;
 +
 +        graph.vertices(1).next().addEdge("knows", graph.addVertex(T.id, 2, 
"name", "b"), T.id, 10, "weight", 0.5d);
 +        try {
 +            graph.tx().commit();
 +            fail("the failure applying the transaction should be reported");
 +        } catch (TransactionException expected) {
 +            assertTrue(expected.getMessage(), 
expected.getMessage().contains("will be present when the graph is reopened"));
 +            assertTrue(expected.getMessage(), 
expected.getMessage().contains("may show only part of it"));
 +        }
 +        assertTrue(graph.storage.isFailed());
 +        graph.close();
 +
 +        graph = TinkerStorageGraph.open(conf);
 +        assertEquals(1L, graph.traversal().V().has("name", 
"b").count().next().longValue());
 +        assertEquals(1L, graph.traversal().E().has("weight", 
0.5d).count().next().longValue());
 +        assertEquals(2, 
graph.vertices(1).next().edges(Direction.OUT).next().inVertex().id());
 +        graph.close();
 +    }
 +
 +    private static final String PROPERTY_NAME = "name";
 +    private static final String PROPERTY_WEIGHT = "weight";
 +    private static final String EDGE_LABEL = "tests";
 +    private static final int EDGE_ID = 200;
 +    final Object vid = 100;
 +
 +    ///// vertex tests
 +
 +    @Test
 +    public void shouldCountVerticesByLabelAccountingForMultipleLabels() {
 +        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
 +        
config.setProperty(org.apache.tinkerpop.gremlin.structure.Graph.GRAPH, 
TinkerStorageGraph.class.getName());
 +        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
 +        final TinkerStorageGraph g = TinkerStorageGraph.open(config);
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +        gtx.addV("person").next();
 +        final Vertex multiLabelled = 
gtx.addV("person").addLabel("employee").next();
 +        gtx.tx().commit();
 +
 +        assertEquals(2L, g.countVerticesByLabel("person"));
 +        assertEquals(1L, g.countVerticesByLabel("employee"));
 +        assertEquals(0L, g.countVerticesByLabel("customer"));
 +
 +        gtx.V(multiLabelled.id()).next();
 +        ((Vertex) gtx.V(multiLabelled.id()).next()).dropLabel("person");
 +        gtx.tx().commit();
 +
 +        assertEquals(1L, g.countVerticesByLabel("person"));
 +        assertEquals(1L, g.countVerticesByLabel("employee"));
 +    }
 +
 +    @Test
 +    public void shouldRollbackWithVertexIndexButNoEdgeIndex() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("name", Vertex.class);
 +        g.addVertex("name", "marko");
 +        g.tx().rollback();
 +
 +        assertEquals(0L, (long) g.traversal().V().has("name", 
"marko").count().next());
 +        g.tx().rollback();
 +    }
 +
 +    @Test
 +    public void shouldReturnSameVertexInstanceInsideTransaction() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex vertex = gtx.addV().property(T.id, vid).next();
 +        assertEquals(vertex, gtx.V().next());
 +        assertEquals(vertex, gtx.V(vid).next());
 +        assertEquals(vertex, gtx.V().toList().get(0));
 +    }
 +
 +    @Test
 +    public void shouldDeleteVertexOnCommit() throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().iterate();
 +        gtx.tx().commit();
 +
 +        assertEquals(1, (long) gtx.V().count().next());
 +
 +        gtx.V().drop().iterate();
 +        assertEquals(0, (long) gtx.V().count().next());
 +
 +        countElementsInNewThreadTx(g, 1, 0);
 +
 +        gtx.tx().commit();
 +
 +        assertEquals(0, (long) gtx.V().count().next());
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(0L, (long) gtx3.V().count().next());
 +
 +        countElementsInNewThreadTx(g, 0, 0);
 +    }
 +
 +    @Test
 +    public void shouldDeleteCreatedVertexOnCommit() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        // create and drop Vertex in same transaction
 +        gtx.addV().property(T.id, vid).iterate();
 +        gtx.V(vid).drop().iterate();
 +
 +        assertEquals(0, (long) gtx.V().count().next());
 +        countElementsInNewThreadTx(g, 0, 0);
 +
 +        gtx.tx().commit();
 +
 +        assertEquals(0, (long) gtx.V().count().next());
 +        countElementsInNewThreadTx(g, 0, 0);
 +        // should remove unused container
 +        assertEquals(0, g.getVertices().size());
 +    }
 +
 +    @Test
 +    public void shouldCleanUpVertexContainerOnCommit() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property(T.id, vid).iterate();
 +        gtx.tx().commit();
 +
 +        // read vertex in new TX
 +        final GraphTraversalSource gtx2 = g.tx().begin();
 +        final Vertex v = gtx.V(vid).next();
 +        gtx.V(vid).iterate();
 +        // should be the same instance
 +        assertEquals(v, g.getVertices().get(vid).getModified());
 +        // commit without modifications
 +        gtx.tx().commit();
 +
 +        // should clean up unused value in container
 +        assertEquals(1, g.getVertices().size());
 +        assertNull(g.getVertices().get(vid).getModified());
 +    }
 +
 +    @Test
 +    public void shouldCleanUpVertexContainerOnRollback() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property(T.id, vid).iterate();
 +        gtx.tx().commit();
 +
 +        // read vertex in new TX
 +        final GraphTraversalSource gtx2 = g.tx().begin();
 +        final Vertex v = gtx.V(vid).next();
 +        gtx.V(vid).iterate();
 +        // should be the same instance
 +        assertEquals(v, g.getVertices().get(vid).getModified());
 +        // rollback without modifications
 +        gtx.tx().rollback();
 +
 +        // should clean up unused value in container
 +        assertEquals(1, g.getVertices().size());
 +        assertNull(g.getVertices().get(vid).getModified());
 +    }
 +
 +    @Test
 +    public void shouldDeleteUnusedVertexContainerOnCommit() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property(T.id, vid).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.V(vid).drop().iterate();
 +        gtx.tx().commit();
 +
 +        // should remove unused container
 +        assertEquals(0, g.getVertices().size());
 +    }
 +
 +    @Test
 +    public void shouldDeleteUnusedVertexContainerOnRollback() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property(T.id, vid).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.V(vid).properties("test-property").iterate();
 +        // 1 container for existing vertex
 +        assertEquals(1, g.getVertices().size());
 +
 +        // other tx try to add new vertices and modify existing
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2.addV().addV().iterate();
 +            gtx.V(vid).properties("test-property").iterate();
 +
 +            // 1 container for existing vertex + 2 for new ones
 +            assertEquals(3, g.getVertices().size());
 +
 +            assertEquals(3, (long) gtx2.V().count().next());
 +
 +            gtx2.tx().rollback();
 +            // containers for new vertices should be removed
 +            assertEquals(1, g.getVertices().size());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().rollback();
 +
 +        // should keep container because vertex not created in this 
transaction
 +        assertEquals(1, g.getVertices().size());
 +    }
 +
 +    @Test
 +    public void shouldDeleteUnusedVertexContainerOnConcurrentVertexDelete() 
throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final TinkerVertex v1 = (TinkerVertex) gtx.addV().next();
 +        gtx.tx().commit();
 +
 +        gtx.V(v1.id()).drop().iterate();
 +
 +        // other tx try to remove same vertex
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2.V(v1.id()).drop().iterate();
 +
 +            // should be ok
 +            assertEquals(0, (long) gtx2.V().count().next());
 +            assertEquals(0, (long) gtx2.E().count().next());
 +
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        // try do delete in initial tx
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        // should remove unused container
 +        assertEquals(0, g.getVertices().size());
 +    }
 +
 +    ///// Edge tests
 +
 +    @Test
 +    public void shouldRemoveContainerAndReferenceFromVertexOnDeleteEdge() 
throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final TinkerVertex v1 = (TinkerVertex) gtx.addV().next();
 +        final TinkerVertex v2 = (TinkerVertex) gtx.addV().next();
 +        gtx.addE(EDGE_LABEL).from(v1).to(v2).next();
 +        gtx.tx().commit();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(1, (long) gtx.E().count().next());
 +
 +        gtx.E().hasLabel(EDGE_LABEL).drop().iterate();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        // test count in other thread transaction
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(2L, (long) gtx2.V().count().next());
 +            assertEquals(1L, (long) gtx2.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        // should remove reference from edge to parent vertices
 +        final TinkerVertex v1afterTx = g.getVertices().get(v1.id()).get();
 +        final TinkerVertex v2afterTx = g.getVertices().get(v2.id()).get();
 +        assertNull(v1afterTx.inEdgesId);
 +        assertEquals(0, v1afterTx.outEdgesId.get(EDGE_LABEL).size());
 +        assertEquals(0, v2afterTx.inEdgesId.get(EDGE_LABEL).size());
 +        assertNull(v2afterTx.outEdgesId);
 +
 +        // should remove unused container
 +        assertEquals(0, g.getEdges().size());
 +
 +        countElementsInNewThreadTx(g, 2, 0);
 +    }
 +
 +    @Test
 +    public void 
shouldRemoveContainerAndReferenceFromVertexOnConcurrentDeleteEdge() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final TinkerVertex v1 = (TinkerVertex) gtx.addV().next();
 +        final TinkerVertex v2 = (TinkerVertex) gtx.addV().next();
 +        gtx.addE(EDGE_LABEL).from(v1).to(v2).next();
 +        gtx.tx().commit();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(1, (long) gtx.E().count().next());
 +
 +        gtx.E().hasLabel(EDGE_LABEL).drop().iterate();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        // other tx try to remove same edge
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(2L, (long) gtx2.V().count().next());
 +            assertEquals(1L, (long) gtx2.E().count().next());
 +
 +            // try to remove edge
 +            gtx2.E().hasLabel(EDGE_LABEL).drop().iterate();
 +
 +            // should be ok
 +            assertEquals(2, (long) gtx2.V().count().next());
 +            assertEquals(0, (long) gtx2.E().count().next());
 +
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        // try do delete in initial tx
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        // should remove unused container
 +        assertEquals(0, g.getEdges().size());
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        // should remove reference from edge to parent vertices
 +        final TinkerVertex v1afterTx = g.getVertices().get(v1.id()).get();
 +        final TinkerVertex v2afterTx = g.getVertices().get(v2.id()).get();
 +        assertNull(v1afterTx.inEdgesId);
 +        assertEquals(0, v1afterTx.outEdgesId.get(EDGE_LABEL).size());
 +        assertEquals(0, v2afterTx.inEdgesId.get(EDGE_LABEL).size());
 +        assertNull(v2afterTx.outEdgesId);
 +
 +        countElementsInNewThreadTx(g, 2, 0);
 +    }
 +
 +    // index tests for vertex
 +
 +    @Test
 +    public void shouldCreateIndexForNewVertex() throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Vertex.class);
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property(T.id, vid).property("test-property", 1).iterate();
 +        gtx.addV().property("test-property", 2).iterate();
 +
 +        assertEquals(1L, (long) gtx.V().has("test-property", 
1).count().next());
 +        assertEquals(vid, gtx.V().has("test-property", 1).id().next());
 +        assertEquals(0L, (long) gtx.V().has("test-property", 
3).count().next());
 +        assertEquals(2L, (long) gtx.V().count().next());
 +
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(0L, (long) gtx2.V().has("test-property", 
1).count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(1L, (long) gtx3.V().has("test-property", 
1).count().next());
 +        assertEquals(vid, gtx3.V().has("test-property", 1).id().next());
 +        assertEquals(0L, (long) gtx3.V().has("test-property", 
3).count().next());
 +        assertEquals(2L, (long) gtx3.V().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 0);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.vertexIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(1, index.get(1).size());
 +        assertEquals(vid, index.get(1).iterator().next().get().id());
 +    }
 +
 +    @Test
 +    public void shouldCreateIndexForNullVertexProperty() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Vertex.class);
 +        g.allowNullPropertyValues = true;
 +        final Integer nullValue = null;
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property(T.id, vid).property("test-property", 
nullValue).iterate();
 +        gtx.addV().property("test-property", 2).iterate();
 +
 +        assertEquals(1L, (long) gtx.V().has("test-property", 
nullValue).count().next());
 +        assertEquals(vid, gtx.V().has("test-property", 
nullValue).id().next());
 +        assertEquals(0L, (long) gtx.V().has("test-property", 
1).count().next());
 +        assertEquals(2L, (long) gtx.V().count().next());
 +
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(0L, (long) gtx2.V().has("test-property", 
nullValue).count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(1L, (long) gtx3.V().has("test-property", 
nullValue).count().next());
 +        assertEquals(vid, gtx3.V().has("test-property", 
nullValue).id().next());
 +        assertEquals(0L, (long) gtx3.V().has("test-property", 
1).count().next());
 +        assertEquals(2L, (long) gtx3.V().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 0);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.vertexIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(1, 
index.get(AbstractTinkerIndex.IndexedNull.instance()).size());
 +        assertEquals(vid, 
index.get(AbstractTinkerIndex.IndexedNull.instance()).iterator().next().get().id());
 +    }
 +
 +    @Test
 +    public void shouldRemoveIndexForRemovedVertex() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Vertex.class);
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property(T.id, vid).property("test-property", 1).iterate();
 +
 +        gtx.tx().commit();
 +
 +        gtx.V(vid).drop().iterate();
 +
 +        assertEquals(0L, (long) gtx.V().has("test-property", 
1).count().next());
 +        assertEquals(0L, (long) gtx.V().count().next());
 +
 +        // in another thread elements exists before commit
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(1L, (long) gtx2.V().has("test-property", 
1).count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(0L, (long) gtx3.V().has("test-property", 
1).count().next());
 +        assertEquals(0L, (long) gtx3.V().count().next());
 +
 +        countElementsInNewThreadTx(g, 0, 0);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.vertexIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(0, index.size());
 +     }
 +
 +    @Test
 +    public void shouldRemoveIndexForRemovedVertexProperty() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Vertex.class);
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property(T.id, vid).property("test-property", 1).iterate();
 +
 +        gtx.tx().commit();
 +
 +        gtx.V(vid).properties("test-property").drop().iterate();
 +
 +        assertEquals(0L, (long) gtx.V().has("test-property", 
1).count().next());
 +        assertEquals(1L, (long) gtx.V().count().next());
 +
 +        // in another thread elements exists before commit
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(1L, (long) gtx2.V().has("test-property", 
1).count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(0L, (long) gtx3.V().has("test-property", 
1).count().next());
 +        assertEquals(1L, (long) gtx3.V().count().next());
 +
 +        countElementsInNewThreadTx(g, 1, 0);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.vertexIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(0, index.size());
 +    }
 +
 +    // index tests for edge
 +
 +    @Test
 +    public void shouldCreateIndexForNewEdge() throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Edge.class);
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final TinkerVertex v1 = (TinkerVertex) gtx.addV().next();
 +        final TinkerVertex v2 = (TinkerVertex) gtx.addV().next();
 +        final TinkerEdge edge = (TinkerEdge) 
gtx.addE(EDGE_LABEL).property("test-property", 1).from(v1).to(v2).next();
 +
 +        assertEquals(1L, (long) gtx.E().has("test-property", 
1).count().next());
 +        assertEquals(1L, (long) gtx.E().count().next());
 +
 +        // in another thread index not updated yet
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(0L, (long) gtx2.E().has("test-property", 
1).count().next());
 +            assertEquals(0L, (long) gtx2.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(1L, (long) gtx.E().has("test-property", 
1).count().next());
 +        assertEquals(1L, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 1);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.edgeIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(1, index.size());
 +        assertEquals(1, index.get(1).size());
 +        assertEquals(edge.id(), index.get(1).iterator().next().get().id());
 +    }
 +
 +    @Test
 +    public void shouldChangeIndexForChangedEdge() throws InterruptedException 
{
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Edge.class);
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final TinkerVertex v1 = (TinkerVertex) gtx.addV().next();
 +        final TinkerVertex v2 = (TinkerVertex) gtx.addV().next();
 +        final TinkerEdge edge = (TinkerEdge) 
gtx.addE(EDGE_LABEL).property("test-property", 1).from(v1).to(v2).next();
 +        gtx.tx().commit();
 +
 +        gtx.E(edge.id()).property("test-property", 2).iterate();
 +
 +        assertEquals(0L, (long) gtx.E().has("test-property", 
1).count().next());
 +        assertEquals(1L, (long) gtx.E().has("test-property", 
2).count().next());
 +        assertEquals(1L, (long) gtx.E().count().next());
 +
 +        // in another thread index not updated yet
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(1L, (long) gtx2.E().has("test-property", 
1).count().next());
 +            assertEquals(0L, (long) gtx2.E().has("test-property", 
2).count().next());
 +            assertEquals(1L, (long) gtx2.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(0L, (long) gtx.E().has("test-property", 
1).count().next());
 +        assertEquals(1L, (long) gtx.E().has("test-property", 
2).count().next());
 +        assertEquals(1L, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 1);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.edgeIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(1, index.size());
 +        assertNull(index.get(1));
 +        assertEquals(1, index.get(2).size());
 +        assertEquals(edge.id(), index.get(2).iterator().next().get().id());
 +    }
 +
 +    @Test
 +    public void shouldRemoveIndexForRemovedEdge() throws InterruptedException 
{
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Edge.class);
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final TinkerVertex v1 = (TinkerVertex) gtx.addV().next();
 +        final TinkerVertex v2 = (TinkerVertex) gtx.addV().next();
 +        final TinkerEdge edge = (TinkerEdge) 
gtx.addE(EDGE_LABEL).property("test-property", 1).from(v1).to(v2).next();
 +        gtx.tx().commit();
 +
 +        gtx.E(edge.id()).drop().iterate();
 +
 +        assertEquals(0L, (long) gtx.E().has("test-property", 
1).count().next());
 +        assertEquals(0L, (long) gtx.E().count().next());
 +
 +        // in another thread elements exists before commit
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(1L, (long) gtx2.E().has("test-property", 
1).count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(0L, (long) gtx3.E().has("test-property", 
1).count().next());
 +        assertEquals(0L, (long) gtx3.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 0);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.edgeIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(0, index.size());
 +    }
 +
 +    @Test
 +    public void shouldCreateIndexForNullProperty() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Edge.class);
 +        g.allowNullPropertyValues = true;
 +        final Integer nullValue = null;
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final TinkerVertex v1 = (TinkerVertex) gtx.addV().next();
 +        final TinkerVertex v2 = (TinkerVertex) gtx.addV().next();
 +        final TinkerEdge edge = (TinkerEdge) 
gtx.addE(EDGE_LABEL).property("test-property", nullValue).
 +                from(v1).to(v2).next();
 +
 +        assertEquals(1L, (long) gtx.E().has("test-property", 
nullValue).count().next());
 +        assertEquals(1L, (long) gtx.E().count().next());
 +
 +        // in another thread index not updated yet
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(0L, (long) gtx2.E().has("test-property", 
nullValue).count().next());
 +            assertEquals(0L, (long) gtx2.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(1L, (long) gtx3.E().has("test-property", 
nullValue).count().next());
 +        assertEquals(1L, (long) gtx3.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 1);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.edgeIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(1, index.size());
 +        assertEquals(1, 
index.get(AbstractTinkerIndex.IndexedNull.instance()).size());
 +        assertEquals(edge.id(), 
index.get(AbstractTinkerIndex.IndexedNull.instance()).iterator().next().get().id());
 +    }
 +
 +    @Test
 +    public void shouldRemoveIndexForNullProperty() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        g.createIndex("test-property", Edge.class);
 +        final Integer nullValue = null;
 +
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        // create initial edge with indexed property
 +        final TinkerVertex v1 = (TinkerVertex) gtx.addV().next();
 +        final TinkerVertex v2 = (TinkerVertex) gtx.addV().next();
 +        final TinkerEdge edge = (TinkerEdge) 
gtx.addE(EDGE_LABEL).property("test-property", 1).
 +                from(v1).to(v2).next();
 +
 +        gtx.tx().commit();
 +
 +        // remove property from index
 +        gtx.E(edge.id()).property("test-property", nullValue).iterate();
 +
 +        assertEquals(0L, (long) gtx.E().has("test-property", 
1).count().next());
 +        assertEquals(1L, (long) gtx.E().count().next());
 +
 +        // in another thread index not updated yet
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(1L, (long) gtx2.E().has("test-property", 
1).count().next());
 +            assertEquals(1L, (long) gtx2.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(0L, (long) gtx3.E().has("test-property", 
nullValue).count().next());
 +        assertEquals(1L, (long) gtx3.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 1);
 +
 +        final Map<Object, Set<TinkerElementContainer<?>>> index =
 +                (Map<Object, Set<TinkerElementContainer<?>>>) 
((TinkerTransactionalIndex) g.edgeIndex).index.get("test-property");
 +        assertNotNull(index);
 +        // should be only vertex vid in set
 +        assertEquals(0, index.size());
 +    }
 +
 +    // tests for cloning elements
 +
 +    @Test
 +    public void vertexCloneTest() {
 +        final TinkerVertex vertex = new TinkerVertex(123, "label", 
TinkerStorageGraph.open());
 +        final TinkerVertexProperty vp = new TinkerVertexProperty(vertex, 
"test", "qq");
 +        final TinkerEdge edge = new TinkerEdge(1, vertex, "label", vertex);
 +        vertex.properties = new ConcurrentHashMap<>();
 +        vertex.properties.put("test", new ArrayList<>());
 +        vertex.properties.get("test").add(vp);
 +        vertex.inEdgesId = new ConcurrentHashMap<>();
 +        vertex.inEdgesId.put("label", ConcurrentHashMap.newKeySet());
 +        vertex.inEdgesId.get("label").add(edge);
 +
 +        final TinkerVertex copy = (TinkerVertex) vertex.clone();
 +        vertex.properties.get("test").remove(vp);
 +        assertEquals(1, copy.properties.get("test").size());
 +        vertex.inEdgesId.get("label").remove(edge);
 +        assertEquals(1, copy.inEdgesId.get("label").size());
 +    }
 +
 +    @Test
 +    public void edgeCloneTest() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final TinkerVertex v1 = (TinkerVertex) g.addVertex();
 +        final TinkerVertex v2 = (TinkerVertex) g.addVertex();
 +        final TinkerEdge edge = new TinkerEdge(3, v1, "label", v2);
 +        final TinkerProperty property = new TinkerProperty(edge, "test", 
"qq");
 +        edge.properties = new ConcurrentHashMap<>();
 +        edge.properties.put(property.key(), property);
 +
 +        final TinkerEdge copy = (TinkerEdge) edge.clone();
 +        edge.properties.remove(property.key());
 +        assertEquals(1, copy.properties.size());
 +    }
 +
 +    // utility methods
 +
 +    private void countElementsInNewThreadTx(final TinkerStorageGraph g, final 
long verticesCount, final long edgesCount) throws InterruptedException {
 +        final AtomicLong vCount = new AtomicLong(-1);
 +        final AtomicLong eCount = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx = g.tx().begin();
 +            vCount.set(gtx.V().count().next());
 +            eCount.set(gtx.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(verticesCount, vCount.get());
 +        assertEquals(edgesCount, eCount.get());
 +    }
 +
 +    @Test
 +    public void shouldNotAllowDirtyReadsOfVertexForReadOnlyTransaction() 
throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().next();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set(gtx2.V().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(0, gtx2Read.get());
 +        assertEquals(1, (long) gtx.V().count().next());
 +    }
 +
 +    @Test
 +    public void shouldNotAllowDirtyReadsOfVertexDropForReadOnlyTransaction() 
throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().next();
 +        gtx.tx().commit();
 +
 +        gtx.V().drop().iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set(gtx2.V().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(1, gtx2Read.get());
 +        assertEquals(0, (long) gtx.V().count().next());
 +    }
 +
 +    @Test
 +    public void shouldNotAllowDirtyReadsOfEdgeForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        gtx.addE("test").from(v1).to(v2).iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set(gtx2.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(0, gtx2Read.get());
 +        assertEquals(1, (long) gtx.E().count().next());
 +    }
 +
 +    @Test
 +    public void shouldNotAllowDirtyReadsOfEdgeDropForReadOnlyTransaction() 
throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        gtx.addE("test").from(v1).to(v2).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.E().drop().iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set(gtx2.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(1, gtx2Read.get());
 +        assertEquals(0, (long) gtx.E().count().next());
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfVertexPropertyAddForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property("a", 1).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.V().property("b", 2).iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set(gtx2.V().properties().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(1, gtx2Read.get());
 +        assertEquals(2, (long) gtx.V().properties().count().next());
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfVertexPropertyDropForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property("a", 1).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.V().properties().drop().iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set(gtx2.V().properties().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(1, gtx2Read.get());
 +        assertEquals(0, (long) gtx.V().properties().count().next());
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfVertexPropertyUpdateForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        gtx.addV().property("a", 1L).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.V().property("a", 2L).iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set((long) gtx2.V().values("a").next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(1, gtx2Read.get());
 +        assertEquals(2, (long) gtx.V().values("a").next());
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfEdgePropertyAddForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        gtx.addE("test").from(v1).to(v2).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.E().property("a", 1).iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set(gtx2.E().properties().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(0, gtx2Read.get());
 +        assertEquals(1, (long) gtx.E().properties().count().next());
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfEdgePropertyDropForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        gtx.addE("test").from(v1).to(v2).property("a", 1).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.E().properties().drop().iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set(gtx2.E().properties().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(1, gtx2Read.get());
 +        assertEquals(0, (long) gtx.E().properties().count().next());
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfEdgePropertyUpdateForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        gtx.addE("test").from(v1).to(v2).property("a", 1L).iterate();
 +        gtx.tx().commit();
 +
 +        gtx.E().property("a", 2L).iterate();
 +
 +        final AtomicLong gtx2Read = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2Read.set((long) gtx2.E().values("a").next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        assertEquals(1, gtx2Read.get());
 +        assertEquals(2, (long) gtx.E().values("a").next());
 +    }
 +
 +    @Test
 +    public void shouldHandleAddingPropertyWhenOtherTxDeleteEdge() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        final Edge edge = gtx.addE(EDGE_LABEL).from(v1).to(v2).next();
 +        gtx.tx().commit();
 +
 +        // tx1 try to add property
 +        gtx.E(edge.id()).property("test", 1).iterate();
 +
 +        // tx2 in same time delete edge used by tx1
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.E(edge.id()).drop().iterate();
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 0);
 +    }
 +
 +    //tx1 adds a property to v1, tx2 deletes v1
 +    @Test
 +    public void shouldHandleAddingPropertyWhenOtherTxDeleteVertex() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        gtx.tx().commit();
 +
 +        // tx1 try to add property
 +        gtx.V(v1.id()).property("test", 1).iterate();
 +
 +        // tx2 in same time delete vertex used by tx1
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v1.id()).drop().iterate();
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        assertEquals(0, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 0, 0);
 +    }
 +
 +    @Test
 +    public void shouldDeleteEdgeOnCommit() throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        gtx.addE(EDGE_LABEL).from(v1).to(v2).next();
 +        gtx.tx().commit();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(1, (long) gtx.E().count().next());
 +
 +        gtx.E().hasLabel(EDGE_LABEL).drop().iterate();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        // test count in other thread transaction
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(2L, (long) gtx2.V().count().next());
 +            assertEquals(1L, (long) gtx2.E().count().next());
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 0);
 +    }
 +
 +    @Test
 +    public void shouldHandleConcurrentChangeForProperty() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        final Edge edge = gtx.addE(EDGE_LABEL).from(v1).to(v2).next();
 +        gtx.tx().commit();
 +
 +        // change test property
 +        gtx.E(edge.id()).property("test", 1).iterate();
 +
 +        // change property in other tx
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.E(edge.id()).property("test", 2).iterate();
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        // should be only 1 edge with updated property
 +        assertEquals(1L, (long) gtx.E(edge.id()).count().next());
 +        assertEquals(2, gtx.E(edge.id()).values("test").next());
 +    }
 +
 +    // tx1 adds an edge from v1 to v2, tx2 deletes v1 or v2
 +    @Test
 +    public void shouldHandleAddingEdgeWhenOtherTxDeleteVertex() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        gtx.tx().commit();
 +
 +        // tx1 try to add Edge
 +        gtx.addE("test").from(v1).to(v2).iterate();
 +
 +        // tx2 in same time delete one of vertices used by tx1
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v1.id()).drop().iterate();
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        assertEquals(1, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 1, 0);
 +    }
 +
 +    @Test
 +    public void shouldHandleConcurrentChangeForVertexProperty() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        gtx.tx().commit();
 +
 +        // change test property
 +        gtx.V(v1.id()).property("test", 1).iterate();
 +
 +        // change property in other tx
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v1.id()).property("test", 2).iterate();
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        // should be only 1 vertex with updated property
 +        assertEquals(1L, (long) gtx.V().count().next());
 +        assertEquals(2, gtx.V(v1.id()).values("test").next());
 +    }
 +
 +    @Test
 +    public void shouldHandleConcurrentDeleteEdge() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        final Vertex v2 = gtx.addV().next();
 +        gtx.addE(EDGE_LABEL).from(v1).to(v2).next();
 +        gtx.tx().commit();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(1, (long) gtx.E().count().next());
 +
 +        gtx.E().hasLabel(EDGE_LABEL).drop().iterate();
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        // other tx try to remove same edge
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            assertEquals(2L, (long) gtx2.V().count().next());
 +            assertEquals(1L, (long) gtx2.E().count().next());
 +
 +            // try to remove edge
 +            gtx2.E().hasLabel(EDGE_LABEL).drop().iterate();
 +
 +            // should be ok
 +            assertEquals(2, (long) gtx2.V().count().next());
 +            assertEquals(0, (long) gtx2.E().count().next());
 +
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        // try do delete in initial tx
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        assertEquals(2, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 2, 0);
 +    }
 +
 +    // tx1 adds a new vertex v1, tx2 adds the same vertex
 +    @Test
 +    public void shouldHandleAddingSameVertexInDifferentTx() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +
 +        // tx2 in same time add vertex with same id
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.addV().property(T.id, v1.id()).iterate();
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        assertEquals(1, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 1, 0);
 +    }
 +
 +    @Test(expected = IllegalArgumentException.class)
 +    public void shouldThrowExceptionWhenTryToAddVertexWithUsedId() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        // add vertex
 +        final Vertex v1 = gtx.addV().next();
 +        gtx.tx().commit();
 +
 +        // try to add vertex with same id
 +        gtx.addV().property(T.id, v1.id()).iterate();
 +        gtx.tx().commit();
 +    }
 +
 +    @Test
 +    public void shouldHandleConcurrentVertexDelete() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        gtx.tx().commit();
 +
 +        gtx.V(v1.id()).drop().iterate();
 +
 +        // other tx try to remove same vertex
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +
 +            gtx2.V(v1.id()).drop().iterate();
 +
 +            // should be ok
 +            assertEquals(0, (long) gtx2.V().count().next());
 +            assertEquals(0, (long) gtx2.E().count().next());
 +
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        // try do delete in initial tx
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +        assertEquals(0, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 0, 0);
 +    }
 +
 +    @Test
 +    public void shouldHandleConcurrentChangeForVertexMetaProperty() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().property("test", 1).next();
 +        gtx.tx().commit();
 +
 +        // change meta property
 +        gtx.V(v1.id()).properties("test").property("meta1", "tx1").iterate();
 +
 +        // change same property in other tx
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v1.id()).properties("test").property("meta2", 
"tx2").iterate();
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +
 +        }
 +
 +        // tx2 committed first and wins; tx1 conflicted and rolled back, so 
its meta1 was never persisted
 +        assertEquals(1L, (long) gtx.V().count().next());
 +        assertEquals(1L, (long) 
gtx.V(v1.id()).properties("test").properties().count().next());
 +        assertEquals("tx2", 
gtx.V(v1.id()).properties("test").values("meta2").next());
 +        
assertFalse(gtx.V(v1.id()).properties("test").values("meta1").hasNext());
 +    }
 +
 +    @Test
 +    public void shouldReopenClosedTransaction() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +        gtx.addV().iterate();
 +        gtx.tx().commit();
 +
 +        gtx.addV().iterate();
 +        assertTrue(gtx.tx().isOpen());
 +        gtx.tx().commit();
 +
 +        final GraphTraversalSource gtx2 = g.tx().begin();
 +        assertEquals(2L, (long) gtx2.V().count().next());
 +    }
 +
 +    @Test
 +    public void shouldBeIdempotentAndNonLossyWhenBeginCalledWhileOpen() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        // stage an uncommitted change in the open transaction
 +        gtx.addV().iterate();
 +        assertTrue(gtx.tx().isOpen());
 +        assertEquals(1L, (long) gtx.V().count().next());
 +
 +        // calling begin() again while already open must be idempotent and 
non-lossy: it must NOT
 +        // start a new transaction or discard the in-flight one, so the 
staged change still exists
 +        final GraphTraversalSource gtx2 = g.tx().begin();
 +        assertTrue(gtx.tx().isOpen());
 +        assertEquals(1L, (long) gtx2.V().count().next());
 +
 +        // the change is part of one continuous transaction - committing once 
persists exactly it
 +        gtx.tx().commit();
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        assertEquals(1L, (long) gtx3.V().count().next());
 +    }
 +
 +    @Test
 +    public void 
shouldHandleAddingPropertyWhenOtherTxAttemptsDeleteThenRollsback() throws 
InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +        gtx.tx().commit();
 +
 +        // tx1 try to add property
 +        gtx.V(v1.id()).property("test", 1).iterate();
 +
 +        // tx2 in same time delete vertex used by tx1
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v1.id()).drop().iterate();
 +            gtx2.tx().rollback();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        assertEquals(1, (long) gtx.V().count().next());
 +        assertEquals(1, gtx.V(v1.id()).values("test").next());
 +
 +        countElementsInNewThreadTx(g, 1, 0);
 +    }
 +
 +    // tx1 adds vertex v1, tx2 removes vertex v1
 +    @Test
 +    public void shouldHandleAddingVertexWhenOtherTxTryToDeleteSameVertex() 
throws InterruptedException {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v1 = gtx.addV().next();
 +
 +        // tx2 in same time try to delete same vertex
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v1.id()).drop().iterate();
 +
 +            assertEquals(0, (long) gtx.V().count().next());
 +            assertEquals(0, (long) gtx.E().count().next());
 +            gtx2.tx().commit();
 +        });
 +        thread.start();
 +        thread.join();
 +
 +        gtx.tx().commit();
 +
 +        assertEquals(1, (long) gtx.V().count().next());
 +        assertEquals(0, (long) gtx.E().count().next());
 +
 +        countElementsInNewThreadTx(g, 1, 0);
 +    }
 +
 +    @Test
 +    public void shouldHandleSequenceOfCreateReadDeleteCreateSameVertex() {
 +        final TinkerStorageGraph graph = TinkerStorageGraph.open();
 +        final GraphTraversalSource g = graph.traversal();
 +
 +        g.addV().property(T.id, 1).next();
 +        graph.tx().commit();
 +        g.V().next();
 +        graph.tx().commit();
 +        g.V().drop().iterate();
 +        graph.tx().commit();
 +
 +        assertEquals(false, graph.hasVertex(1));
 +
 +        g.addV().property(T.id, 1).next();
 +        graph.tx().commit();
 +
 +        assertFalse(graph.getVertices().get(1).inUse());
 +        assertEquals(true, graph.hasVertex(1));
 +    }
 +
 +    @Test
 +    public void shouldHandleCorrectlyHandleCountForChangedAndReadElement() {
 +        final TinkerStorageGraph graph = TinkerStorageGraph.open();
 +        final GraphTraversalSource g = graph.traversal();
 +
 +        g.addV().property(T.id, 1).next();
 +        graph.tx().commit();
 +        TinkerElementContainer vertex = graph.getVertices().get(1);
 +
 +        g.V().next();
 +        g.V().property("prop", 5).next();
 +        g.V().next();
 +        g.V().property("prop2", "foo").next();
 +        assertTrue(vertex.isChanged());
 +        assertTrue(vertex.isRead());
 +        assertTrue(vertex.inUse());
 +
 +        graph.tx().commit();
 +        assertFalse(vertex.isChanged());
 +        assertFalse(vertex.isRead());
 +
 +        assertFalse(vertex.inUse());
 +    }
 +
 +    @Test
 +    public void shouldAllowDropAddVertexInSameTransaction() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final TinkerTransaction tx = (TinkerTransaction) g.tx();
 +
 +        // create initial vertex with a property
 +        final GraphTraversalSource gtx = tx.begin();
 +        Vertex vertex = gtx.addV().property(T.id, 
vid).property(PROPERTY_NAME, "test").next();
 +        // add self-referencing edge
 +        gtx.addE(EDGE_LABEL).from(vertex).to(vertex).next();
 +        gtx.tx().commit();
 +
 +        verifyCommittedSingleVertex(g, "test");
 +        assertEquals(1, g.getEdgesCount());
 +        final long originalVersion = 
g.getVertices().get(vid).getUnmodified().version();
 +
 +        // drop the vertex and re-create it without any properties or edges 
in the same transaction
 +        final GraphTraversalSource gtx2 = tx.begin();
 +        gtx2.V().drop().iterate();
 +        gtx2.addV().property(T.id, vid).next();
 +        gtx2.tx().commit();
 +
 +        verifyCommittedSingleVertex(g);
 +        final TinkerVertex updatedVertex = 
g.getVertices().get(vid).getUnmodified();
 +        assertEquals(0, g.getEdgesCount());
 +        // version should have been updated
 +        assertNotEquals(originalVersion, updatedVertex.version());
 +    }
 +
 +    @Test
 +    public void shouldAllowAddDropAddVertexInSameTransaction() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final TinkerTransaction tx = (TinkerTransaction) g.tx();
 +
 +        final GraphTraversalSource gtx = tx.begin();
 +        // create vertex with a property
 +        final Vertex vertex = gtx.addV().property(T.id, 
vid).property(PROPERTY_NAME, "test").next();
 +        // add self-referencing edge
 +        gtx.addE(EDGE_LABEL).from(vertex).to(vertex).next();
 +        // drop vertex
 +        gtx.V(vid).drop().iterate();
 +        // add vertex again with different properties but no edges
 +        gtx.addV().property(T.id, vid).property(PROPERTY_NAME, "updated 
name").next();
 +        gtx.tx().commit();
 +        
 +        verifyCommittedSingleVertex(g, "updated name");
 +        assertEquals(0, g.getEdgesCount());
 +    }
 +
 +    @Test
 +    public void shouldAllowAddDropAddDropVertexInSameTransaction() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final TinkerTransaction tx = (TinkerTransaction) g.tx();
 +
 +        final GraphTraversalSource gtx = tx.begin();
 +        // create vertex with a property
 +        final Vertex vertex = gtx.addV().property(T.id, 
vid).property(PROPERTY_NAME, "test").next();
 +        // add self-referencing edge
 +        gtx.addE(EDGE_LABEL).from(vertex).to(vertex).next();
 +        // drop vertex
 +        gtx.V(vid).drop().iterate();
 +        // add vertex again with different properties but no edges
 +        gtx.addV().property(T.id, vid).property(PROPERTY_NAME, "updated 
name").next();
 +        // drop the vertex again
 +        gtx.V(vid).drop().iterate();
 +        gtx.tx().commit();
 +        
 +        assertEquals(0, g.getVerticesCount());
 +        assertEquals(0, g.getEdgesCount());
 +    }
 +
 +    @Test
 +    public void shouldAllowAddDropAddDropVertexInMultipleTransactions() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final TinkerTransaction tx = (TinkerTransaction) g.tx();
 +
 +        // first transaction
 +        final GraphTraversalSource gtx1 = tx.begin();
 +        // create vertex with a property
 +        final Vertex vertex = gtx1.addV().property(T.id, 
vid).property(PROPERTY_NAME, "test").next();
 +        // add self-referencing edge
 +        gtx1.addE(EDGE_LABEL).from(vertex).to(vertex).next();
 +        // drop vertex
 +        gtx1.V(vid).drop().iterate();
 +        gtx1.tx().commit();
 +
 +        assertEquals(0, g.getVerticesCount());
 +        assertEquals(0, g.getEdgesCount());
 +
 +        // second transaction
 +        final GraphTraversalSource gtx2 = tx.begin();
 +        // add vertex again with different properties but no edges
 +        gtx2.addV().property(T.id, vid).property(PROPERTY_NAME, "updated 
name").next();
 +        // drop the vertex again
 +        gtx2.V(vid).drop().iterate();
 +        gtx2.tx().commit();
 +
 +        assertEquals(0, g.getVerticesCount());
 +        assertEquals(0, g.getEdgesCount());
 +    }
 +
 +    @Test
 +    public void shouldAllowDropAddUniqueVertexInConcurrentTransactions() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final TinkerTransaction tx = (TinkerTransaction) g.tx();
 +
 +        // create first vertex and commit
 +        final GraphTraversalSource gtx = tx.begin();
 +        gtx.addV().property(T.id, vid).next();
 +        gtx.tx().commit();
 +        verifyCommittedSingleVertex(g);
 +        
 +        // drop and add but don't commit
 +        gtx.V(vid).drop().iterate();
 +        gtx.addV().property(T.id, vid).property(PROPERTY_NAME, "main 
thread").next();
 +        
 +        // in another thread add, drop, add a different vertex and commit
 +        int vid2 = (int) vid + 1;
 +        runInNewThread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.addV().property(T.id, vid2).next();
 +            gtx2.V(vid2).drop().iterate();
 +            gtx2.addV().property(T.id, vid2).property(PROPERTY_NAME, "another 
thread").next();
 +            gtx2.tx().commit();
 +        });
 +
 +        // commit the drop add
 +        gtx.tx().commit();
 +        // both vertices should exist
 +        assertEquals(2, g.getVerticesCount());
 +        assertEquals("main thread", 
g.getVertices().get(vid).getUnmodified().properties.get(PROPERTY_NAME).get(0).value());
 +        assertEquals("another thread", 
g.getVertices().get(vid2).getUnmodified().properties.get(PROPERTY_NAME).get(0).value());
 +    }
 +
 +    @Test
 +    public void shouldPreventDropAddVertexInConcurrentTransactions() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final TinkerTransaction tx = (TinkerTransaction) g.tx();
 +
 +        // create initial vertex and commit transaction
 +        final GraphTraversalSource gtx = tx.begin();
 +        gtx.addV().property(T.id, vid).property(PROPERTY_NAME, 
"original").next();
 +        gtx.tx().commit();
 +        verifyCommittedSingleVertex(g, "original");
 +
 +        // drop the vertex and add it back but don't commit
 +        gtx.V(vid).drop().iterate();
 +        gtx.addV().property(T.id, vid).property(PROPERTY_NAME, "should 
fail!").next();
 +
 +        // drop the vertex and add it back in another thread and commit
 +        runInNewThread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(vid).drop().iterate();
 +            gtx2.addV().property(T.id, vid).property(PROPERTY_NAME, 
"updated").next();
 +            gtx2.tx().commit();
 +        });
 +
 +        // try to commit the drop and add in main thread - should fail
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +            assertEquals("Conflict: element modified in another transaction", 
e.getMessage());
 +            verifyCommittedSingleVertex(g, "updated");
 +        }
 +    }
 +
 +    @Test
 +    public void shouldPreventDropUpdateVertexInConcurrentTransactions() {
 +        final TinkerStorageGraph g = TinkerStorageGraph.open();
 +        final TinkerTransaction tx = (TinkerTransaction) g.tx();
 +
 +        // create initial vertex and commit transaction
 +        final GraphTraversalSource gtx = tx.begin();
 +        gtx.addV().property(T.id, vid).property(PROPERTY_NAME, 
"original").next();
 +        gtx.tx().commit();
 +        verifyCommittedSingleVertex(g, "original");
 +        
 +        // drop but don't commit
 +        gtx.V(vid).drop().iterate();
 +        
 +        // modify the vertex in another thread and commit
 +        runInNewThread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(vid).property(PROPERTY_NAME, "updated").next();
 +            gtx2.tx().commit();
 +        });
 +        
 +        // try to commit the drop vertex - should fail
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +            assertEquals("Conflict: element modified in another transaction", 
e.getMessage());
 +            verifyCommittedSingleVertex(g, "updated");
 +        }
 +    }
 +
 +    @Test
 +    public void 
readVertexShouldNotConflictWithDropAddUpdateVertexInSeparateTransaction() 
throws InterruptedException {
 +        final TinkerStorageGraph graph = TinkerStorageGraph.open();
 +        final GraphTraversalSource g = graph.traversal();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +        final AtomicReference<AssertionError> validationException = new 
AtomicReference<>();
 +
 +        // commit the initial vertex
 +        gtx.addV().property(T.id, vid).next();
 +        gtx.tx().commit();
 +
 +        // reader threads which are constantly reading the vertex ids in a 
separate transaction
 +        CountDownLatch signal = new CountDownLatch(1);
 +        List<Thread> readers = startInNewThreads(() -> {
 +            while (signal.getCount() > 0) {
 +                List<Object> vertexIds = g.V().id().toList();
 +                // vertexIds should be empty or have a single id equal to the 
vertexId
 +                if (validationException.get() == null && vertexIds.size() > 1 
|| (vertexIds.size() == 1 && !vertexIds.get(0).equals(vid))) {
 +                    validationException.set(new AssertionError("Unexpected 
vertexIds: " + vertexIds));
 +                }
 +            }
 +        }, 2);
 +
 +        // drop, add, update the same vertex a number of times to validate 
that the reader thread should not interfere with the updates
 +        for (int i = 0; i < 50; i++) {
 +            Thread.sleep(50);
 +            g.V(vid).drop().iterate();
 +            g.tx().commit();
 +            Thread.sleep(50);
 +            g.addV().property(T.id, vid).next();
 +            g.tx().commit();
 +            g.V(vid).property(VertexProperty.Cardinality.single, "name", 
"test").next();
 +            g.tx().commit();
 +            g.V(vid).property(VertexProperty.Cardinality.single, "name", 
"updated").next();
 +            g.tx().commit();
 +        }
 +
 +        // stop the reader thread
 +        signal.countDown();
 +        readers.forEach(t -> {
 +            try {
 +                t.join();
 +            } catch (InterruptedException e) {
 +                throw new RuntimeException(e);
 +            }
 +        });
 +
 +        verifyCommittedSingleVertex(graph, "updated");
 +        if (validationException.get() != null) {
 +            throw validationException.get();
 +        }
 +    }
 +
 +    @Test
 +    public void 
readEdgeShouldNotConflictWithDropAddUpdateEdgeInSeparateTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph graph = TinkerStorageGraph.open();
 +        final GraphTraversalSource g = graph.traversal();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +        final AtomicReference<AssertionError> validationException = new 
AtomicReference<>();
 +
 +        // commit the initial vertex with self-referencing edge
 +        Vertex vertex = gtx.addV().property(T.id, vid).next();
 +        gtx.addE(EDGE_LABEL).from(vertex).to(vertex).property(T.id, 
EDGE_ID).next();
 +        gtx.tx().commit();
 +
 +        // reader threads which are constantly reading the edge ids in a 
separate transaction
 +        CountDownLatch signal = new CountDownLatch(1);
 +        List<Thread> readers = startInNewThreads(() -> {
 +            while (signal.getCount() > 0) {
 +                List<Object> edgIds = g.E().id().toList();
 +                if (validationException.get() == null && (edgIds.size() > 1 
|| (edgIds.size() == 1 && !edgIds.get(0).equals(EDGE_ID)))) {
 +                    validationException.set(new AssertionError("Unexpected 
edgeIds: " + edgIds));
 +                }
 +
 +            }
 +        }, 2);
 +
 +        // drop, add, update the same edge a number of times to validate that 
the reader thread should not interfere with the updates
 +        for (int i = 0; i < 50; i++) {
 +            Thread.sleep(50);
 +            g.E().drop().iterate();
 +            g.tx().commit();
 +            Thread.sleep(50);
 +            Vertex v = g.V(vid).next();
 +            g.addE(EDGE_LABEL).property(T.id, EDGE_ID).from(v).to(v).next();
 +            g.tx().commit();
 +            g.E(EDGE_ID).property(PROPERTY_WEIGHT, 10.12).next();
 +            g.tx().commit();
 +            g.E(EDGE_ID).property(PROPERTY_WEIGHT, 8.6).next();
 +            g.tx().commit();
 +        }
 +
 +        // stop the reader threads
 +        signal.countDown();
 +        readers.forEach(t -> {
 +            try {
 +                t.join();
 +            } catch (InterruptedException e) {
 +                throw new RuntimeException(e);
 +            }
 +        });
 +        
 +        verifyCommittedSingleVertex(graph);
 +        verifyCommittedSingleEdge(graph, 8.6);
 +        if (validationException.get() != null) {
 +            throw validationException.get();
 +        }
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfVertexLabelAddForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").next();
 +        gtx.tx().commit();
 +
 +        // tx1 adds a label but does not commit
 +        gtx.V(v.id()).addLabel("employee").iterate();
 +
 +        // a read-only transaction on another thread must not observe the 
uncommitted label
 +        final Set<String> readerLabels = readLabelsInNewThreadTx(g, v.id());
 +        assertEquals(Set.of("person"), readerLabels);
 +
 +        // tx1 sees its own change
 +        assertEquals(Set.of("person", "employee"),
 +                new HashSet<>(gtx.V(v.id()).next().labels()));
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfVertexLabelDropForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").addLabel("employee").next();
 +        gtx.tx().commit();
 +
 +        // tx1 drops a label but does not commit
 +        gtx.V(v.id()).dropLabel("employee").iterate();
 +
 +        // a read-only transaction on another thread must still observe the 
not-yet-dropped label
 +        final Set<String> readerLabels = readLabelsInNewThreadTx(g, v.id());
 +        assertEquals(Set.of("person", "employee"), readerLabels);
 +
 +        // tx1 sees its own change
 +        assertEquals(Set.of("person"),
 +                new HashSet<>(gtx.V(v.id()).next().labels()));
 +    }
 +
 +    @Test
 +    public void 
shouldNotAllowDirtyReadsOfVertexLabelDropAllForReadOnlyTransaction() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").addLabel("employee").next();
 +        gtx.tx().commit();
 +
 +        // tx1 drops all labels but does not commit
 +        gtx.V(v.id()).dropLabels().iterate();
 +
 +        // a read-only transaction on another thread must still observe both 
labels
 +        final Set<String> readerLabels = readLabelsInNewThreadTx(g, v.id());
 +        assertEquals(Set.of("person", "employee"), readerLabels);
 +
 +        // tx1 sees the empty label set (ZERO_OR_MORE cardinality)
 +        assertTrue(gtx.V(v.id()).next().labels().isEmpty());
 +    }
 +
 +    // tx1 adds label "employee", tx2 concurrently adds label "customer" to 
the same vertex
 +    @Test
 +    public void shouldHandleConcurrentChangeForVertexLabel() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").next();
 +        gtx.tx().commit();
 +
 +        // tx1 adds a label
 +        gtx.V(v.id()).addLabel("employee").iterate();
 +
 +        // tx2 concurrently adds a different label and commits first
 +        runInNewThread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v.id()).addLabel("customer").iterate();
 +            gtx2.tx().commit();
 +        });
 +
 +        // tx1 must fail because the vertex was modified in another 
transaction
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +            assertEquals("Conflict: element modified in another transaction", 
e.getMessage());
 +        }
 +
 +        // committed state should reflect only tx2's change
 +        assertEquals(Set.of("person", "customer"),
 +                readLabelsInNewThreadTx(g, v.id()));
 +    }
 +
 +    // tx1 drops label "employee", tx2 concurrently drops the same label
 +    @Test
 +    public void shouldHandleConcurrentDropOfSameVertexLabel() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").addLabel("employee").next();
 +        gtx.tx().commit();
 +
 +        // tx1 drops the label
 +        gtx.V(v.id()).dropLabel("employee").iterate();
 +
 +        // tx2 concurrently drops the same label and commits first
 +        runInNewThread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v.id()).dropLabel("employee").iterate();
 +            gtx2.tx().commit();
 +        });
 +
 +        // tx1 must fail because the vertex was modified in another 
transaction
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +            assertEquals("Conflict: element modified in another transaction", 
e.getMessage());
 +        }
 +
 +        // committed state should reflect only tx2's change
 +        assertEquals(Set.of("person"),
 +                readLabelsInNewThreadTx(g, v.id()));
 +    }
 +
 +    // tx1 adds a label to v1, tx2 deletes v1
 +    @Test
 +    public void shouldHandleAddingLabelWhenOtherTxDeleteVertex() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").next();
 +        gtx.tx().commit();
 +
 +        // tx1 adds a label
 +        gtx.V(v.id()).addLabel("employee").iterate();
 +
 +        // tx2 concurrently deletes the vertex and commits first
 +        runInNewThread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v.id()).drop().iterate();
 +            gtx2.tx().commit();
 +        });
 +
 +        // tx1 must fail because the vertex was deleted in another transaction
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +            assertEquals("Conflict: element modified in another transaction", 
e.getMessage());
 +        }
 +
 +        assertEquals(0, (long) gtx.V().count().next());
 +        countElementsInNewThreadTx(g, 0, 0);
 +    }
 +
 +    // tx1 adds a label to v1, tx2 attempts to delete v1 but rolls back
 +    @Test
 +    public void 
shouldHandleAddingLabelWhenOtherTxAttemptsDeleteThenRollsback() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").next();
 +        gtx.tx().commit();
 +
 +        // tx1 adds a label
 +        gtx.V(v.id()).addLabel("employee").iterate();
 +
 +        // tx2 attempts to delete the vertex but rolls back
 +        runInNewThread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v.id()).drop().iterate();
 +            gtx2.tx().rollback();
 +        });
 +
 +        // tx1 should commit successfully
 +        gtx.tx().commit();
 +
 +        assertEquals(1, (long) gtx.V().count().next());
 +        assertEquals(Set.of("person", "employee"),
 +                readLabelsInNewThreadTx(g, v.id()));
 +    }
 +
 +    // tx1 adds a label to v1, tx2 concurrently updates a property of v1 - 
both touch the vertex
 +    @Test
 +    public void shouldHandleConcurrentLabelAddAndPropertyChange() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").next();
 +        gtx.tx().commit();
 +
 +        // tx1 adds a label
 +        gtx.V(v.id()).addLabel("employee").iterate();
 +
 +        // tx2 concurrently changes a property on the same vertex and commits 
first
 +        runInNewThread(() -> {
 +            final GraphTraversalSource gtx2 = g.tx().begin();
 +            gtx2.V(v.id()).property("age", 30).iterate();
 +            gtx2.tx().commit();
 +        });
 +
 +        // tx1 must fail because the vertex was modified in another 
transaction
 +        try {
 +            gtx.tx().commit();
 +            fail("should throw TransactionException");
 +        } catch (TransactionException e) {
 +            assertEquals("Conflict: element modified in another transaction", 
e.getMessage());
 +        }
 +
 +        // committed state should reflect only tx2's property change, no 
"employee" label
 +        final GraphTraversalSource gtx3 = g.tx().begin();
 +        final Vertex committed = gtx3.V(v.id()).next();
 +        assertEquals(30, (int) committed.value("age"));
 +        assertFalse(committed.labels().contains("employee"));
 +        assertTrue(committed.labels().contains("person"));
 +    }
 +
 +    @Test
 +    public void shouldIsolateVertexLabelCountAcrossTransactions() throws 
InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").next();
 +        gtx.tx().commit();
 +
 +        // tx1 adds a label but does not commit
 +        gtx.V(v.id()).addLabel("employee").iterate();
 +
 +        // another transaction must not observe the uncommitted label in its 
counts
 +        assertEquals(0L, countByLabelInNewThreadTx(g, "employee"));
 +        assertEquals(1L, countByLabelInNewThreadTx(g, "person"));
 +
 +        // tx1 observes its own uncommitted change
 +        assertEquals(1L, g.countVerticesByLabel("employee"));
 +        assertEquals(1L, g.countVerticesByLabel("person"));
 +
 +        // rolling back tx1 must restore the committed counts
 +        gtx.tx().rollback();
 +        assertEquals(0L, g.countVerticesByLabel("employee"));
 +        assertEquals(1L, g.countVerticesByLabel("person"));
 +        assertEquals(Set.of("person"),
 +                readLabelsInNewThreadTx(g, v.id()));
 +    }
 +
 +    @Test
 +    public void shouldRollbackVertexLabelDrop() throws InterruptedException {
 +        final TinkerStorageGraph g = openMultiLabelGraph();
 +        final GraphTraversalSource gtx = g.tx().begin();
 +
 +        final Vertex v = gtx.addV("person").addLabel("employee").next();
 +        gtx.tx().commit();
 +
 +        // tx1 drops a label
 +        gtx.V(v.id()).dropLabel("employee").iterate();
 +        // tx1 observes its own uncommitted change
 +        assertFalse(gtx.V(v.id()).next().labels().contains("employee"));
 +
 +        // rolling back must restore the dropped label
 +        gtx.tx().rollback();
 +
 +        assertEquals(Set.of("person", "employee"),
 +                readLabelsInNewThreadTx(g, v.id()));
 +    }
 +
 +    private TinkerStorageGraph openMultiLabelGraph() {
 +        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
 +        
config.setProperty(org.apache.tinkerpop.gremlin.structure.Graph.GRAPH, 
TinkerStorageGraph.class.getName());
 +        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
 +        return TinkerStorageGraph.open(config);
 +    }
 +
 +    private Set<String> readLabelsInNewThreadTx(final TinkerStorageGraph g, 
final Object id) throws InterruptedException {
 +        final AtomicReference<Set<String>> result = new AtomicReference<>();
 +        final Thread thread = new Thread(() -> {
 +            final GraphTraversalSource gtx = g.tx().begin();
 +            result.set(new HashSet<>(gtx.V(id).next().labels()));
 +        });
 +        thread.start();
 +        thread.join();
 +        return result.get();
 +    }
 +
 +    private long countByLabelInNewThreadTx(final TinkerStorageGraph g, final 
String label) throws InterruptedException {
 +        final AtomicLong count = new AtomicLong(-1);
 +        final Thread thread = new Thread(() -> {
 +            g.tx().begin();
 +            count.set(g.countVerticesByLabel(label));
 +        });
 +        thread.start();
 +        thread.join();
 +        return count.get();
 +    }
 +
 +    private void runInNewThread(final Runnable runnable) {
 +        final Thread thread = new Thread(runnable);
 +        thread.start();
 +        try {
 +            thread.join();
 +        } catch (InterruptedException e) {
 +            throw new RuntimeException(e);
 +        }
 +    }
 +
 +    private List<Thread> startInNewThreads(final Runnable runnable, int 
numThreads) {
 +        return IntStream.range(0, numThreads).mapToObj(i -> {
 +            final Thread thread = new Thread(runnable);
 +            thread.start();
 +            return thread;
 +        }).collect(Collectors.toList());
 +    }
 +    
 +    private void verifyCommittedSingleEdge(final TinkerStorageGraph g, final 
Object weightValue) {
 +        // graph should only have a single edge
 +        assertEquals(1, g.getEdges().size());
 +
 +        // the single edge should have the expected id
 +        final TinkerElementContainer<TinkerEdge> edgeContainer = 
g.getEdges().get(EDGE_ID);
 +
 +        // container should be committed and not have an update in progress
 +        assertFalse(edgeContainer.isChanged());
 +        assertFalse(edgeContainer.inUse());
 +
 +        // the element in the container should have the same id
 +        // there are multiple ways to obtain the container element's id and 
they should be consistent
 +        assertEquals(EDGE_ID, edgeContainer.getElementId());
 +        assertEquals(EDGE_ID, edgeContainer.get().id());
 +        final TinkerEdge unmodified = edgeContainer.getUnmodified();
 +        assertEquals(EDGE_ID, unmodified.id());
 +        assertEquals(EDGE_LABEL, unmodified.label());
 +
 +        if (weightValue != null) {
 +            Iterator<Property<Object>> properties = 
unmodified.properties(PROPERTY_WEIGHT);
 +            assertEquals(weightValue, properties.next().value());
 +        } else {
 +            assertNull(unmodified.properties);
 +        }
 +    }
 +
++    @Test
++    public void shouldFindIndexedVertexAfterAddingEdgeToItInSameTransaction() 
{
++        final TinkerStorageGraph g = TinkerStorageGraph.open();
++        g.createIndex(PROPERTY_NAME, Vertex.class);
++        final GraphTraversalSource gtx = g.tx().begin();
++        gtx.addV().property(PROPERTY_NAME, "hub").iterate();
++        gtx.tx().commit();
++
++        gtx.addV().addE(EDGE_LABEL).to(__.V().has(PROPERTY_NAME, 
"hub")).iterate();
++
++        assertEquals(1, (long) gtx.V().has(PROPERTY_NAME, 
"hub").count().next());
++    }
++
++    @Test
++    public void shouldRollbackWithVertexIndexAndNoEdgeIndex() {
++        final TinkerStorageGraph g = TinkerStorageGraph.open();
++        g.createIndex(PROPERTY_NAME, Vertex.class);
++        final GraphTraversalSource gtx = g.tx().begin();
++        gtx.addV().property(PROPERTY_NAME, "a").iterate();
++
++        gtx.tx().rollback();
++
++        assertFalse(gtx.tx().isOpen());
++        assertEquals(0, (long) gtx.V().count().next());
++    }
++
++    @Test
++    public void 
shouldFindIndexedEdgeAfterChangingAnotherPropertyInSameTransaction() {
++        final TinkerStorageGraph g = TinkerStorageGraph.open();
++        g.createIndex(PROPERTY_NAME, Edge.class);
++        final GraphTraversalSource gtx = g.tx().begin();
++        gtx.addV().addE(EDGE_LABEL).to(__.addV()).property(PROPERTY_NAME, 
"link").iterate();
++        gtx.tx().commit();
++
++        gtx.E().has(PROPERTY_NAME, "link").property("weight", 1).iterate();
++
++        assertEquals(1, (long) gtx.E().has(PROPERTY_NAME, 
"link").count().next());
++    }
++
++    @Test
++    public void shouldRollbackWithEdgeIndexAndNoVertexIndex() {
++        final TinkerStorageGraph g = TinkerStorageGraph.open();
++        g.createIndex(PROPERTY_NAME, Edge.class);
++        final GraphTraversalSource gtx = g.tx().begin();
++        gtx.addV().addE(EDGE_LABEL).to(__.addV()).property(PROPERTY_NAME, 
"link").iterate();
++
++        gtx.tx().rollback();
++
++        assertFalse(gtx.tx().isOpen());
++        assertEquals(0, (long) gtx.E().has(PROPERTY_NAME, 
"link").count().next());
++    }
++
 +    private void verifyCommittedSingleVertex(final TinkerStorageGraph g) {
 +        verifyCommittedSingleVertex(g, null);
 +    }
 +
 +    private void verifyCommittedSingleVertex(final TinkerStorageGraph g, 
final String namePropertyValue) {
 +        // graph should only have a single vertex
 +        assertEquals(1, g.getVertices().size());
 +        
 +        // the single vertex should have the expected id
 +        final TinkerElementContainer<TinkerVertex> vertexContainer = 
g.getVertices().get(vid);
 +        
 +        // container should be committed and not have an update in progress
 +        assertFalse(vertexContainer.isChanged());
 +        assertFalse(vertexContainer.inUse());
 +        
 +        // the element in the container should have the same id
 +        // there are multiple ways to obtain the container element's id and 
they should be consistent
 +        assertEquals(vid, vertexContainer.getElementId());
 +        assertEquals(vid, vertexContainer.get().id());
 +        final TinkerVertex unmodified = vertexContainer.getUnmodified();
 +        assertEquals(vid, unmodified.id());
 +
 +        if (namePropertyValue != null) {
 +            final Map<String, List<VertexProperty>> properties = 
unmodified.properties;
 +            assertEquals(1, properties.size());
 +            final List<VertexProperty> nameProperties = 
properties.get(PROPERTY_NAME);
 +            assertEquals(1, nameProperties.size());
 +            assertEquals(namePropertyValue, nameProperties.get(0).value());
 +        } else {
 +            assertNull(unmodified.properties);
 +        }
 +    }
 +}

Reply via email to