@digitalwalletcorp/redis-pooling 1.0.1 → 1.1.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 +49 -27
- package/lib/redis-pooling.d.ts +31 -15
- package/lib/redis-pooling.js +202 -152
- package/lib/redis-pooling.js.map +1 -1
- package/package.json +1 -1
- package/src/redis-pooling.ts +219 -159
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ Designed for both server-side Node.js applications and cron-style background job
|
|
|
10
10
|
* **Connection Pooling**: Efficiently manage multiple Redis connections with `min` and `max` pool size configuration.
|
|
11
11
|
* **Safe Acquire/Release**: Guaranteed handling of Redis client lifecycle with optional database selection (`SELECT dbIndex`).
|
|
12
12
|
* **Custom Utilities**: Built-in helper methods like `getKeys(pattern)` and `deleteKeys(pattern)` for convenient Redis key operations.
|
|
13
|
-
* **Connection Validation**: PING-based validation before borrowing from the pool.
|
|
13
|
+
* **Connection Validation**: PING-based validation with timeout before borrowing from the pool.
|
|
14
14
|
* **TLS Support**: Optional TLS configuration for secure Redis connections.
|
|
15
15
|
|
|
16
|
-
#### 📦
|
|
16
|
+
#### 📦 Installation
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
npm install @digitalwalletcorp/redis-pooling
|
|
@@ -28,15 +28,13 @@ yarn add @digitalwalletcorp/redis-pooling
|
|
|
28
28
|
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.
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
import {
|
|
31
|
+
import { RedisPool } from '@digitalwalletcorp/redis-pooling';
|
|
32
32
|
|
|
33
|
-
const redisPool =
|
|
33
|
+
const redisPool = new RedisPool({
|
|
34
34
|
url: 'redis://localhost:6379',
|
|
35
|
-
|
|
35
|
+
dbIndex: 0,
|
|
36
36
|
max: 10,
|
|
37
|
-
min: 2
|
|
38
|
-
connectTimeout: 5000,
|
|
39
|
-
enableTls: false
|
|
37
|
+
min: 2
|
|
40
38
|
});
|
|
41
39
|
|
|
42
40
|
async function main() {
|
|
@@ -60,15 +58,13 @@ For long-running web applications, a singleton Redis pool ensures efficient reus
|
|
|
60
58
|
|
|
61
59
|
`@/server/singleton/redis-pooling`
|
|
62
60
|
```typescript
|
|
63
|
-
import {
|
|
61
|
+
import { RedisPool } from '@digitalwalletcorp/redis-pooling';
|
|
64
62
|
|
|
65
|
-
export const redisPool =
|
|
63
|
+
export const redisPool = new RedisPool({
|
|
66
64
|
url: 'redis://localhost:6379',
|
|
67
|
-
|
|
65
|
+
dbIndex: 0,
|
|
68
66
|
max: 20,
|
|
69
|
-
min: 5
|
|
70
|
-
connectTimeout: 5000,
|
|
71
|
-
enableTls: false
|
|
67
|
+
min: 5
|
|
72
68
|
});
|
|
73
69
|
```
|
|
74
70
|
|
|
@@ -89,7 +85,7 @@ async function handleRequest() {
|
|
|
89
85
|
|
|
90
86
|
##### Example 3: Using Custom Methods (`getKeys`, `deleteKeys`)
|
|
91
87
|
|
|
92
|
-
`
|
|
88
|
+
`RedisClient` extends the standard `ioredis` client with convenient helper methods.
|
|
93
89
|
`getKeys` and `deleteKeys` use `SCAN` and `UNLINK` internally to avoid blocking Redis with large datasets (unlike `KEYS`).
|
|
94
90
|
All other standard Redis commands remain available.
|
|
95
91
|
|
|
@@ -105,8 +101,10 @@ async function manageCache() {
|
|
|
105
101
|
console.log('Matching keys:', keys);
|
|
106
102
|
|
|
107
103
|
// Delete all matching keys using UNLINK
|
|
108
|
-
const
|
|
109
|
-
|
|
104
|
+
const delResults = await client.deleteKeys('cache:*');
|
|
105
|
+
const succeeded = delResults.filter(a => a.status === 'fulfilled');
|
|
106
|
+
const delCount = succeeded.reduce((acc, cur) => acc + (cur as PromiseFulfilledResult<number>).value, 0);
|
|
107
|
+
console.log(`Deleted ${delCount} keys`);
|
|
110
108
|
} finally {
|
|
111
109
|
await redisPool.release(client);
|
|
112
110
|
}
|
|
@@ -115,35 +113,59 @@ async function manageCache() {
|
|
|
115
113
|
|
|
116
114
|
#### 📚 API Reference
|
|
117
115
|
|
|
118
|
-
##### `
|
|
116
|
+
##### `new RedisPool(config: RedisConfig)`
|
|
119
117
|
|
|
120
|
-
Creates a
|
|
118
|
+
Creates a Redis connection pool.
|
|
121
119
|
|
|
122
120
|
| Property | Type | Default | Description |
|
|
123
121
|
| ---------------- | ------- | -------- | -------------------------------------- |
|
|
124
122
|
| `url` | string | required | Redis connection URL. |
|
|
125
|
-
| `
|
|
126
|
-
| `connectTimeout` | number | 5000 | Connection timeout in milliseconds. |
|
|
123
|
+
| `dbIndex` | number | 0 | Default Redis database index. |
|
|
127
124
|
| `max` | number | 10 | Maximum number of clients in the pool. |
|
|
128
125
|
| `min` | number | 0 | Minimum number of clients in the pool. |
|
|
126
|
+
| `connectTimeout` | number | 5000 | Connection timeout in milliseconds. |
|
|
127
|
+
| `testOnBorrow` | boolean | true | Enable connection validate on borrow. |
|
|
129
128
|
| `enableTls` | boolean | false | Enable TLS for Redis connection. |
|
|
130
129
|
|
|
131
|
-
##### `
|
|
130
|
+
##### `RedisPool` Methods
|
|
132
131
|
|
|
133
132
|
| Method | Signature | Description |
|
|
134
133
|
| ------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------- |
|
|
135
|
-
| `acquire(dbIndex?: number)` | `Promise<
|
|
136
|
-
| `release(client:
|
|
137
|
-
| `destroy()` | `Promise<void>`
|
|
134
|
+
| `acquire(dbIndex?: number)` | `Promise<RedisClient>` | Acquire a Redis client from the pool. Optionally switch to a different database index. |
|
|
135
|
+
| `release(client: RedisClient)` | `Promise<void>` | Release the Redis client back to the pool. |
|
|
136
|
+
| `destroy()` | `Promise<void>` | Drain and clear all connections in the pool. |
|
|
138
137
|
|
|
139
|
-
##### `
|
|
138
|
+
##### `RedisClient` Methods
|
|
140
139
|
|
|
141
140
|
Extends the standard `ioredis` `Redis` client with additional helpers:
|
|
142
141
|
|
|
143
142
|
| Method | Signature | Description |
|
|
144
143
|
| ----------------------------- | ------------------- | ----------------------------------------------------------------------------------------------- |
|
|
145
144
|
| `getKeys(pattern: string)` | `Promise<string[]>` | Scan and return all keys matching a pattern. |
|
|
146
|
-
| `deleteKeys(pattern: string)` | `Promise<number>` | Scan and delete all keys matching a pattern using `UNLINK`. Returns the number of deleted
|
|
145
|
+
| `deleteKeys(pattern: string)` | `Promise<PromiseSettledResult<number>[]>` | Scan and delete all keys matching a pattern using `UNLINK`. Returns an array of results, one for each batch processed by `UNLINK`. Each result contains the number of keys deleted in that batch. |
|
|
146
|
+
|
|
147
|
+
#### 🗄 Database Index Handling
|
|
148
|
+
|
|
149
|
+
This library provides a Redis connection pool capable of managing Redis clients
|
|
150
|
+
connected to different database indexes (`SELECT db`).
|
|
151
|
+
|
|
152
|
+
**Calling `acquire(dbIndex)`:**
|
|
153
|
+
|
|
154
|
+
> When `dbIndex` is specified, the acquired client will be connected to the specified database index.
|
|
155
|
+
|
|
156
|
+
**Calling `acquire()` without `dbIndex`:**
|
|
157
|
+
> When `dbIndex` is not specified, the acquired client will be connected to the default database index configured when the pool was created via **createRedisPool({ dbIndex })**.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// The default db index: 2
|
|
161
|
+
const pool = new RedisPool({ url, dbIndex: 2 });
|
|
162
|
+
|
|
163
|
+
// With dbIndex: connected to DB 1
|
|
164
|
+
const client1 = await pool.acquire(1);
|
|
165
|
+
|
|
166
|
+
// Without dbIndex: connected to DB 2 (default)
|
|
167
|
+
const client2 = await pool.acquire();
|
|
168
|
+
```
|
|
147
169
|
|
|
148
170
|
---
|
|
149
171
|
#### 💡 Notes
|
package/lib/redis-pooling.d.ts
CHANGED
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
import { Redis } from 'ioredis';
|
|
2
2
|
export interface RedisConfig {
|
|
3
3
|
url: string;
|
|
4
|
-
|
|
4
|
+
dbIndex?: number;
|
|
5
5
|
connectTimeout?: number;
|
|
6
6
|
max?: number;
|
|
7
7
|
min?: number;
|
|
8
8
|
testOnBorrow?: boolean;
|
|
9
9
|
enableTls?: boolean;
|
|
10
10
|
}
|
|
11
|
-
export interface
|
|
12
|
-
acquire(dbIndex?: number): Promise<ManagedRedisClient>;
|
|
13
|
-
release(client?: Redis): Promise<void>;
|
|
14
|
-
destroy(timeoutMs?: number): Promise<void>;
|
|
15
|
-
}
|
|
16
|
-
export interface ManagedRedisClient extends Redis {
|
|
11
|
+
export interface RedisClient extends Redis {
|
|
17
12
|
getKeys(pattern: string): Promise<string[]>;
|
|
18
|
-
deleteKeys(pattern: string): Promise<number>;
|
|
13
|
+
deleteKeys(pattern: string): Promise<PromiseSettledResult<number>[]>;
|
|
19
14
|
_originalDbIndex?: number;
|
|
20
15
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
export declare class RedisPool {
|
|
17
|
+
private readonly url;
|
|
18
|
+
private readonly db;
|
|
19
|
+
private readonly connectTimeout;
|
|
20
|
+
private readonly max;
|
|
21
|
+
private readonly min;
|
|
22
|
+
private readonly testOnBorrow;
|
|
23
|
+
private readonly tls?;
|
|
24
|
+
private readonly poolMap;
|
|
25
|
+
private initialized;
|
|
26
|
+
private readonly debug;
|
|
27
|
+
constructor(config: RedisConfig, options?: {
|
|
28
|
+
debug?: boolean;
|
|
29
|
+
});
|
|
30
|
+
acquire(dbIndex?: number): Promise<RedisClient>;
|
|
31
|
+
release(client?: RedisClient): Promise<void>;
|
|
32
|
+
destroy(timeoutMs?: number): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* 初めてacquireが呼ばれた時に、指定された接続情報で接続できるかチェックする
|
|
35
|
+
*
|
|
36
|
+
* @param {number} dbIndex
|
|
37
|
+
*/
|
|
38
|
+
private checkConnectivity;
|
|
39
|
+
private ping;
|
|
40
|
+
debugLog(...args: any[]): void;
|
|
41
|
+
private getPool;
|
|
42
|
+
private createSingleDbPool;
|
|
43
|
+
}
|
package/lib/redis-pooling.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.RedisPool = void 0;
|
|
37
37
|
const ioredis_1 = require("ioredis");
|
|
38
38
|
const genericPool = __importStar(require("generic-pool"));
|
|
39
39
|
const logHeader = '[RedisPooling]';
|
|
@@ -41,56 +41,188 @@ const DEFAULT_CONNECT_TIMEOUT = 5000;
|
|
|
41
41
|
const DEFAULT_MAX_POOLING_SIZE = 10;
|
|
42
42
|
const DEFAULT_MIN_POOLING_SIZE = 0;
|
|
43
43
|
const REDIS_PING_TIMEOUT_MS = 3000; // 3秒
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
class RedisPool {
|
|
45
|
+
url;
|
|
46
|
+
db;
|
|
47
|
+
connectTimeout;
|
|
48
|
+
max;
|
|
49
|
+
min;
|
|
50
|
+
testOnBorrow;
|
|
51
|
+
tls;
|
|
52
|
+
poolMap = new Map();
|
|
53
|
+
initialized = false;
|
|
54
|
+
debug;
|
|
55
|
+
constructor(config, options) {
|
|
56
|
+
if (!config.url) {
|
|
57
|
+
throw new Error(`${logHeader} Redis connection url is required.`);
|
|
58
|
+
}
|
|
59
|
+
this.url = config.url;
|
|
60
|
+
this.db = config.dbIndex ?? 0;
|
|
61
|
+
this.connectTimeout = config.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
|
|
62
|
+
this.max = config.max ?? DEFAULT_MAX_POOLING_SIZE;
|
|
63
|
+
this.min = config.min ?? DEFAULT_MIN_POOLING_SIZE;
|
|
64
|
+
this.testOnBorrow = config.testOnBorrow ?? true;
|
|
65
|
+
this.tls = config.enableTls ? { rejectUnauthorized: false } : undefined;
|
|
66
|
+
this.debug = options?.debug ?? false;
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
async acquire(dbIndex) {
|
|
69
|
+
if (!this.initialized) {
|
|
70
|
+
// 初回acquire呼び出し時のみ接続チェックを行う
|
|
71
|
+
// ホスト不正やパスワード不正による接続不可等を検知する
|
|
72
|
+
// ※ generic-poolのfactoryの方に入ってしまうとエラーを呼び出し元に伝播させることが難しいため、プールとは別の接続でチェックする
|
|
73
|
+
await this.checkConnectivity(dbIndex ?? this.db);
|
|
74
|
+
this.initialized = true;
|
|
75
|
+
}
|
|
76
|
+
const index = dbIndex ?? this.db;
|
|
77
|
+
const pool = this.getPool(index);
|
|
78
|
+
try {
|
|
79
|
+
const client = await pool.acquire();
|
|
80
|
+
this.debugLog(logHeader, `Redis client ${index} has been acquired.`);
|
|
81
|
+
return client;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async release(client) {
|
|
88
|
+
if (client) {
|
|
89
|
+
const dbIndex = client._originalDbIndex ?? this.db;
|
|
90
|
+
const pool = this.poolMap.get(dbIndex);
|
|
91
|
+
if (!pool) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let needDestroy = false;
|
|
95
|
+
switch (true) {
|
|
96
|
+
case client.status === 'end':
|
|
97
|
+
case client.status === 'close':
|
|
98
|
+
// Redisクライアントの状態が再利用できない場合はプールから破棄
|
|
99
|
+
needDestroy = true;
|
|
100
|
+
break;
|
|
101
|
+
case client.status === 'ready':
|
|
102
|
+
try {
|
|
103
|
+
// 元のDBインデックスに戻す
|
|
104
|
+
await client.select(dbIndex);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// selectに失敗する→Redisクライアントが不正な状態にあると判断できるのでプールから破棄
|
|
108
|
+
needDestroy = true;
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
}
|
|
113
|
+
if (needDestroy) {
|
|
114
|
+
// Redisクライアントの破棄
|
|
115
|
+
await pool.destroy(client);
|
|
116
|
+
this.debugLog(logHeader, `Redis client ${dbIndex} destroyed due to invalid status.`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Redisクライアントの返却
|
|
120
|
+
await pool.release(client);
|
|
121
|
+
this.debugLog(logHeader, `Redis client ${dbIndex} released.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async destroy(timeoutMs = 5000) {
|
|
126
|
+
for (const [dbIndex, pool] of this.poolMap.entries()) {
|
|
127
|
+
this.debugLog(logHeader, `Destroying Redis pool for DB index ${dbIndex}...`);
|
|
128
|
+
await Promise.race([
|
|
129
|
+
(async () => {
|
|
130
|
+
await pool.drain();
|
|
131
|
+
await pool.clear();
|
|
132
|
+
})(),
|
|
133
|
+
new Promise((_, reject) => {
|
|
134
|
+
setTimeout(() => reject(new Error(`${logHeader} Timeout while draining Redis pool for DB index ${dbIndex}`)), timeoutMs);
|
|
135
|
+
})
|
|
136
|
+
]);
|
|
137
|
+
this.debugLog(logHeader, `Redis pool for DB index ${dbIndex} destroyed.`);
|
|
138
|
+
this.poolMap.delete(dbIndex);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 初めてacquireが呼ばれた時に、指定された接続情報で接続できるかチェックする
|
|
143
|
+
*
|
|
144
|
+
* @param {number} dbIndex
|
|
145
|
+
*/
|
|
146
|
+
async checkConnectivity(dbIndex) {
|
|
147
|
+
const client = new ioredis_1.Redis(this.url, {
|
|
148
|
+
db: dbIndex,
|
|
149
|
+
retryStrategy: () => null, // 再接続無効
|
|
150
|
+
reconnectOnError: () => false, // 再接続無効
|
|
151
|
+
connectTimeout: this.connectTimeout,
|
|
152
|
+
tls: this.tls,
|
|
153
|
+
});
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const cleanup = () => {
|
|
156
|
+
client.quit().catch(() => client.disconnect());
|
|
157
|
+
};
|
|
158
|
+
client.once('ready', () => {
|
|
159
|
+
cleanup();
|
|
160
|
+
resolve();
|
|
161
|
+
});
|
|
162
|
+
client.once('error', (error) => {
|
|
163
|
+
cleanup();
|
|
164
|
+
reject(error);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async ping(client) {
|
|
169
|
+
try {
|
|
170
|
+
this.debugLog(logHeader, 'start validate');
|
|
171
|
+
const timeout = new Promise((_, reject) => {
|
|
172
|
+
setTimeout(() => reject(new Error(`Redis PING timeout after ${REDIS_PING_TIMEOUT_MS}ms`)), REDIS_PING_TIMEOUT_MS);
|
|
173
|
+
});
|
|
174
|
+
await Promise.race([
|
|
175
|
+
client.ping(),
|
|
176
|
+
timeout
|
|
177
|
+
]);
|
|
178
|
+
this.debugLog(logHeader, 'ping succeeded');
|
|
179
|
+
return client.status === 'ready';
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
this.debugLog(logHeader, 'ping failed');
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
debugLog(...args) {
|
|
187
|
+
if (this.debug) {
|
|
188
|
+
console.debug(...args);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
getPool(dbIndex) {
|
|
192
|
+
let pool = this.poolMap.get(dbIndex);
|
|
193
|
+
if (!pool) {
|
|
194
|
+
pool = this.createSingleDbPool(dbIndex);
|
|
195
|
+
this.poolMap.set(dbIndex, pool);
|
|
196
|
+
}
|
|
197
|
+
return pool;
|
|
198
|
+
}
|
|
199
|
+
createSingleDbPool(dbIndex) {
|
|
63
200
|
const factory = {
|
|
64
201
|
create: async () => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (process.env.NODE_ENV !== 'test') {
|
|
76
|
-
console.debug(logHeader, `retry strategy called ${times} times. delaying ${delay}ms`);
|
|
77
|
-
}
|
|
78
|
-
return delay;
|
|
79
|
-
},
|
|
80
|
-
reconnectOnError: (error) => {
|
|
81
|
-
process.emitWarning(`${logHeader} detected error (on reconnect). ${error.message}`);
|
|
82
|
-
return true;
|
|
202
|
+
const client = new ioredis_1.Redis(this.url, {
|
|
203
|
+
db: dbIndex,
|
|
204
|
+
connectTimeout: this.connectTimeout,
|
|
205
|
+
keepAlive: 1,
|
|
206
|
+
enableOfflineQueue: true,
|
|
207
|
+
tls: this.tls,
|
|
208
|
+
retryStrategy: (times) => {
|
|
209
|
+
const delay = Math.min(times * 50, 1000);
|
|
210
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
211
|
+
this.debugLog(logHeader, `retry strategy called ${times} times. delaying ${delay}ms`);
|
|
83
212
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
213
|
+
return delay;
|
|
214
|
+
},
|
|
215
|
+
reconnectOnError: (error) => {
|
|
216
|
+
process.emitWarning(`${logHeader} detected error (on reconnectOnError). ${error.message}`);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
90
220
|
client._originalDbIndex = dbIndex;
|
|
91
221
|
client.on('error', error => {
|
|
92
222
|
process.emitWarning(`${logHeader} detected error (on error). ${error.message}`);
|
|
93
223
|
});
|
|
224
|
+
// カスタムメソッド内の処理でthis.debugLogなどが参照できなくなるため、poolのインスタンスを変数キャプチャする
|
|
225
|
+
const poolInstance = this;
|
|
94
226
|
// カスタムメソッド START
|
|
95
227
|
/**
|
|
96
228
|
* 指定されたパターンに一致するキーを Redis から全て取得する
|
|
@@ -112,19 +244,29 @@ const createRedisPool = (config) => {
|
|
|
112
244
|
});
|
|
113
245
|
stream.on('end', () => resolve(allKeys));
|
|
114
246
|
stream.on('error', (err) => {
|
|
115
|
-
console.error(logHeader, `Error during getKeys scan for pattern '${pattern}'.`, err);
|
|
116
247
|
reject(err);
|
|
117
248
|
});
|
|
118
249
|
});
|
|
119
250
|
};
|
|
120
251
|
/**
|
|
121
252
|
* 指定されたパターンに一致するキーを Redis から全て削除する (UNLINKを使用)
|
|
253
|
+
* 返却値の配列サイズはscanStreamが'data'を受信した回数で、この受信したデータで削除された件数がvalueに設定されている。
|
|
254
|
+
*
|
|
255
|
+
* [
|
|
256
|
+
* { status: 'fulfilled', value: 100 }, // 1バッチ目で100件削除
|
|
257
|
+
* { status: 'fulfilled', value: 80 } // 2バッチ目で80件削除
|
|
258
|
+
* ]
|
|
259
|
+
*
|
|
260
|
+
* 成功した件数は以下のようにして取得可能
|
|
261
|
+
*
|
|
262
|
+
* const delResults = await redisClient.deleteKeys('pattern');
|
|
263
|
+
* const delCount = delResults.filter(a => a.status === 'fulfilled')
|
|
264
|
+
* .reduce((acc, cur) => acc + (cur as PromiseFulfilledResult<number>).value, 0);
|
|
122
265
|
*
|
|
123
266
|
* @param {string} pattern
|
|
124
|
-
* @returns {Promise<number
|
|
267
|
+
* @returns {Promise<PromiseSettledResult<number>>}
|
|
125
268
|
*/
|
|
126
269
|
client.deleteKeys = async function (pattern) {
|
|
127
|
-
let deletedCount = 0;
|
|
128
270
|
const stream = this.scanStream({
|
|
129
271
|
match: pattern,
|
|
130
272
|
count: 1000 // 1度にスキャンする件数
|
|
@@ -134,24 +276,17 @@ const createRedisPool = (config) => {
|
|
|
134
276
|
stream.on('data', async (keys) => {
|
|
135
277
|
if (keys.length) {
|
|
136
278
|
tasks.push((async () => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
deletedCount += unlinkResult;
|
|
141
|
-
console.debug(logHeader, `Deleted ${unlinkResult} keys in a batch for pattern '${pattern}'. Total: ${deletedCount}`);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
process.emitWarning(`${logHeader} Error during UNLINK for pattern '${pattern}'. ${error.message}`);
|
|
145
|
-
}
|
|
279
|
+
// UNLINK を使用し、現在のインスタンス (this) で実行
|
|
280
|
+
const unlinkResult = await this.unlink(...keys);
|
|
281
|
+
return unlinkResult;
|
|
146
282
|
})());
|
|
147
283
|
}
|
|
148
284
|
});
|
|
149
285
|
stream.on('end', async () => {
|
|
150
|
-
await Promise.
|
|
151
|
-
resolve(
|
|
286
|
+
const results = await Promise.allSettled(tasks);
|
|
287
|
+
resolve(results);
|
|
152
288
|
});
|
|
153
289
|
stream.on('error', (err) => {
|
|
154
|
-
console.error(logHeader, `Error during deleteKeys scan for pattern '${pattern}'.`, err);
|
|
155
290
|
reject(err);
|
|
156
291
|
});
|
|
157
292
|
});
|
|
@@ -180,108 +315,23 @@ const createRedisPool = (config) => {
|
|
|
180
315
|
destroy: async (client) => {
|
|
181
316
|
try {
|
|
182
317
|
await client.quit();
|
|
183
|
-
|
|
318
|
+
this.debugLog(logHeader, 'client quit');
|
|
184
319
|
}
|
|
185
320
|
catch (error) {
|
|
186
321
|
client.disconnect();
|
|
187
|
-
|
|
322
|
+
this.debugLog(logHeader, 'client disconnected');
|
|
188
323
|
}
|
|
189
324
|
},
|
|
190
325
|
validate: async (client) => {
|
|
191
|
-
|
|
192
|
-
console.debug(logHeader, 'start validate');
|
|
193
|
-
const timeout = new Promise((_, reject) => {
|
|
194
|
-
setTimeout(() => reject(new Error(`Redis PING timeout after ${REDIS_PING_TIMEOUT_MS}ms`)), REDIS_PING_TIMEOUT_MS);
|
|
195
|
-
});
|
|
196
|
-
await Promise.race([
|
|
197
|
-
client.ping(),
|
|
198
|
-
timeout
|
|
199
|
-
]);
|
|
200
|
-
console.debug(logHeader, 'ping succeeded');
|
|
201
|
-
return client.status === 'ready';
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
console.debug(logHeader, 'ping failed');
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
326
|
+
return await this.ping(client);
|
|
207
327
|
}
|
|
208
328
|
};
|
|
209
329
|
return genericPool.createPool(factory, {
|
|
210
|
-
max,
|
|
211
|
-
min,
|
|
212
|
-
testOnBorrow
|
|
330
|
+
max: this.max,
|
|
331
|
+
min: this.min,
|
|
332
|
+
testOnBorrow: this.testOnBorrow
|
|
213
333
|
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (!pool) {
|
|
218
|
-
pool = createSingleDbPool(dbIndex);
|
|
219
|
-
poolMap.set(dbIndex, pool);
|
|
220
|
-
}
|
|
221
|
-
return pool;
|
|
222
|
-
};
|
|
223
|
-
return {
|
|
224
|
-
acquire: async (dbIndex) => {
|
|
225
|
-
const index = dbIndex ?? 0;
|
|
226
|
-
const pool = getPool(index);
|
|
227
|
-
const client = await pool.acquire();
|
|
228
|
-
console.debug(logHeader, `Redis client ${index} has been acquired.`);
|
|
229
|
-
return client;
|
|
230
|
-
},
|
|
231
|
-
release: async (client) => {
|
|
232
|
-
if (client) {
|
|
233
|
-
const dbIndex = client._originalDbIndex ?? db;
|
|
234
|
-
let needDestroy = false;
|
|
235
|
-
switch (true) {
|
|
236
|
-
case client.status === 'end':
|
|
237
|
-
case client.status === 'close':
|
|
238
|
-
// Redisクライアントの状態が再利用できない場合はプールから破棄
|
|
239
|
-
needDestroy = true;
|
|
240
|
-
break;
|
|
241
|
-
case client.status === 'ready':
|
|
242
|
-
try {
|
|
243
|
-
// 元のDBインデックスに戻す
|
|
244
|
-
await client.select(dbIndex);
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
// selectに失敗する→Redisクライアントが不正な状態にあると判断できるのでプールから破棄
|
|
248
|
-
needDestroy = true;
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
251
|
-
default:
|
|
252
|
-
}
|
|
253
|
-
const pool = poolMap.get(dbIndex);
|
|
254
|
-
if (pool) {
|
|
255
|
-
if (needDestroy) {
|
|
256
|
-
// Redisクライアントの破棄
|
|
257
|
-
await pool.destroy(client);
|
|
258
|
-
console.debug(logHeader, `Redis client ${dbIndex} destroyed due to invalid status.`);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
// Redisクライアントの返却
|
|
262
|
-
await pool.release(client);
|
|
263
|
-
console.debug(logHeader, `Redis client ${dbIndex} released.`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
destroy: async (timeoutMs = 5000) => {
|
|
269
|
-
for (const [dbIndex, pool] of poolMap.entries()) {
|
|
270
|
-
console.debug(logHeader, `Destroying Redis pool for DB index ${dbIndex}...`);
|
|
271
|
-
await Promise.race([
|
|
272
|
-
(async () => {
|
|
273
|
-
await pool.drain();
|
|
274
|
-
await pool.clear();
|
|
275
|
-
})(),
|
|
276
|
-
new Promise((_, reject) => {
|
|
277
|
-
setTimeout(() => reject(new Error(`${logHeader} Timeout while draining Redis pool for DB index ${dbIndex}`)), timeoutMs);
|
|
278
|
-
})
|
|
279
|
-
]);
|
|
280
|
-
console.debug(logHeader, `Redis pool for DB index ${dbIndex} destroyed.`);
|
|
281
|
-
poolMap.delete(dbIndex);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
};
|
|
286
|
-
exports.createRedisPool = createRedisPool;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
exports.RedisPool = RedisPool;
|
|
287
337
|
//# sourceMappingURL=redis-pooling.js.map
|
package/lib/redis-pooling.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-pooling.js","sourceRoot":"","sources":["../src/redis-pooling.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAgC;AAChC,0DAA4C;
|
|
1
|
+
{"version":3,"file":"redis-pooling.js","sourceRoot":"","sources":["../src/redis-pooling.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/redis-pooling.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as genericPool from 'generic-pool';
|
|
|
3
3
|
|
|
4
4
|
export interface RedisConfig {
|
|
5
5
|
url: string;
|
|
6
|
-
|
|
6
|
+
dbIndex?: number;
|
|
7
7
|
connectTimeout?: number;
|
|
8
8
|
max?: number;
|
|
9
9
|
min?: number;
|
|
@@ -11,15 +11,9 @@ export interface RedisConfig {
|
|
|
11
11
|
enableTls?: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export interface
|
|
15
|
-
acquire(dbIndex?: number): Promise<ManagedRedisClient>;
|
|
16
|
-
release(client?: Redis): Promise<void>;
|
|
17
|
-
destroy(timeoutMs?: number): Promise<void>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ManagedRedisClient extends Redis {
|
|
14
|
+
export interface RedisClient extends Redis {
|
|
21
15
|
getKeys(pattern: string): Promise<string[]>;
|
|
22
|
-
deleteKeys(pattern: string): Promise<number>;
|
|
16
|
+
deleteKeys(pattern: string): Promise<PromiseSettledResult<number>[]>;
|
|
23
17
|
_originalDbIndex?: number; // 内部状態管理用変数
|
|
24
18
|
}
|
|
25
19
|
|
|
@@ -29,61 +23,207 @@ const DEFAULT_MAX_POOLING_SIZE = 10;
|
|
|
29
23
|
const DEFAULT_MIN_POOLING_SIZE = 0;
|
|
30
24
|
const REDIS_PING_TIMEOUT_MS = 3000; // 3秒
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
export class RedisPool {
|
|
27
|
+
|
|
28
|
+
private readonly url: string;
|
|
29
|
+
private readonly db: number;
|
|
30
|
+
private readonly connectTimeout: number;
|
|
31
|
+
private readonly max: number;
|
|
32
|
+
private readonly min: number;
|
|
33
|
+
private readonly testOnBorrow: boolean;
|
|
34
|
+
private readonly tls?: { rejectUnauthorized: false };
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
private readonly poolMap = new Map<number, genericPool.Pool<RedisClient>>();
|
|
37
|
+
private initialized = false;
|
|
38
|
+
private readonly debug: boolean;
|
|
39
|
+
|
|
40
|
+
constructor(config: RedisConfig, options?: {
|
|
41
|
+
debug?: boolean;
|
|
42
|
+
}) {
|
|
43
|
+
if (!config.url) {
|
|
44
|
+
throw new Error(`${logHeader} Redis connection url is required.`);
|
|
45
|
+
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
this.url = config.url;
|
|
48
|
+
this.db = config.dbIndex ?? 0;
|
|
49
|
+
this.connectTimeout = config.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
|
|
50
|
+
this.max = config.max ?? DEFAULT_MAX_POOLING_SIZE;
|
|
51
|
+
this.min = config.min ?? DEFAULT_MIN_POOLING_SIZE;
|
|
52
|
+
this.testOnBorrow = config.testOnBorrow ?? true;
|
|
53
|
+
this.tls = config.enableTls ? { rejectUnauthorized: false } : undefined;
|
|
54
|
+
|
|
55
|
+
this.debug = options?.debug ?? false;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
public async acquire(dbIndex?: number): Promise<RedisClient> {
|
|
59
|
+
if (!this.initialized) {
|
|
60
|
+
// 初回acquire呼び出し時のみ接続チェックを行う
|
|
61
|
+
// ホスト不正やパスワード不正による接続不可等を検知する
|
|
62
|
+
// ※ generic-poolのfactoryの方に入ってしまうとエラーを呼び出し元に伝播させることが難しいため、プールとは別の接続でチェックする
|
|
63
|
+
await this.checkConnectivity(dbIndex ?? this.db);
|
|
64
|
+
this.initialized = true;
|
|
65
|
+
}
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
const index = dbIndex ?? this.db;
|
|
68
|
+
const pool = this.getPool(index);
|
|
69
|
+
try {
|
|
70
|
+
const client = await pool.acquire();
|
|
71
|
+
|
|
72
|
+
this.debugLog(logHeader, `Redis client ${index} has been acquired.`);
|
|
73
|
+
return client;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async release(client?: RedisClient): Promise<void> {
|
|
80
|
+
if (client) {
|
|
81
|
+
const dbIndex = client._originalDbIndex ?? this.db;
|
|
82
|
+
const pool = this.poolMap.get(dbIndex);
|
|
83
|
+
if (!pool) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let needDestroy = false;
|
|
87
|
+
switch (true) {
|
|
88
|
+
case client.status === 'end':
|
|
89
|
+
case client.status === 'close':
|
|
90
|
+
// Redisクライアントの状態が再利用できない場合はプールから破棄
|
|
91
|
+
needDestroy = true;
|
|
92
|
+
break;
|
|
93
|
+
case client.status === 'ready':
|
|
94
|
+
try {
|
|
95
|
+
// 元のDBインデックスに戻す
|
|
96
|
+
await client.select(dbIndex);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// selectに失敗する→Redisクライアントが不正な状態にあると判断できるのでプールから破棄
|
|
99
|
+
needDestroy = true;
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
}
|
|
104
|
+
if (needDestroy) {
|
|
105
|
+
// Redisクライアントの破棄
|
|
106
|
+
await pool.destroy(client);
|
|
107
|
+
this.debugLog(logHeader, `Redis client ${dbIndex} destroyed due to invalid status.`);
|
|
108
|
+
} else {
|
|
109
|
+
// Redisクライアントの返却
|
|
110
|
+
await pool.release(client);
|
|
111
|
+
this.debugLog(logHeader, `Redis client ${dbIndex} released.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public async destroy(timeoutMs = 5000): Promise<void> {
|
|
117
|
+
for (const [dbIndex, pool] of this.poolMap.entries()) {
|
|
118
|
+
this.debugLog(logHeader, `Destroying Redis pool for DB index ${dbIndex}...`);
|
|
119
|
+
await Promise.race([
|
|
120
|
+
(async () => {
|
|
121
|
+
await pool.drain();
|
|
122
|
+
await pool.clear();
|
|
123
|
+
})(),
|
|
124
|
+
new Promise<void>((_, reject) => {
|
|
125
|
+
setTimeout(() => reject(new Error(`${logHeader} Timeout while draining Redis pool for DB index ${dbIndex}`)), timeoutMs);
|
|
126
|
+
})
|
|
127
|
+
]);
|
|
128
|
+
this.debugLog(logHeader, `Redis pool for DB index ${dbIndex} destroyed.`);
|
|
129
|
+
this.poolMap.delete(dbIndex);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 初めてacquireが呼ばれた時に、指定された接続情報で接続できるかチェックする
|
|
135
|
+
*
|
|
136
|
+
* @param {number} dbIndex
|
|
137
|
+
*/
|
|
138
|
+
private async checkConnectivity(dbIndex: number): Promise<void> {
|
|
139
|
+
const client = new Redis(this.url, {
|
|
140
|
+
db: dbIndex,
|
|
141
|
+
retryStrategy: () => null, // 再接続無効
|
|
142
|
+
reconnectOnError: () => false, // 再接続無効
|
|
143
|
+
connectTimeout: this.connectTimeout,
|
|
144
|
+
tls: this.tls,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return new Promise<void>((resolve, reject) => {
|
|
148
|
+
const cleanup = () => {
|
|
149
|
+
client.quit().catch(() => client.disconnect());
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
client.once('ready', () => {
|
|
153
|
+
cleanup();
|
|
154
|
+
resolve();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
client.once('error', (error) => {
|
|
158
|
+
cleanup();
|
|
159
|
+
reject(error);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async ping(client: Redis): Promise<boolean> {
|
|
165
|
+
try {
|
|
166
|
+
this.debugLog(logHeader, 'start validate');
|
|
167
|
+
const timeout = new Promise<void>((_, reject) => {
|
|
168
|
+
setTimeout(() => reject(new Error(`Redis PING timeout after ${REDIS_PING_TIMEOUT_MS}ms`)), REDIS_PING_TIMEOUT_MS);
|
|
169
|
+
});
|
|
170
|
+
await Promise.race([
|
|
171
|
+
client.ping(),
|
|
172
|
+
timeout
|
|
173
|
+
]);
|
|
174
|
+
this.debugLog(logHeader, 'ping succeeded');
|
|
175
|
+
return client.status === 'ready';
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
this.debugLog(logHeader, 'ping failed');
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public debugLog(...args: any[]): void {
|
|
183
|
+
if (this.debug) {
|
|
184
|
+
console.debug(...args);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private getPool(dbIndex: number): genericPool.Pool<RedisClient> {
|
|
189
|
+
let pool = this.poolMap.get(dbIndex);
|
|
190
|
+
if (!pool) {
|
|
191
|
+
pool = this.createSingleDbPool(dbIndex);
|
|
192
|
+
this.poolMap.set(dbIndex, pool);
|
|
193
|
+
}
|
|
194
|
+
return pool;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private createSingleDbPool(dbIndex: number): genericPool.Pool<RedisClient> {
|
|
198
|
+
const factory: genericPool.Factory<RedisClient> = {
|
|
199
|
+
create: async (): Promise<RedisClient> => {
|
|
200
|
+
const client = new Redis(this.url, {
|
|
201
|
+
db: dbIndex,
|
|
202
|
+
connectTimeout: this.connectTimeout,
|
|
203
|
+
keepAlive: 1,
|
|
204
|
+
enableOfflineQueue: true,
|
|
205
|
+
tls: this.tls,
|
|
206
|
+
retryStrategy: (times) => {
|
|
207
|
+
const delay = Math.min(times * 50, 1000);
|
|
208
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
209
|
+
this.debugLog(logHeader, `retry strategy called ${times} times. delaying ${delay}ms`);
|
|
75
210
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
211
|
+
return delay;
|
|
212
|
+
},
|
|
213
|
+
reconnectOnError: (error) => {
|
|
214
|
+
process.emitWarning(`${logHeader} detected error (on reconnectOnError). ${error.message}`);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}) as RedisClient;
|
|
81
218
|
|
|
82
219
|
client._originalDbIndex = dbIndex;
|
|
83
220
|
client.on('error', error => {
|
|
84
221
|
process.emitWarning(`${logHeader} detected error (on error). ${error.message}`);
|
|
85
222
|
});
|
|
86
223
|
|
|
224
|
+
// カスタムメソッド内の処理でthis.debugLogなどが参照できなくなるため、poolのインスタンスを変数キャプチャする
|
|
225
|
+
const poolInstance = this;
|
|
226
|
+
|
|
87
227
|
// カスタムメソッド START
|
|
88
228
|
|
|
89
229
|
/**
|
|
@@ -107,46 +247,50 @@ export const createRedisPool = (config: RedisConfig): ManagedRedisPool => {
|
|
|
107
247
|
});
|
|
108
248
|
stream.on('end', () => resolve(allKeys));
|
|
109
249
|
stream.on('error', (err: Error) => {
|
|
110
|
-
console.error(logHeader, `Error during getKeys scan for pattern '${pattern}'.`, err);
|
|
111
250
|
reject(err);
|
|
112
251
|
});
|
|
113
252
|
});
|
|
114
253
|
};
|
|
115
254
|
/**
|
|
116
255
|
* 指定されたパターンに一致するキーを Redis から全て削除する (UNLINKを使用)
|
|
256
|
+
* 返却値の配列サイズはscanStreamが'data'を受信した回数で、この受信したデータで削除された件数がvalueに設定されている。
|
|
257
|
+
*
|
|
258
|
+
* [
|
|
259
|
+
* { status: 'fulfilled', value: 100 }, // 1バッチ目で100件削除
|
|
260
|
+
* { status: 'fulfilled', value: 80 } // 2バッチ目で80件削除
|
|
261
|
+
* ]
|
|
262
|
+
*
|
|
263
|
+
* 成功した件数は以下のようにして取得可能
|
|
264
|
+
*
|
|
265
|
+
* const delResults = await redisClient.deleteKeys('pattern');
|
|
266
|
+
* const delCount = delResults.filter(a => a.status === 'fulfilled')
|
|
267
|
+
* .reduce((acc, cur) => acc + (cur as PromiseFulfilledResult<number>).value, 0);
|
|
117
268
|
*
|
|
118
269
|
* @param {string} pattern
|
|
119
|
-
* @returns {Promise<number
|
|
270
|
+
* @returns {Promise<PromiseSettledResult<number>>}
|
|
120
271
|
*/
|
|
121
|
-
client.deleteKeys = async function(pattern: string): Promise<number> {
|
|
122
|
-
let deletedCount = 0;
|
|
272
|
+
client.deleteKeys = async function(pattern: string): Promise<PromiseSettledResult<number>[]> {
|
|
123
273
|
const stream = this.scanStream({
|
|
124
274
|
match: pattern,
|
|
125
275
|
count: 1000 // 1度にスキャンする件数
|
|
126
276
|
});
|
|
127
277
|
|
|
128
278
|
return new Promise((resolve, reject) => {
|
|
129
|
-
const tasks: Promise<
|
|
279
|
+
const tasks: Promise<number>[] = [];
|
|
130
280
|
stream.on('data', async (keys: string[]) => {
|
|
131
281
|
if (keys.length) {
|
|
132
282
|
tasks.push((async () => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
deletedCount += unlinkResult;
|
|
137
|
-
console.debug(logHeader, `Deleted ${unlinkResult} keys in a batch for pattern '${pattern}'. Total: ${deletedCount}`);
|
|
138
|
-
} catch (error: any) {
|
|
139
|
-
process.emitWarning(`${logHeader} Error during UNLINK for pattern '${pattern}'. ${error.message}`);
|
|
140
|
-
}
|
|
283
|
+
// UNLINK を使用し、現在のインスタンス (this) で実行
|
|
284
|
+
const unlinkResult = await this.unlink(...keys);
|
|
285
|
+
return unlinkResult;
|
|
141
286
|
})());
|
|
142
287
|
}
|
|
143
288
|
});
|
|
144
289
|
stream.on('end', async () => {
|
|
145
|
-
await Promise.
|
|
146
|
-
resolve(
|
|
290
|
+
const results = await Promise.allSettled(tasks);
|
|
291
|
+
resolve(results);
|
|
147
292
|
});
|
|
148
293
|
stream.on('error', (err: Error) => {
|
|
149
|
-
console.error(logHeader, `Error during deleteKeys scan for pattern '${pattern}'.`, err);
|
|
150
294
|
reject(err);
|
|
151
295
|
});
|
|
152
296
|
});
|
|
@@ -177,105 +321,21 @@ export const createRedisPool = (config: RedisConfig): ManagedRedisPool => {
|
|
|
177
321
|
destroy: async (client: Redis) => {
|
|
178
322
|
try {
|
|
179
323
|
await client.quit();
|
|
180
|
-
|
|
324
|
+
this.debugLog(logHeader, 'client quit');
|
|
181
325
|
} catch (error) {
|
|
182
326
|
client.disconnect();
|
|
183
|
-
|
|
327
|
+
this.debugLog(logHeader, 'client disconnected');
|
|
184
328
|
}
|
|
185
329
|
},
|
|
186
330
|
validate: async (client: Redis) => {
|
|
187
|
-
|
|
188
|
-
console.debug(logHeader, 'start validate');
|
|
189
|
-
const timeout = new Promise<void>((_, reject) => {
|
|
190
|
-
setTimeout(() => reject(new Error(`Redis PING timeout after ${REDIS_PING_TIMEOUT_MS}ms`)), REDIS_PING_TIMEOUT_MS);
|
|
191
|
-
});
|
|
192
|
-
await Promise.race([
|
|
193
|
-
client.ping(),
|
|
194
|
-
timeout
|
|
195
|
-
]);
|
|
196
|
-
console.debug(logHeader, 'ping succeeded');
|
|
197
|
-
return client.status === 'ready';
|
|
198
|
-
} catch (error) {
|
|
199
|
-
console.debug(logHeader, 'ping failed');
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
331
|
+
return await this.ping(client);
|
|
202
332
|
}
|
|
203
333
|
};
|
|
204
334
|
|
|
205
335
|
return genericPool.createPool(factory, {
|
|
206
|
-
max,
|
|
207
|
-
min,
|
|
208
|
-
testOnBorrow
|
|
336
|
+
max: this.max,
|
|
337
|
+
min: this.min,
|
|
338
|
+
testOnBorrow: this.testOnBorrow
|
|
209
339
|
});
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const getPool = (dbIndex: number) => {
|
|
213
|
-
let pool = poolMap.get(dbIndex);
|
|
214
|
-
if (!pool) {
|
|
215
|
-
pool = createSingleDbPool(dbIndex);
|
|
216
|
-
poolMap.set(dbIndex, pool);
|
|
217
|
-
}
|
|
218
|
-
return pool;
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
acquire: async (dbIndex?: number): Promise<ManagedRedisClient> => {
|
|
223
|
-
const index = dbIndex ?? 0;
|
|
224
|
-
const pool = getPool(index);
|
|
225
|
-
const client = await pool.acquire();
|
|
226
|
-
console.debug(logHeader, `Redis client ${index} has been acquired.`);
|
|
227
|
-
return client;
|
|
228
|
-
},
|
|
229
|
-
release: async (client?: ManagedRedisClient): Promise<void> => {
|
|
230
|
-
if (client) {
|
|
231
|
-
const dbIndex = client._originalDbIndex ?? db;
|
|
232
|
-
let needDestroy = false;
|
|
233
|
-
switch (true) {
|
|
234
|
-
case client.status === 'end':
|
|
235
|
-
case client.status === 'close':
|
|
236
|
-
// Redisクライアントの状態が再利用できない場合はプールから破棄
|
|
237
|
-
needDestroy = true;
|
|
238
|
-
break;
|
|
239
|
-
case client.status === 'ready':
|
|
240
|
-
try {
|
|
241
|
-
// 元のDBインデックスに戻す
|
|
242
|
-
await client.select(dbIndex);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
// selectに失敗する→Redisクライアントが不正な状態にあると判断できるのでプールから破棄
|
|
245
|
-
needDestroy = true;
|
|
246
|
-
}
|
|
247
|
-
break;
|
|
248
|
-
default:
|
|
249
|
-
}
|
|
250
|
-
const pool = poolMap.get(dbIndex);
|
|
251
|
-
if (pool) {
|
|
252
|
-
if (needDestroy) {
|
|
253
|
-
// Redisクライアントの破棄
|
|
254
|
-
await pool.destroy(client);
|
|
255
|
-
console.debug(logHeader, `Redis client ${dbIndex} destroyed due to invalid status.`);
|
|
256
|
-
} else {
|
|
257
|
-
// Redisクライアントの返却
|
|
258
|
-
await pool.release(client);
|
|
259
|
-
console.debug(logHeader, `Redis client ${dbIndex} released.`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
destroy: async (timeoutMs = 5000): Promise<void> => {
|
|
265
|
-
for (const [dbIndex, pool] of poolMap.entries()) {
|
|
266
|
-
console.debug(logHeader, `Destroying Redis pool for DB index ${dbIndex}...`);
|
|
267
|
-
await Promise.race([
|
|
268
|
-
(async () => {
|
|
269
|
-
await pool.drain();
|
|
270
|
-
await pool.clear();
|
|
271
|
-
})(),
|
|
272
|
-
new Promise<void>((_, reject) => {
|
|
273
|
-
setTimeout(() => reject(new Error(`${logHeader} Timeout while draining Redis pool for DB index ${dbIndex}`)), timeoutMs);
|
|
274
|
-
})
|
|
275
|
-
]);
|
|
276
|
-
console.debug(logHeader, `Redis pool for DB index ${dbIndex} destroyed.`);
|
|
277
|
-
poolMap.delete(dbIndex);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
340
|
}
|
|
281
|
-
}
|
|
341
|
+
}
|