/* * Copyright (c) 2002-2017 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. * * Licensed 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.neo4j.driver.v1.integration; import org.hamcrest.MatcherAssert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.neo4j.driver.internal.DriverFactory; import org.neo4j.driver.internal.cluster.RoutingContext; import org.neo4j.driver.internal.cluster.RoutingSettings; import org.neo4j.driver.internal.logging.DevNullLogging; import org.neo4j.driver.internal.retry.RetrySettings; import org.neo4j.driver.internal.util.DriverFactoryWithFixedRetryLogic; import org.neo4j.driver.internal.util.ServerVersion; import org.neo4j.driver.v1.AccessMode; import org.neo4j.driver.v1.AuthToken; import org.neo4j.driver.v1.AuthTokens; import org.neo4j.driver.v1.Config; import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.GraphDatabase; import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; import org.neo4j.driver.v1.StatementRunner; import org.neo4j.driver.v1.Transaction; import org.neo4j.driver.v1.TransactionWork; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.exceptions.Neo4jException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.TransientException; import org.neo4j.driver.v1.util.TestNeo4j; import static java.lang.String.format; import static java.util.concurrent.Executors.newSingleThreadExecutor; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.neo4j.driver.internal.util.ServerVersion.v3_1_0; import static org.neo4j.driver.v1.Values.parameters; import static org.neo4j.driver.v1.util.DaemonThreadFactory.daemon; import static org.neo4j.driver.v1.util.Neo4jRunner.DEFAULT_AUTH_TOKEN; public class SessionIT { @Rule public TestNeo4j neo4j = new TestNeo4j(); @Rule public ExpectedException exception = ExpectedException.none(); @Test public void shouldKnowSessionIsClosed() throws Throwable { // Given try ( Driver driver = newDriver() ) { Session session = driver.session(); // When session.close(); // Then assertFalse( session.isOpen() ); } } @Test public void shouldHandleNullConfig() throws Throwable { // Given try ( Driver driver = GraphDatabase.driver( neo4j.uri(), AuthTokens.none(), null ) ) { Session session = driver.session(); // When session.close(); // Then assertFalse( session.isOpen() ); } } @SuppressWarnings( "ConstantConditions" ) @Test public void shouldHandleNullAuthToken() throws Throwable { // Given AuthToken token = null; try ( Driver driver = GraphDatabase.driver( neo4j.uri(), token ) ) { Session session = driver.session(); // When session.close(); // Then assertFalse( session.isOpen() ); } } @Test public void shouldKillLongRunningStatement() throws Throwable { neo4j.ensureProcedures( "longRunningStatement.jar" ); // Given Driver driver = newDriver(); int executionTimeout = 10; // 10s final int killTimeout = 1; // 1s long startTime = -1, endTime; try ( Session session = driver.session() ) { StatementResult result = session.run( "CALL test.driver.longRunningStatement({seconds})", parameters( "seconds", executionTimeout ) ); resetSessionAfterTimeout( session, killTimeout ); // When startTime = System.currentTimeMillis(); result.consume();// blocking to run the statement fail( "Should have got an exception about statement get killed." ); } catch ( Neo4jException e ) { endTime = System.currentTimeMillis(); assertTrue( startTime > 0 ); assertTrue( endTime - startTime > killTimeout * 1000 ); // get reset by session.reset assertTrue( endTime - startTime < executionTimeout * 1000 / 2 ); // finished before execution finished } catch ( Exception e ) { fail( "Should be a Neo4jException" ); } } @Test public void shouldKillLongStreamingResult() throws Throwable { neo4j.ensureProcedures( "longRunningStatement.jar" ); // Given Driver driver = newDriver(); int executionTimeout = 10; // 10s final int killTimeout = 1; // 1s long startTime = -1, endTime; int recordCount = 0; try ( final Session session = driver.session() ) { StatementResult result = session.run( "CALL test.driver.longStreamingResult({seconds})", parameters( "seconds", executionTimeout ) ); resetSessionAfterTimeout( session, killTimeout ); // When startTime = System.currentTimeMillis(); while ( result.hasNext() ) { result.next(); recordCount++; } fail( "Should have got an exception about streaming get killed." ); } catch ( ClientException e ) { endTime = System.currentTimeMillis(); assertThat( e.code(), equalTo( "Neo.ClientError.Procedure.ProcedureCallFailed" ) ); assertThat( recordCount, greaterThan( 1 ) ); assertTrue( startTime > 0 ); assertTrue( endTime - startTime > killTimeout * 1000 ); // get reset by session.reset assertTrue( endTime - startTime < executionTimeout * 1000 / 2 ); // finished before execution finished } } @SuppressWarnings( "deprecation" ) @Test public void shouldNotAllowBeginTxIfResetFailureIsNotConsumed() throws Throwable { // Given neo4j.ensureProcedures( "longRunningStatement.jar" ); Driver driver = newDriver(); try ( Session session = driver.session() ) { Transaction tx = session.beginTransaction(); tx.run( "CALL test.driver.longRunningStatement({seconds})", parameters( "seconds", 10 ) ); Thread.sleep( 1000 ); session.reset(); exception.expect( ClientException.class ); exception.expectMessage( startsWith( "An error has occurred due to the cancellation of executing a previous statement." ) ); // When & Then tx = session.beginTransaction(); assertThat( tx, notNullValue() ); } } @SuppressWarnings( "deprecation" ) @Test public void shouldThrowExceptionOnCloseIfResetFailureIsNotConsumed() throws Throwable { // Given neo4j.ensureProcedures( "longRunningStatement.jar" ); Driver driver = newDriver(); Session session = driver.session(); session.run( "CALL test.driver.longRunningStatement({seconds})", parameters( "seconds", 10 ) ); Thread.sleep( 1000 ); session.reset(); exception.expect( ClientException.class ); exception.expectMessage( startsWith( "An error has occurred due to the cancellation of executing a previous statement." ) ); // When & Then session.close(); } @SuppressWarnings( "deprecation" ) @Test public void shouldBeAbleToBeginTxAfterResetFailureIsConsumed() throws Throwable { // Given neo4j.ensureProcedures( "longRunningStatement.jar" ); Driver driver = newDriver(); try ( Session session = driver.session() ) { Transaction tx = session.beginTransaction(); StatementResult procedureResult = tx.run( "CALL test.driver.longRunningStatement({seconds})", parameters( "seconds", 10 ) ); Thread.sleep( 1000 ); session.reset(); try { procedureResult.consume(); fail( "Should procedure throw an exception as we interrupted procedure call" ); } catch ( Neo4jException e ) { assertThat( e.getMessage(), containsString( "The transaction has been terminated" ) ); } catch ( Throwable e ) { fail( "Expected exception is different from what we've received: " + e.getMessage() ); } // When tx = session.beginTransaction(); tx.run( "CREATE (n:FirstNode)" ); tx.success(); tx.close(); // Then StatementResult result = session.run( "MATCH (n) RETURN count(n)" ); long nodes = result.single().get( "count(n)" ).asLong(); MatcherAssert.assertThat( nodes, equalTo( 1L ) ); } } @SuppressWarnings( "deprecation" ) private void resetSessionAfterTimeout( final Session session, final int timeout ) { new Thread( new Runnable() { @Override public void run() { try { Thread.sleep( timeout * 1000 ); // let the statement executing for timeout seconds } catch ( InterruptedException e ) { e.printStackTrace(); } finally { session.reset(); // reset the session after timeout } } } ).start(); } @SuppressWarnings( "deprecation" ) @Test public void shouldAllowMoreStatementAfterSessionReset() { // Given try ( Driver driver = newDriver(); Session session = driver.session() ) { session.run( "Return 1" ).consume(); // When reset the state of this session session.reset(); // Then can run successfully more statements without any error session.run( "Return 2" ).consume(); } } @SuppressWarnings( "deprecation" ) @Test public void shouldAllowMoreTxAfterSessionReset() { // Given try ( Driver driver = newDriver(); Session session = driver.session() ) { try ( Transaction tx = session.beginTransaction() ) { tx.run( "Return 1" ); tx.success(); } // When reset the state of this session session.reset(); // Then can run more Tx try ( Transaction tx = session.beginTransaction() ) { tx.run( "Return 2" ); tx.success(); } } } @SuppressWarnings( "deprecation" ) @Test public void shouldMarkTxAsFailedAndDisallowRunAfterSessionReset() { // Given try ( Driver driver = newDriver(); Session session = driver.session() ) { try ( Transaction tx = session.beginTransaction() ) { // When reset the state of this session session.reset(); // Then tx.run( "Return 1" ); fail( "Should not allow tx run as tx is already failed." ); } catch ( Exception e ) { assertThat( e.getMessage(), startsWith( "Cannot run more statements in this transaction" ) ); } } } @SuppressWarnings( "deprecation" ) @Test public void shouldAllowMoreTxAfterSessionResetInTx() { // Given try ( Driver driver = newDriver(); Session session = driver.session() ) { try ( Transaction tx = session.beginTransaction() ) { // When reset the state of this session session.reset(); } // Then can run more Tx try ( Transaction tx = session.beginTransaction() ) { tx.run( "Return 2" ); tx.success(); } } } @Test public void executeReadTxInReadSession() { testExecuteReadTx( AccessMode.READ ); } @Test public void executeReadTxInWriteSession() { testExecuteReadTx( AccessMode.WRITE ); } @Test public void executeWriteTxInReadSession() { testExecuteWriteTx( AccessMode.READ ); } @Test public void executeWriteTxInWriteSession() { testExecuteWriteTx( AccessMode.WRITE ); } @Test public void rollsBackWriteTxInReadSessionWhenFunctionThrows() { testTxRollbackWhenFunctionThrows( AccessMode.READ ); } @Test public void rollsBackWriteTxInWriteSessionWhenFunctionThrows() { testTxRollbackWhenFunctionThrows( AccessMode.WRITE ); } @Test public void readTxRetriedUntilSuccess() { int failures = 6; int retries = failures + 1; try ( Driver driver = newDriverWithFixedRetries( retries ) ) { try ( Session session = driver.session() ) { session.run( "CREATE (:Person {name: 'Bruce Banner'})" ); } ThrowingWork work = newThrowingWorkSpy( "MATCH (n) RETURN n.name", failures ); try ( Session session = driver.session() ) { Record record = session.readTransaction( work ); assertEquals( "Bruce Banner", record.get( 0 ).asString() ); } verify( work, times( retries ) ).execute( any( Transaction.class ) ); } } @Test public void writeTxRetriedUntilSuccess() { int failures = 4; int retries = failures + 1; try ( Driver driver = newDriverWithFixedRetries( retries ) ) { ThrowingWork work = newThrowingWorkSpy( "CREATE (p:Person {name: 'Hulk'}) RETURN p", failures ); try ( Session session = driver.session() ) { Record record = session.writeTransaction( work ); assertEquals( "Hulk", record.get( 0 ).asNode().get( "name" ).asString() ); } try ( Session session = driver.session() ) { Record record = session.run( "MATCH (p: Person {name: 'Hulk'}) RETURN count(p)" ).single(); assertEquals( 1, record.get( 0 ).asInt() ); } verify( work, times( retries ) ).execute( any( Transaction.class ) ); } } @Test public void readTxRetriedUntilFailure() { int failures = 3; int retries = failures - 1; try ( Driver driver = newDriverWithFixedRetries( retries ) ) { ThrowingWork work = newThrowingWorkSpy( "MATCH (n) RETURN n.name", failures ); try ( Session session = driver.session() ) { try { session.readTransaction( work ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( ServiceUnavailableException.class ) ); } } verify( work, times( failures ) ).execute( any( Transaction.class ) ); } } @Test public void writeTxRetriedUntilFailure() { int failures = 8; int retries = failures - 1; try ( Driver driver = newDriverWithFixedRetries( retries ) ) { ThrowingWork work = newThrowingWorkSpy( "CREATE (:Person {name: 'Ronan'})", failures ); try ( Session session = driver.session() ) { try { session.writeTransaction( work ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( ServiceUnavailableException.class ) ); } } try ( Session session = driver.session() ) { StatementResult result = session.run( "MATCH (p:Person {name: 'Ronan'}) RETURN count(p)" ); assertEquals( 0, result.single().get( 0 ).asInt() ); } verify( work, times( failures ) ).execute( any( Transaction.class ) ); } } @Test public void writeTxRetryErrorsAreCollected() { try ( Driver driver = newDriverWithLimitedRetries( 5, TimeUnit.SECONDS ) ) { ThrowingWork work = newThrowingWorkSpy( "CREATE (:Person {name: 'Ronan'})", Integer.MAX_VALUE ); int suppressedErrors = 0; try ( Session session = driver.session() ) { try { session.writeTransaction( work ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( ServiceUnavailableException.class ) ); assertThat( e.getSuppressed(), not( emptyArray() ) ); suppressedErrors = e.getSuppressed().length; } } try ( Session session = driver.session() ) { StatementResult result = session.run( "MATCH (p:Person {name: 'Ronan'}) RETURN count(p)" ); assertEquals( 0, result.single().get( 0 ).asInt() ); } verify( work, times( suppressedErrors + 1 ) ).execute( any( Transaction.class ) ); } } @Test public void readTxRetryErrorsAreCollected() { try ( Driver driver = newDriverWithLimitedRetries( 4, TimeUnit.SECONDS ) ) { ThrowingWork work = newThrowingWorkSpy( "MATCH (n) RETURN n.name", Integer.MAX_VALUE ); int suppressedErrors = 0; try ( Session session = driver.session() ) { try { session.readTransaction( work ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( ServiceUnavailableException.class ) ); assertThat( e.getSuppressed(), not( emptyArray() ) ); suppressedErrors = e.getSuppressed().length; } } verify( work, times( suppressedErrors + 1 ) ).execute( any( Transaction.class ) ); } } @Test public void readTxCommittedWithoutTxSuccess() { try ( Driver driver = newDriverWithoutRetries(); Session session = driver.session() ) { assumeBookmarkSupport( driver ); assertNull( session.lastBookmark() ); long answer = session.readTransaction( new TransactionWork<Long>() { @Override public Long execute( Transaction tx ) { return tx.run( "RETURN 42" ).single().get( 0 ).asLong(); } } ); assertEquals( 42, answer ); // bookmark should be not-null after commit assertNotNull( session.lastBookmark() ); } } @Test public void writeTxCommittedWithoutTxSuccess() { try ( Driver driver = newDriverWithoutRetries() ) { try ( Session session = driver.session() ) { long answer = session.writeTransaction( new TransactionWork<Long>() { @Override public Long execute( Transaction tx ) { return tx.run( "CREATE (:Person {name: 'Thor Odinson'}) RETURN 42" ).single().get( 0 ).asLong(); } } ); assertEquals( 42, answer ); } try ( Session session = driver.session() ) { StatementResult result = session.run( "MATCH (p:Person {name: 'Thor Odinson'}) RETURN count(p)" ); assertEquals( 1, result.single().get( 0 ).asInt() ); } } } @Test public void readTxRolledBackWithTxFailure() { try ( Driver driver = newDriverWithoutRetries(); Session session = driver.session() ) { assumeBookmarkSupport( driver ); assertNull( session.lastBookmark() ); long answer = session.readTransaction( new TransactionWork<Long>() { @Override public Long execute( Transaction tx ) { StatementResult result = tx.run( "RETURN 42" ); tx.failure(); return result.single().get( 0 ).asLong(); } } ); assertEquals( 42, answer ); // bookmark should remain null after rollback assertNull( session.lastBookmark() ); } } @Test public void writeTxRolledBackWithTxFailure() { try ( Driver driver = newDriverWithoutRetries() ) { try ( Session session = driver.session() ) { int answer = session.writeTransaction( new TransactionWork<Integer>() { @Override public Integer execute( Transaction tx ) { tx.run( "CREATE (:Person {name: 'Natasha Romanoff'})" ); tx.failure(); return 42; } } ); assertEquals( 42, answer ); } try ( Session session = driver.session() ) { StatementResult result = session.run( "MATCH (p:Person {name: 'Natasha Romanoff'}) RETURN count(p)" ); assertEquals( 0, result.single().get( 0 ).asInt() ); } } } @Test public void readTxRolledBackWhenExceptionIsThrown() { try ( Driver driver = newDriverWithoutRetries(); Session session = driver.session() ) { assumeBookmarkSupport( driver ); assertNull( session.lastBookmark() ); try { session.readTransaction( new TransactionWork<Long>() { @Override public Long execute( Transaction tx ) { StatementResult result = tx.run( "RETURN 42" ); if ( result.single().get( 0 ).asLong() == 42 ) { throw new IllegalStateException(); } return 1L; } } ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( IllegalStateException.class ) ); } // bookmark should remain null after rollback assertNull( session.lastBookmark() ); } } @Test public void writeTxRolledBackWhenExceptionIsThrown() { try ( Driver driver = newDriverWithoutRetries() ) { try ( Session session = driver.session() ) { try { session.writeTransaction( new TransactionWork<Integer>() { @Override public Integer execute( Transaction tx ) { tx.run( "CREATE (:Person {name: 'Loki Odinson'})" ); throw new IllegalStateException(); } } ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( IllegalStateException.class ) ); } } try ( Session session = driver.session() ) { StatementResult result = session.run( "MATCH (p:Person {name: 'Natasha Romanoff'}) RETURN count(p)" ); assertEquals( 0, result.single().get( 0 ).asInt() ); } } } @Test public void readTxRolledBackWhenMarkedBothSuccessAndFailure() { try ( Driver driver = newDriverWithoutRetries(); Session session = driver.session() ) { assumeBookmarkSupport( driver ); assertNull( session.lastBookmark() ); long answer = session.readTransaction( new TransactionWork<Long>() { @Override public Long execute( Transaction tx ) { StatementResult result = tx.run( "RETURN 42" ); tx.success(); tx.failure(); return result.single().get( 0 ).asLong(); } } ); assertEquals( 42, answer ); // bookmark should remain null after rollback assertNull( session.lastBookmark() ); } } @Test public void writeTxRolledBackWhenMarkedBothSuccessAndFailure() { try ( Driver driver = newDriverWithoutRetries() ) { try ( Session session = driver.session() ) { int answer = session.writeTransaction( new TransactionWork<Integer>() { @Override public Integer execute( Transaction tx ) { tx.run( "CREATE (:Person {name: 'Natasha Romanoff'})" ); tx.success(); tx.failure(); return 42; } } ); assertEquals( 42, answer ); } try ( Session session = driver.session() ) { StatementResult result = session.run( "MATCH (p:Person {name: 'Natasha Romanoff'}) RETURN count(p)" ); assertEquals( 0, result.single().get( 0 ).asInt() ); } } } @Test public void readTxRolledBackWhenMarkedAsSuccessAndThrowsException() { try ( Driver driver = newDriverWithoutRetries(); Session session = driver.session() ) { assumeBookmarkSupport( driver ); assertNull( session.lastBookmark() ); try { session.readTransaction( new TransactionWork<Long>() { @Override public Long execute( Transaction tx ) { tx.run( "RETURN 42" ); tx.success(); throw new IllegalStateException(); } } ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( IllegalStateException.class ) ); } // bookmark should remain null after rollback assertNull( session.lastBookmark() ); } } @Test public void writeTxRolledBackWhenMarkedAsSuccessAndThrowsException() { try ( Driver driver = newDriverWithoutRetries() ) { try ( Session session = driver.session() ) { try { session.writeTransaction( new TransactionWork<Integer>() { @Override public Integer execute( Transaction tx ) { tx.run( "CREATE (:Person {name: 'Natasha Romanoff'})" ); tx.success(); throw new IllegalStateException(); } } ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( IllegalStateException.class ) ); } } try ( Session session = driver.session() ) { StatementResult result = session.run( "MATCH (p:Person {name: 'Natasha Romanoff'}) RETURN count(p)" ); assertEquals( 0, result.single().get( 0 ).asInt() ); } } } @Test( timeout = 20_000 ) public void resetShouldStopQueryWaitingForALock() throws Exception { assumeServerIs31OrLater(); testResetOfQueryWaitingForLock( new NodeIdUpdater() { @Override void performUpdate( Driver driver, int nodeId, int newNodeId, AtomicReference<Session> usedSessionRef, CountDownLatch latchToWait ) throws Exception { try ( Session session = driver.session() ) { usedSessionRef.set( session ); latchToWait.await(); StatementResult result = updateNodeId( session, nodeId, newNodeId ); result.consume(); } } } ); } @Test( timeout = 20_000 ) public void resetShouldStopTransactionWaitingForALock() throws Exception { assumeServerIs31OrLater(); testResetOfQueryWaitingForLock( new NodeIdUpdater() { @Override public void performUpdate( Driver driver, int nodeId, int newNodeId, AtomicReference<Session> usedSessionRef, CountDownLatch latchToWait ) throws Exception { try ( Session session = neo4j.driver().session(); Transaction tx = session.beginTransaction() ) { usedSessionRef.set( session ); latchToWait.await(); StatementResult result = updateNodeId( tx, nodeId, newNodeId ); result.consume(); } } } ); } @Test( timeout = 20_000 ) public void resetShouldStopWriteTransactionWaitingForALock() throws Exception { assumeServerIs31OrLater(); final AtomicInteger invocationsOfWork = new AtomicInteger(); testResetOfQueryWaitingForLock( new NodeIdUpdater() { @Override public void performUpdate( Driver driver, final int nodeId, final int newNodeId, AtomicReference<Session> usedSessionRef, CountDownLatch latchToWait ) throws Exception { try ( Session session = driver.session() ) { usedSessionRef.set( session ); latchToWait.await(); session.writeTransaction( new TransactionWork<Void>() { @Override public Void execute( Transaction tx ) { invocationsOfWork.incrementAndGet(); StatementResult result = updateNodeId( tx, nodeId, newNodeId ); result.consume(); return null; } } ); } } } ); assertEquals( 1, invocationsOfWork.get() ); } @Test( timeout = 20_000 ) public void transactionRunShouldFailOnDeadlocks() throws Exception { final int nodeId1 = 42; final int nodeId2 = 4242; final int newNodeId1 = 1; final int newNodeId2 = 2; createNodeWithId( nodeId1 ); createNodeWithId( nodeId2 ); final CountDownLatch latch1 = new CountDownLatch( 1 ); final CountDownLatch latch2 = new CountDownLatch( 1 ); try ( final Driver driver = newDriver() ) { Future<Void> result1 = executeInDifferentThread( new Callable<Void>() { @Override public Void call() throws Exception { try ( Session session = driver.session(); Transaction tx = session.beginTransaction() ) { // lock first node updateNodeId( tx, nodeId1, newNodeId1 ).consume(); latch1.await(); latch2.countDown(); // lock second node updateNodeId( tx, nodeId2, newNodeId1 ).consume(); tx.success(); } return null; } } ); Future<Void> result2 = executeInDifferentThread( new Callable<Void>() { @Override public Void call() throws Exception { try ( Session session = driver.session(); Transaction tx = session.beginTransaction() ) { // lock second node updateNodeId( tx, nodeId2, newNodeId2 ).consume(); latch1.countDown(); latch2.await(); // lock first node updateNodeId( tx, nodeId1, newNodeId2 ).consume(); tx.success(); } return null; } } ); boolean firstResultFailed = assertOneOfTwoFuturesFailWithDeadlock( result1, result2 ); if ( firstResultFailed ) { assertEquals( 0, countNodesWithId( newNodeId1 ) ); assertEquals( 2, countNodesWithId( newNodeId2 ) ); } else { assertEquals( 2, countNodesWithId( newNodeId1 ) ); assertEquals( 0, countNodesWithId( newNodeId2 ) ); } } } @Test( timeout = 20_000 ) public void writeTransactionFunctionShouldRetryDeadlocks() throws Exception { final int nodeId1 = 42; final int nodeId2 = 4242; final int nodeId3 = 424242; final int newNodeId1 = 1; final int newNodeId2 = 2; createNodeWithId( nodeId1 ); createNodeWithId( nodeId2 ); final CountDownLatch latch1 = new CountDownLatch( 1 ); final CountDownLatch latch2 = new CountDownLatch( 1 ); try ( final Driver driver = newDriver() ) { Future<Void> result1 = executeInDifferentThread( new Callable<Void>() { @Override public Void call() throws Exception { try ( Session session = driver.session(); Transaction tx = session.beginTransaction() ) { // lock first node updateNodeId( tx, nodeId1, newNodeId1 ).consume(); latch1.await(); latch2.countDown(); // lock second node updateNodeId( tx, nodeId2, newNodeId1 ).consume(); tx.success(); } return null; } } ); Future<Void> result2 = executeInDifferentThread( new Callable<Void>() { @Override public Void call() throws Exception { try ( Session session = driver.session() ) { session.writeTransaction( new TransactionWork<Void>() { @Override public Void execute( Transaction tx ) { // lock second node updateNodeId( tx, nodeId2, newNodeId2 ).consume(); latch1.countDown(); await( latch2 ); // lock first node updateNodeId( tx, nodeId1, newNodeId2 ).consume(); createNodeWithId( nodeId3 ); return null; } } ); } return null; } } ); boolean firstResultFailed = false; try { // first future may: // 1) succeed, when it's tx was able to grab both locks and tx in other future was // terminated because of a deadlock // 2) fail, when it's tx was terminated because of a deadlock assertNull( result1.get( 20, TimeUnit.SECONDS ) ); } catch ( ExecutionException e ) { firstResultFailed = true; } // second future can't fail because deadlocks are retried assertNull( result2.get( 20, TimeUnit.SECONDS ) ); if ( firstResultFailed ) { // tx with retries was successful and updated ids assertEquals( 0, countNodesWithId( newNodeId1 ) ); assertEquals( 2, countNodesWithId( newNodeId2 ) ); } else { // tx without retries was successful and updated ids // tx with retries did not manage to find nodes because their ids were updated assertEquals( 2, countNodesWithId( newNodeId1 ) ); assertEquals( 0, countNodesWithId( newNodeId2 ) ); } // tx with retries was successful and created an additional node assertEquals( 1, countNodesWithId( nodeId3 ) ); } } private void assumeServerIs31OrLater() { ServerVersion serverVersion = ServerVersion.version( neo4j.driver() ); assumeTrue( "Ignored on `" + serverVersion + "`", serverVersion.greaterThanOrEqual( v3_1_0 ) ); } private void testExecuteReadTx( AccessMode sessionMode ) { Driver driver = neo4j.driver(); // write some test data try ( Session session = driver.session() ) { session.run( "CREATE (:Person {name: 'Tony Stark'})" ); session.run( "CREATE (:Person {name: 'Steve Rogers'})" ); } // read previously committed data try ( Session session = driver.session( sessionMode ) ) { Set<String> names = session.readTransaction( new TransactionWork<Set<String>>() { @Override public Set<String> execute( Transaction tx ) { List<Record> records = tx.run( "MATCH (p:Person) RETURN p.name AS name" ).list(); Set<String> names = new HashSet<>( records.size() ); for ( Record record : records ) { names.add( record.get( "name" ).asString() ); } return names; } } ); assertThat( names, containsInAnyOrder( "Tony Stark", "Steve Rogers" ) ); } } private void testExecuteWriteTx( AccessMode sessionMode ) { Driver driver = neo4j.driver(); // write some test data try ( Session session = driver.session( sessionMode ) ) { String material = session.writeTransaction( new TransactionWork<String>() { @Override public String execute( Transaction tx ) { StatementResult result = tx.run( "CREATE (s:Shield {material: 'Vibranium'}) RETURN s" ); tx.success(); Record record = result.single(); return record.get( 0 ).asNode().get( "material" ).asString(); } } ); assertEquals( "Vibranium", material ); } // read previously committed data try ( Session session = driver.session() ) { Record record = session.run( "MATCH (s:Shield) RETURN s.material" ).single(); assertEquals( "Vibranium", record.get( 0 ).asString() ); } } private void testTxRollbackWhenFunctionThrows( AccessMode sessionMode ) { Driver driver = neo4j.driver(); try ( Session session = driver.session( sessionMode ) ) { try { session.writeTransaction( new TransactionWork<Void>() { @Override public Void execute( Transaction tx ) { tx.run( "CREATE (:Person {name: 'Thanos'})" ); // trigger division by zero error: tx.run( "UNWIND range(0, 1) AS i RETURN 10/i" ); tx.success(); return null; } } ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( ClientException.class ) ); } } // no data should have been committed try ( Session session = driver.session() ) { Record record = session.run( "MATCH (p:Person {name: 'Thanos'}) RETURN count(p)" ).single(); assertEquals( 0, record.get( 0 ).asInt() ); } } @SuppressWarnings( "deprecation" ) private void testResetOfQueryWaitingForLock( NodeIdUpdater nodeIdUpdater ) throws Exception { int nodeId = 42; int newNodeId1 = 4242; int newNodeId2 = 424242; createNodeWithId( nodeId ); CountDownLatch nodeLocked = new CountDownLatch( 1 ); AtomicReference<Session> otherSessionRef = new AtomicReference<>(); try ( Driver driver = newDriver(); Session session = driver.session(); Transaction tx = session.beginTransaction() ) { Future<Void> txResult = nodeIdUpdater.update( driver, nodeId, newNodeId1, otherSessionRef, nodeLocked ); StatementResult result = updateNodeId( tx, nodeId, newNodeId2 ); result.consume(); tx.success(); nodeLocked.countDown(); // give separate thread some time to block on a lock Thread.sleep( 2_000 ); otherSessionRef.get().reset(); assertTransactionTerminated( txResult ); } try ( Session session = neo4j.driver().session() ) { StatementResult result = session.run( "MATCH (n) RETURN n.id AS id" ); int value = result.single().get( "id" ).asInt(); assertEquals( newNodeId2, value ); } } private Driver newDriverWithoutRetries() { return newDriverWithFixedRetries( 0 ); } private Driver newDriverWithFixedRetries( int maxRetriesCount ) { DriverFactory driverFactory = new DriverFactoryWithFixedRetryLogic( maxRetriesCount ); RoutingSettings routingConf = new RoutingSettings( 1, 1, RoutingContext.EMPTY ); AuthToken auth = DEFAULT_AUTH_TOKEN; return driverFactory.newInstance( neo4j.uri(), auth, routingConf, RetrySettings.DEFAULT, noLoggingConfig() ); } private Driver newDriver() { return GraphDatabase.driver( neo4j.uri(), neo4j.authToken(), noLoggingConfig() ); } private Driver newDriverWithLimitedRetries( int maxTxRetryTime, TimeUnit unit ) { Config config = Config.build() .withLogging( DevNullLogging.DEV_NULL_LOGGING ) .withMaxTransactionRetryTime( maxTxRetryTime, unit ) .toConfig(); return GraphDatabase.driver( neo4j.uri(), neo4j.authToken(), config ); } private static Config noLoggingConfig() { return Config.build().withLogging( DevNullLogging.DEV_NULL_LOGGING ).toConfig(); } private static ThrowingWork newThrowingWorkSpy( String query, int failures ) { return spy( new ThrowingWork( query, failures ) ); } private static void assumeBookmarkSupport( Driver driver ) { ServerVersion serverVersion = ServerVersion.version( driver ); assumeTrue( format( "Server version `%s` does not support bookmark", serverVersion ), serverVersion.greaterThanOrEqual( v3_1_0 ) ); } private int countNodesWithId( int id ) { try ( Session session = neo4j.driver().session() ) { StatementResult result = session.run( "MATCH (n {id: {id}}) RETURN count(n)", parameters( "id", id ) ); return result.single().get( 0 ).asInt(); } } private void createNodeWithId( int id ) { try ( Session session = neo4j.driver().session() ) { session.run( "CREATE (n {id: {id}})", parameters( "id", id ) ); } } private static StatementResult updateNodeId( StatementRunner statementRunner, int currentId, int newId ) { return statementRunner.run( "MATCH (n {id: {currentId}}) SET n.id = {newId}", parameters( "currentId", currentId, "newId", newId ) ); } private static void assertTransactionTerminated( Future<Void> work ) throws Exception { try { work.get( 20, TimeUnit.SECONDS ); fail( "Exception expected" ); } catch ( ExecutionException e ) { assertThat( e.getCause(), instanceOf( TransientException.class ) ); assertThat( e.getCause().getMessage(), startsWith( "The transaction has been terminated" ) ); } } private static boolean assertOneOfTwoFuturesFailWithDeadlock( Future<Void> future1, Future<Void> future2 ) throws Exception { boolean firstFailed = false; try { assertNull( future1.get( 20, TimeUnit.SECONDS ) ); } catch ( ExecutionException e ) { assertDeadlockDetectedError( e ); firstFailed = true; } try { assertNull( future2.get( 20, TimeUnit.SECONDS ) ); } catch ( ExecutionException e ) { assertFalse( "Both futures failed, ", firstFailed ); assertDeadlockDetectedError( e ); } return firstFailed; } private static void assertDeadlockDetectedError( ExecutionException e ) { assertThat( e.getCause(), instanceOf( TransientException.class ) ); String errorCode = ((TransientException) e.getCause()).code(); assertEquals( "Neo.TransientError.Transaction.DeadlockDetected", errorCode ); } private static <T> Future<T> executeInDifferentThread( Callable<T> callable ) { ExecutorService executor = newSingleThreadExecutor( daemon( "test-thread-" ) ); return executor.submit( callable ); } private static void await( CountDownLatch latch ) { try { latch.await(); } catch ( InterruptedException e ) { Thread.currentThread().interrupt(); throw new RuntimeException( e ); } } private static abstract class NodeIdUpdater { final Future<Void> update( final Driver driver, final int nodeId, final int newNodeId, final AtomicReference<Session> usedSessionRef, final CountDownLatch latchToWait ) { return executeInDifferentThread( new Callable<Void>() { @Override public Void call() throws Exception { performUpdate( driver, nodeId, newNodeId, usedSessionRef, latchToWait ); return null; } } ); } abstract void performUpdate( Driver driver, int nodeId, int newNodeId, AtomicReference<Session> usedSessionRef, CountDownLatch latchToWait ) throws Exception; } private static class ThrowingWork implements TransactionWork<Record> { final String query; final int failures; int invoked; ThrowingWork( String query, int failures ) { this.query = query; this.failures = failures; } @Override public Record execute( Transaction tx ) { StatementResult result = tx.run( query ); if ( invoked++ < failures ) { throw new ServiceUnavailableException( "" ); } tx.success(); return result.single(); } } }