neo4j-contrib / neo4j-tinkerpop-api-impl

Implementation of Apache Licensed Neo4j API for Tinkerpop3
Other
24 stars 15 forks source link

Multiple threaded access to graph? #19

Closed kuzeko closed 4 years ago

kuzeko commented 4 years ago

I'm trying to find a way to have multiple threads to access (query) the same graph (e.g., assume each thread is a client running some set of queries, each client should work in parallel).

The way I use the graph now is like this.

Graph g = Neo4jGraph.open(db_path);

For instance having each thread creating a graph object, or having the graph object shared among threads. It seems that none of this option is supported yet. Am I missing something?

A little bit more details:

If you create one, share among threads and then modify concurrently it just cause a deadlock. If you try create many objects, the second time the locks on filesystem blocks the operation. While if you create a ThreadedTransaction (i.e. one Graph instance in a transaction in a thread) like g.tx().createThreadedTx() it raises NotSupported error.

spmallette commented 4 years ago

TinkerPop transaction semantics offer two approaches: thread local transactions and threaded transactions.

Neo4j only supports thread local transactions meaning that the underlying neo4j transaction is bound to the thread that created it and that transaction creation is "automatic" in that you don't need to explicitly start or create a transaction - you only need to worry about calling tx().commit() or tx().rollback(). When the next read/write occurs a new transaction will automatically be started within that thread of execution. You mention that you have an issue with this approach in that:

If you try create many objects, the second time the locks on filesystem blocks the operation.

but i think that it's because you are creating multiple Graph objects attempting to open the same data files which Neo4j embedded does not allow. Instead, create one Graph object and simply share it among threads. As an example, here's a simple groovy script I ran in Gremlin Console displaying how multiple threads can each work with the same instance of "g":

graph = Neo4jGraph.open('/tmp/neo4j')
g = graph.traversal()
rand = new java.util.Random()
t1 = Thread.start{ 
  while(true) {
    if (Thread.interrupted()) break
    g.addV('person').property('num', System.currentTimeMillis()).iterate()
    g.tx().commit()
    Thread.sleep(1000) 
  }
}
t2 = Thread.start{ 
  while(true) {
    if (Thread.interrupted()) break
    g.addV('software').property('num', System.currentTimeMillis()).iterate()
    g.tx().commit()
    Thread.sleep(2000) 
  }
}
t3 = Thread.start{ 
  while(true) {
    if (Thread.interrupted()) break
    g.addV('location').property('num', System.currentTimeMillis()).iterate() 
    if (rand.nextBoolean())
    g.tx().commit() 
  else
    g.tx().rollback() 
    Thread.sleep(500) 
  }
}

While those threads were running I did some other operations in the console:

gremlin> g.V().groupCount().by(label)
==>[software:2,person:3,location:2]
gremlin> g.V().groupCount().by(label)
==>[software:3,person:5,location:5]
gremlin> g.V().groupCount().by(label)
==>[software:5,person:8,location:6]
gremlin> g.V().groupCount().by(label)
==>[software:6,person:10,location:8]
gremlin> g.V().groupCount().by(label)
==>[software:7,person:12,location:10]
gremlin> g.V().hasLabel('software').drop()
gremlin> g.tx().commit()
==>null
gremlin> g.V().groupCount().by(label)
==>[software:4,person:37,location:39]
gremlin> g.V().groupCount().by(label)
==>[software:5,person:39,location:42]

As you can see there shouldn't be any problem sharing an instance of "g" when used in this fashion (or a Graph instance for that matter, but you really should just be writing Gremlin and not coding at the Graph API level - the Graph API is for those developing a TinkerPop implementation).

As for threaded transactions you get a "NotSupported" error as I don't think Neo4j has the ability to to allow for that functionality. Multiple threads cant operate on the same neo4j transaction. As far as I know only JanusGraph supports such functionality and quite honestly, I wouldn't look to depend on that as I could see TinkerPop deprecating it away in the future.

kuzeko commented 4 years ago

Thanks a lot for the detailed answer! I'll test it and let you know. I will double check my code, maybe I was doing something wrong.

kuzeko commented 4 years ago

[UPDATE]

There seems to be a bug when we execute a delete. All works well with concurrent read/insertions.

Here is the relevant code snippet: pastebin

We find that the code get stuck and the only exception we can extract is org.neo4j.kernel.DeadlockDetectedException

spmallette commented 4 years ago

Instead of gts.V(v).next().remove(); I'd prefer gts.V(v).drop().iterate() which uses the Gremlin language to do your delete. That aside, I suspect that the exception you're seeing is something that can happen in the normal usage of neo4j - I think you should build in some form of transaction retry when you get that exception:

https://neo4j.com/developer/kb/explanation-of-error-deadlockdetectedexception-forseticlient-0-cant-acquire-exclusivelock/

kuzeko commented 4 years ago

Thanks a lot, the new remove code still raises deadlocks but implementing a retry scheme solved the issue.