Skip to content

Conversation

@arturmkr
Copy link

@arturmkr arturmkr commented Nov 4, 2025

Summary

This PR introduces optional JDBC connection pooling using HikariCP in JdbcConnectionManager, solves issue #377

Details

  • Added an optional field maxPoolSize to DataStoreConfiguration.
  • When maxPoolSize is configured (non-null and > 0):
    • JdbcConnectionManager uses a Hikari connection pool for routing-group database connections.
  • When maxPoolSize is not set (null):
    • Connections are created directly (no pooling).

Tests

Added TestJdbcConnectionManagerPool:

  1. blocksWhenExceedingMaxPoolSize
    • Verifies that when a pool is enabled, new connections block after the pool limit.
  2. doesNotBlockWhenMaxPoolSizeIsNull
    • Verifies that when the pool is disabled, connections are created directly and do not block.

Why

This allows controlling connection usage and improving DB efficiency under load, while keeping the old behavior as the default (no pooling).

Summary by Sourcery

Introduce optional JDBC connection pooling using HikariCP controlled by a new maxPoolSize setting, while preserving default direct connections, and add tests to verify pooling behavior

New Features:

  • Enable JDBC connection pooling via HikariCP when maxPoolSize > 0

Enhancements:

  • Add maxPoolSize field with constructors, getter, and setter to DataStoreConfiguration
  • Update JdbcConnectionManager#getJdbi to use a HikariCP DataSource when pooling is enabled

Build:

  • Add HikariCP dependency to the project

Tests:

  • Introduce TestJdbcConnectionManagerPool with tests for pool blocking and no-pool scenarios

@cla-bot cla-bot bot added the cla-signed label Nov 4, 2025
@sourcery-ai
Copy link

sourcery-ai bot commented Nov 4, 2025

Reviewer's Guide

Introduces optional JDBC connection pooling with HikariCP based on a new maxPoolSize setting, updating configuration and connection manager logic, adding the required dependency, and covering behavior with new tests.

Sequence diagram for connection creation with and without pooling

sequenceDiagram
participant "Client"
participant "JdbcConnectionManager"
participant "HikariCP Pool"
participant "Database"

alt Pooling enabled (maxPoolSize > 0)
  "Client"->>"JdbcConnectionManager": getJdbi()
  "JdbcConnectionManager"->>"HikariCP Pool": request connection
  "HikariCP Pool"->>"Database": get connection
  "HikariCP Pool"-->>"JdbcConnectionManager": pooled connection
  "JdbcConnectionManager"-->>"Client": Jdbi instance
else Pooling disabled (maxPoolSize is null)
  "Client"->>"JdbcConnectionManager": getJdbi()
  "JdbcConnectionManager"->>"Database": create connection
  "Database"-->>"JdbcConnectionManager": connection
  "JdbcConnectionManager"-->>"Client": Jdbi instance
end
Loading

ER diagram for DataStoreConfiguration with new maxPoolSize field

erDiagram
  DATASTORECONFIGURATION {
    String jdbcUrl
    String user
    String password
    String driver
    Integer queryHistoryHoursRetention
    boolean runMigrationsEnabled
    Integer maxPoolSize
  }
Loading

Class diagram for updated DataStoreConfiguration and JdbcConnectionManager

classDiagram
class DataStoreConfiguration {
  -String jdbcUrl
  -String user
  -String password
  -String driver
  -Integer queryHistoryHoursRetention
  -boolean runMigrationsEnabled
  -Integer maxPoolSize
  +DataStoreConfiguration(jdbcUrl, user, password, driver, queryHistoryHoursRetention, runMigrationsEnabled, maxPoolSize)
  +DataStoreConfiguration(jdbcUrl, user, password, driver, queryHistoryHoursRetention, runMigrationsEnabled)
  +getMaxPoolSize()
  +setMaxPoolSize(maxPoolSize)
}

class JdbcConnectionManager {
  +Jdbi getJdbi(routingGroupDatabase)
}

DataStoreConfiguration <.. JdbcConnectionManager : uses
Loading

File-Level Changes

Change Details Files
Add maxPoolSize field to DataStoreConfiguration
  • Introduce Integer maxPoolSize property
  • Extend constructor to accept maxPoolSize and provide an overloaded default constructor
  • Implement getter and setter for maxPoolSize
DataStoreConfiguration.java
Enable conditional HikariCP pooling in JdbcConnectionManager
  • Check if maxPoolSize is non-null and >0 to trigger pooling
  • Configure HikariConfig with JDBC URL, credentials, driver, and maximum pool size
  • Create Jdbi instance from HikariDataSource when pooling is enabled
  • Retain direct Jdbi.create path when maxPoolSize is unset
JdbcConnectionManager.java
Add HikariCP dependency to project
  • Include com.zaxxer:HikariCP:7.0.2 as a runtime dependency in pom.xml
pom.xml
Implement tests for pooled and non-pooled behaviors
  • Verify blocking behavior when pool limit is reached
  • Verify non-blocking behavior when pooling is disabled
  • Assert session counts to confirm physical connections in no-pool mode
TestJdbcConnectionManagerPool.java

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Rather than creating a new HikariDataSource on every getJdbi() call, initialize and cache a single pooled DataSource per routing group so you don’t spawn multiple pools and can manage its lifecycle more cleanly.
  • Add a shutdown or close hook for the HikariDataSource instances to ensure the connection pool is properly terminated and avoids resource leaks when the application stops.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Rather than creating a new HikariDataSource on every getJdbi() call, initialize and cache a single pooled DataSource per routing group so you don’t spawn multiple pools and can manage its lifecycle more cleanly.
- Add a shutdown or close hook for the HikariDataSource instances to ensure the connection pool is properly terminated and avoids resource leaks when the application stops.

## Individual Comments

### Comment 1
<location> `gateway-ha/src/main/java/io/trino/gateway/ha/persistence/JdbcConnectionManager.java:66-72` </location>
<code_context>

+        if (configuration.getMaxPoolSize() != null && configuration.getMaxPoolSize() > 0) {
+            HikariConfig hikariConfig = new HikariConfig();
+            hikariConfig.setJdbcUrl(buildJdbcUrl(routingGroupDatabase));
+            hikariConfig.setUsername(configuration.getUser());
+            hikariConfig.setPassword(configuration.getPassword());
+            if (configuration.getDriver() != null) {
+                hikariConfig.setDriverClassName(configuration.getDriver());
+            }
+            hikariConfig.setMaximumPoolSize(configuration.getMaxPoolSize());
+            return Jdbi.create(new HikariDataSource(hikariConfig))
+                    .installPlugin(new SqlObjectPlugin())
</code_context>

<issue_to_address>
**suggestion (performance):** HikariConfig is instantiated for every call; consider reusing or pooling.

Repeatedly creating HikariConfig and HikariDataSource instances can waste resources and fragment connection pools. Reuse the HikariDataSource or manage its lifecycle to improve efficiency.

Suggested implementation:

```java
        if (configuration.getMaxPoolSize() != null && configuration.getMaxPoolSize() > 0) {
            return Jdbi.create(getOrCreateHikariDataSource(routingGroupDatabase, configuration))
                    .installPlugin(new SqlObjectPlugin())
                    .registerRowMapper(new RecordAndAnnotatedConstructorMapper());
        }

```

```java
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import io.airlift.log.Logger;
import io.trino.gateway.ha.config.DataStoreConfiguration;
import io.trino.gateway.ha.persistence.dao.QueryHistoryDao;
import java.util.concurrent.atomic.AtomicReference;

    private final AtomicReference<HikariDataSource> hikariDataSourceRef = new AtomicReference<>();

    private HikariDataSource getOrCreateHikariDataSource(String routingGroupDatabase, DataStoreConfiguration configuration) {
        HikariDataSource existing = hikariDataSourceRef.get();
        if (existing != null && !existing.isClosed()) {
            return existing;
        }
        synchronized (hikariDataSourceRef) {
            existing = hikariDataSourceRef.get();
            if (existing != null && !existing.isClosed()) {
                return existing;
            }
            HikariConfig hikariConfig = new HikariConfig();
            hikariConfig.setJdbcUrl(buildJdbcUrl(routingGroupDatabase));
            hikariConfig.setUsername(configuration.getUser());
            hikariConfig.setPassword(configuration.getPassword());
            if (configuration.getDriver() != null) {
                hikariConfig.setDriverClassName(configuration.getDriver());
            }
            hikariConfig.setMaximumPoolSize(configuration.getMaxPoolSize());
            HikariDataSource newDataSource = new HikariDataSource(hikariConfig);
            hikariDataSourceRef.set(newDataSource);
            return newDataSource;
        }
    }

    // Optionally, add a close method to clean up the datasource
    public void close() {
        HikariDataSource ds = hikariDataSourceRef.getAndSet(null);
        if (ds != null && !ds.isClosed()) {
            ds.close();
        }
    }
            return jdbi;
        }

```

- You may need to ensure thread safety and proper lifecycle management for the `HikariDataSource` depending on how `JdbcConnectionManager` is used.
- If the manager is used for multiple databases/configurations, you may need to use a map keyed by database/configuration instead of a single reference.
- Make sure to call the `close()` method when the manager is shut down to release resources.
- If this is a singleton or managed bean, consider integrating with your application's lifecycle management.
</issue_to_address>

### Comment 2
<location> `gateway-ha/src/test/java/io/trino/gateway/ha/persistence/TestJdbcConnectionManagerPool.java:15` </location>
<code_context>
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+final class TestJdbcConnectionManagerPool {
+    @Test
+    void blocksWhenExceedingMaxPoolSize() throws Exception {
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for invalid maxPoolSize values (e.g., zero, negative).

Tests only cover null and positive maxPoolSize. Please add cases for zero and negative values to ensure correct fallback or error handling.

Suggested implementation:

```java
final class TestJdbcConnectionManagerPool {
    @Test
    void blocksWhenExceedingMaxPoolSize() throws Exception {
        String dbPath = Path.of(System.getProperty("java.io.tmpdir"), "h2db-pool-" + System.currentTimeMillis()).toString();
        String jdbcUrl = "jdbc:h2:" + dbPath;

        DataStoreConfiguration cfg = new DataStoreConfiguration(
                jdbcUrl, "sa", "sa", "org.h2.Driver",
                4, true,
                2
        );

        JdbcConnectionManager cm = new JdbcConnectionManager(Jdbi.create(jdbcUrl, "sa", "sa"), cfg);
    }

    @Test
    void handlesZeroMaxPoolSize() {
        String dbPath = Path.of(System.getProperty("java.io.tmpdir"), "h2db-pool-zero-" + System.currentTimeMillis()).toString();
        String jdbcUrl = "jdbc:h2:" + dbPath;

        DataStoreConfiguration cfg = new DataStoreConfiguration(
                jdbcUrl, "sa", "sa", "org.h2.Driver",
                4, true,
                0
        );

        try {
            JdbcConnectionManager cm = new JdbcConnectionManager(Jdbi.create(jdbcUrl, "sa", "sa"), cfg);
            // Depending on implementation, either fallback to default or throw
            // If fallback, check pool size is not zero
            assertThat(cm.getMaxPoolSize()).isGreaterThan(0);
        } catch (Exception e) {
            // If exception is expected, assert it is thrown
            assertThat(e).isInstanceOfAny(IllegalArgumentException.class, RuntimeException.class);
        }
    }

    @Test
    void handlesNegativeMaxPoolSize() {
        String dbPath = Path.of(System.getProperty("java.io.tmpdir"), "h2db-pool-negative-" + System.currentTimeMillis()).toString();
        String jdbcUrl = "jdbc:h2:" + dbPath;

        DataStoreConfiguration cfg = new DataStoreConfiguration(
                jdbcUrl, "sa", "sa", "org.h2.Driver",
                4, true,
                -5
        );

        try {
            JdbcConnectionManager cm = new JdbcConnectionManager(Jdbi.create(jdbcUrl, "sa", "sa"), cfg);
            // Depending on implementation, either fallback to default or throw
            // If fallback, check pool size is not negative
            assertThat(cm.getMaxPoolSize()).isGreaterThan(0);
        } catch (Exception e) {
            // If exception is expected, assert it is thrown
            assertThat(e).isInstanceOfAny(IllegalArgumentException.class, RuntimeException.class);
        }
    }

```

- If `JdbcConnectionManager` does not have a `getMaxPoolSize()` method, you should add it or otherwise access the pool size for assertion.
- Adjust the exception types in the assertions to match the actual exceptions thrown by your implementation.
- If your implementation logs or handles these cases differently, update the assertions accordingly.
</issue_to_address>

### Comment 3
<location> `gateway-ha/src/test/java/io/trino/gateway/ha/persistence/TestJdbcConnectionManagerPool.java:84` </location>
<code_context>
+
+
+    @Test
+    void doesNotBlockWhenMaxPoolSizeIsNull() throws Exception {
+        String dbPath = Path.of(System.getProperty("java.io.tmpdir"), "h2db-nopool-" + System.currentTimeMillis()).toString();
+        String jdbcUrl = "jdbc:h2:" + dbPath;
</code_context>

<issue_to_address>
**suggestion (testing):** Consider testing with a very high maxPoolSize to ensure no artificial bottleneck.

Add a test with maxPoolSize set to a high value (e.g., 1000) to verify that connections remain non-blocking under high concurrency.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@arturmkr arturmkr force-pushed the feature/add-db-connection-pool-support branch from e52be50 to 5ac762c Compare November 4, 2025 10:32
Copy link
Contributor

@rdsarvar rdsarvar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nitpick on configurability but open to @mosabua et al opinions

private String driver;
private Integer queryHistoryHoursRetention = 4;
private boolean runMigrationsEnabled = true;
private Integer maxPoolSize;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: IMO if we're adding HikariCP we should be giving the users complete flexibility to all of it's settings and not just max pool size:

Example of what I mean from previously unpushed code I had laying around:

+    // HikariCP connection pool settings
+    private int maximumPoolSize = 10;
+    private int minimumIdle = 5;
+    private long connectionTimeout = 30000; // 30 seconds
+    private long idleTimeout = 600000; // 10 minutes
+    private long maxLifetime = 1800000; // 30 minutes
+    private long leakDetectionThreshold = 60000; // 1 minute
+    private boolean autoCommit = true;
+    private String connectionTestQuery;

For the above to work with your logic max pool size can default to 0

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I think exposing all HikariCP parameters would add unnecessary complexity for this use case as Hikari’s default settings are already well-optimized and sufficient.
For now I kept it minimal with maxPoolSize, since it’s the only setting that typically needs adjustment.

});
}

public void close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is this only used by tests? I don't see where it's being called otherwise

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@arturmkr arturmkr requested a review from mosabua November 5, 2025 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants