4

I have the following 2 existing nodes in my graph.

A Customer node identified by a unique customer number. A Product node identified by a unique ISBN identifier.

I want to create an association between one Customer node and one Product node. But I want to represent this association as a new node called a License node which will have one link to the Customer node and one link to the Product node.

This License node will have a new internal identifier generated as a random GUID.

My logic in my application which creates the new License node and links them to the other 2 nodes is executed in one transaction.

if (Product NOT already associated with the License for that Customer) create a new License node with a new random GUID create a relationship from the new License Node to the Product Node create a relationship from the Customer Node to the new License Node

However multiple requests can arrive at the same time with the same ISBN and customer number. When this happens I am sometimes getting duplicate License nodes created for the same Customer and Product nodes. The transaction in spring data neo4j does not seem to prevent this from happening.

Example of correctly added License

Example of License added twice

How can I ensure that only one License node will get created between the Customer node and the Product node?

Donal Hurley
  • 159
  • 2
  • 9
  • How looks like the Cypher query you are using to execute the logic? – Bruno Peres Aug 16 '17 at 15:16
  • 1
    Thanks Bruno, I am using Java 1.8 and Spring Data Neo4j 4.2.0 and the cypher gets generated by the framework. The transaction is also defined as an annotation. I will try out the suggestions made by frant.hartm in his answer below. – Donal Hurley Aug 16 '17 at 16:36

1 Answers1

0

The transaction in spring data neo4j does not seem to prevent this from happening.

Neo4j has read commited isolation level for transactions. To prevent this you would need serializable.

To achieve what you need you could:

  • lock Product and Customer node before doing the Product NOT already associated with... check. You could use a query like this to do that (within the same transaction):

    MATCH (n:Product) WHERE ID(n) = {id} REMOVE n._lock
    

    and similar for Customer.

  • add a special key to License which is a concatenation of Product and Customer ids - then create a unique constraint on that.

František Hartman
  • 14,436
  • 2
  • 40
  • 60
  • Thanks Frant. I took the first approach and this has worked for me. Using `MATCH (p:Product) WHERE ID(p) = {id} SET p._lock = 1` at the start of the transaction and `MATCH (p:Product) WHERE ID(p) = {id} REMOVE p._lock` at the end of the transaction. – Donal Hurley Aug 17 '17 at 08:51
  • The `REMOVE n._lock` at the _start_ of the transaction should work as well and will save you one query at the end of the transaction. – František Hartman Aug 17 '17 at 08:55
  • Yep, That worked too. I think I understand, because removing the property ensures a write lock is taken on the node. http://neo4j.com/docs/java-reference/current/#transactions-locking `When adding, changing or removing a property on a node or relationship a write lock will be taken on the specific node or relationship.` – Donal Hurley Aug 17 '17 at 14:37
  • I have noticed after further testing that I can now reconstruct a test where only removing the property with the one call doesn't work. However setting the property before the check and then removing the property at the end of the tx code has worked for all my tests (I can never test this 100%). I am wondering if Neo4j maybe doesn't take the write lock on the node if it detects that you are trying to remove a property that doesn't exist and maybe in earlier versions it may have been doing that? – Donal Hurley Aug 21 '17 at 12:12
  • @DonalHurley what version of Neoj4 is that? Could you share the test case? – František Hartman Aug 21 '17 at 12:28
  • We are using this version of Neo4j `neo4j-enterprise-3.1.1-unix.tar.gz`. There are 5 concurrent threads in a spring boot application trying to create the one *License* node for the same *Customer* and *Product* node and arriving at the same time. I'm using `2.1.1`and `3.2.2` libraries using a Repository class for each node and `@Transactional` to bind the logic in a Tx for the method. Then added named cypher queries in the repositories for the setting and removing of the 'fake' property. – Donal Hurley Aug 21 '17 at 12:44
  • I created a demo project in github which reconstructs my problem in a test see https://github.com/donalthurley/neo4j-locks-test. I also started looking at the documentation for Neo4j 3.2 (we're only using 3.1) and I think it is recommending setting and removing the property under this section http://neo4j.com/docs/java-reference/current/#_lost_updates_in_cypher for a similar scenario. – Donal Hurley Aug 22 '17 at 15:08
  • @DonalHurley There is a typo at this line https://github.com/donalthurley/neo4j-locks-test/blob/master/src/main/java/com/example/demo/repository/CustomerRepository.java#L19 – František Hartman Aug 22 '17 at 15:57
  • I corrected the typo. After doing that I am still seeing the same problem when I call the implementation that only removes the property. My suspicion is that this behaviour may not be supported now and that you need to do a set and then remove. This implementation is working for my test. – Donal Hurley Aug 22 '17 at 16:27