@digitalwalletcorp/pg-pooling 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/pg-pooling.d.ts +38 -0
- package/lib/pg-pooling.js +164 -0
- package/lib/pg-pooling.js.map +1 -0
- package/package.json +51 -0
- package/src/index.ts +1 -0
- package/src/pg-pooling.ts +181 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 digitalwalletcorp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# PG Pooling
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@digitalwalletcorp/pg-pooling) [](https://opensource.org/licenses/MIT) [](https://github.com/digitalwalletcorp/pg-pooling/actions) [](https://codecov.io/gh/digitalwalletcorp/pg-pooling)
|
|
4
|
+
|
|
5
|
+
A lightweight TypeScript/JavaScript library for managing PostgreSQL connections with pooling support.
|
|
6
|
+
Designed for both server-side Node.js applications and cron-style background jobs, it wraps the `pg` `Pool` with safe connect/release handling, transaction helpers, slow-acquire and leak detection, and optional SQL logging.
|
|
7
|
+
|
|
8
|
+
#### ✨ Features
|
|
9
|
+
|
|
10
|
+
* **Connection Pooling**: Thin wrapper around the `pg` `Pool` with sensible timeout defaults.
|
|
11
|
+
* **Safe Acquire/Release**: Each checked-out client gets its own error listener and a leak-detection timer, so a forgotten `release()` is surfaced instead of silently exhausting the pool.
|
|
12
|
+
* **Pool Exhaustion Diagnostics**: Warns on slow acquires (`POOL ACQUIRE SLOW`), failed acquires (`POOL CONNECT FAILED`) and suspected leaks (`POOL LEAK SUSPECT`).
|
|
13
|
+
* **Transaction Helpers**: `begin()` / `commit()` / `rollback()` convenience methods.
|
|
14
|
+
* **Optional SQL Logging**: Emits the executed SQL and a truncated result set to `console.debug` when `debug` is enabled.
|
|
15
|
+
* **DATE Type Parsing**: PostgreSQL `DATE` (OID 1082) is returned as a `YYYY-MM-DD` string instead of a timezone-dependent `Date`.
|
|
16
|
+
|
|
17
|
+
#### 📦 Installation
|
|
18
|
+
|
|
19
|
+
`pg` is a **peer dependency**. Install it alongside this library so the application and the pool share a single `pg` instance.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @digitalwalletcorp/pg-pooling pg
|
|
23
|
+
# or
|
|
24
|
+
yarn add @digitalwalletcorp/pg-pooling pg
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### 📖 Usage
|
|
28
|
+
|
|
29
|
+
Environment variables and SSL are the responsibility of the application. This library only consumes the `config` object it is given.
|
|
30
|
+
|
|
31
|
+
##### Example 1: Pure NodeJS Style Instantiation
|
|
32
|
+
|
|
33
|
+
This pattern is ideal for short-lived Node.js scripts, such as cron jobs, where a pool is created, used, and destroyed within a single run.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { PgPool } from '@digitalwalletcorp/pg-pooling';
|
|
37
|
+
|
|
38
|
+
const pool = new PgPool({
|
|
39
|
+
host: 'localhost',
|
|
40
|
+
port: 5432,
|
|
41
|
+
database: 'app',
|
|
42
|
+
user: 'app',
|
|
43
|
+
password: 'secret',
|
|
44
|
+
max: 10
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const client = await pool.connect();
|
|
49
|
+
try {
|
|
50
|
+
const res = await client.query('SELECT * FROM users WHERE id = $1', [1]);
|
|
51
|
+
console.log(res.rows);
|
|
52
|
+
} finally {
|
|
53
|
+
client.release();
|
|
54
|
+
}
|
|
55
|
+
await pool.end();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
main().catch(console.error);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
##### Example 2: Singleton Pool Instantiation using a Web-App
|
|
62
|
+
|
|
63
|
+
For long-running web applications, a singleton pool ensures efficient reuse of connections across requests.
|
|
64
|
+
The application reads its own environment variables and builds the `config` (including `application_name` and `ssl`).
|
|
65
|
+
|
|
66
|
+
`@/server/singleton/connection-pooling`
|
|
67
|
+
```typescript
|
|
68
|
+
import { PgPool } from '@digitalwalletcorp/pg-pooling';
|
|
69
|
+
|
|
70
|
+
let pool: PgPool | undefined;
|
|
71
|
+
|
|
72
|
+
export function connectionPooling(): PgPool {
|
|
73
|
+
if (!pool) {
|
|
74
|
+
pool = new PgPool({
|
|
75
|
+
host: process.env.PG_HOST,
|
|
76
|
+
port: Number(process.env.PG_PORT),
|
|
77
|
+
database: process.env.PG_DATABASE,
|
|
78
|
+
user: process.env.PG_USER,
|
|
79
|
+
password: process.env.PG_PASSWORD,
|
|
80
|
+
max: Number(process.env.PG_POOL_SIZE),
|
|
81
|
+
application_name: `myapp-${process.env.APP_ENV}`,
|
|
82
|
+
ssl: process.env.APP_ENV === 'production' ? { rejectUnauthorized: false } : false
|
|
83
|
+
}, {
|
|
84
|
+
debug: process.env.NODE_ENV !== 'production'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return pool;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`@/server/api/some-rest-api`
|
|
92
|
+
```typescript
|
|
93
|
+
import { connectionPooling } from '@/server/singleton/connection-pooling';
|
|
94
|
+
|
|
95
|
+
async function handleRequest() {
|
|
96
|
+
const pool = connectionPooling();
|
|
97
|
+
const client = await pool.connect();
|
|
98
|
+
try {
|
|
99
|
+
const res = await client.query('SELECT * FROM sessions WHERE token = $1', ['abc']);
|
|
100
|
+
return res.rows;
|
|
101
|
+
} finally {
|
|
102
|
+
client.release();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
##### Example 3: Transactions
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const client = await pool.connect();
|
|
111
|
+
try {
|
|
112
|
+
await client.begin();
|
|
113
|
+
await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1]);
|
|
114
|
+
await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2]);
|
|
115
|
+
await client.commit();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
await client.rollback();
|
|
118
|
+
throw error;
|
|
119
|
+
} finally {
|
|
120
|
+
client.release();
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### 📚 API Reference
|
|
125
|
+
|
|
126
|
+
##### `new PgPool(config: pg.PoolConfig, options?: PgPoolOptions)`
|
|
127
|
+
|
|
128
|
+
Creates a PostgreSQL connection pool. `config` is passed through to the underlying `pg` `Pool`. The following timeout-related properties fall back to the library defaults when they are not provided in `config`.
|
|
129
|
+
|
|
130
|
+
| Property | Type | Default | Description |
|
|
131
|
+
| ------------------------------------- | ----------------- | --------- | --------------------------------------------------------------------------- |
|
|
132
|
+
| `connectionTimeoutMillis` | number | 10000 | Fail instead of waiting forever when the pool is exhausted. |
|
|
133
|
+
| `statement_timeout` | number | 30000 | Server-side statement timeout. |
|
|
134
|
+
| `query_timeout` | number | 35000 | Client-side query timeout. Longer than the server-side one. |
|
|
135
|
+
| `idle_in_transaction_session_timeout` | number | 60000 | Server-side timeout for sessions left idle inside a transaction. |
|
|
136
|
+
| `keepAlive` | boolean | true | Enable TCP keep-alive. |
|
|
137
|
+
| `keepAliveInitialDelayMillis` | number | 10000 | Initial delay before the first keep-alive probe. |
|
|
138
|
+
|
|
139
|
+
All other `pg.PoolConfig` properties (`host`, `port`, `database`, `user`, `password`, `max`, `application_name`, `ssl`, ...) are passed through unchanged.
|
|
140
|
+
|
|
141
|
+
`PgPoolOptions`:
|
|
142
|
+
|
|
143
|
+
| Property | Type | Default | Description |
|
|
144
|
+
| -------- | ------- | ------- | ------------------------------------------------------------------ |
|
|
145
|
+
| `debug` | boolean | false | When `true`, executed SQL and results are logged to `console.debug`. |
|
|
146
|
+
|
|
147
|
+
##### `PgPool` Methods
|
|
148
|
+
|
|
149
|
+
| Method | Signature | Description |
|
|
150
|
+
| ------------------- | ------------------------------- | ----------------------------------------------------------------- |
|
|
151
|
+
| `connect()` | `Promise<PgClient>` | Acquire a client from the pool. |
|
|
152
|
+
| `end()` | `Promise<void>` | Drain and close the pool. |
|
|
153
|
+
| `getOriginalPool()` | `pg.Pool` | Access the underlying `pg` `Pool`. |
|
|
154
|
+
| `totalCount` | `number` | Number of connections the pool is holding. |
|
|
155
|
+
| `idleCount` | `number` | Number of idle connections available to borrow. |
|
|
156
|
+
| `waitingCount` | `number` | Number of requests waiting for a connection. |
|
|
157
|
+
|
|
158
|
+
##### `PgClient` Methods
|
|
159
|
+
|
|
160
|
+
| Method | Signature | Description |
|
|
161
|
+
| --------------------------------------- | ---------------------- | ------------------------------------------------------------------ |
|
|
162
|
+
| `query(text, values?, options?)` | `Promise<pg.QueryResult>` | Run a query. Pass `{ suppressLog: true }` to skip SQL logging. |
|
|
163
|
+
| `begin()` / `commit()` / `rollback()` | `Promise<pg.QueryResult>` | Transaction control statements. |
|
|
164
|
+
| `release()` | `void` | Return the client to the pool. |
|
|
165
|
+
|
|
166
|
+
#### 💡 Notes
|
|
167
|
+
|
|
168
|
+
* Always release clients back to the pool using `release()` to avoid connection leaks.
|
|
169
|
+
* Use `connect()` and `release()` inside `try/finally` blocks for safe resource management.
|
|
170
|
+
* Warnings (slow acquire, connect failure, leak suspect, idle/checked-out client errors) are emitted via `process.emitWarning`. To suppress them, run Node with the `--no-warnings` flag (e.g. `node --no-warnings app.js`, or `NODE_OPTIONS=--no-warnings`).
|
|
171
|
+
|
|
172
|
+
#### 📜 License
|
|
173
|
+
|
|
174
|
+
This project is licensed under the MIT License. See the [LICENSE](https://opensource.org/licenses/MIT) file for details.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './pg-pooling';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./pg-pooling"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA6B"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
export interface PgPoolOptions {
|
|
3
|
+
debug?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare class PgPool {
|
|
6
|
+
private pool;
|
|
7
|
+
private readonly debug;
|
|
8
|
+
constructor(config: pg.PoolConfig, options?: PgPoolOptions);
|
|
9
|
+
getOriginalPool(): pg.Pool;
|
|
10
|
+
connect(): Promise<PgClient>;
|
|
11
|
+
/**
|
|
12
|
+
* プールの利用状況
|
|
13
|
+
*
|
|
14
|
+
* @returns {{ total: number; idle: number; waiting: number; }}
|
|
15
|
+
*/
|
|
16
|
+
private poolStats;
|
|
17
|
+
end(): Promise<void>;
|
|
18
|
+
get totalCount(): number;
|
|
19
|
+
get idleCount(): number;
|
|
20
|
+
get waitingCount(): number;
|
|
21
|
+
}
|
|
22
|
+
export declare class PgClient {
|
|
23
|
+
private client;
|
|
24
|
+
private readonly debug;
|
|
25
|
+
private acquiredAt;
|
|
26
|
+
private leakTimer;
|
|
27
|
+
private leakWarned;
|
|
28
|
+
private errorListener;
|
|
29
|
+
constructor(client: pg.PoolClient, debug?: boolean);
|
|
30
|
+
query<R extends pg.QueryResult = pg.QueryResult>(text: string, values?: any[], options?: {
|
|
31
|
+
suppressLog?: boolean;
|
|
32
|
+
}): Promise<R>;
|
|
33
|
+
release(): void;
|
|
34
|
+
begin(): Promise<pg.QueryResult>;
|
|
35
|
+
commit(): Promise<pg.QueryResult>;
|
|
36
|
+
rollback(): Promise<pg.QueryResult>;
|
|
37
|
+
private debugLog;
|
|
38
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PgClient = exports.PgPool = void 0;
|
|
7
|
+
const pg_1 = __importDefault(require("pg"));
|
|
8
|
+
const util_1 = __importDefault(require("util"));
|
|
9
|
+
// PostgreSQLのDATE型をYYYY-MM-DD文字列に変換して返却する設定
|
|
10
|
+
// PGドライバーのデフォルトの挙動だとprocess.env.TZに依存したJSDateオブジェクトに変換されてしまうため、DATE型なのに予期しないゾーン情報が含まれてしまうことがある
|
|
11
|
+
// SQLの段階でカラムを指定してセレクトできる場合は`SELECT xx_date::TEXT`のように記述すればSQLレベルでYYYY-MM-DD文字列に変換される
|
|
12
|
+
pg_1.default.types.setTypeParser(1082, (v) => v);
|
|
13
|
+
const logHeader = '[PgPooling]';
|
|
14
|
+
// 各種タイムアウトの既定値。configで未指定の項目にだけ適用する
|
|
15
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 10 * 1000; // プールが枯渇したときに無限待ちせず失敗させる
|
|
16
|
+
const DEFAULT_STATEMENT_TIMEOUT_MS = 30 * 1000; // サーバ側でクエリを打ち切る
|
|
17
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 35 * 1000; // 応答が返らない(ソケットが死んでいる)場合。サーバ側より長くする
|
|
18
|
+
const DEFAULT_IDLE_IN_TRANSACTION_SESSION_TIMEOUT_MS = 60 * 1000; // トランザクションを開いたまま放置されたセッションをサーバ側で終了させる
|
|
19
|
+
const DEFAULT_KEEP_ALIVE = true;
|
|
20
|
+
const DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS = 10 * 1000;
|
|
21
|
+
// 接続の取得がこの時間を超えた場合に警告する。正常時の取得は新規接続の確立を含めても数百msで収まる
|
|
22
|
+
const ACQUIRE_WARN_MS = 1000;
|
|
23
|
+
// 接続の保持がこの時間を超えた場合にリーク疑いとして警告する。
|
|
24
|
+
// 1クエリの上限(statement_timeout/query_timeout)より長く、サーバ側が放置セッションを切断するidle_in_transaction_session_timeoutと同じ長さ
|
|
25
|
+
const LEAK_WARN_MS = 60 * 1000;
|
|
26
|
+
class PgPool {
|
|
27
|
+
pool;
|
|
28
|
+
debug;
|
|
29
|
+
constructor(config, options) {
|
|
30
|
+
this.debug = options?.debug ?? false;
|
|
31
|
+
// 各種タイムアウトの既定値を適用する。configで指定された項目はそのまま優先する
|
|
32
|
+
const mergedConfig = {
|
|
33
|
+
connectionTimeoutMillis: DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
34
|
+
statement_timeout: DEFAULT_STATEMENT_TIMEOUT_MS,
|
|
35
|
+
query_timeout: DEFAULT_QUERY_TIMEOUT_MS,
|
|
36
|
+
idle_in_transaction_session_timeout: DEFAULT_IDLE_IN_TRANSACTION_SESSION_TIMEOUT_MS,
|
|
37
|
+
keepAlive: DEFAULT_KEEP_ALIVE,
|
|
38
|
+
keepAliveInitialDelayMillis: DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS,
|
|
39
|
+
...config
|
|
40
|
+
};
|
|
41
|
+
this.pool = new pg_1.default.Pool(mergedConfig);
|
|
42
|
+
this.pool.on('error', (error) => {
|
|
43
|
+
// アイドル接続のエラーはプールに通知する
|
|
44
|
+
// リスナーが無いとプロセスが落ちるため必ず登録する
|
|
45
|
+
process.emitWarning(`${logHeader} Idle client error. ${error.message}`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
getOriginalPool() {
|
|
49
|
+
return this.pool;
|
|
50
|
+
}
|
|
51
|
+
async connect() {
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
try {
|
|
54
|
+
const client = await this.pool.connect();
|
|
55
|
+
const elapsedMs = Date.now() - start;
|
|
56
|
+
const stats = this.poolStats();
|
|
57
|
+
// 取得に時間がかかった、あるいは取得待ちの要求が滞留している場合はプール枯渇の前兆として警告する
|
|
58
|
+
if (ACQUIRE_WARN_MS <= elapsedMs || 0 < stats.waiting) {
|
|
59
|
+
process.emitWarning(`${logHeader} POOL ACQUIRE SLOW. ${JSON.stringify({ elapsedMs, ...stats })}`);
|
|
60
|
+
}
|
|
61
|
+
return new PgClient(client, this.debug);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// connectionTimeoutMillisによる取得失敗を無言にしない
|
|
65
|
+
process.emitWarning(`${logHeader} POOL CONNECT FAILED. ${JSON.stringify({ elapsedMs: Date.now() - start, ...this.poolStats() })} ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* プールの利用状況
|
|
71
|
+
*
|
|
72
|
+
* @returns {{ total: number; idle: number; waiting: number; }}
|
|
73
|
+
*/
|
|
74
|
+
poolStats() {
|
|
75
|
+
return {
|
|
76
|
+
total: this.pool.totalCount, // プールが保持している接続数
|
|
77
|
+
idle: this.pool.idleCount, // うち貸出可能な接続数
|
|
78
|
+
waiting: this.pool.waitingCount // 接続を取得できずに待機している要求数
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async end() {
|
|
82
|
+
return this.pool.end();
|
|
83
|
+
}
|
|
84
|
+
get totalCount() {
|
|
85
|
+
return this.pool.totalCount;
|
|
86
|
+
}
|
|
87
|
+
get idleCount() {
|
|
88
|
+
return this.pool.idleCount;
|
|
89
|
+
}
|
|
90
|
+
get waitingCount() {
|
|
91
|
+
return this.pool.waitingCount;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.PgPool = PgPool;
|
|
95
|
+
class PgClient {
|
|
96
|
+
client;
|
|
97
|
+
debug;
|
|
98
|
+
acquiredAt = Date.now();
|
|
99
|
+
leakTimer;
|
|
100
|
+
leakWarned = false;
|
|
101
|
+
// 貸出中のクライアントに個別のエラーリスナーを設定
|
|
102
|
+
// これが無いとDB側の瞬断でリスナー不在のerrorイベントが発生し、プロセスが落ちる
|
|
103
|
+
errorListener = (error) => {
|
|
104
|
+
process.emitWarning(`${logHeader} Checked-out client error. ${error.message}`);
|
|
105
|
+
};
|
|
106
|
+
constructor(client, debug = false) {
|
|
107
|
+
this.client = client;
|
|
108
|
+
this.debug = debug;
|
|
109
|
+
this.client.on('error', this.errorListener);
|
|
110
|
+
// リークした接続の取得元を特定できるように、取得時点のスタックトレースを保持する
|
|
111
|
+
const acquireStack = new Error('acquired here').stack;
|
|
112
|
+
this.leakTimer = setTimeout(() => {
|
|
113
|
+
this.leakWarned = true;
|
|
114
|
+
process.emitWarning(`${logHeader} POOL LEAK SUSPECT. ${JSON.stringify({ heldMs: Date.now() - this.acquiredAt })}\n${acquireStack}`);
|
|
115
|
+
}, LEAK_WARN_MS);
|
|
116
|
+
// タイマーが残っていてもプロセスの終了を妨げないようにする
|
|
117
|
+
this.leakTimer.unref();
|
|
118
|
+
}
|
|
119
|
+
async query(text, values, options) {
|
|
120
|
+
if (!options?.suppressLog) {
|
|
121
|
+
this.debugLog(logHeader, 'SQL', text, values);
|
|
122
|
+
}
|
|
123
|
+
const result = await this.client.query(text, values);
|
|
124
|
+
if (!options?.suppressLog) {
|
|
125
|
+
this.debugLog(logHeader, 'SQL RESULT', {
|
|
126
|
+
command: result.command,
|
|
127
|
+
rowCount: result.rowCount,
|
|
128
|
+
rows: Array.isArray(result.rows)
|
|
129
|
+
? util_1.default.inspect(result.rows.slice(0, 3), false, null)
|
|
130
|
+
: util_1.default.inspect(result.rows, false, null)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
release() {
|
|
136
|
+
clearTimeout(this.leakTimer);
|
|
137
|
+
if (this.leakWarned) {
|
|
138
|
+
// リーク疑い警告後に返却されたことを記録する。警告後にこのログが無ければ本物のリークと判断できる
|
|
139
|
+
process.emitWarning(`${logHeader} POOL RELEASE (after leak suspect). ${JSON.stringify({ heldMs: Date.now() - this.acquiredAt })}`);
|
|
140
|
+
}
|
|
141
|
+
// 返却後はプール側のリスナー(pool.on('error'))が受け持つ
|
|
142
|
+
this.client.removeListener('error', this.errorListener);
|
|
143
|
+
this.client.release();
|
|
144
|
+
}
|
|
145
|
+
async begin() {
|
|
146
|
+
this.debugLog(logHeader, 'BEGIN');
|
|
147
|
+
return this.client.query('BEGIN');
|
|
148
|
+
}
|
|
149
|
+
async commit() {
|
|
150
|
+
this.debugLog(logHeader, 'COMMIT');
|
|
151
|
+
return this.client.query('COMMIT');
|
|
152
|
+
}
|
|
153
|
+
async rollback() {
|
|
154
|
+
this.debugLog(logHeader, 'ROLLBACK');
|
|
155
|
+
return this.client.query('ROLLBACK');
|
|
156
|
+
}
|
|
157
|
+
debugLog(...args) {
|
|
158
|
+
if (this.debug) {
|
|
159
|
+
console.debug(...args);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
exports.PgClient = PgClient;
|
|
164
|
+
//# sourceMappingURL=pg-pooling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-pooling.js","sourceRoot":"","sources":["../src/pg-pooling.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@digitalwalletcorp/pg-pooling",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "This is a library for PostgreSQL connection pooling",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc --project tsconfig.build.json",
|
|
9
|
+
"install:dev": "rm -rf lib dest && find node_modules -mindepth 1 -delete && yarn install",
|
|
10
|
+
"version:patch": "npm version patch",
|
|
11
|
+
"version:minor": "npm version minor",
|
|
12
|
+
"version:major": "npm version major",
|
|
13
|
+
"prepack": "rm -rf lib && npm run build",
|
|
14
|
+
"pack": "rm -rf dest && mkdir -p dest && npm pack --pack-destination=dest",
|
|
15
|
+
"publish:npm": "bash ../bin/publish.sh",
|
|
16
|
+
"test:unit": "NODE_OPTIONS=--max-old-space-size=2048 npx jest --runInBand --logHeapUsage --detectOpenHandles --forceExit --errorOnDeprecated --coverage --silent --unhandled-rejections=strict"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"postgres",
|
|
20
|
+
"postgresql",
|
|
21
|
+
"pg",
|
|
22
|
+
"pooling"
|
|
23
|
+
],
|
|
24
|
+
"author": "satoshi kotaki",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/digitalwalletcorp/pg-pooling.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/digitalwalletcorp/pg-pooling",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/digitalwalletcorp/pg-pooling/issues"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"lib/**/*",
|
|
36
|
+
"src/**/*"
|
|
37
|
+
],
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"pg": ">=8.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/jest": "^30.0.0",
|
|
43
|
+
"@types/pg": "^8.16.0",
|
|
44
|
+
"jest": "^30.3.0",
|
|
45
|
+
"pg": "^8.16.3",
|
|
46
|
+
"ts-jest": "^29.4.9",
|
|
47
|
+
"ts-node": "^10.9.2",
|
|
48
|
+
"tsconfig-paths": "^4.2.0",
|
|
49
|
+
"typescript": "^6.0.3"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './pg-pooling';
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import util from 'util';
|
|
3
|
+
|
|
4
|
+
// PostgreSQLのDATE型をYYYY-MM-DD文字列に変換して返却する設定
|
|
5
|
+
// PGドライバーのデフォルトの挙動だとprocess.env.TZに依存したJSDateオブジェクトに変換されてしまうため、DATE型なのに予期しないゾーン情報が含まれてしまうことがある
|
|
6
|
+
// SQLの段階でカラムを指定してセレクトできる場合は`SELECT xx_date::TEXT`のように記述すればSQLレベルでYYYY-MM-DD文字列に変換される
|
|
7
|
+
pg.types.setTypeParser(1082, (v) => v);
|
|
8
|
+
|
|
9
|
+
export interface PgPoolOptions {
|
|
10
|
+
debug?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const logHeader = '[PgPooling]';
|
|
14
|
+
|
|
15
|
+
// 各種タイムアウトの既定値。configで未指定の項目にだけ適用する
|
|
16
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 10 * 1000; // プールが枯渇したときに無限待ちせず失敗させる
|
|
17
|
+
const DEFAULT_STATEMENT_TIMEOUT_MS = 30 * 1000; // サーバ側でクエリを打ち切る
|
|
18
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 35 * 1000; // 応答が返らない(ソケットが死んでいる)場合。サーバ側より長くする
|
|
19
|
+
const DEFAULT_IDLE_IN_TRANSACTION_SESSION_TIMEOUT_MS = 60 * 1000; // トランザクションを開いたまま放置されたセッションをサーバ側で終了させる
|
|
20
|
+
const DEFAULT_KEEP_ALIVE = true;
|
|
21
|
+
const DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS = 10 * 1000;
|
|
22
|
+
|
|
23
|
+
// 接続の取得がこの時間を超えた場合に警告する。正常時の取得は新規接続の確立を含めても数百msで収まる
|
|
24
|
+
const ACQUIRE_WARN_MS = 1000;
|
|
25
|
+
// 接続の保持がこの時間を超えた場合にリーク疑いとして警告する。
|
|
26
|
+
// 1クエリの上限(statement_timeout/query_timeout)より長く、サーバ側が放置セッションを切断するidle_in_transaction_session_timeoutと同じ長さ
|
|
27
|
+
const LEAK_WARN_MS = 60 * 1000;
|
|
28
|
+
|
|
29
|
+
export class PgPool {
|
|
30
|
+
private pool: pg.Pool;
|
|
31
|
+
private readonly debug: boolean;
|
|
32
|
+
|
|
33
|
+
constructor(config: pg.PoolConfig, options?: PgPoolOptions) {
|
|
34
|
+
this.debug = options?.debug ?? false;
|
|
35
|
+
// 各種タイムアウトの既定値を適用する。configで指定された項目はそのまま優先する
|
|
36
|
+
const mergedConfig: pg.PoolConfig = {
|
|
37
|
+
connectionTimeoutMillis: DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
38
|
+
statement_timeout: DEFAULT_STATEMENT_TIMEOUT_MS,
|
|
39
|
+
query_timeout: DEFAULT_QUERY_TIMEOUT_MS,
|
|
40
|
+
idle_in_transaction_session_timeout: DEFAULT_IDLE_IN_TRANSACTION_SESSION_TIMEOUT_MS,
|
|
41
|
+
keepAlive: DEFAULT_KEEP_ALIVE,
|
|
42
|
+
keepAliveInitialDelayMillis: DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS,
|
|
43
|
+
...config
|
|
44
|
+
};
|
|
45
|
+
this.pool = new pg.Pool(mergedConfig);
|
|
46
|
+
this.pool.on('error', (error: Error) => {
|
|
47
|
+
// アイドル接続のエラーはプールに通知する
|
|
48
|
+
// リスナーが無いとプロセスが落ちるため必ず登録する
|
|
49
|
+
process.emitWarning(`${logHeader} Idle client error. ${error.message}`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public getOriginalPool(): pg.Pool {
|
|
54
|
+
return this.pool;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async connect(): Promise<PgClient> {
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
try {
|
|
60
|
+
const client = await this.pool.connect();
|
|
61
|
+
const elapsedMs = Date.now() - start;
|
|
62
|
+
const stats = this.poolStats();
|
|
63
|
+
// 取得に時間がかかった、あるいは取得待ちの要求が滞留している場合はプール枯渇の前兆として警告する
|
|
64
|
+
if (ACQUIRE_WARN_MS <= elapsedMs || 0 < stats.waiting) {
|
|
65
|
+
process.emitWarning(`${logHeader} POOL ACQUIRE SLOW. ${JSON.stringify({ elapsedMs, ...stats })}`);
|
|
66
|
+
}
|
|
67
|
+
return new PgClient(client, this.debug);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// connectionTimeoutMillisによる取得失敗を無言にしない
|
|
70
|
+
process.emitWarning(`${logHeader} POOL CONNECT FAILED. ${JSON.stringify({ elapsedMs: Date.now() - start, ...this.poolStats() })} ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* プールの利用状況
|
|
77
|
+
*
|
|
78
|
+
* @returns {{ total: number; idle: number; waiting: number; }}
|
|
79
|
+
*/
|
|
80
|
+
private poolStats(): { total: number; idle: number; waiting: number; } {
|
|
81
|
+
return {
|
|
82
|
+
total: this.pool.totalCount, // プールが保持している接続数
|
|
83
|
+
idle: this.pool.idleCount, // うち貸出可能な接続数
|
|
84
|
+
waiting: this.pool.waitingCount // 接続を取得できずに待機している要求数
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async end(): Promise<void> {
|
|
89
|
+
return this.pool.end();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public get totalCount(): number {
|
|
93
|
+
return this.pool.totalCount;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public get idleCount(): number {
|
|
97
|
+
return this.pool.idleCount;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public get waitingCount(): number {
|
|
101
|
+
return this.pool.waitingCount;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class PgClient {
|
|
106
|
+
private client: pg.PoolClient;
|
|
107
|
+
private readonly debug: boolean;
|
|
108
|
+
private acquiredAt = Date.now();
|
|
109
|
+
private leakTimer: ReturnType<typeof setTimeout>;
|
|
110
|
+
private leakWarned = false;
|
|
111
|
+
// 貸出中のクライアントに個別のエラーリスナーを設定
|
|
112
|
+
// これが無いとDB側の瞬断でリスナー不在のerrorイベントが発生し、プロセスが落ちる
|
|
113
|
+
private errorListener = (error: Error) => {
|
|
114
|
+
process.emitWarning(`${logHeader} Checked-out client error. ${error.message}`);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
constructor(client: pg.PoolClient, debug = false) {
|
|
118
|
+
this.client = client;
|
|
119
|
+
this.debug = debug;
|
|
120
|
+
this.client.on('error', this.errorListener);
|
|
121
|
+
// リークした接続の取得元を特定できるように、取得時点のスタックトレースを保持する
|
|
122
|
+
const acquireStack = new Error('acquired here').stack;
|
|
123
|
+
this.leakTimer = setTimeout(() => {
|
|
124
|
+
this.leakWarned = true;
|
|
125
|
+
process.emitWarning(`${logHeader} POOL LEAK SUSPECT. ${JSON.stringify({ heldMs: Date.now() - this.acquiredAt })}\n${acquireStack}`);
|
|
126
|
+
}, LEAK_WARN_MS);
|
|
127
|
+
// タイマーが残っていてもプロセスの終了を妨げないようにする
|
|
128
|
+
this.leakTimer.unref();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public async query<R extends pg.QueryResult = pg.QueryResult>(text: string, values?: any[], options?: {
|
|
132
|
+
suppressLog?: boolean
|
|
133
|
+
}): Promise<R> {
|
|
134
|
+
if (!options?.suppressLog) {
|
|
135
|
+
this.debugLog(logHeader, 'SQL', text, values);
|
|
136
|
+
}
|
|
137
|
+
const result = await this.client.query(text, values);
|
|
138
|
+
if (!options?.suppressLog) {
|
|
139
|
+
this.debugLog(logHeader, 'SQL RESULT', {
|
|
140
|
+
command: result.command,
|
|
141
|
+
rowCount: result.rowCount,
|
|
142
|
+
rows: Array.isArray(result.rows)
|
|
143
|
+
? util.inspect(result.rows.slice(0, 3), false, null)
|
|
144
|
+
: util.inspect(result.rows, false, null)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return result as unknown as R;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public release(): void {
|
|
151
|
+
clearTimeout(this.leakTimer);
|
|
152
|
+
if (this.leakWarned) {
|
|
153
|
+
// リーク疑い警告後に返却されたことを記録する。警告後にこのログが無ければ本物のリークと判断できる
|
|
154
|
+
process.emitWarning(`${logHeader} POOL RELEASE (after leak suspect). ${JSON.stringify({ heldMs: Date.now() - this.acquiredAt })}`);
|
|
155
|
+
}
|
|
156
|
+
// 返却後はプール側のリスナー(pool.on('error'))が受け持つ
|
|
157
|
+
this.client.removeListener('error', this.errorListener);
|
|
158
|
+
this.client.release();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public async begin(): Promise<pg.QueryResult> {
|
|
162
|
+
this.debugLog(logHeader, 'BEGIN');
|
|
163
|
+
return this.client.query('BEGIN');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public async commit(): Promise<pg.QueryResult> {
|
|
167
|
+
this.debugLog(logHeader, 'COMMIT');
|
|
168
|
+
return this.client.query('COMMIT');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public async rollback(): Promise<pg.QueryResult> {
|
|
172
|
+
this.debugLog(logHeader, 'ROLLBACK');
|
|
173
|
+
return this.client.query('ROLLBACK');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private debugLog(...args: any[]): void {
|
|
177
|
+
if (this.debug) {
|
|
178
|
+
console.debug(...args);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|