4.5. Deadlocks

4.5.1. Understanding deadlocks

Since locks are used it is possible for deadlocks to happen. Neo4j will however detect any deadlock (caused by acquiring a lock) before they happen and throw an exception. Before the exception is thrown the transaction is marked for rollback. All locks acquired by the transaction are still being held but will be released when the transaction is finished (in the finally block as pointed out earlier). Once the locks are released other transactions that were waiting for locks held by the transaction causing the deadlock can proceed. The work performed by the transaction causing the deadlock can then be retried by the user if needed.

Experiencing frequent deadlocks is an indication of concurrent write requests happening in such a way that it is not possible to execute them while at the same time live up to the intended isolation and consistency. The solution is to make sure concurrent updates happen in a reasonable way. For example, given two specific nodes (A and B), adding or deleting relationships to both these nodes in random order for each transaction will result in deadlocks when there are two or more transactions doing that concurrently. One option is to make sure that updates always happens in the same order (first A then B). Another option is to make sure that each thread/transaction does not have any conflicting writes to a node or relationship as some other concurrent transaction. This can, for example, be achieved by letting a single thread do all updates of a specific type.

Deadlocks caused by the use of other synchronization than the locks managed by Neo4j can still happen. Since all operations in the Neo4j API are thread safe unless specified otherwise, there is no need for external synchronization. Other code that requires synchronization should be synchronized in such a way that it never performs any Neo4j operation in the synchronized block.

4.5.2. Deadlock handling example code

Below, you will find examples of how deadlocks can be handled in procedures, server extensions, or when using Neo4j embedded.

The full source code used for the code snippets can be found at DeadlockDocTest.java.

When dealing with deadlocks in code, there are several issues you may want to address:

  • Only do a limited amount of retries, and fail if a threshold is reached.
  • Pause between each attempt to allow the other transaction to finish before trying again.
  • A retry-loop can be useful not only for deadlocks, but for other types of transient errors as well.

Below is an example that shows how this can be implemented.

Example 4.1. Handling deadlocks using a retry loop

This example shows how to use a retry loop for handling deadlocks:

        Throwable txEx = null;
        int RETRIES = 5;
        int BACKOFF = 3000;
        for ( int i = 0; i < RETRIES; i++ )
        {
            try ( Transaction tx = databaseService.beginTx() )
            {
                Object result = doStuff(tx);
                tx.commit();
                return result;
            }
            catch ( Throwable ex )
            {
                txEx = ex;

                // Add whatever exceptions to retry on here
                if ( !(ex instanceof DeadlockDetectedException) )
                {
                    break;
                }
            }

            // Wait so that we don't immediately get into the same deadlock
            if ( i < RETRIES - 1 )
            {
                try
                {
                    Thread.sleep( BACKOFF );
                }
                catch ( InterruptedException e )
                {
                    throw new TransactionFailureException( "Interrupted", e );
                }
            }
        }

        if ( txEx instanceof TransactionFailureException )
        {
            throw ((TransactionFailureException) txEx);
        }
        else if ( txEx instanceof Error )
        {
            throw ((Error) txEx);
        }
        else
        {
            throw ((RuntimeException) txEx);
        }