0

I'm trying to update some properties in a Neo4j graph using Py2neo. I'd like to perform an atomic counter increment, then update some other properties only if the counter is a certain value. I also need to do this in a thread safe way so that other agents can't mess up the increment, or overwrite the property values before the entire set of values is committed. I believe it will look something like this:

tx = graph.cypher.begin()
try:
    tx.acquireWriteLock(relationship) # from Java, not sure what this is in py2neo
    count = relationship.getProperty("count", 1);
    relationship.setProperty("count", count+1 );
    if count == threshold:
        relationship.setProperty("another_property", some_value );
    tx.success();
finally:
    tx.finish();

Some of the code above is guessed or taken from Java examples, If anyone could help with the python equivilant or point me in the direction of some example code that does the same I'd really appreciate it.

LaserJesus
  • 8,230
  • 7
  • 47
  • 65

2 Answers2

1

Transactions are only available in Py2neo for Cypher statements, therefore you'll need to use Cypher to increment the counter and update properties.

In this example, you'll execute a Cypher statement to increment the counter and return the new value, compare that new value to the threshold, then optionally execute another statement to update the relationship property if that threshold is exceeded. Calling tx.process will allow you to execute the first statement, but not commit the transaction, thus enabling the comparison to the threshold:

THRESHOLD = 10

tx = graph.cypher.begin()

statement = '''
MATCH (:Person {name: 'Bob'})-[r:EATS]->(:Food {name: 'Bagel'})
SET r.count = r.count + 1
RETURN r.count AS count_value
'''

tx.append(statement)
result = tx.process()

for record in result:
    count = record.one

    if count > THRESHOLD:
        update_statement = '''
        MATCH (:Person {name: 'Bob'})-[r:EATS]->(:Food {name: 'Bagel'})
        SET r.another_property = 'some value'
        '''
        tx.append(update_statement)
        tx.process()

tx.commit()

Single Cypher statement

Note that you can also accomplish this using a single Cypher statement:

MATCH (:Person {name: 'Bob'})-[r:EATS]->(:Food {name: 'Bagel'})
SET r.count = r.count + 1
WITH r,
CASE WHEN r.count > 10 THEN [1]
ELSE [] END
AS exceeded
FOREACH (i IN exceeded | SET r.another_property = "some value")
Moshe Slavin
  • 5,127
  • 5
  • 23
  • 38
William Lyon
  • 8,371
  • 1
  • 17
  • 22
  • What if I set a numeric property if and only if it was less than a new value. Would it be possible for another thread (t2) to commit the same transaction and cause an error in the following way: t1 (read property, = 2, new property = 4), t2 (read property = 2, new property = 3) ... t1(update property to 4), t2 (update property to 3). If these actions were executed in serial the value of 4 would not have been over written by the lower value of 3. Would the transaction scope also avoid this scenario? Or do I need to explicitly lock the relationship? – LaserJesus Feb 01 '16 at 21:15
0

I ultimately found a cleaner more thread safe way to accomplish my requirements using Cypher. Thanks to the neo4j-users Slack channel I was informed that locks need to be acquired to avoid 'lost updates' in concurrent use cases like this. Cypher has no explicit ability to lock a node, however adding or in fact removing a non existent property will cause Cypher to lock the nodes for the update. This is discussed here: http://neo4j.com/docs/stable/transactions-isolation.html

My Cypher query ended up looking like this:

MATCH (a:Person {name: 'a'}), (b:Person {name: 'b'})
REMOVE a.__notexisitng__, b.__notexisiting__
WITH a,b
MATCH (a)-[r:KNOWS]-(b)
SET r.count = r.count + 1
WHERE r.count = threshold 
SET r.another_property = 'some value'

To run this in py2neo, the following code does the job:

statement = "cypher query above ^"
parameters = {}
response = graph.cypher.post(statement, parameters)
LaserJesus
  • 8,230
  • 7
  • 47
  • 65