package uk.ac.rothamsted.neo4j.utils; import static org.neo4j.driver.v1.Values.parameters; import java.util.function.Consumer; import java.util.function.Function; import org.apache.commons.lang3.ArrayUtils; import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; import org.neo4j.driver.v1.Values; import org.neo4j.driver.v1.exceptions.DatabaseException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.TransientException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.ebi.utils.runcontrol.MultipleAttemptsExecutor; /** * Wrapper to manage access to Cypher and Neo4j. * * @author brandizi * <dl><dt>Date:</dt><dd>26 Feb 2018</dd></dl> * */ public class Neo4jDataManager { private Driver neo4jDriver; private int maxRetries = 10; private Logger log = LoggerFactory.getLogger ( this.getClass () ); public Neo4jDataManager ( Driver neo4jDriver ) { super (); this.neo4jDriver = neo4jDriver; } /** * <p>Runs a Neo4j client session, which is created and given to the action as a parameter. The action can return * a value useful for the invoker of this method.</p> * * <p>Because parallelism sometimes raises exceptions about race conditions, we use {@link MultipleAttemptsExecutor} * to re-attempt the command execution a couple of times, after such exceptions.</p> * */ @SuppressWarnings ( "unchecked" ) public <V> V runSession ( Function<Session, V> action ) { MultipleAttemptsExecutor attempter = new MultipleAttemptsExecutor ( TransientException.class, DatabaseException.class, ServiceUnavailableException.class ); attempter.setMaxAttempts ( this.getMaxRetries () ); attempter.setMinPauseTime ( 30 * 1000 ); attempter.setMaxPauseTime ( 3 * 60 * 1000 ); Object[] result = new Object [ 1 ]; attempter.execute ( () -> { try ( Session session = this.neo4jDriver.session () ) { result [ 0 ] = action.apply ( session ); } }); return (V) result [ 0 ]; } /** * A convenience wrapper of {@link #runSession(Function)} that doesn't force the action executor to return a value, if that's not * expected by the invoker of this method. * */ public void runSessionVoid ( Consumer<Session> action ) { // TODO: Java commons collections 4.x runSession ( session -> { action.accept ( session ); return null; } ); } /** * <p>Runs a Cypher commands against the current {@link #getNeo4jDriver()}.</p> * * <p>The keyVals parameter is passed to {@link Values#parameters(Object...)}.</p> * * <p>The command is wrapped into a single transaction, which is committed within the method. This also * means the command is wrapped in a Neo4j session, using {@link #runSession(Function)} and that * is affected by {@link #getMaxRetries()}.</p> * */ public void runCypher ( String cypher, Object... keyVals ) { if ( log.isTraceEnabled () ) log.trace ( "Cypher: {} params: {}", cypher, ArrayUtils.toString ( keyVals ) ); this.runSession ( session -> session.run ( cypher, parameters ( keyVals ) ) ); } /** * Gets {@link Record} instances from the 'cypher' command and, for each instance, runs the action, which * is supposed to do something with a record. * * keyVals are parameters for the Cypher query. * * <p>The command is wrapped into a single transaction, which is committed within the method. This also * means the command is wrapped in a Neo4j session, using {@link #runSession(Function)} and that * is affected by {@link #getMaxRetries()}.</p> * */ public void processCypherMatches ( Consumer<Record> action, String cypher, Object... keyVals ) { if ( log.isTraceEnabled () ) log.trace ( "Cypher: {} params: {}", cypher, ArrayUtils.toString ( keyVals ) ); this.runSessionVoid ( session -> { StatementResult cursor = session.run ( cypher, parameters ( keyVals ) ); cursor.forEachRemaining ( action ); }); } /** * The driver and target Neo4j destination used to send Cypher elements mapped from RDF. * We don't care about closing this, so the caller has to do it. */ public Driver getNeo4jDriver () { return neo4jDriver; } public void setNeo4jDriver ( Driver neo4jDriver ) { this.neo4jDriver = neo4jDriver; } /** * The methods of this class try Cypher queries multiple times, until they run successfully, or this no. of * attempts is reached. This is useful in case of parallel writing threads/processes, where it might happen * that exceptions due to concurrent access to the database server can be recovered by simply retrying after a * random time. * * Default is 10, must be at least 1. * */ public int getMaxRetries () { return maxRetries; } /** * @throws IllegalArgumentException if it's <1 */ public void setMaxRetries ( int maxRetries ) { if ( maxRetries < 1 ) throw new IllegalArgumentException ( "maxRetries property for Neo4jDataManager must be 1 at least" ); this.maxRetries = maxRetries; } }