@digitalwalletcorp/pg-pooling 0.1.0 → 0.2.1
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/README.md +32 -6
- package/lib/pg-pooling.d.ts +33 -4
- package/lib/pg-pooling.js +68 -31
- package/lib/pg-pooling.js.map +1 -1
- package/package.json +2 -2
- package/src/pg-pooling.ts +99 -32
package/README.md
CHANGED
|
@@ -11,7 +11,8 @@ Designed for both server-side Node.js applications and cron-style background job
|
|
|
11
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
12
|
* **Pool Exhaustion Diagnostics**: Warns on slow acquires (`POOL ACQUIRE SLOW`), failed acquires (`POOL CONNECT FAILED`) and suspected leaks (`POOL LEAK SUSPECT`).
|
|
13
13
|
* **Transaction Helpers**: `begin()` / `commit()` / `rollback()` convenience methods.
|
|
14
|
-
* **
|
|
14
|
+
* **Pluggable Logging**: Pass your own logger (`debug` / `info` / `warn` / `error`) to route logs into your application's logging. Without one, SQL logs go to the console when `debug` is enabled and warnings go through `process.emitWarning`.
|
|
15
|
+
* **Configurable Thresholds**: The slow-acquire and leak-suspect thresholds can be tuned or disabled.
|
|
15
16
|
* **DATE Type Parsing**: PostgreSQL `DATE` (OID 1082) is returned as a `YYYY-MM-DD` string instead of a timezone-dependent `Date`.
|
|
16
17
|
|
|
17
18
|
#### 📦 Installation
|
|
@@ -66,6 +67,7 @@ The application reads its own environment variables and builds the `config` (inc
|
|
|
66
67
|
`@/server/singleton/connection-pooling`
|
|
67
68
|
```typescript
|
|
68
69
|
import { PgPool } from '@digitalwalletcorp/pg-pooling';
|
|
70
|
+
import { appLogger } from '@/server/common/logger';
|
|
69
71
|
|
|
70
72
|
let pool: PgPool | undefined;
|
|
71
73
|
|
|
@@ -81,7 +83,8 @@ export function connectionPooling(): PgPool {
|
|
|
81
83
|
application_name: `myapp-${process.env.APP_ENV}`,
|
|
82
84
|
ssl: process.env.APP_ENV === 'production' ? { rejectUnauthorized: false } : false
|
|
83
85
|
}, {
|
|
84
|
-
|
|
86
|
+
logger: appLogger, // any object with debug/info/warn/error
|
|
87
|
+
sqlLogLevel: 'info'
|
|
85
88
|
});
|
|
86
89
|
}
|
|
87
90
|
return pool;
|
|
@@ -140,9 +143,32 @@ All other `pg.PoolConfig` properties (`host`, `port`, `database`, `user`, `passw
|
|
|
140
143
|
|
|
141
144
|
`PgPoolOptions`:
|
|
142
145
|
|
|
143
|
-
| Property
|
|
144
|
-
|
|
|
145
|
-
| `
|
|
146
|
+
| Property | Type | Default | Description |
|
|
147
|
+
| --------------- | ------------------- | --------- | ------------------------------------------------------------------------------------------------------------- |
|
|
148
|
+
| `logger` | `PgPoolLogger` | - | Destination of all logs. When given, the logger decides which levels are output (`debug` is ignored). |
|
|
149
|
+
| `debug` | boolean | false | Only used without `logger`. When `true`, `debug` / `info` logs are written to `console.debug` / `console.info`. |
|
|
150
|
+
| `sqlLogLevel` | `'debug' \| 'info'` | `'debug'` | Level used for `SQL` / `SQL RESULT` / `BEGIN` / `COMMIT` / `ROLLBACK` logs. |
|
|
151
|
+
| `leakWarnMs` | number | 60000 | Warn `POOL LEAK SUSPECT` when a client is held longer than this. `0` disables it. |
|
|
152
|
+
| `acquireWarnMs` | number | 1000 | Warn `POOL ACQUIRE SLOW` when acquiring takes longer than this or requests are waiting. `0` disables it. |
|
|
153
|
+
|
|
154
|
+
`PgPoolLogger`:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
interface PgPoolLogger {
|
|
158
|
+
debug(...args: any[]): void;
|
|
159
|
+
info(...args: any[]): void;
|
|
160
|
+
warn(...args: any[]): void;
|
|
161
|
+
error(...args: any[]): void;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Each log is called with the header `'[PgPooling]'`, a message, and optional details, e.g. `logger.warn('[PgPooling]', 'POOL ACQUIRE SLOW', { elapsedMs, total, idle, waiting })`.
|
|
166
|
+
|
|
167
|
+
| Level | Messages |
|
|
168
|
+
| ------- | ---------------------------------------------------------------------------------- |
|
|
169
|
+
| `debug` / `info` | `SQL`, `SQL RESULT`, `BEGIN`, `COMMIT`, `ROLLBACK` (chosen by `sqlLogLevel`) |
|
|
170
|
+
| `warn` | `POOL ACQUIRE SLOW`, `POOL LEAK SUSPECT`, `POOL RELEASE (after leak suspect)` |
|
|
171
|
+
| `error` | `POOL CONNECT FAILED`, `Idle client error`, `Checked-out client error` |
|
|
146
172
|
|
|
147
173
|
##### `PgPool` Methods
|
|
148
174
|
|
|
@@ -167,7 +193,7 @@ All other `pg.PoolConfig` properties (`host`, `port`, `database`, `user`, `passw
|
|
|
167
193
|
|
|
168
194
|
* Always release clients back to the pool using `release()` to avoid connection leaks.
|
|
169
195
|
* Use `connect()` and `release()` inside `try/finally` blocks for safe resource management.
|
|
170
|
-
*
|
|
196
|
+
* Without `logger`, `warn` / `error` logs are emitted via `process.emitWarning` so that callers can suppress them with the `--no-warnings` flag (e.g. `node --no-warnings app.js`, or `NODE_OPTIONS=--no-warnings`) or handle them with `process.on('warning')`.
|
|
171
197
|
|
|
172
198
|
#### 📜 License
|
|
173
199
|
|
package/lib/pg-pooling.d.ts
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
|
+
/**
|
|
3
|
+
* ログの出力先
|
|
4
|
+
* 渡した場合、どのレベルを出力するかはこのロガーが決める
|
|
5
|
+
*/
|
|
6
|
+
export interface PgPoolLogger {
|
|
7
|
+
debug(...args: any[]): void;
|
|
8
|
+
info(...args: any[]): void;
|
|
9
|
+
warn(...args: any[]): void;
|
|
10
|
+
error(...args: any[]): void;
|
|
11
|
+
}
|
|
2
12
|
export interface PgPoolOptions {
|
|
13
|
+
/** ログの出力先。未指定の場合はconsole(debug/info)とprocess.emitWarning(warn/error)に出力する */
|
|
14
|
+
logger?: PgPoolLogger;
|
|
15
|
+
/** loggerを渡さない場合のみ有効。trueの場合はdebug/infoも出力する */
|
|
3
16
|
debug?: boolean;
|
|
17
|
+
/** SQL/SQL RESULT/BEGIN/COMMIT/ROLLBACKを出力するレベル */
|
|
18
|
+
sqlLogLevel?: 'debug' | 'info';
|
|
19
|
+
/** 接続の保持がこの時間を超えた場合にリーク疑いとして警告する。0の場合は警告しない */
|
|
20
|
+
leakWarnMs?: number;
|
|
21
|
+
/** 接続の取得がこの時間を超えた場合に警告する。0の場合は警告しない */
|
|
22
|
+
acquireWarnMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* PgClientの動作設定。PgPoolOptionsの既定値を解決したもの
|
|
26
|
+
*/
|
|
27
|
+
export interface PgClientSettings {
|
|
28
|
+
logger: PgPoolLogger;
|
|
29
|
+
sqlLogLevel: 'debug' | 'info';
|
|
30
|
+
leakWarnMs: number;
|
|
4
31
|
}
|
|
5
32
|
export declare class PgPool {
|
|
6
33
|
private pool;
|
|
7
|
-
private readonly
|
|
34
|
+
private readonly logger;
|
|
35
|
+
private readonly acquireWarnMs;
|
|
36
|
+
private readonly clientSettings;
|
|
8
37
|
constructor(config: pg.PoolConfig, options?: PgPoolOptions);
|
|
9
38
|
getOriginalPool(): pg.Pool;
|
|
10
39
|
connect(): Promise<PgClient>;
|
|
@@ -21,12 +50,12 @@ export declare class PgPool {
|
|
|
21
50
|
}
|
|
22
51
|
export declare class PgClient {
|
|
23
52
|
private client;
|
|
24
|
-
private readonly
|
|
53
|
+
private readonly settings;
|
|
25
54
|
private acquiredAt;
|
|
26
55
|
private leakTimer;
|
|
27
56
|
private leakWarned;
|
|
28
57
|
private errorListener;
|
|
29
|
-
constructor(client: pg.PoolClient,
|
|
58
|
+
constructor(client: pg.PoolClient, settings: PgClientSettings);
|
|
30
59
|
query<R extends pg.QueryResult = pg.QueryResult>(text: string, values?: any[], options?: {
|
|
31
60
|
suppressLog?: boolean;
|
|
32
61
|
}): Promise<R>;
|
|
@@ -34,5 +63,5 @@ export declare class PgClient {
|
|
|
34
63
|
begin(): Promise<pg.QueryResult>;
|
|
35
64
|
commit(): Promise<pg.QueryResult>;
|
|
36
65
|
rollback(): Promise<pg.QueryResult>;
|
|
37
|
-
private
|
|
66
|
+
private sqlLog;
|
|
38
67
|
}
|
package/lib/pg-pooling.js
CHANGED
|
@@ -19,15 +19,46 @@ const DEFAULT_IDLE_IN_TRANSACTION_SESSION_TIMEOUT_MS = 60 * 1000; // トラン
|
|
|
19
19
|
const DEFAULT_KEEP_ALIVE = true;
|
|
20
20
|
const DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS = 10 * 1000;
|
|
21
21
|
// 接続の取得がこの時間を超えた場合に警告する。正常時の取得は新規接続の確立を含めても数百msで収まる
|
|
22
|
-
const
|
|
22
|
+
const DEFAULT_ACQUIRE_WARN_MS = 1000;
|
|
23
23
|
// 接続の保持がこの時間を超えた場合にリーク疑いとして警告する。
|
|
24
24
|
// 1クエリの上限(statement_timeout/query_timeout)より長く、サーバ側が放置セッションを切断するidle_in_transaction_session_timeoutと同じ長さ
|
|
25
|
-
const
|
|
25
|
+
const DEFAULT_LEAK_WARN_MS = 60 * 1000;
|
|
26
|
+
/**
|
|
27
|
+
* loggerが渡されなかった場合のロガー
|
|
28
|
+
* 警告とエラーは呼び出し元が--no-warningsやprocess.on('warning')で抑止・捕捉できるようにprocess.emitWarningで出力する
|
|
29
|
+
*
|
|
30
|
+
* @param {boolean} debug trueの場合はdebug/infoもconsoleに出力する
|
|
31
|
+
* @returns {PgPoolLogger}
|
|
32
|
+
*/
|
|
33
|
+
function createDefaultLogger(debug) {
|
|
34
|
+
return {
|
|
35
|
+
debug: (...args) => {
|
|
36
|
+
if (debug) {
|
|
37
|
+
console.debug(...args);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
info: (...args) => {
|
|
41
|
+
if (debug) {
|
|
42
|
+
console.info(...args);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
warn: (...args) => process.emitWarning(util_1.default.format(...args)),
|
|
46
|
+
error: (...args) => process.emitWarning(util_1.default.format(...args))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
26
49
|
class PgPool {
|
|
27
50
|
pool;
|
|
28
|
-
|
|
51
|
+
logger;
|
|
52
|
+
acquireWarnMs;
|
|
53
|
+
clientSettings;
|
|
29
54
|
constructor(config, options) {
|
|
30
|
-
this.
|
|
55
|
+
this.logger = options?.logger ?? createDefaultLogger(options?.debug ?? false);
|
|
56
|
+
this.acquireWarnMs = options?.acquireWarnMs ?? DEFAULT_ACQUIRE_WARN_MS;
|
|
57
|
+
this.clientSettings = {
|
|
58
|
+
logger: this.logger,
|
|
59
|
+
sqlLogLevel: options?.sqlLogLevel ?? 'debug',
|
|
60
|
+
leakWarnMs: options?.leakWarnMs ?? DEFAULT_LEAK_WARN_MS
|
|
61
|
+
};
|
|
31
62
|
// 各種タイムアウトの既定値を適用する。configで指定された項目はそのまま優先する
|
|
32
63
|
const mergedConfig = {
|
|
33
64
|
connectionTimeoutMillis: DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
@@ -42,7 +73,7 @@ class PgPool {
|
|
|
42
73
|
this.pool.on('error', (error) => {
|
|
43
74
|
// アイドル接続のエラーはプールに通知する
|
|
44
75
|
// リスナーが無いとプロセスが落ちるため必ず登録する
|
|
45
|
-
|
|
76
|
+
this.logger.error(logHeader, 'Idle client error', error);
|
|
46
77
|
});
|
|
47
78
|
}
|
|
48
79
|
getOriginalPool() {
|
|
@@ -55,14 +86,17 @@ class PgPool {
|
|
|
55
86
|
const elapsedMs = Date.now() - start;
|
|
56
87
|
const stats = this.poolStats();
|
|
57
88
|
// 取得に時間がかかった、あるいは取得待ちの要求が滞留している場合はプール枯渇の前兆として警告する
|
|
58
|
-
if (
|
|
59
|
-
|
|
89
|
+
if (0 < this.acquireWarnMs && (this.acquireWarnMs <= elapsedMs || 0 < stats.waiting)) {
|
|
90
|
+
this.logger.warn(logHeader, 'POOL ACQUIRE SLOW', { elapsedMs, ...stats });
|
|
60
91
|
}
|
|
61
|
-
return new PgClient(client, this.
|
|
92
|
+
return new PgClient(client, this.clientSettings);
|
|
62
93
|
}
|
|
63
94
|
catch (error) {
|
|
64
95
|
// connectionTimeoutMillisによる取得失敗を無言にしない
|
|
65
|
-
|
|
96
|
+
this.logger.error(logHeader, 'POOL CONNECT FAILED', {
|
|
97
|
+
elapsedMs: Date.now() - start,
|
|
98
|
+
...this.poolStats()
|
|
99
|
+
}, error);
|
|
66
100
|
throw error;
|
|
67
101
|
}
|
|
68
102
|
}
|
|
@@ -94,35 +128,40 @@ class PgPool {
|
|
|
94
128
|
exports.PgPool = PgPool;
|
|
95
129
|
class PgClient {
|
|
96
130
|
client;
|
|
97
|
-
|
|
131
|
+
settings;
|
|
98
132
|
acquiredAt = Date.now();
|
|
99
133
|
leakTimer;
|
|
100
134
|
leakWarned = false;
|
|
101
135
|
// 貸出中のクライアントに個別のエラーリスナーを設定
|
|
102
136
|
// これが無いとDB側の瞬断でリスナー不在のerrorイベントが発生し、プロセスが落ちる
|
|
103
137
|
errorListener = (error) => {
|
|
104
|
-
|
|
138
|
+
this.settings.logger.error(logHeader, 'Checked-out client error', error);
|
|
105
139
|
};
|
|
106
|
-
constructor(client,
|
|
140
|
+
constructor(client, settings) {
|
|
107
141
|
this.client = client;
|
|
108
|
-
this.
|
|
142
|
+
this.settings = settings;
|
|
109
143
|
this.client.on('error', this.errorListener);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
if (0 < settings.leakWarnMs) {
|
|
145
|
+
// リークした接続の取得元を特定できるように、取得時点のスタックトレースを保持する
|
|
146
|
+
const acquireStack = new Error('acquired here').stack;
|
|
147
|
+
this.leakTimer = setTimeout(() => {
|
|
148
|
+
this.leakWarned = true;
|
|
149
|
+
settings.logger.warn(logHeader, 'POOL LEAK SUSPECT', {
|
|
150
|
+
heldMs: Date.now() - this.acquiredAt,
|
|
151
|
+
acquireStack
|
|
152
|
+
});
|
|
153
|
+
}, settings.leakWarnMs);
|
|
154
|
+
// タイマーが残っていてもプロセスの終了を妨げないようにする
|
|
155
|
+
this.leakTimer.unref();
|
|
156
|
+
}
|
|
118
157
|
}
|
|
119
158
|
async query(text, values, options) {
|
|
120
159
|
if (!options?.suppressLog) {
|
|
121
|
-
this.
|
|
160
|
+
this.sqlLog(logHeader, 'SQL', text, values);
|
|
122
161
|
}
|
|
123
162
|
const result = await this.client.query(text, values);
|
|
124
163
|
if (!options?.suppressLog) {
|
|
125
|
-
this.
|
|
164
|
+
this.sqlLog(logHeader, 'SQL RESULT', {
|
|
126
165
|
command: result.command,
|
|
127
166
|
rowCount: result.rowCount,
|
|
128
167
|
rows: Array.isArray(result.rows)
|
|
@@ -136,28 +175,26 @@ class PgClient {
|
|
|
136
175
|
clearTimeout(this.leakTimer);
|
|
137
176
|
if (this.leakWarned) {
|
|
138
177
|
// リーク疑い警告後に返却されたことを記録する。警告後にこのログが無ければ本物のリークと判断できる
|
|
139
|
-
|
|
178
|
+
this.settings.logger.warn(logHeader, 'POOL RELEASE (after leak suspect)', { heldMs: Date.now() - this.acquiredAt });
|
|
140
179
|
}
|
|
141
180
|
// 返却後はプール側のリスナー(pool.on('error'))が受け持つ
|
|
142
181
|
this.client.removeListener('error', this.errorListener);
|
|
143
182
|
this.client.release();
|
|
144
183
|
}
|
|
145
184
|
async begin() {
|
|
146
|
-
this.
|
|
185
|
+
this.sqlLog(logHeader, 'BEGIN');
|
|
147
186
|
return this.client.query('BEGIN');
|
|
148
187
|
}
|
|
149
188
|
async commit() {
|
|
150
|
-
this.
|
|
189
|
+
this.sqlLog(logHeader, 'COMMIT');
|
|
151
190
|
return this.client.query('COMMIT');
|
|
152
191
|
}
|
|
153
192
|
async rollback() {
|
|
154
|
-
this.
|
|
193
|
+
this.sqlLog(logHeader, 'ROLLBACK');
|
|
155
194
|
return this.client.query('ROLLBACK');
|
|
156
195
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
console.debug(...args);
|
|
160
|
-
}
|
|
196
|
+
sqlLog(...args) {
|
|
197
|
+
this.settings.logger[this.settings.sqlLogLevel](...args);
|
|
161
198
|
}
|
|
162
199
|
}
|
|
163
200
|
exports.PgClient = PgClient;
|
package/lib/pg-pooling.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-pooling.js","sourceRoot":"","sources":["../src/pg-pooling.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,gDAAwB;AAExB,4CAA4C;AAC5C,+FAA+F;AAC/F,qFAAqF;AACrF,YAAE,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"pg-pooling.js","sourceRoot":"","sources":["../src/pg-pooling.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalwalletcorp/pg-pooling",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "This is a library for PostgreSQL connection pooling",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"ts-jest": "^29.4.9",
|
|
47
47
|
"ts-node": "^10.9.2",
|
|
48
48
|
"tsconfig-paths": "^4.2.0",
|
|
49
|
-
"typescript": "
|
|
49
|
+
"typescript": "6.0.3"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/pg-pooling.ts
CHANGED
|
@@ -6,8 +6,37 @@ import util from 'util';
|
|
|
6
6
|
// SQLの段階でカラムを指定してセレクトできる場合は`SELECT xx_date::TEXT`のように記述すればSQLレベルでYYYY-MM-DD文字列に変換される
|
|
7
7
|
pg.types.setTypeParser(1082, (v) => v);
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* ログの出力先
|
|
11
|
+
* 渡した場合、どのレベルを出力するかはこのロガーが決める
|
|
12
|
+
*/
|
|
13
|
+
export interface PgPoolLogger {
|
|
14
|
+
debug(...args: any[]): void;
|
|
15
|
+
info(...args: any[]): void;
|
|
16
|
+
warn(...args: any[]): void;
|
|
17
|
+
error(...args: any[]): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
export interface PgPoolOptions {
|
|
21
|
+
/** ログの出力先。未指定の場合はconsole(debug/info)とprocess.emitWarning(warn/error)に出力する */
|
|
22
|
+
logger?: PgPoolLogger;
|
|
23
|
+
/** loggerを渡さない場合のみ有効。trueの場合はdebug/infoも出力する */
|
|
10
24
|
debug?: boolean;
|
|
25
|
+
/** SQL/SQL RESULT/BEGIN/COMMIT/ROLLBACKを出力するレベル */
|
|
26
|
+
sqlLogLevel?: 'debug' | 'info';
|
|
27
|
+
/** 接続の保持がこの時間を超えた場合にリーク疑いとして警告する。0の場合は警告しない */
|
|
28
|
+
leakWarnMs?: number;
|
|
29
|
+
/** 接続の取得がこの時間を超えた場合に警告する。0の場合は警告しない */
|
|
30
|
+
acquireWarnMs?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* PgClientの動作設定。PgPoolOptionsの既定値を解決したもの
|
|
35
|
+
*/
|
|
36
|
+
export interface PgClientSettings {
|
|
37
|
+
logger: PgPoolLogger;
|
|
38
|
+
sqlLogLevel: 'debug' | 'info';
|
|
39
|
+
leakWarnMs: number;
|
|
11
40
|
}
|
|
12
41
|
|
|
13
42
|
const logHeader = '[PgPooling]';
|
|
@@ -21,17 +50,49 @@ const DEFAULT_KEEP_ALIVE = true;
|
|
|
21
50
|
const DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS = 10 * 1000;
|
|
22
51
|
|
|
23
52
|
// 接続の取得がこの時間を超えた場合に警告する。正常時の取得は新規接続の確立を含めても数百msで収まる
|
|
24
|
-
const
|
|
53
|
+
const DEFAULT_ACQUIRE_WARN_MS = 1000;
|
|
25
54
|
// 接続の保持がこの時間を超えた場合にリーク疑いとして警告する。
|
|
26
55
|
// 1クエリの上限(statement_timeout/query_timeout)より長く、サーバ側が放置セッションを切断するidle_in_transaction_session_timeoutと同じ長さ
|
|
27
|
-
const
|
|
56
|
+
const DEFAULT_LEAK_WARN_MS = 60 * 1000;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* loggerが渡されなかった場合のロガー
|
|
60
|
+
* 警告とエラーは呼び出し元が--no-warningsやprocess.on('warning')で抑止・捕捉できるようにprocess.emitWarningで出力する
|
|
61
|
+
*
|
|
62
|
+
* @param {boolean} debug trueの場合はdebug/infoもconsoleに出力する
|
|
63
|
+
* @returns {PgPoolLogger}
|
|
64
|
+
*/
|
|
65
|
+
function createDefaultLogger(debug: boolean): PgPoolLogger {
|
|
66
|
+
return {
|
|
67
|
+
debug: (...args: any[]) => {
|
|
68
|
+
if (debug) {
|
|
69
|
+
console.debug(...args);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
info: (...args: any[]) => {
|
|
73
|
+
if (debug) {
|
|
74
|
+
console.info(...args);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
warn: (...args: any[]) => process.emitWarning(util.format(...args)),
|
|
78
|
+
error: (...args: any[]) => process.emitWarning(util.format(...args))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
28
81
|
|
|
29
82
|
export class PgPool {
|
|
30
83
|
private pool: pg.Pool;
|
|
31
|
-
private readonly
|
|
84
|
+
private readonly logger: PgPoolLogger;
|
|
85
|
+
private readonly acquireWarnMs: number;
|
|
86
|
+
private readonly clientSettings: PgClientSettings;
|
|
32
87
|
|
|
33
88
|
constructor(config: pg.PoolConfig, options?: PgPoolOptions) {
|
|
34
|
-
this.
|
|
89
|
+
this.logger = options?.logger ?? createDefaultLogger(options?.debug ?? false);
|
|
90
|
+
this.acquireWarnMs = options?.acquireWarnMs ?? DEFAULT_ACQUIRE_WARN_MS;
|
|
91
|
+
this.clientSettings = {
|
|
92
|
+
logger: this.logger,
|
|
93
|
+
sqlLogLevel: options?.sqlLogLevel ?? 'debug',
|
|
94
|
+
leakWarnMs: options?.leakWarnMs ?? DEFAULT_LEAK_WARN_MS
|
|
95
|
+
};
|
|
35
96
|
// 各種タイムアウトの既定値を適用する。configで指定された項目はそのまま優先する
|
|
36
97
|
const mergedConfig: pg.PoolConfig = {
|
|
37
98
|
connectionTimeoutMillis: DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
@@ -46,7 +107,7 @@ export class PgPool {
|
|
|
46
107
|
this.pool.on('error', (error: Error) => {
|
|
47
108
|
// アイドル接続のエラーはプールに通知する
|
|
48
109
|
// リスナーが無いとプロセスが落ちるため必ず登録する
|
|
49
|
-
|
|
110
|
+
this.logger.error(logHeader, 'Idle client error', error);
|
|
50
111
|
});
|
|
51
112
|
}
|
|
52
113
|
|
|
@@ -61,13 +122,16 @@ export class PgPool {
|
|
|
61
122
|
const elapsedMs = Date.now() - start;
|
|
62
123
|
const stats = this.poolStats();
|
|
63
124
|
// 取得に時間がかかった、あるいは取得待ちの要求が滞留している場合はプール枯渇の前兆として警告する
|
|
64
|
-
if (
|
|
65
|
-
|
|
125
|
+
if (0 < this.acquireWarnMs && (this.acquireWarnMs <= elapsedMs || 0 < stats.waiting)) {
|
|
126
|
+
this.logger.warn(logHeader, 'POOL ACQUIRE SLOW', { elapsedMs, ...stats });
|
|
66
127
|
}
|
|
67
|
-
return new PgClient(client, this.
|
|
128
|
+
return new PgClient(client, this.clientSettings);
|
|
68
129
|
} catch (error) {
|
|
69
130
|
// connectionTimeoutMillisによる取得失敗を無言にしない
|
|
70
|
-
|
|
131
|
+
this.logger.error(logHeader, 'POOL CONNECT FAILED', {
|
|
132
|
+
elapsedMs: Date.now() - start,
|
|
133
|
+
...this.poolStats()
|
|
134
|
+
}, error);
|
|
71
135
|
throw error;
|
|
72
136
|
}
|
|
73
137
|
}
|
|
@@ -104,39 +168,44 @@ export class PgPool {
|
|
|
104
168
|
|
|
105
169
|
export class PgClient {
|
|
106
170
|
private client: pg.PoolClient;
|
|
107
|
-
private readonly
|
|
171
|
+
private readonly settings: PgClientSettings;
|
|
108
172
|
private acquiredAt = Date.now();
|
|
109
|
-
private leakTimer: ReturnType<typeof setTimeout
|
|
173
|
+
private leakTimer: ReturnType<typeof setTimeout> | undefined;
|
|
110
174
|
private leakWarned = false;
|
|
111
175
|
// 貸出中のクライアントに個別のエラーリスナーを設定
|
|
112
176
|
// これが無いとDB側の瞬断でリスナー不在のerrorイベントが発生し、プロセスが落ちる
|
|
113
177
|
private errorListener = (error: Error) => {
|
|
114
|
-
|
|
178
|
+
this.settings.logger.error(logHeader, 'Checked-out client error', error);
|
|
115
179
|
};
|
|
116
180
|
|
|
117
|
-
constructor(client: pg.PoolClient,
|
|
181
|
+
constructor(client: pg.PoolClient, settings: PgClientSettings) {
|
|
118
182
|
this.client = client;
|
|
119
|
-
this.
|
|
183
|
+
this.settings = settings;
|
|
120
184
|
this.client.on('error', this.errorListener);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
185
|
+
if (0 < settings.leakWarnMs) {
|
|
186
|
+
// リークした接続の取得元を特定できるように、取得時点のスタックトレースを保持する
|
|
187
|
+
const acquireStack = new Error('acquired here').stack;
|
|
188
|
+
this.leakTimer = setTimeout(() => {
|
|
189
|
+
this.leakWarned = true;
|
|
190
|
+
settings.logger.warn(logHeader, 'POOL LEAK SUSPECT', {
|
|
191
|
+
heldMs: Date.now() - this.acquiredAt,
|
|
192
|
+
acquireStack
|
|
193
|
+
});
|
|
194
|
+
}, settings.leakWarnMs);
|
|
195
|
+
// タイマーが残っていてもプロセスの終了を妨げないようにする
|
|
196
|
+
this.leakTimer.unref();
|
|
197
|
+
}
|
|
129
198
|
}
|
|
130
199
|
|
|
131
200
|
public async query<R extends pg.QueryResult = pg.QueryResult>(text: string, values?: any[], options?: {
|
|
132
201
|
suppressLog?: boolean
|
|
133
202
|
}): Promise<R> {
|
|
134
203
|
if (!options?.suppressLog) {
|
|
135
|
-
this.
|
|
204
|
+
this.sqlLog(logHeader, 'SQL', text, values);
|
|
136
205
|
}
|
|
137
206
|
const result = await this.client.query(text, values);
|
|
138
207
|
if (!options?.suppressLog) {
|
|
139
|
-
this.
|
|
208
|
+
this.sqlLog(logHeader, 'SQL RESULT', {
|
|
140
209
|
command: result.command,
|
|
141
210
|
rowCount: result.rowCount,
|
|
142
211
|
rows: Array.isArray(result.rows)
|
|
@@ -151,7 +220,7 @@ export class PgClient {
|
|
|
151
220
|
clearTimeout(this.leakTimer);
|
|
152
221
|
if (this.leakWarned) {
|
|
153
222
|
// リーク疑い警告後に返却されたことを記録する。警告後にこのログが無ければ本物のリークと判断できる
|
|
154
|
-
|
|
223
|
+
this.settings.logger.warn(logHeader, 'POOL RELEASE (after leak suspect)', { heldMs: Date.now() - this.acquiredAt });
|
|
155
224
|
}
|
|
156
225
|
// 返却後はプール側のリスナー(pool.on('error'))が受け持つ
|
|
157
226
|
this.client.removeListener('error', this.errorListener);
|
|
@@ -159,23 +228,21 @@ export class PgClient {
|
|
|
159
228
|
}
|
|
160
229
|
|
|
161
230
|
public async begin(): Promise<pg.QueryResult> {
|
|
162
|
-
this.
|
|
231
|
+
this.sqlLog(logHeader, 'BEGIN');
|
|
163
232
|
return this.client.query('BEGIN');
|
|
164
233
|
}
|
|
165
234
|
|
|
166
235
|
public async commit(): Promise<pg.QueryResult> {
|
|
167
|
-
this.
|
|
236
|
+
this.sqlLog(logHeader, 'COMMIT');
|
|
168
237
|
return this.client.query('COMMIT');
|
|
169
238
|
}
|
|
170
239
|
|
|
171
240
|
public async rollback(): Promise<pg.QueryResult> {
|
|
172
|
-
this.
|
|
241
|
+
this.sqlLog(logHeader, 'ROLLBACK');
|
|
173
242
|
return this.client.query('ROLLBACK');
|
|
174
243
|
}
|
|
175
244
|
|
|
176
|
-
private
|
|
177
|
-
|
|
178
|
-
console.debug(...args);
|
|
179
|
-
}
|
|
245
|
+
private sqlLog(...args: any[]): void {
|
|
246
|
+
this.settings.logger[this.settings.sqlLogLevel](...args);
|
|
180
247
|
}
|
|
181
248
|
}
|