Neo4j OGM + Spring config doesn't work

Hi,

When using the Neo4j-OGM dependency for Java, we run into a problem where the amount of threads keeps increasing until the server doesn't have enough CPU power left to sustain the threads. We think we've narrowed down the issue to the amount of connections to the database. On every request (Servlet), a new connection is opened and never closed. Even with the config specifying the connectionLivenessCheckTimeout and connectionPoolSize. Currently this is how theNeo4jService is setup as a singleton. Below is a screenshot attached showing the amount of connections made to the database.

package com.caraer.Caraer.config;

import com.caraer.Caraer.model.User;
import lombok.Getter;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Config;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.ogm.config.Configuration;
import org.neo4j.ogm.config.DatabaseSelection;
import org.neo4j.ogm.config.DatabaseSelectionProvider;
import org.neo4j.ogm.drivers.bolt.driver.BoltDriver;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.concurrent.TimeUnit;

public class Neo4jService {

    private final Session mainDatabaseSession;
    private final Session userDatabaseSession;

    private final SessionFactory mainFactory;
    private final SessionFactory userFactory;

    GenerateUuid generateUuid = new GenerateUuid();

    public Neo4jService() {

        Configuration mainConfiguration = new Configuration.Builder()
                .uri("bolt://localhost:7687")
                .connectionLivenessCheckTimeout(10)
                .connectionPoolSize(50)
                .database("neo4j")
                .credentials("neo4j", "****")
                .build();

        Configuration userConfiguration = new Configuration.Builder()
                .uri("bolt://localhost:7687")
                .connectionLivenessCheckTimeout(10)
                .connectionPoolSize(50)
                .databaseSelectionProvider(new UserDatabase())
                .credentials("neo4j", "****")
                .build();

        this.mainFactory = new SessionFactory(mainConfiguration, "<packages>");
        this.userFactory = new SessionFactory(userConfiguration, "<packages>");

        this.mainFactory.register(generateUuid);
        this.userFactory.register(generateUuid);

        this.mainDatabaseSession = mainFactory.openSession();
        this.userDatabaseSession = userFactory.openSession();
    }

    private static final class InstanceHolder {
        private static final Neo4jService instance = new Neo4jService();
    }

    public static Neo4jService getInstance() {
        return InstanceHolder.instance;
    }

    public Session getMainSession() {
        return InstanceHolder.instance.mainDatabaseSession;
    }

    public Session getUserSession() {
        return InstanceHolder.instance.userDatabaseSession;
    }
}

class UserDatabase implements DatabaseSelectionProvider {
    @Override
    public DatabaseSelection getDatabaseSelection() {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if(user == null) {
            return null;
        } else {
            return DatabaseSelection.select(user.getDatabase());
        }
    }
}

Not sure what is supposed to be set differently and why a new connection is created on every request.

In relation to this post we've also tried this version with the same issue.

package com.caraer.Caraer.config;

import com.caraer.Caraer.model.User;
import jakarta.annotation.PostConstruct;
import org.neo4j.ogm.config.Configuration;
import org.neo4j.ogm.config.DatabaseSelection;
import org.neo4j.ogm.config.DatabaseSelectionProvider;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.security.core.context.SecurityContextHolder;

public class Neo4jService {

    private Session mainDatabaseSession;
    private Session userDatabaseSession;

    private SessionFactory mainFactory;
    private SessionFactory userFactory;

    private GenerateUuid generateUuid;

    // Private constructor to prevent instantiation
    private Neo4jService() {
        // Avoid initialization here
    }

    @PostConstruct
    private void init() {
        generateUuid = new GenerateUuid();

        Configuration mainConfiguration = new Configuration.Builder()
                .uri("bolt://localhost:7687")
                .connectionLivenessCheckTimeout(10)
                .connectionPoolSize(50)
                .database("neo4j")
                .credentials("neo4j", "***")
                .build();

        Configuration userConfiguration = new Configuration.Builder()
                .uri("bolt://localhost:7687")
                .connectionLivenessCheckTimeout(10)
                .connectionPoolSize(50)
                .databaseSelectionProvider(new UserDatabase())
                .credentials("neo4j", "***")
                .build();

        this.mainFactory = new SessionFactory(mainConfiguration, "com.caraer.Caraer");
        this.userFactory = new SessionFactory(userConfiguration, "com.caraer.Caraer");

        this.mainFactory.register(generateUuid);
        this.userFactory.register(generateUuid);

        this.mainDatabaseSession = mainFactory.openSession();
        this.userDatabaseSession = userFactory.openSession();
    }

    // Static inner class for lazy initialization
    private static final class InstanceHolder {
        private static final Neo4jService INSTANCE = new Neo4jService();

        static {
            INSTANCE.init(); // Ensure initialization is done only once
        }
    }

    public static Neo4jService getInstance() {
        return InstanceHolder.INSTANCE;
    }

    public Session getMainSession() {
        return InstanceHolder.INSTANCE.mainDatabaseSession;
    }

    public Session getUserSession() {
        return InstanceHolder.INSTANCE.userDatabaseSession;
    }
}

class UserDatabase implements DatabaseSelectionProvider {
    @Override
    public DatabaseSelection getDatabaseSelection() {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (user == null) {
            return null;
        } else {
            return DatabaseSelection.select(user.getDatabase());
        }
    }
}

Taking the discussion from withEventLoopThreads Config for the driver gets overwritten when setting DatabaseSelectionProvider · Issue #1133 · neo4j/neo4j-ogm · GitHub to here.
How to you measure the amount of connections open?
As we can clearly see in the picture, the reported user-agent is the browser. This means you have to subtract those connections from the ones the Java driver creates.

On a side note: It is not expected that the Java driver closes connections (as in physical close) but it puts them back into the connection pool to be used again.

Thanks for getting back at me. You're right, I included the browser connections. I ran the same test and after a couple of requests this is what happens.

  • The connections go above 100 and don't decrease when idle.
  • The threads keep increasing.
  • The CPU load is relatively high.

See below screenshots for more info.

Config:

I can set up a test project to reproduce if this isn't enough.

Profiler:

Connections:

When putting breakpoints at the exception that gets thrown when creating a new session I get either two of the following:

  • org.neo4j.ogm.session.delegates.ExecuteQueriesDelegate.extractColumnValue(ExecuteQueriesDelegate.java:236) -> java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
  • Netty: failed to create a child event loop -> failed to open a new selector

It seems like all these issues are caused by the increase in thread activity and the number of open files. When upgrading the server, it takes longer before these exceptions kick in, but infinitely upgrading is impossible.

This would be awesome. We tried already to come up with something that might reproduce your observations but for the threading part we couldn't see any problem on our side.

Is Neo4j-OGM really necessary for the reproducer or could it get boiled down to the Java driver? Trying to reduce the surface where we need to inspect the problem.

Did you find a solution to your problem?
I am asking because you accepted my comment as the solution.

Hi Gerrit,

I discovered however that the mistake was somewhere deeper in our own source-code. We've managed to solve it and are now getting response times as expected. Thanks a lot for your help.

Thanks for the update :bowing_man:
If anything else occurs, you know where to find us ;)