Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
public class ConnectionPoolConfig {
@NonNull @Nonnegative @Builder.Default Integer maxConnections = 16;

// max idle connections as a percentage of max connections; -1 means pin to maxConnections
@NonNull @Builder.Default Integer maxIdlePercent = -1;

// min idle connections as a percentage of max connections; -1 means pin to maxConnections
@NonNull @Builder.Default Integer minIdlePercent = -1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

why would you keep idle connections equal to max connections?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's the default, in case nothing's specified.

Copy link
Copy Markdown
Contributor Author

@suddendust suddendust Apr 28, 2026

Choose a reason for hiding this comment

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

Did some cusory digging on this, static pools seem to be the preferred way almost everywhere:

  1. Oracle DB: https://docs.oracle.com/en/database/oracle/oracle-database/26/adfns/connection_strategies.html#GUID-82AE88EA-B3C8-4416-99AF-7342FE700BD5

  2. Hikari: https://github.com/brettwooldridge/hikaricp

" However, for maximum performance and responsiveness to spike demands, we recommend not setting this value and instead allowing HikariCP to act as a fixed size connection pool. Default: same as maximumPoolSize"

  1. Our own tests show it's much better than dynamic resizing. Here's a comparison of mergeAndUpsert

10 connections, static size:

{
  "Run ID": "k6-1777399865",
  "Strategy": "spike",
  "Total requests": 306289,
  "Successful": 306289,
  "Error rate": "0.00%",
  "Avg (ms)": "40.71",
  "p50 (ms)": "30.18",
  "p90 (ms)": "77.84",
  "p95 (ms)": "96.99",
  "p99 (ms)": "N/A",
  "Max (ms)": "1993.25",
  "Max VUs": 300
}
Total throughput sustained: 120k RPM

10 connections, minIdle = 10%, maxIdle = 20% (existing values):

{
  "Run ID": "k6-1777370356",
  "Strategy": "spike",
  "Total requests": 186585,
  "Successful": 186585,
  "Error rate": "0.00%",
  "Avg (ms)": "134.81",
  "p50 (ms)": "63.80",
  "p90 (ms)": "330.89",
  "p95 (ms)": "477.42",
  "p99 (ms)": "N/A",
  "Max (ms)": "5901.84",
  "Max VUs": 300
}

Total throughput sustained: 61k RPM

Also, anyway this is just a configuration change and users can still set whatever they desire. Just that the fallback pins everything, which seems like the way to go..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As discussed, the defaults are now 10% and 20%.


// Time duration to wait for obtaining a connection from the pool
@NonNull @Builder.Default Duration connectionAccessTimeout = Duration.ofSeconds(10);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public class TypesafeConfigDatastoreConfigExtractor {
private static final String DEFAULT_MAX_POOL_SIZE_KEY = "maxPoolSize";
private static final String DEFAULT_CONNECTION_ACCESS_TIMEOUT_KEY = "connectionAccessTimeout";
private static final String DEFAULT_CONNECTION_IDLE_TIME_KEY = "connectionIdleTime";
private static final String DEFAULT_MAX_IDLE_PERCENT_KEY = "maxIdlePercent";
private static final String DEFAULT_MIN_IDLE_PERCENT_KEY = "minIdlePercent";
private static final String DEFAULT_AGGREGATION_PIPELINE_MODE_KEY = "aggregationPipelineMode";
private static final String DEFAULT_DATA_FRESHNESS_KEY = "dataFreshness";
private static final String DEFAULT_QUERY_TIMEOUT_KEY = "queryTimeout";
Expand Down Expand Up @@ -84,6 +86,8 @@ private TypesafeConfigDatastoreConfigExtractor(
.poolMaxConnectionsKey(DEFAULT_MAX_POOL_SIZE_KEY)
.poolConnectionAccessTimeoutKey(DEFAULT_CONNECTION_ACCESS_TIMEOUT_KEY)
.poolConnectionSurrenderTimeoutKey(DEFAULT_CONNECTION_IDLE_TIME_KEY)
.poolMaxIdlePercentKey(DEFAULT_MAX_IDLE_PERCENT_KEY)
.poolMinIdlePercentKey(DEFAULT_MIN_IDLE_PERCENT_KEY)
.aggregationPipelineMode(DEFAULT_AGGREGATION_PIPELINE_MODE_KEY)
.dataFreshnessKey(DEFAULT_DATA_FRESHNESS_KEY)
.queryTimeoutKey(DEFAULT_QUERY_TIMEOUT_KEY)
Expand Down Expand Up @@ -228,6 +232,20 @@ public TypesafeConfigDatastoreConfigExtractor poolConnectionSurrenderTimeoutKey(
return this;
}

public TypesafeConfigDatastoreConfigExtractor poolMaxIdlePercentKey(@NonNull final String key) {
if (config.hasPath(key)) {
connectionPoolConfigBuilder.maxIdlePercent(config.getInt(key));
}
return this;
}

public TypesafeConfigDatastoreConfigExtractor poolMinIdlePercentKey(@NonNull final String key) {
if (config.hasPath(key)) {
connectionPoolConfigBuilder.minIdlePercent(config.getInt(key));
}
return this;
}

public TypesafeConfigDatastoreConfigExtractor aggregationPipelineMode(@NonNull final String key) {
if (config.hasPath(key)) {
connectionConfigBuilder.aggregationPipelineMode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,18 @@ private void setPoolProperties(
final AbandonedConfig abandonedConfig = getAbandonedConfig(poolConfig);
final int maxConnections = poolConfig.maxConnections();
connectionPool.setMaxTotal(maxConnections);
// max idle connections are 20% of max connections
connectionPool.setMaxIdle(getPercentOf(20, maxConnections));
// min idle connections are 10% of max connections
connectionPool.setMinIdle(getPercentOf(10, maxConnections));
connectionPool.setMaxIdle(getIdleCount(poolConfig.maxIdlePercent(), maxConnections));
connectionPool.setMinIdle(getIdleCount(poolConfig.minIdlePercent(), maxConnections));
connectionPool.setBlockWhenExhausted(true);
connectionPool.setMaxWaitMillis(poolConfig.connectionAccessTimeout().toMillis());
connectionPool.setAbandonedConfig(abandonedConfig);
log.debug(
"Postgres connection pool properties - maxTotal: {}, maxIdle: {}, minIdle: {}, maxWaitMillis: {}, connectionSurrenderTimeout: {}",
connectionPool.getMaxTotal(),
connectionPool.getMaxIdle(),
connectionPool.getMinIdle(),
connectionPool.getMaxWaitMillis(),
poolConfig.connectionSurrenderTimeout());
}

private void setFactoryProperties(
Expand All @@ -115,7 +120,10 @@ private AbandonedConfig getAbandonedConfig(final ConnectionPoolConfig poolConfig
return abandonedConfig;
}

private int getPercentOf(final int percent, final int maxConnections) {
private int getIdleCount(final int percent, final int maxConnections) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If nothing's specified, then we pin both to maxConnections.

if (percent < 0) {
return maxConnections;
}
final int value = (maxConnections * percent) / 100;
return Math.max(value, 1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class TypesafeConfigDatastoreConfigExtractorTest {
private static final String MAX_CONNECTIONS_KEY = "maxConnectionsKey";
private static final String CONNECTION_ACCESS_TIMEOUT_KEY = "connectionAccessTimeout";
private static final String CONNECTION_SURRENDER_TIMEOUT_KEY = "connectionSurrenderTimeout";
private static final String MAX_IDLE_PERCENT_KEY = "maxIdlePercent";
private static final String MIN_IDLE_PERCENT_KEY = "minIdlePercent";
private static final String AGGREGATION_PIPELINE_MODE_KEY = "aggregationPipelineMode";
private static final String DATA_FRESHNESS_KEY = "dataFreshness";
private static final String QUERY_TIMEOUT_KEY = "queryTimeout";
Expand All @@ -52,6 +54,8 @@ class TypesafeConfigDatastoreConfigExtractorTest {
private static final int maxConnections = 7;
private static final Duration accessTimeout = Duration.ofSeconds(67);
private static final Duration surrenderTimeout = Duration.ofSeconds(56);
private static final int maxIdlePercent = 30;
private static final int minIdlePercent = 15;
private static final AggregatePipelineMode aggregatePipelineMode = SORT_OPTIMIZED_IF_POSSIBLE;
private static final DataFreshness dataFreshness = NEAR_REALTIME_FRESHNESS;
private static final Duration queryTimeout = Duration.ofSeconds(45);
Expand Down Expand Up @@ -423,6 +427,66 @@ void testBuildPostgresWithEmptyTimestampFields() {
assertFalse(collectionConfig.get().getTimestampFields().get().getLastUpdated().isPresent());
}

@Test
void testDefaultIdlePercentValues() {
final PostgresConnectionConfig config =
(PostgresConnectionConfig)
TypesafeConfigDatastoreConfigExtractor.from(buildConfigMap(), DatabaseType.POSTGRES)
.hostKey(HOST_KEY)
.portKey(PORT_KEY)
.databaseKey(DATABASE_KEY)
.usernameKey(USER_KEY)
.passwordKey(PASSWORD_KEY)
.poolMaxConnectionsKey(MAX_CONNECTIONS_KEY)
.poolConnectionAccessTimeoutKey(CONNECTION_ACCESS_TIMEOUT_KEY)
.poolConnectionSurrenderTimeoutKey(CONNECTION_SURRENDER_TIMEOUT_KEY)
.extract()
.connectionConfig();

final ConnectionPoolConfig poolConfig = config.connectionPoolConfig();
assertEquals(-1, poolConfig.maxIdlePercent());
assertEquals(-1, poolConfig.minIdlePercent());
}

@Test
void testCustomIdlePercentValues() {
final PostgresConnectionConfig config =
(PostgresConnectionConfig)
TypesafeConfigDatastoreConfigExtractor.from(
buildConfigMapWithIdlePercent(), DatabaseType.POSTGRES)
.hostKey(HOST_KEY)
.portKey(PORT_KEY)
.databaseKey(DATABASE_KEY)
.usernameKey(USER_KEY)
.passwordKey(PASSWORD_KEY)
.poolMaxConnectionsKey(MAX_CONNECTIONS_KEY)
.poolConnectionAccessTimeoutKey(CONNECTION_ACCESS_TIMEOUT_KEY)
.poolConnectionSurrenderTimeoutKey(CONNECTION_SURRENDER_TIMEOUT_KEY)
.poolMaxIdlePercentKey(MAX_IDLE_PERCENT_KEY)
.poolMinIdlePercentKey(MIN_IDLE_PERCENT_KEY)
.extract()
.connectionConfig();

final ConnectionPoolConfig poolConfig = config.connectionPoolConfig();
assertEquals(maxIdlePercent, poolConfig.maxIdlePercent());
assertEquals(minIdlePercent, poolConfig.minIdlePercent());
}

private Config buildConfigMapWithIdlePercent() {
return ConfigFactory.parseMap(
Map.ofEntries(
entry(HOST_KEY, host),
entry(PORT_KEY, port),
entry(DATABASE_KEY, database),
entry(USER_KEY, user),
entry(PASSWORD_KEY, password),
entry(MAX_CONNECTIONS_KEY, maxConnections),
entry(CONNECTION_ACCESS_TIMEOUT_KEY, accessTimeout),
entry(CONNECTION_SURRENDER_TIMEOUT_KEY, surrenderTimeout),
entry(MAX_IDLE_PERCENT_KEY, maxIdlePercent),
entry(MIN_IDLE_PERCENT_KEY, minIdlePercent)));
}

private Config buildConfigMap() {
return ConfigFactory.parseMap(
Map.ofEntries(
Expand Down
Loading