Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add an option to define minimum pool size #639

Merged
merged 6 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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