Skip to content

Commit

Permalink
feat: add an option to define minimum pool size (#639)
Browse files Browse the repository at this point in the history
* feat: add an option to define minimum pool size

* feat: add an option to define minimum pool size

* feat: add an option to define minimum pool size

* feat: add an option to define minimum pool size

* feat: add an option to define minimum pool size

* feat: add an option to define minimum pool size
  • Loading branch information
gajus authored Aug 24, 2024
1 parent 270fa07 commit 97924d6
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/young-pears-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@slonik/driver": minor
"slonik": minor
---

add minimumPoolSize option
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,8 @@ createPool(
* @property idleInTransactionSessionTimeout Timeout (in milliseconds) after which idle clients are closed. Use 'DISABLE_TIMEOUT' constant to disable the timeout. (Default: 60000)
* @property idleTimeout Timeout (in milliseconds) after which idle clients are closed. Use 'DISABLE_TIMEOUT' constant to disable the timeout. (Default: 5000)
* @property interceptors An array of [Slonik interceptors](https://github.com/gajus/slonik#interceptors).
* @property maximumPoolSize Do not allow more than this many connections. Use 'DISABLE_TIMEOUT' constant to disable the timeout. (Default: 10)
* @property maximumPoolSize Do not allow more than this many connections. (Default: 10)
* @property minimumPoolSize Ensure that at least this many connections are available in the pool. (Default: 0)
* @property queryRetryLimit Number of times a query failing with Transaction Rollback class error, that doesn't belong to a transaction, is retried. (Default: 5)
* @property ssl [tls.connect options](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)
* @property statementTimeout Timeout (in milliseconds) after which database is instructed to abort the query. Use 'DISABLE_TIMEOUT' constant to disable the timeout. (Default: 60000)
Expand All @@ -638,6 +639,7 @@ type ClientConfiguration = {
idleTimeout?: number | 'DISABLE_TIMEOUT',
interceptors?: Interceptor[],
maximumPoolSize?: number,
maximumPoolSize?: number,
queryRetryLimit?: number,
ssl?: Parameters<tls.connect>[0],
statementTimeout?: number | 'DISABLE_TIMEOUT',
Expand Down
1 change: 1 addition & 0 deletions packages/driver/src/factories/createDriverFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type DriverConfiguration = {
readonly idleInTransactionSessionTimeout: number | 'DISABLE_TIMEOUT';
readonly idleTimeout?: number | 'DISABLE_TIMEOUT';
readonly maximumPoolSize?: number;
readonly minimumPoolSize?: number;
readonly resetConnection?: (connection: BasicConnection) => Promise<void>;
readonly ssl?: TlsConnectionOptions;
readonly statementTimeout: number | 'DISABLE_TIMEOUT';
Expand Down
13 changes: 13 additions & 0 deletions packages/slonik/src/factories/createClientConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const createClientConfiguration = (
idleTimeout: 5_000,
interceptors: [],
maximumPoolSize: 10,
minimumPoolSize: 0,
queryRetryLimit: 5,
resetConnection: ({ query }) => {
return query(`DISCARD ALL`);
Expand All @@ -39,6 +40,18 @@ export const createClientConfiguration = (
);
}

if (configuration.minimumPoolSize < 0) {
throw new InvalidConfigurationError(
'minimumPoolSize must be equal to or greater than 0.',
);
}

if (configuration.maximumPoolSize < configuration.minimumPoolSize) {
throw new InvalidConfigurationError(
'maximumPoolSize must be equal to or greater than minimumPoolSize.',
);
}

if (!configuration.typeParsers || configuration.typeParsers === typeParsers) {
configuration.typeParsers = createTypeParserPreset();
}
Expand Down
20 changes: 14 additions & 6 deletions packages/slonik/src/factories/createConnectionPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ export type ConnectionPool = {

export const createConnectionPool = ({
driver,
poolSize = 1,
maximumPoolSize,
minimumPoolSize,
}: {
driver: Driver;
idleTimeout?: number;
// TODO rename to `maxPoolSize`
poolSize?: number;
idleTimeout: number;
maximumPoolSize: number;
minimumPoolSize: number;
}): ConnectionPool => {
// See test "waits for all connections to be established before attempting to terminate the pool"
// for explanation of why `pendingConnections` is needed.
Expand Down Expand Up @@ -162,6 +163,12 @@ export const createConnectionPool = ({

const waitingClient = waitingClients.shift();

if (!isEnding && !isEnded && connections.length < minimumPoolSize) {
addConnection();

return;
}

if (!waitingClient) {
return;
}
Expand Down Expand Up @@ -195,7 +202,7 @@ export const createConnectionPool = ({
return idleConnection;
}

if (pendingConnections.length + connections.length < poolSize) {
if (pendingConnections.length + connections.length < maximumPoolSize) {
const newConnection = await addConnection();

newConnection.acquire();
Expand All @@ -214,8 +221,9 @@ export const createConnectionPool = ({
logger.warn(
{
connections: connections.length,
maximumPoolSize,
minimumPoolSize,
pendingConnections: pendingConnections.length,
poolSize,
waitingClients: waitingClients.length,
},
`connection pool full; client has been queued`,
Expand Down
14 changes: 10 additions & 4 deletions packages/slonik/src/factories/createPoolConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import { Logger as log } from '../Logger';
import { type ClientConfiguration } from '../types';

type PoolConfiguration = {
idleTimeout?: number;
poolSize?: number;
idleTimeout: number;
maximumPoolSize: number;
minimumPoolSize: number;
};

export const createPoolConfiguration = (
clientConfiguration: ClientConfiguration,
): PoolConfiguration => {
const poolConfiguration = {
idleTimeout: 10_000,
poolSize: 10,
maximumPoolSize: 10,
minimumPoolSize: 0,
};

if (clientConfiguration.idleTimeout !== 'DISABLE_TIMEOUT') {
Expand All @@ -29,7 +31,11 @@ export const createPoolConfiguration = (
}

if (clientConfiguration.maximumPoolSize) {
poolConfiguration.poolSize = clientConfiguration.maximumPoolSize;
poolConfiguration.maximumPoolSize = clientConfiguration.maximumPoolSize;
}

if (clientConfiguration.minimumPoolSize) {
poolConfiguration.minimumPoolSize = clientConfiguration.minimumPoolSize;
}

return poolConfiguration;
Expand Down
108 changes: 108 additions & 0 deletions packages/slonik/src/helpers.test/createIntegrationTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,114 @@ export const createIntegrationTests = (
);
});

test('removes connections from the pool after the idle timeout', async (t) => {
const pool = await createPool(t.context.dsn, {
driverFactory,
idleTimeout: 100,
});

t.deepEqual(
pool.state(),
{
acquiredConnections: 0,
idleConnections: 0,
pendingDestroyConnections: 0,
pendingReleaseConnections: 0,
state: 'ACTIVE',
waitingClients: 0,
},
'initial state',
);

await pool.query(sql.unsafe`
SELECT 1
`);

t.deepEqual(
pool.state(),
{
acquiredConnections: 0,
idleConnections: 1,
pendingDestroyConnections: 0,
pendingReleaseConnections: 0,
state: 'ACTIVE',
waitingClients: 0,
},
'shows idle clients',
);

await delay(100);

t.deepEqual(
pool.state(),
{
acquiredConnections: 0,
idleConnections: 0,
pendingDestroyConnections: 0,
pendingReleaseConnections: 0,
state: 'ACTIVE',
waitingClients: 0,
},
'shows no idle clients',
);
});

test('retains a minimum number of connections in the pool', async (t) => {
const pool = await createPool(t.context.dsn, {
driverFactory,
idleTimeout: 100,
minimumPoolSize: 1,
});

t.deepEqual(
pool.state(),
{
acquiredConnections: 0,
// TODO we might want to add an option to warm up the pool, in which case this value should be 1
idleConnections: 0,
pendingDestroyConnections: 0,
pendingReleaseConnections: 0,
state: 'ACTIVE',
waitingClients: 0,
},
'initial state',
);

await pool.query(sql.unsafe`
SELECT 1
`);

t.deepEqual(
pool.state(),
{
acquiredConnections: 0,
idleConnections: 1,
pendingDestroyConnections: 0,
pendingReleaseConnections: 0,
state: 'ACTIVE',
waitingClients: 0,
},
'shows idle clients',
);

await delay(150);

t.deepEqual(
pool.state(),
{
acquiredConnections: 0,
idleConnections: 1,
pendingDestroyConnections: 0,
pendingReleaseConnections: 0,
state: 'ACTIVE',
waitingClients: 0,
},
'shows idle clients because minimum pool size is 1',
);

await pool.end();
});

test('retains explicit transaction beyond the idle timeout', async (t) => {
const pool = await createPool(t.context.dsn, {
driverFactory,
Expand Down
8 changes: 6 additions & 2 deletions packages/slonik/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,13 @@ export type ClientConfiguration = {
*/
readonly interceptors: readonly Interceptor[];
/**
* Do not allow more than this many connections. Use 'DISABLE_TIMEOUT' constant to disable the timeout. (Default: 10)
* Do not allow more than this many connections. (Default: 10)
*/
readonly maximumPoolSize: number;
readonly maximumPoolSize?: number;
/**
* Ensure that at least this many connections are available in the pool. (Default: 0)
*/
readonly minimumPoolSize?: number;
/**
* Number of times a query failing with Transaction Rollback class error, that doesn't belong to a transaction, is retried. (Default: 5)
*/
Expand Down

0 comments on commit 97924d6

Please sign in to comment.