Skip to content

Commit

Permalink
Use ProcedureGDSFactory to prevent procedures from invoking blocked/u…
Browse files Browse the repository at this point in the history
…nsupported operations, like db.shutdown

o Also fixes bug where draining an internal iterator in a ProcedureExecutionResult forgot to call success()
  • Loading branch information
boggle committed Feb 18, 2016
1 parent 78b0917 commit f282a66
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 38 deletions.
Expand Up @@ -58,8 +58,11 @@ class ProcedureExecutionResult[E <: Exception](context: QueryContext,
signature.mode.call(context, signature, args.map(javaValues.asDeepJavaResultValue)) signature.mode.call(context, signature, args.map(javaValues.asDeepJavaResultValue))


override protected def createInner = new util.Iterator[util.Map[String, Any]]() { override protected def createInner = new util.Iterator[util.Map[String, Any]]() {
override def next(): util.Map[String, Any] = resultAsMap(executionResults.next()) override def next(): util.Map[String, Any] =
override def hasNext: Boolean = executionResults.hasNext try { resultAsMap( executionResults.next( ) ) }
catch { case e: NoSuchElementException => success(); throw e }

override def hasNext: Boolean = if (executionResults.hasNext) true else { success(); false }
} }


override def accept[EX <: Exception](visitor: InternalResultVisitor[EX]) = { override def accept[EX <: Exception](visitor: InternalResultVisitor[EX]) = {
Expand Down
Expand Up @@ -56,7 +56,9 @@
import org.neo4j.kernel.impl.core.StartupStatisticsProvider; import org.neo4j.kernel.impl.core.StartupStatisticsProvider;
import org.neo4j.kernel.impl.core.ThreadToStatementContextBridge; import org.neo4j.kernel.impl.core.ThreadToStatementContextBridge;
import org.neo4j.kernel.impl.core.TokenNotFoundException; import org.neo4j.kernel.impl.core.TokenNotFoundException;
import org.neo4j.kernel.impl.coreapi.CoreAPIAvailabilityGuard;
import org.neo4j.kernel.impl.logging.LogService; import org.neo4j.kernel.impl.logging.LogService;
import org.neo4j.kernel.impl.proc.ProcedureGDSFactory;
import org.neo4j.kernel.impl.proc.Procedures; import org.neo4j.kernel.impl.proc.Procedures;
import org.neo4j.kernel.impl.proc.TypeMappers.SimpleConverter; import org.neo4j.kernel.impl.proc.TypeMappers.SimpleConverter;
import org.neo4j.kernel.impl.query.QueryEngineProvider; import org.neo4j.kernel.impl.query.QueryEngineProvider;
Expand Down Expand Up @@ -154,7 +156,11 @@ public DataSourceModule( final GraphDatabaseFacadeFactory.Dependencies dependenc
logging.getInternalLog( DatabaseHealth.class ) ) ); logging.getInternalLog( DatabaseHealth.class ) ) );


autoIndexing = new InternalAutoIndexing( platformModule.config, editionModule.propertyKeyTokenHolder ); autoIndexing = new InternalAutoIndexing( platformModule.config, editionModule.propertyKeyTokenHolder );
Procedures procedures = setupProcedures( platformModule );

AtomicReference<QueryExecutionEngine> queryExecutor = new AtomicReference<>( QueryEngineProvider.noEngine() );
this.queryExecutor = queryExecutor::get;
Procedures procedures = setupProcedures( platformModule, editionModule.coreAPIAvailabilityGuard );


neoStoreDataSource = deps.satisfyDependency( new NeoStoreDataSource( storeDir, config, neoStoreDataSource = deps.satisfyDependency( new NeoStoreDataSource( storeDir, config,
editionModule.idGeneratorFactory, logging, platformModule.jobScheduler, editionModule.idGeneratorFactory, logging, platformModule.jobScheduler,
Expand Down Expand Up @@ -182,9 +188,6 @@ public DataSourceModule( final GraphDatabaseFacadeFactory.Dependencies dependenc
// Kernel event handlers should be the very last, i.e. very first to receive shutdown events // Kernel event handlers should be the very last, i.e. very first to receive shutdown events
life.add( kernelEventHandlers ); life.add( kernelEventHandlers );


final AtomicReference<QueryExecutionEngine> queryExecutor = new AtomicReference<>( QueryEngineProvider
.noEngine() );

dataSourceManager.addListener( new DataSourceManager.Listener() dataSourceManager.addListener( new DataSourceManager.Listener()
{ {
private QueryExecutionEngine engine; private QueryExecutionEngine engine;
Expand Down Expand Up @@ -212,7 +215,6 @@ public void unregistered( NeoStoreDataSource dataSource )


this.storeId = neoStoreDataSource::getStoreId; this.storeId = neoStoreDataSource::getStoreId;
this.kernelAPI = neoStoreDataSource::getKernel; this.kernelAPI = neoStoreDataSource::getKernel;
this.queryExecutor = queryExecutor::get;
} }


protected RelationshipProxy.RelationshipActions createRelationshipActions( protected RelationshipProxy.RelationshipActions createRelationshipActions(
Expand Down Expand Up @@ -308,7 +310,7 @@ public Relationship newRelationshipProxy( long id, long startNodeId, int typeId,
}; };
} }


private Procedures setupProcedures( PlatformModule platform ) private Procedures setupProcedures( PlatformModule platform, CoreAPIAvailabilityGuard coreAPIAvailabilityGuard )
{ {
File pluginDir = platform.config.get( GraphDatabaseSettings.plugin_dir ); File pluginDir = platform.config.get( GraphDatabaseSettings.plugin_dir );
Log internalLog = platform.logging.getInternalLog( Procedures.class ); Log internalLog = platform.logging.getInternalLog( Procedures.class );
Expand All @@ -321,14 +323,14 @@ private Procedures setupProcedures( PlatformModule platform )
procedures.registerType( Relationship.class, new SimpleConverter( NTRelationship, Relationship.class ) ); procedures.registerType( Relationship.class, new SimpleConverter( NTRelationship, Relationship.class ) );
procedures.registerType( Path.class, new SimpleConverter( NTPath, Path.class ) ); procedures.registerType( Path.class, new SimpleConverter( NTPath, Path.class ) );


// Register injectables // Register injected public API components
procedures.registerComponent( GraphDatabaseService.class,
(ctx) -> platform.dependencies.resolveDependency( GraphDatabaseService.class ) );

Log proceduresLog = platform.logging.getUserLog( Procedures.class ); Log proceduresLog = platform.logging.getUserLog( Procedures.class );
procedures.registerComponent( Log.class, (ctx) -> proceduresLog ); procedures.registerComponent( Log.class, (ctx) -> proceduresLog );


// Not public API, but useful to have available in procedures to access the kernel etc. // Register injected private API components: useful to have available in procedures to access the kernel etc.
ProcedureGDSFactory gdsFactory = new ProcedureGDSFactory( platform.config, platform.storeDir, platform.dependencies, storeId, this.queryExecutor, coreAPIAvailabilityGuard, platform.urlAccessRule );
procedures.registerComponent( GraphDatabaseService.class, gdsFactory::apply );

procedures.registerComponent( DependencyResolver.class, (ctx) -> platform.dependencies ); procedures.registerComponent( DependencyResolver.class, (ctx) -> platform.dependencies );


return procedures; return procedures;
Expand Down
Expand Up @@ -21,33 +21,34 @@


import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Function;


import org.neo4j.function.ThrowingFunction;
import org.neo4j.kernel.api.exceptions.ProcedureException;
import org.neo4j.kernel.api.proc.CallableProcedure; import org.neo4j.kernel.api.proc.CallableProcedure;


/** /**
* Tracks components that can be injected into user-defined procedures. * Tracks components that can be injected into user-defined procedures.
*/ */
public class ComponentRegistry public class ComponentRegistry
{ {
private final Map<Class<?>, Function<CallableProcedure.Context, ?>> suppliers; private final Map<Class<?>, ThrowingFunction<CallableProcedure.Context, ?,ProcedureException>> suppliers;


public ComponentRegistry() public ComponentRegistry()
{ {
this( new HashMap<>() ); this( new HashMap<>() );
} }


public ComponentRegistry( Map<Class<?>,Function<CallableProcedure.Context,?>> suppliers ) public ComponentRegistry( Map<Class<?>,ThrowingFunction<CallableProcedure.Context, ?,ProcedureException>> suppliers )
{ {
this.suppliers = suppliers; this.suppliers = suppliers;
} }


public Function<CallableProcedure.Context,?> supplierFor( Class<?> type ) public ThrowingFunction<CallableProcedure.Context, ?,ProcedureException> supplierFor( Class<?> type )
{ {
return suppliers.get( type ); return suppliers.get( type );
} }


public <T> void register( Class<T> cls, Function<CallableProcedure.Context,T> supplier ) public <T> void register( Class<T> cls, ThrowingFunction<CallableProcedure.Context, ?,ProcedureException> supplier )
{ {
suppliers.put( cls, supplier ); suppliers.put( cls, supplier );
} }
Expand Down
Expand Up @@ -25,8 +25,8 @@
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.function.Function;


import org.neo4j.function.ThrowingFunction;
import org.neo4j.kernel.api.exceptions.ProcedureException; import org.neo4j.kernel.api.exceptions.ProcedureException;
import org.neo4j.kernel.api.exceptions.Status; import org.neo4j.kernel.api.exceptions.Status;
import org.neo4j.kernel.api.proc.CallableProcedure; import org.neo4j.kernel.api.proc.CallableProcedure;
Expand All @@ -51,9 +51,9 @@ public static class FieldSetter
{ {
private final Field field; private final Field field;
private final MethodHandle setter; private final MethodHandle setter;
private final Function<CallableProcedure.Context,?> supplier; private final ThrowingFunction<CallableProcedure.Context, ?, ProcedureException> supplier;


public FieldSetter( Field field, MethodHandle setter, Function<CallableProcedure.Context,?> supplier ) public FieldSetter( Field field, MethodHandle setter, ThrowingFunction<CallableProcedure.Context, ?,ProcedureException> supplier )
{ {
this.field = field; this.field = field;
this.setter = setter; this.setter = setter;
Expand Down Expand Up @@ -115,7 +115,7 @@ private FieldSetter createInjector( Class<?> cls, Field field ) throws Procedure
{ {
try try
{ {
Function<CallableProcedure.Context,?> supplier = components.supplierFor( field.getType() ); ThrowingFunction<CallableProcedure.Context, ?,ProcedureException> supplier = components.supplierFor( field.getType() );
if( supplier == null ) if( supplier == null )
{ {
throw new ProcedureException( Status.Procedure.FailedRegistration, throw new ProcedureException( Status.Procedure.FailedRegistration,
Expand Down
Expand Up @@ -21,9 +21,9 @@


import java.io.File; import java.io.File;
import java.util.Set; import java.util.Set;
import java.util.function.Function;


import org.neo4j.collection.RawIterator; import org.neo4j.collection.RawIterator;
import org.neo4j.function.ThrowingFunction;
import org.neo4j.kernel.api.exceptions.KernelException; import org.neo4j.kernel.api.exceptions.KernelException;
import org.neo4j.kernel.api.exceptions.ProcedureException; import org.neo4j.kernel.api.exceptions.ProcedureException;
import org.neo4j.kernel.api.proc.CallableProcedure; import org.neo4j.kernel.api.proc.CallableProcedure;
Expand Down Expand Up @@ -95,7 +95,7 @@ public void registerType( Class<?> javaClass, TypeMappers.NeoValueConverter toNe
* @param cls the type of component to be registered (this is what users 'ask' for in their field declaration) * @param cls the type of component to be registered (this is what users 'ask' for in their field declaration)
* @param supplier a function that supplies the actual component, given the context of a procedure invocation * @param supplier a function that supplies the actual component, given the context of a procedure invocation
*/ */
public <T> void registerComponent( Class<T> cls, Function<CallableProcedure.Context, T> supplier ) public <T> void registerComponent( Class<T> cls, ThrowingFunction<CallableProcedure.Context, ?,ProcedureException> supplier )
{ {
components.register( cls, supplier ); components.register( cls, supplier );
} }
Expand Down
76 changes: 62 additions & 14 deletions integrationtests/src/test/java/org/neo4j/procedure/ProcedureIT.java
Expand Up @@ -19,36 +19,45 @@
*/ */
package org.neo4j.procedure; package org.neo4j.procedure;


import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.QueryExecutionException;
import org.neo4j.graphdb.Result;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.kernel.impl.proc.JarBuilder; import org.neo4j.kernel.impl.proc.JarBuilder;
import org.neo4j.kernel.impl.proc.Procedures; import org.neo4j.kernel.impl.proc.Procedures;
import org.neo4j.logging.AssertableLogProvider; import org.neo4j.logging.AssertableLogProvider;
import org.neo4j.logging.Log; import org.neo4j.logging.Log;
import org.neo4j.test.TestGraphDatabaseFactory; import org.neo4j.test.TestGraphDatabaseFactory;


import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static java.lang.System.lineSeparator; import static java.lang.System.lineSeparator;
import static java.util.Spliterator.IMMUTABLE; import static java.util.Spliterator.IMMUTABLE;
import static java.util.Spliterator.ORDERED; import static java.util.Spliterator.ORDERED;
import static java.util.Spliterators.spliteratorUnknownSize; import static java.util.Spliterators.spliteratorUnknownSize;
import static java.util.stream.StreamSupport.stream; import static java.util.stream.StreamSupport.stream;

import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;

import static org.neo4j.graphdb.Label.label; import static org.neo4j.graphdb.Label.label;
import static org.neo4j.helpers.collection.IteratorUtil.single;
import static org.neo4j.helpers.collection.MapUtil.map; import static org.neo4j.helpers.collection.MapUtil.map;
import static org.neo4j.logging.AssertableLogProvider.inLog; import static org.neo4j.logging.AssertableLogProvider.inLog;


Expand Down Expand Up @@ -260,7 +269,7 @@ public void shouldLogLikeThereIsNoTomorrow() throws Throwable
} }


@Test @Test
public void readOnlyProcedureCannotWrite() throws Throwable public void shouldDenyReadOnlyProcedureToPerformWrites() throws Throwable
{ {
// Expect // Expect
exception.expect( QueryExecutionException.class ); exception.expect( QueryExecutionException.class );
Expand All @@ -274,13 +283,12 @@ public void readOnlyProcedureCannotWrite() throws Throwable
} }


@Test @Test
public void procedureMarkedForWritingCanWrite() throws Throwable public void shouldAllowWriteProcedureToPerformWrites() throws Throwable
{ {
// When // When
try ( Transaction tx = db.beginTx() ) try ( Transaction tx = db.beginTx() )
{ {
// TODO: #hasNext should not be needed here, result of writes should be eagerized db.execute( "CALL org.neo4j.procedure.writingProcedure()" );
db.execute( "CALL org.neo4j.procedure.writingProcedure()" ).hasNext();
tx.success(); tx.success();
} }


Expand All @@ -307,13 +315,12 @@ public void shouldNotBeAbleToCallWriteProcedureThroughReadProcedure() throws Thr
} }


@Test @Test
public void procedureMarkedForWritingCanCallOtherWritingProcedure() throws Throwable public void shouldBeAbleToCallWriteProcedureThroughWriteProcedure() throws Throwable
{ {
// When // When
try ( Transaction tx = db.beginTx() ) try ( Transaction tx = db.beginTx() )
{ {
// TODO: #hasNext should not be needed here, result of writes should be eagerized db.execute( "CALL org.neo4j.procedure.writeProcedureCallingWriteProcedure()" );
db.execute( "CALL org.neo4j.procedure.writeProcedureCallingWriteProcedure()" ).hasNext();
tx.success(); tx.success();
} }


Expand Down Expand Up @@ -405,6 +412,42 @@ public void shouldBeAbleToCallDelegatingVoidProcedure() throws Throwable
} }
} }


@Test
public void shouldBeAbleToPerformWritesOnNodesReturnedFromReadOnlyProcedure() throws Throwable
{
// When
try (Transaction tx = db.beginTx())
{
long nodeId = db.createNode().getId();
Node node = single( db.execute( "CALL org.neo4j.procedure.node", map( "id", nodeId ) ).columnAs( "node" ) );
node.setProperty( "name", "Stefan" );
tx.success();
};
}

@Test
public void shouldFailToShutdown()
{
// Expect
exception.expect( QueryExecutionException.class );
exception.expectMessage( "Failed to invoke procedure `org.neo4j.procedure.shutdown`: `java.lang.UnsupportedOperationException` was thrown when invoking the procedure." );

try ( Transaction ignore = db.beginTx() )
{
db.execute( "CALL org.neo4j.procedure.shutdown()" );
}
}

@Test
public void shouldBeAbleToWriteAfterCallingReadOnlyProcedure()
{
try ( Transaction ignore = db.beginTx() )
{
db.execute( "CALL org.neo4j.procedure.simpleArgument(12)" );
db.createNode();
}
}

@Before @Before
public void setUp() throws IOException public void setUp() throws IOException
{ {
Expand Down Expand Up @@ -638,6 +681,11 @@ public void sideEffect( @Name( "value" ) String value )
db.createNode( Label.label( value ) ); db.createNode( Label.label( value ) );
} }


@Procedure
public void shutdown() {
db.shutdown();
}

@Procedure @Procedure
@PerformsWrites @PerformsWrites
public void delegatingSideEffect( @Name( "value" ) String value ) public void delegatingSideEffect( @Name( "value" ) String value )
Expand Down

0 comments on commit f282a66

Please sign in to comment.