diff --git a/community/cypher/cypher/src/main/java/org/neo4j/cypher/javacompat/internal/GraphDatabaseCypherService.java b/community/cypher/cypher/src/main/java/org/neo4j/cypher/javacompat/internal/GraphDatabaseCypherService.java index 332b03236a79d..9ac0120a72357 100644 --- a/community/cypher/cypher/src/main/java/org/neo4j/cypher/javacompat/internal/GraphDatabaseCypherService.java +++ b/community/cypher/cypher/src/main/java/org/neo4j/cypher/javacompat/internal/GraphDatabaseCypherService.java @@ -28,8 +28,8 @@ import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.security.URLAccessValidationError; import org.neo4j.kernel.GraphDatabaseQueryService; -import org.neo4j.kernel.api.security.AccessMode; import org.neo4j.kernel.api.KernelTransaction; +import org.neo4j.kernel.api.security.AccessMode; import org.neo4j.kernel.impl.coreapi.InternalTransaction; import org.neo4j.kernel.impl.factory.GraphDatabaseFacade; @@ -78,6 +78,13 @@ public InternalTransaction beginTransaction( KernelTransaction.Type type, Access return graph.beginTransaction( type, accessMode ); } + @Override + public InternalTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode, + long timeout ) + { + return graph.beginTransaction( type, accessMode, timeout ); + } + @Override public URL validateURLAccess( URL url ) throws URLAccessValidationError { diff --git a/community/kernel/src/main/java/org/neo4j/kernel/GraphDatabaseQueryService.java b/community/kernel/src/main/java/org/neo4j/kernel/GraphDatabaseQueryService.java index f5b443da1995d..19fac858a85e5 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/GraphDatabaseQueryService.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/GraphDatabaseQueryService.java @@ -26,8 +26,8 @@ import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.security.URLAccessValidationError; -import org.neo4j.kernel.api.security.AccessMode; import org.neo4j.kernel.api.KernelTransaction; +import org.neo4j.kernel.api.security.AccessMode; import org.neo4j.kernel.impl.coreapi.InternalTransaction; /* @@ -41,6 +41,10 @@ public interface GraphDatabaseQueryService Node createNode( Label... labels ); Node getNodeById(long id); Relationship getRelationshipById(long id); + InternalTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode ); + + InternalTransaction beginTransaction( KernelTransaction.Type implicit, AccessMode accessMode, long timeout ); + URL validateURLAccess( URL url ) throws URLAccessValidationError; } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/api/KernelAPI.java b/community/kernel/src/main/java/org/neo4j/kernel/api/KernelAPI.java index 10a382c93daa8..4a29b3a3ec16a 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/api/KernelAPI.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/api/KernelAPI.java @@ -40,10 +40,21 @@ public interface KernelAPI * underlying graph. * * @param type the type of the new transaction: implicit (internally created) or explicit (created by the user) - * @param accessMode + * @param accessMode transaction access mode */ KernelTransaction newTransaction( KernelTransaction.Type type, AccessMode accessMode ) throws TransactionFailureException; + /** + * Creates and returns a new {@link KernelTransaction} capable of modifying the + * underlying graph. + * + * @param type the type of the new transaction: implicit (internally created) or explicit (created by the user) + * @param accessMode transaction access mode + * @param timeout transaction timeout + */ + KernelTransaction newTransaction( KernelTransaction.Type type, AccessMode accessMode, long timeout ) + throws TransactionFailureException; + /** * Registers a {@link TransactionHook} that will receive notifications about committing transactions * and the changes they commit. diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/api/Kernel.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/api/Kernel.java index 1b6af0675cecd..a35a1eb113f76 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/api/Kernel.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/api/Kernel.java @@ -87,9 +87,16 @@ public Kernel( KernelTransactions transactionFactory, TransactionHooks hooks, Da @Override public KernelTransaction newTransaction( KernelTransaction.Type type, AccessMode accessMode ) throws TransactionFailureException + { + return newTransaction( type, accessMode, defaultTransactionTimeout ); + } + + @Override + public KernelTransaction newTransaction( KernelTransaction.Type type, AccessMode accessMode, long timeout ) + throws TransactionFailureException { health.assertHealthy( TransactionFailureException.class ); - KernelTransaction transaction = transactions.newInstance( type, accessMode, defaultTransactionTimeout ); + KernelTransaction transaction = transactions.newInstance( type, accessMode, timeout ); transactionMonitor.transactionStarted(); return transaction; } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/ClassicCoreSPI.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/ClassicCoreSPI.java index ef4c135536e43..28e12190fdbc8 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/ClassicCoreSPI.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/ClassicCoreSPI.java @@ -28,13 +28,14 @@ import org.neo4j.graphdb.Result; import org.neo4j.graphdb.event.KernelEventHandler; import org.neo4j.graphdb.event.TransactionEventHandler; +import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.graphdb.security.URLAccessValidationError; import org.neo4j.kernel.GraphDatabaseQueryService; -import org.neo4j.kernel.api.security.AccessMode; import org.neo4j.kernel.api.KernelTransaction; import org.neo4j.kernel.api.Statement; import org.neo4j.kernel.api.exceptions.TransactionFailureException; import org.neo4j.kernel.api.legacyindex.AutoIndexing; +import org.neo4j.kernel.api.security.AccessMode; import org.neo4j.kernel.impl.coreapi.CoreAPIAvailabilityGuard; import org.neo4j.kernel.impl.query.QueryExecutionKernelException; import org.neo4j.kernel.impl.query.QuerySession; @@ -53,6 +54,7 @@ class ClassicCoreSPI implements GraphDatabaseFacade.SPI private final DataSourceModule dataSource; private final Logger msgLog; private final CoreAPIAvailabilityGuard availability; + private final long defaultTransactionTimeout; public ClassicCoreSPI(PlatformModule platform, DataSourceModule dataSource, Logger msgLog, CoreAPIAvailabilityGuard availability ) { @@ -60,6 +62,7 @@ public ClassicCoreSPI(PlatformModule platform, DataSourceModule dataSource, Logg this.dataSource = dataSource; this.msgLog = msgLog; this.availability = availability; + defaultTransactionTimeout = platform.config.get( GraphDatabaseSettings.transaction_timeout ); } @Override @@ -166,11 +169,17 @@ public void shutdown() @Override public KernelTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode ) + { + return beginTransaction( type, accessMode, defaultTransactionTimeout ); + } + + @Override + public KernelTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode, long timeout ) { try { availability.assertDatabaseAvailable(); - KernelTransaction kernelTx = dataSource.kernelAPI.get().newTransaction( type, accessMode ); + KernelTransaction kernelTx = dataSource.kernelAPI.get().newTransaction( type, accessMode, timeout ); kernelTx.registerCloseListener( (s) -> dataSource.threadToTransactionBridge.unbindTransactionFromCurrentThread() ); dataSource.threadToTransactionBridge.bindTransactionToCurrentThread( kernelTx ); return kernelTx; diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/GraphDatabaseFacade.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/GraphDatabaseFacade.java index 5c0dcde8f9485..5b1fc7d721f47 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/GraphDatabaseFacade.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/GraphDatabaseFacade.java @@ -23,6 +23,7 @@ import java.net.URL; import java.util.Collections; import java.util.Map; +import java.util.function.Supplier; import org.neo4j.collection.primitive.PrimitiveLongCollections; import org.neo4j.collection.primitive.PrimitiveLongIterator; @@ -147,6 +148,16 @@ public interface SPI */ KernelTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode ); + /** + * Begin a new kernel transaction with specified timeout. + * If a transaction is already associated to the current context + * (meaning, non-null is returned from {@link #currentTransaction()}), this should fail. + * + * @throws org.neo4j.graphdb.TransactionFailureException if unable to begin, or a transaction already exists. + * @see SPI#beginTransaction(KernelTransaction.Type, AccessMode) + */ + KernelTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode, long timeout ); + /** * Retrieve the transaction associated with the current context. For the classic implementation of the Core API, * the context is the current thread. @@ -333,19 +344,19 @@ public Transaction beginTx() public InternalTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode ) { - if ( spi.isInOpenTransaction() ) - { - // FIXME: perhaps we should check that the new type and access mode are compatible with the current tx - return new PlaceboTransaction( spi::currentTransaction, spi::currentStatement ); - } + return beginTransaction( () -> spi.beginTransaction( type, accessMode ) ); + } - return new TopLevelTransaction( spi.beginTransaction( type, accessMode ), spi::currentStatement ); + public InternalTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode, + long timeout ) + { + return beginTransaction( () -> spi.beginTransaction( type, accessMode, timeout ) ); } @Override public Result execute( String query ) throws QueryExecutionException { - return execute( query, Collections.emptyMap() ); + return execute( query, Collections.emptyMap() ); } @Override @@ -403,6 +414,7 @@ public ResourceIterable getAllRelationshipTypesInUse() { return allInUse( TokenAccess.RELATIONSHIP_TYPES ); } + private ResourceIterable allInUse( final TokenAccess tokens ) { assertTransactionOpen(); @@ -495,6 +507,16 @@ public ResourceIterator findNodes( final Label myLabel ) return allNodesWithLabel( myLabel ); } + private InternalTransaction beginTransaction( Supplier transactionSupplier ) + { + if ( spi.isInOpenTransaction() ) + { + // FIXME: perhaps we should check that the new type and access mode are compatible with the current tx + return new PlaceboTransaction( spi::currentTransaction, spi::currentStatement ); + } + return new TopLevelTransaction( transactionSupplier.get(), spi::currentStatement ); + } + private ResourceIterator nodesByLabelAndProperty( Label myLabel, String key, Object value ) { Statement statement = spi.currentStatement(); diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/proc/ProcedureGDBFacadeSPI.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/proc/ProcedureGDBFacadeSPI.java index 6eade30d2e2a4..16327ddb31a4e 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/proc/ProcedureGDBFacadeSPI.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/proc/ProcedureGDBFacadeSPI.java @@ -200,4 +200,10 @@ public KernelTransaction beginTransaction( KernelTransaction.Type type, AccessMo { throw new UnsupportedOperationException(); } + + @Override + public KernelTransaction beginTransaction( KernelTransaction.Type type, AccessMode accessMode, long timeout ) + { + throw new UnsupportedOperationException(); + } } diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/api/constraints/ConstraintIndexCreatorTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/api/constraints/ConstraintIndexCreatorTest.java index 37629bc184ecb..100bd92a0c480 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/api/constraints/ConstraintIndexCreatorTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/api/constraints/ConstraintIndexCreatorTest.java @@ -163,111 +163,15 @@ public class StubKernel implements KernelAPI @Override public KernelTransaction newTransaction( KernelTransaction.Type type, AccessMode accessMode ) { - return new KernelTransaction() - { - @Override - public void success() - { - } - - @Override - public void failure() - { - } - - @Override - public void close() throws TransactionFailureException - { - } - - @Override - public Statement acquireStatement() - { - return remember( mockedState() ); - } - - private Statement remember( KernelStatement mockedState ) - { - statements.add( mockedState ); - return mockedState; - } - - @Override - public boolean isOpen() - { - return true; - } - - @Override - public AccessMode mode() - { - throw new UnsupportedOperationException(); - } - - @Override - public Status getReasonIfTerminated() - { - return null; - } - - @Override - public void markForTermination( Status reason ) - { - } - - @Override - public long lastTransactionTimestampWhenStarted() - { - return 0; - } - - @Override - public void registerCloseListener( CloseListener listener ) - { - } - - @Override - public Type transactionType() - { - return null; - } - - @Override - public long getTransactionId() - { - return -1; - } - - @Override - public long getCommitTime() - { - return -1; - } - - @Override - public Revertable restrict( AccessMode read ) - { - return null; - } - - @Override - public long lastTransactionIdWhenStarted() - { - return 0; - } - - @Override - public long startTime() - { - return 0; - } - - @Override - public long timeout() - { - return 0; - } - }; + return new StubKernelTransaction(); + } + + @Override + public KernelTransaction newTransaction( KernelTransaction.Type type, AccessMode accessMode, + long timeout ) + throws TransactionFailureException + { + return new StubKernelTransaction(timeout); } @Override @@ -287,5 +191,122 @@ public void registerProcedure( CallableProcedure signature ) { throw new UnsupportedOperationException(); } + + private class StubKernelTransaction implements KernelTransaction + { + private long timeout = 0; + + public StubKernelTransaction() + { + } + + public StubKernelTransaction( long timeout ) + { + this.timeout = timeout; + } + + @Override + public void success() + { + } + + @Override + public void failure() + { + } + + @Override + public void close() throws TransactionFailureException + { + } + + @Override + public Statement acquireStatement() + { + return remember( mockedState() ); + } + + private Statement remember( KernelStatement mockedState ) + { + statements.add( mockedState ); + return mockedState; + } + + @Override + public boolean isOpen() + { + return true; + } + + @Override + public AccessMode mode() + { + throw new UnsupportedOperationException(); + } + + @Override + public Status getReasonIfTerminated() + { + return null; + } + + @Override + public void markForTermination( Status reason ) + { + } + + @Override + public long lastTransactionTimestampWhenStarted() + { + return 0; + } + + @Override + public void registerCloseListener( CloseListener listener ) + { + } + + @Override + public Type transactionType() + { + return null; + } + + @Override + public long getTransactionId() + { + return -1; + } + + @Override + public long getCommitTime() + { + return -1; + } + + @Override + public Revertable restrict( AccessMode read ) + { + return null; + } + + @Override + public long lastTransactionIdWhenStarted() + { + return 0; + } + + @Override + public long startTime() + { + return 0; + } + + @Override + public long timeout() + { + return timeout; + } + } } } diff --git a/community/server/src/main/java/org/neo4j/server/AbstractNeoServer.java b/community/server/src/main/java/org/neo4j/server/AbstractNeoServer.java index c0c682151c428..2dbba241f48d3 100644 --- a/community/server/src/main/java/org/neo4j/server/AbstractNeoServer.java +++ b/community/server/src/main/java/org/neo4j/server/AbstractNeoServer.java @@ -195,7 +195,7 @@ public void start() throws ServerStartupException transactionFacade = createTransactionalActions(); - cypherExecutor = new CypherExecutor( database ); + cypherExecutor = new CypherExecutor( database, config, logProvider ); configureWebServer(); diff --git a/community/server/src/main/java/org/neo4j/server/database/CypherExecutor.java b/community/server/src/main/java/org/neo4j/server/database/CypherExecutor.java index 4c590422c64d8..d4f4afc239c70 100644 --- a/community/server/src/main/java/org/neo4j/server/database/CypherExecutor.java +++ b/community/server/src/main/java/org/neo4j/server/database/CypherExecutor.java @@ -22,9 +22,11 @@ import javax.servlet.http.HttpServletRequest; import org.neo4j.cypher.internal.javacompat.ExecutionEngine; +import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.kernel.GraphDatabaseQueryService; -import org.neo4j.kernel.api.security.AccessMode; import org.neo4j.kernel.api.KernelTransaction; +import org.neo4j.kernel.api.security.AccessMode; +import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.impl.core.ThreadToStatementContextBridge; import org.neo4j.kernel.impl.coreapi.InternalTransaction; import org.neo4j.kernel.impl.coreapi.PropertyContainerLocker; @@ -33,20 +35,28 @@ import org.neo4j.kernel.impl.query.QuerySession; import org.neo4j.kernel.impl.query.TransactionalContext; import org.neo4j.kernel.lifecycle.LifecycleAdapter; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; import org.neo4j.server.rest.web.ServerQuerySession; public class CypherExecutor extends LifecycleAdapter { + private static final String MAX_EXECUTION_TIME_HEADER = "max-execution-time"; + private final Database database; private ExecutionEngine executionEngine; private GraphDatabaseQueryService service; private ThreadToStatementContextBridge txBridge; private static final PropertyContainerLocker locker = new PropertyContainerLocker(); + private final boolean guardEnabled; + private final Log log; - public CypherExecutor( Database database ) + public CypherExecutor( Database database, Config config, LogProvider logProvider ) { this.database = database; + log = logProvider.getLog( getClass() ); + guardEnabled = config.get( GraphDatabaseSettings.execution_guard_enabled ); } public ExecutionEngine getExecutionEngine() @@ -73,8 +83,48 @@ public void stop() throws Throwable public QuerySession createSession( HttpServletRequest request ) { - InternalTransaction transaction = service.beginTransaction( KernelTransaction.Type.implicit, AccessMode.Static.FULL ); + InternalTransaction transaction = getInternalTransaction( request ); TransactionalContext context = new Neo4jTransactionalContext( service, transaction, txBridge.get(), locker ); return new ServerQuerySession( request, context ); } + + private InternalTransaction getInternalTransaction( HttpServletRequest request ) + { + if ( guardEnabled ) + { + long customTimeout = getTransactionTimeLimit( request ); + if ( customTimeout > 0 ) + { + return beginCustomTransaction( customTimeout ); + } + } + return beginDefaultTransaction(); + } + + private InternalTransaction beginCustomTransaction( long customTimeout ) + { + return service.beginTransaction( KernelTransaction.Type.implicit, AccessMode.Static.FULL, customTimeout ); + } + + private InternalTransaction beginDefaultTransaction() + { + return service.beginTransaction( KernelTransaction.Type.implicit, AccessMode.Static.FULL ); + } + + private long getTransactionTimeLimit( HttpServletRequest request ) + { + String headerValue = request.getHeader( MAX_EXECUTION_TIME_HEADER ); + if ( headerValue != null ) + { + try + { + return Long.parseLong( headerValue ); + } + catch ( NumberFormatException e ) + { + log.error( "Fail to parse `" + MAX_EXECUTION_TIME_HEADER + "` header with value: " + headerValue, e ); + } + } + return -1; + } } diff --git a/community/server/src/test/java/org/neo4j/server/rest/CypherSessionDocTest.java b/community/server/src/test/java/org/neo4j/server/rest/CypherSessionDocTest.java index c6eaea28cf104..ea2f8dc80680f 100644 --- a/community/server/src/test/java/org/neo4j/server/rest/CypherSessionDocTest.java +++ b/community/server/src/test/java/org/neo4j/server/rest/CypherSessionDocTest.java @@ -19,11 +19,12 @@ */ package org.neo4j.server.rest; -import javax.servlet.http.HttpServletRequest; - import org.junit.Test; +import javax.servlet.http.HttpServletRequest; + import org.neo4j.helpers.collection.Pair; +import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.impl.factory.GraphDatabaseFacade; import org.neo4j.logging.NullLogProvider; import org.neo4j.server.database.CypherExecutor; @@ -43,7 +44,7 @@ public void shouldReturnASingleNode() throws Throwable { GraphDatabaseFacade graphdb = (GraphDatabaseFacade) new TestGraphDatabaseFactory().newImpermanentDatabase(); Database database = new WrappedDatabase( graphdb ); - CypherExecutor executor = new CypherExecutor( database ); + CypherExecutor executor = new CypherExecutor( database, Config.defaults(), NullLogProvider.getInstance() ); executor.start(); try {