Spring Command Line application doesn't terminate after Neo4j query

Hi, I added Neo4j dependencies to an existing Spring Boot command line application — that is already connecting to a PostgreSQL database.

The issue is that after Neo4j connectivity addition, the application no longer exits after its main() method finishes.

I've posted this on StackOverflow and received a suggestion to inspect the threads. In fact I saw that if no query is issued to the database, everything is fine and the application exits.

But if a query is executed, there are a bunch of Neo4j threads that stay active even though the main() thread is finished.

I managed to replicate it in a smaller project, find the sources at the bottom (you'll need to provide database uri and credentials); just change the variable runQuery to true or false to run or skip the query.

I'm using Spring Boot 2.3.9 with neo4j-java-driver-spring-boot-starter 4.2.4.0


/boot-neo4j-test/src/main/java/my/test/DatabaseGraphService.java

package my.test;

import javax.annotation.PreDestroy;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.neo4j.driver.Driver;
import org.springframework.stereotype.Service;

@Service
public class DatabaseGraphService {
	
	private final Driver driver;
	
	private final Logger LOG;
	
	public DatabaseGraphService(Driver driver) {
		this.driver = driver;
		LOG = LogManager.getLogger(getClass());
	}
	
	@PreDestroy
	public void close() throws Exception {
		LOG.info("Shutting down Neo4j driver...");
	}
	
	public Driver getDriver() {
		return driver;
	}
	
}

/boot-neo4j-test/src/main/java/my/test/SpringBootConsoleApplication.java

package my.test;

import java.util.ArrayList;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.neo4j.driver.Record;
import org.neo4j.driver.Result;
import org.neo4j.driver.Session;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.TransactionWork;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = { "my.test" })
public class SpringBootConsoleApplication implements CommandLineRunner {
	
	@Autowired
	private DatabaseGraphService graphService;
	
	private static Logger LOG = LogManager.getLogger(SpringBootConsoleApplication.class);
	
	public static void main(String[] args) {
		LOG.info("STARTING THE APPLICATION");
		
		SpringApplication.run(SpringBootConsoleApplication.class, args);
		LOG.info("APPLICATION FINISHED");
	}
	
	@Override
	public void run(String... args) {
		LOG.info("EXECUTING : command line runner");
		
		final boolean runQuery = true;
		
		// wait a little to allow opening the process in VisualVM or similar
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			LOG.error(e.getMessage(), e);
		}
		
		try {
			List<String> names = null;
			
			if (runQuery) {
				try (Session session = graphService.getDriver().session()) {
					
					names = session.readTransaction(new TransactionWork<List<String>>() {
						@Override
						public List<String> execute(Transaction tx) {
							List<String> names = new ArrayList<>();
							
							Result result = tx.run("MATCH (a) RETURN a.name");
							while (result.hasNext()) {
								Record record = result.next();
								names.add(record.get("name").asString());
							}
							
							return names;
						}
					});
				}
			}
			
			LOG.info("Names found: " + (names == null ? 0 : names.size()));
		} catch (Exception e) {
			LOG.error(e.getMessage(), e);
		}
	}
	
}

/boot-neo4j-test/src/main/resources/application.properties

spring.main.web-application-type=NONE

org.neo4j.driver.uri=
org.neo4j.driver.authentication.username=
org.neo4j.driver.authentication.password=

/boot-neo4j-test/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>my.test</groupId>
    <artifactId>boot-neo4j-test</artifactId>
    <packaging>jar</packaging>
    <version>0.0.1-SNAPSHOT</version>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath></relativePath>
    </parent>
    
  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
     
    <dependencies>
    
    
    	<!-- Local -->
    	  
      	<!-- Spring Boot -->
      	
      	<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter</artifactId>
		</dependency>
			
		<dependency>
  			<groupId>org.springframework.boot</groupId>
  			<artifactId>spring-boot-starter-log4j2</artifactId>
		</dependency>
		
		<dependency>
			<groupId>org.neo4j.driver</groupId>
			<artifactId>neo4j-java-driver-spring-boot-starter</artifactId>
			<version>4.2.4.0</version>
		</dependency>
		
		<!--  other -->
		
		<dependency>
		    <groupId>org.apache.commons</groupId>
		    <artifactId>commons-lang3</artifactId>
		</dependency>
		
    </dependencies>
    
</project>

I think Netty keeps an active thread pool for the driver.
You need to close the driver before your application exits.

Hi florent and thank you.

Though, I don't completely agree with you for the following opinions / questions (that ultimately led me to open an issue on Github):

  1. the connection to Neo4j is not explicitly made by my program, but comes autoconfigured (can't tell if by Spring or Neo4j-driver), then why should my program be responsible of its teardown?
  2. given that the connection setup happens beside my code, I may factor out and hide several database-related objects even in a separate jar / dependency, but then for Neo4j I would have to expose the driver shutdown functionality and remember to close the connection
  3. I guess main() conclusion may not really be program termination: some threads may have been spawned and still have instructions to execute before the program is finally completed, so having to explicitly close the driver feels a bit error-prone to me
  4. let me point you to one of the last comments on my question on SO, where it is stated that a similar issue was in place with another database and that it may be responsibility of Spring or Neo4j to handle it
  5. I don't want to start a comparison about which one is better of course, but if another driver such as Hikari (being autoconfigured too) is closing itself at program termination why shouldn't the same happen for Neo4j driver?

You can disable the auto-configuration and explicitly manage the driver instantiation yourself, so that it is not instantiated when you do not need it.

Whether you rely on auto-configuration or not, the Driver lifetime management is the responsibility of your application.