From 0035520d81d321cd21e393978b273efbfd8bfd9c Mon Sep 17 00:00:00 2001 From: lutovich Date: Wed, 8 Mar 2017 00:44:18 +0100 Subject: [PATCH 1/2] Dedicated functional interface for readTx and writeTx Added dedicated functional interface `TransactionWork` to be used in `Session#readTransaction()` and `Session#writeTransaction()` methods. It does not change how code looks for Java 8 lambdas but looks better with Java 7 where users do not need to implement `Function` where first generic type parameter is always `Transaction`. --- .../neo4j/driver/internal/NetworkSession.java | 10 ++--- .../java/org/neo4j/driver/v1/Session.java | 9 ++--- .../org/neo4j/driver/v1/TransactionWork.java | 37 +++++++++++++++++ .../driver/internal/NetworkSessionTest.java | 40 +++++++++---------- .../internal/RoutingDriverBoltKitTest.java | 7 ++-- .../driver/v1/integration/SessionIT.java | 30 +++++++------- .../driver/v1/util/TestNeo4jSession.java | 5 ++- 7 files changed, 88 insertions(+), 50 deletions(-) create mode 100644 driver/src/main/java/org/neo4j/driver/v1/TransactionWork.java diff --git a/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java b/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java index b91875bf12..6c269f7e86 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java +++ b/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java @@ -37,11 +37,11 @@ import org.neo4j.driver.v1.Statement; import org.neo4j.driver.v1.StatementResult; import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.TransactionWork; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.Values; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.types.TypeSystem; -import org.neo4j.driver.v1.util.Function; import static org.neo4j.driver.v1.Values.value; @@ -182,13 +182,13 @@ public synchronized Transaction beginTransaction( String bookmark ) } @Override - public T readTransaction( Function work ) + public T readTransaction( TransactionWork work ) { return transaction( AccessMode.READ, work ); } @Override - public T writeTransaction( Function work ) + public T writeTransaction( TransactionWork work ) { return transaction( AccessMode.WRITE, work ); } @@ -244,7 +244,7 @@ public synchronized void onConnectionError( boolean recoverable ) } } - private synchronized T transaction( AccessMode mode, Function work ) + private synchronized T transaction( AccessMode mode, TransactionWork work ) { RetryDecision decision = null; List errors = null; @@ -253,7 +253,7 @@ private synchronized T transaction( AccessMode mode, Function { try ( Transaction tx = beginTransaction( mode ) ) { - return work.apply( tx ); + return work.execute( tx ); } catch ( Throwable newError ) { diff --git a/driver/src/main/java/org/neo4j/driver/v1/Session.java b/driver/src/main/java/org/neo4j/driver/v1/Session.java index f4aa56e7de..4ba13b3fee 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Session.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Session.java @@ -18,7 +18,6 @@ */ package org.neo4j.driver.v1; -import org.neo4j.driver.v1.util.Function; import org.neo4j.driver.v1.util.Resource; /** @@ -86,20 +85,20 @@ public interface Session extends Resource, StatementRunner /** * Execute given unit of work in a {@link AccessMode#READ read} transaction. * - * @param work the {@link Function} to be applied to a new read transaction. + * @param work the {@link TransactionWork} to be applied to a new read transaction. * @param the return type of the given unit of work. * @return a result as returned by the given unit of work. */ - T readTransaction( Function work ); + T readTransaction( TransactionWork work ); /** * Execute given unit of work in a {@link AccessMode#WRITE write} transaction. * - * @param work the {@link Function} to be applied to a new write transaction. + * @param work the {@link TransactionWork} to be applied to a new write transaction. * @param the return type of the given unit of work. * @return a result as returned by the given unit of work. */ - T writeTransaction( Function work ); + T writeTransaction( TransactionWork work ); /** * Return the bookmark received following the last completed diff --git a/driver/src/main/java/org/neo4j/driver/v1/TransactionWork.java b/driver/src/main/java/org/neo4j/driver/v1/TransactionWork.java new file mode 100644 index 0000000000..baa6adf9ae --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/v1/TransactionWork.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Callback that executes operations against a given {@link Transaction}. + * To be used with {@link Session#readTransaction(TransactionWork)} and + * {@link Session#writeTransaction(TransactionWork)} methods. + * + * @param the return type of this work. + */ +public interface TransactionWork +{ + /** + * Executes all given operations against the same transaction. + * + * @param tx the transaction to use. + * @return some result object or {@code null} if none. + */ + T execute( Transaction tx ); +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/NetworkSessionTest.java b/driver/src/test/java/org/neo4j/driver/internal/NetworkSessionTest.java index 1c0de8d9ff..346df9f6e7 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/NetworkSessionTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/NetworkSessionTest.java @@ -38,11 +38,11 @@ import org.neo4j.driver.v1.AccessMode; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.TransactionWork; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.SessionExpiredException; -import org.neo4j.driver.v1.util.Function; import static java.util.Collections.singletonMap; import static org.hamcrest.CoreMatchers.containsString; @@ -764,10 +764,10 @@ private static void testConnectionAcquisition( AccessMode sessionMode, AccessMod when( connectionProvider.acquireConnection( transactionMode ) ).thenReturn( connection ); NetworkSession session = newSession( connectionProvider, sessionMode ); - Function work = new Function() + TransactionWork work = new TransactionWork() { @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { tx.success(); return 42; @@ -789,10 +789,10 @@ private static void testTxCommitOrRollback( AccessMode transactionMode, final bo when( connectionProvider.acquireConnection( transactionMode ) ).thenReturn( connection ); NetworkSession session = newSession( connectionProvider, WRITE ); - Function work = new Function() + TransactionWork work = new TransactionWork() { @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { if ( commit ) { @@ -831,10 +831,10 @@ private static void testTxRollbackWhenThrows( AccessMode transactionMode ) NetworkSession session = newSession( connectionProvider, WRITE ); final RuntimeException error = new IllegalStateException( "Oh!" ); - Function work = new Function() + TransactionWork work = new TransactionWork() { @Override - public Void apply( Transaction tx ) + public Void execute( Transaction tx ) { throw error; } @@ -864,12 +864,12 @@ private static void testTxIsRetriedUntilSuccessWhenFunctionThrows( AccessMode mo when( connectionProvider.acquireConnection( mode ) ).thenReturn( connection ); NetworkSession session = newSession( connectionProvider, retryLogic ); - int answer = executeTransaction( session, mode, new Function() + int answer = executeTransaction( session, mode, new TransactionWork() { int invoked; @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { if ( invoked++ < failures ) { @@ -896,10 +896,10 @@ private static void testTxIsRetriedUntilSuccessWhenTxCloseThrows( AccessMode mod when( connectionProvider.acquireConnection( mode ) ).thenReturn( connection ); NetworkSession session = newSession( connectionProvider, retryLogic ); - int answer = executeTransaction( session, mode, new Function() + int answer = executeTransaction( session, mode, new TransactionWork() { @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { tx.success(); return 43; @@ -925,12 +925,12 @@ private static void testTxIsRetriedUntilFailureWhenFunctionThrows( AccessMode mo try { - executeTransaction( session, mode, new Function() + executeTransaction( session, mode, new TransactionWork() { int invoked; @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { if ( invoked++ < failures ) { @@ -963,10 +963,10 @@ private static void testTxIsRetriedUntilFailureWhenTxCloseThrows( AccessMode mod try { - executeTransaction( session, mode, new Function() + executeTransaction( session, mode, new TransactionWork() { @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { tx.success(); return 42; @@ -992,12 +992,12 @@ private static void testRetryErrorsAreCombined( AccessMode mode ) try { - executeTransaction( session, mode, new Function() + executeTransaction( session, mode, new TransactionWork() { int invoked; @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { if ( invoked++ < failures ) { @@ -1035,12 +1035,12 @@ private static void testRetryErrorsAreNotCombinedWhenSameErrorIsThrown( AccessMo final ServiceUnavailableException error = new ServiceUnavailableException( "Oh!" ); try { - executeTransaction( session, mode, new Function() + executeTransaction( session, mode, new TransactionWork() { int invoked; @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { if ( invoked++ < failures ) { @@ -1060,7 +1060,7 @@ public Integer apply( Transaction tx ) } } - private static T executeTransaction( Session session, AccessMode mode, Function work ) + private static T executeTransaction( Session session, AccessMode mode, TransactionWork work ) { if ( mode == READ ) { diff --git a/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverBoltKitTest.java b/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverBoltKitTest.java index 80de1cb5e1..be1d6ebf8c 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverBoltKitTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverBoltKitTest.java @@ -44,6 +44,7 @@ import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.TransactionWork; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.SessionExpiredException; import org.neo4j.driver.v1.util.Function; @@ -877,12 +878,12 @@ private static Driver newDriver( String uriString, DriverFactory driverFactory ) return driverFactory.newInstance( uri, auth, routingConf, RetrySettings.DEFAULT, config ); } - private static Function> queryWork( final String query, final AtomicInteger invocations ) + private static TransactionWork> queryWork( final String query, final AtomicInteger invocations ) { - return new Function>() + return new TransactionWork>() { @Override - public List apply( Transaction tx ) + public List execute( Transaction tx ) { invocations.incrementAndGet(); return tx.run( query ).list(); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java index 4c80fd1302..bf388a1a72 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java @@ -40,10 +40,10 @@ import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; 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.util.Function; import org.neo4j.driver.v1.util.TestNeo4j; import static org.hamcrest.CoreMatchers.containsString; @@ -457,7 +457,7 @@ public void readTxRetriedUntilSuccess() assertEquals( "Bruce Banner", record.get( 0 ).asString() ); } - verify( work, times( retries ) ).apply( any( Transaction.class ) ); + verify( work, times( retries ) ).execute( any( Transaction.class ) ); } } @@ -481,7 +481,7 @@ public void writeTxRetriedUntilSuccess() assertEquals( 1, record.get( 0 ).asInt() ); } - verify( work, times( retries ) ).apply( any( Transaction.class ) ); + verify( work, times( retries ) ).execute( any( Transaction.class ) ); } } @@ -507,7 +507,7 @@ public void readTxRetriedUntilFailure() } } - verify( work, times( failures ) ).apply( any( Transaction.class ) ); + verify( work, times( failures ) ).execute( any( Transaction.class ) ); } } @@ -539,7 +539,7 @@ public void writeTxRetriedUntilFailure() assertEquals( 0, result.single().get( 0 ).asInt() ); } - verify( work, times( failures ) ).apply( any( Transaction.class ) ); + verify( work, times( failures ) ).execute( any( Transaction.class ) ); } } @@ -548,10 +548,10 @@ public void writeTxDoesNotCommitWhenMarkedForFailure() { try ( Session session = neo4j.driver().session() ) { - int answer = session.writeTransaction( new Function() + int answer = session.writeTransaction( new TransactionWork() { @Override - public Integer apply( Transaction tx ) + public Integer execute( Transaction tx ) { tx.run( "CREATE (:Person {name: 'Natasha Romanoff'})" ); tx.failure(); @@ -583,10 +583,10 @@ private void testExecuteReadTx( AccessMode sessionMode ) // read previously committed data try ( Session session = driver.session( sessionMode ) ) { - Set names = session.readTransaction( new Function>() + Set names = session.readTransaction( new TransactionWork>() { @Override - public Set apply( Transaction tx ) + public Set execute( Transaction tx ) { List records = tx.run( "MATCH (p:Person) RETURN p.name AS name" ).list(); Set names = new HashSet<>( records.size() ); @@ -609,10 +609,10 @@ private void testExecuteWriteTx( AccessMode sessionMode ) // write some test data try ( Session session = driver.session( sessionMode ) ) { - String material = session.writeTransaction( new Function() + String material = session.writeTransaction( new TransactionWork() { @Override - public String apply( Transaction tx ) + public String execute( Transaction tx ) { StatementResult result = tx.run( "CREATE (s:Shield {material: 'Vibranium'}) RETURN s" ); tx.success(); @@ -640,10 +640,10 @@ private void testTxRollbackWhenFunctionThrows( AccessMode sessionMode ) { try { - session.writeTransaction( new Function() + session.writeTransaction( new TransactionWork() { @Override - public Void apply( Transaction tx ) + public Void execute( Transaction tx ) { tx.run( "CREATE (:Person {name: 'Thanos'})" ); // trigger division by zero error: @@ -681,7 +681,7 @@ private static ThrowingWork newThrowingWorkSpy( String query, int failures ) return spy( new ThrowingWork( query, failures ) ); } - private static class ThrowingWork implements Function + private static class ThrowingWork implements TransactionWork { final String query; final int failures; @@ -695,7 +695,7 @@ private static class ThrowingWork implements Function } @Override - public Record apply( Transaction tx ) + public Record execute( Transaction tx ) { StatementResult result = tx.run( query ); if ( invoked++ < failures ) diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java index a73268243a..10f2f899b2 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java +++ b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java @@ -27,6 +27,7 @@ import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.TransactionWork; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.types.TypeSystem; @@ -107,13 +108,13 @@ public Transaction beginTransaction( String bookmark ) } @Override - public T readTransaction( Function work ) + public T readTransaction( TransactionWork work ) { return realSession.readTransaction( work ); } @Override - public T writeTransaction( Function work ) + public T writeTransaction( TransactionWork work ) { return realSession.writeTransaction( work ); } From 17755a330486a7768710feaa7330fc7cd00439b8 Mon Sep 17 00:00:00 2001 From: lutovich Date: Wed, 8 Mar 2017 11:19:10 +0100 Subject: [PATCH 2/2] Internalised marking for success in readTx & writeTx This commit makes `Session#readTransaction()` and `Session#writeTransaction()` automatically commit when transaction was not marked as failure and no exception was thrown. This effectively removes the need to call `tx.success()` as the last statement in a transaction. --- .../neo4j/driver/internal/NetworkSession.java | 16 +- .../main/java/org/neo4j/driver/v1/Config.java | 10 +- .../java/org/neo4j/driver/v1/Session.java | 6 + .../driver/v1/integration/SessionIT.java | 294 +++++++++++++++++- driver/src/test/resources/read_server.script | 1 + 5 files changed, 312 insertions(+), 15 deletions(-) diff --git a/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java b/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java index 6c269f7e86..dbb426ac03 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java +++ b/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java @@ -253,7 +253,21 @@ private synchronized T transaction( AccessMode mode, TransactionWork work { try ( Transaction tx = beginTransaction( mode ) ) { - return work.execute( tx ); + T result; + try + { + result = work.execute( tx ); + } + catch ( Throwable t ) + { + // mark transaction for failure if the given unit of work threw exception + // this will override any success marks that were made by the unit of work + tx.failure(); + throw t; + } + // given unit of work completed successfully, mark transaction for commit + tx.success(); + return result; } catch ( Throwable newError ) { diff --git a/driver/src/main/java/org/neo4j/driver/v1/Config.java b/driver/src/main/java/org/neo4j/driver/v1/Config.java index f87afe7a72..2eea9f359f 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Config.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Config.java @@ -29,7 +29,6 @@ import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.SessionExpiredException; import org.neo4j.driver.v1.exceptions.TransientException; -import org.neo4j.driver.v1.util.Function; import org.neo4j.driver.v1.util.Immutable; import org.neo4j.driver.v1.util.Resource; @@ -449,10 +448,11 @@ public ConfigBuilder withConnectionTimeout( long value, TimeUnit unit ) } /** - * Specify the maximum time transactions are allowed to retry via {@link Session#readTransaction(Function)} and - * {@link Session#writeTransaction(Function)} methods. These methods will retry the given unit of work on - * {@link ServiceUnavailableException}, {@link SessionExpiredException} and {@link TransientException} with - * exponential backoff using initial delay of 1 second. + * Specify the maximum time transactions are allowed to retry via + * {@link Session#readTransaction(TransactionWork)} and {@link Session#writeTransaction(TransactionWork)} + * methods. These methods will retry the given unit of work on {@link ServiceUnavailableException}, + * {@link SessionExpiredException} and {@link TransientException} with exponential backoff using initial + * delay of 1 second. *

* Default value is 30 seconds. * diff --git a/driver/src/main/java/org/neo4j/driver/v1/Session.java b/driver/src/main/java/org/neo4j/driver/v1/Session.java index 4ba13b3fee..63f6f0986a 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Session.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Session.java @@ -84,6 +84,9 @@ public interface Session extends Resource, StatementRunner /** * Execute given unit of work in a {@link AccessMode#READ read} transaction. + *

+ * Transaction will automatically be committed unless exception is thrown from the unit of work itself or from + * {@link Transaction#close()} or transaction is explicitly marked for failure via {@link Transaction#failure()}. * * @param work the {@link TransactionWork} to be applied to a new read transaction. * @param the return type of the given unit of work. @@ -93,6 +96,9 @@ public interface Session extends Resource, StatementRunner /** * Execute given unit of work in a {@link AccessMode#WRITE write} transaction. + *

+ * Transaction will automatically be committed unless exception is thrown from the unit of work itself or from + * {@link Transaction#close()} or transaction is explicitly marked for failure via {@link Transaction#failure()}. * * @param work the {@link TransactionWork} to be applied to a new write transaction. * @param the return type of the given unit of work. diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java index bf388a1a72..e355125142 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java @@ -55,6 +55,8 @@ import static org.hamcrest.Matchers.greaterThan; 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; @@ -544,28 +546,297 @@ public void writeTxRetriedUntilFailure() } @Test - public void writeTxDoesNotCommitWhenMarkedForFailure() + public void readTxCommittedWithoutTxSuccess() { - try ( Session session = neo4j.driver().session() ) + try ( Driver driver = newDriverWithoutRetries(); + Session session = driver.session() ) { - int answer = session.writeTransaction( new TransactionWork() + assertNull( session.lastBookmark() ); + + long answer = session.readTransaction( new TransactionWork() + { + @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() + { + @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() ) + { + assertNull( session.lastBookmark() ); + + long answer = session.readTransaction( new TransactionWork() { @Override - public Integer execute( Transaction tx ) + public Long execute( Transaction tx ) { - tx.run( "CREATE (:Person {name: 'Natasha Romanoff'})" ); + StatementResult result = tx.run( "RETURN 42" ); tx.failure(); - return 42; + 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() + { + @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() ) + { + assertNull( session.lastBookmark() ); + + try + { + session.readTransaction( new TransactionWork() + { + @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() + { + @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() ) + { + assertNull( session.lastBookmark() ); + + long answer = session.readTransaction( new TransactionWork() + { + @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() ); } + } - try ( Session session = neo4j.driver().session() ) + @Test + public void writeTxRolledBackWhenMarkedBothSuccessAndFailure() + { + try ( Driver driver = newDriverWithoutRetries() ) { - StatementResult result = session.run( "MATCH (p:Person {name: 'Natasha Romanoff'}) RETURN count(p)" ); - assertEquals( 0, result.single().get( 0 ).asInt() ); + try ( Session session = driver.session() ) + { + int answer = session.writeTransaction( new TransactionWork() + { + @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() ) + { + assertNull( session.lastBookmark() ); + + try + { + session.readTransaction( new TransactionWork() + { + @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() + { + @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() ); + } } } @@ -668,6 +939,11 @@ public Void execute( Transaction tx ) } } + private Driver newDriverWithoutRetries() + { + return newDriverWithFixedRetries( 0 ); + } + private Driver newDriverWithFixedRetries( int maxRetriesCount ) { DriverFactory driverFactory = new DriverFactoryWithFixedRetryLogic( maxRetriesCount ); diff --git a/driver/src/test/resources/read_server.script b/driver/src/test/resources/read_server.script index 9530070d63..7fb3d6c810 100644 --- a/driver/src/test/resources/read_server.script +++ b/driver/src/test/resources/read_server.script @@ -1,6 +1,7 @@ !: AUTO INIT !: AUTO RESET !: AUTO PULL_ALL +!: AUTO RUN "COMMIT" {} !: AUTO RUN "ROLLBACK" {} !: AUTO RUN "BEGIN" {}