@digitalwalletcorp/redis-pooling 1.0.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 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,156 @@
1
+ # Redis Pooling
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/%40digitalwalletcorp%2Fredis-pooling)](https://www.npmjs.com/package/@digitalwalletcorp/redis-pooling) [![License](https://img.shields.io/npm/l/%40digitalwalletcorp%2Fredis-pooling)](https://opensource.org/licenses/MIT) [![Build Status](https://img.shields.io/github/actions/workflow/status/digitalwalletcorp/redis-pooling/ci.yml?branch=main)](https://github.com/digitalwalletcorp/redis-pooling/actions) [![Test Coverage](https://img.shields.io/codecov/c/github/digitalwalletcorp/redis-pooling.svg)](https://codecov.io/gh/digitalwalletcorp/redis-pooling)
4
+
5
+ A lightweight TypeScript/JavaScript library for managing Redis connections with pooling support.
6
+ Designed for both server-side Node.js applications and cron-style background jobs, it wraps ioredis clients in a pool with safe acquire/release methods, connection validation, and optional custom utility functions like getKeys and deleteKeys.
7
+
8
+ #### ✨ Features
9
+
10
+ * **Connection Pooling**: Efficiently manage multiple Redis connections with `min` and `max` pool size configuration.
11
+ * **Safe Acquire/Release**: Guaranteed handling of Redis client lifecycle with optional database selection (`SELECT dbIndex`).
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.
14
+ * **TLS Support**: Optional TLS configuration for secure Redis connections.
15
+
16
+ #### 📦 Instllation
17
+
18
+ ```bash
19
+ npm install @digitalwalletcorp/redis-pooling
20
+ # or
21
+ yarn add @digitalwalletcorp/redis-pooling
22
+ ```
23
+
24
+ #### 📖 Usage
25
+
26
+ ##### Example 1: Pure NodeJS Style Instantiation
27
+
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
+
30
+ ```typescript
31
+ import { createRedisPool, ManagedRedisClient } from '@digitalwalletcorp/redis-pooling';
32
+
33
+ const redisPool = createRedisPool({
34
+ url: 'redis://localhost:6379',
35
+ db: 0,
36
+ max: 10,
37
+ min: 2,
38
+ connectTimeout: 5000,
39
+ enableTls: false
40
+ });
41
+
42
+ async function main() {
43
+ const client: ManagedRedisClient = await redisPool.acquire();
44
+ try {
45
+ await client.set('key', 'value');
46
+ const value = await client.get('key');
47
+ console.log('Redis value:', value);
48
+ } finally {
49
+ await redisPool.release(client);
50
+ }
51
+ }
52
+
53
+ main().catch(console.error);
54
+ ```
55
+
56
+ ##### Example 2: Singleton Pool Instantiation using Web-App
57
+
58
+ For long-running web applications, a singleton Redis pool ensures efficient reuse of connections across requests.
59
+
60
+
61
+ `@/server/singleton/redis-pooling`
62
+ ```typescript
63
+ import { createRedisPool, ManagedRedisClient } from '@digitalwalletcorp/redis-pooling';
64
+
65
+ export const redisPool = createRedisPool({
66
+ url: 'redis://localhost:6379',
67
+ db: 0,
68
+ max: 20,
69
+ min: 5,
70
+ connectTimeout: 5000,
71
+ enableTls: false
72
+ });
73
+ ```
74
+
75
+ `@/server/api/some-rest-api`
76
+ ```typescript
77
+ import { redisPool } from '@/server/singleton/redis-pooling';
78
+
79
+ async function handleRequest() {
80
+ const client: ManagedRedisClient = await redisPool.acquire();
81
+ try {
82
+ const value = await client.get('session:1234');
83
+ console.log('Session value:', value);
84
+ } finally {
85
+ await redisPool.release(client);
86
+ }
87
+ }
88
+ ```
89
+
90
+ ##### Example 3: Using Custom Methods (`getKeys`, `deleteKeys`)
91
+
92
+ `ManagedRedisClient` extends the standard `ioredis` client with convenient helper methods.
93
+ `getKeys` and `deleteKeys` use `SCAN` and `UNLINK` internally to avoid blocking Redis with large datasets (unlike `KEYS`).
94
+ All other standard Redis commands remain available.
95
+
96
+ ```typescript
97
+ import { redisPool } from '@/server/singleton/redis-pooling';
98
+
99
+ async function manageCache() {
100
+ const client = await redisPool.acquire();
101
+
102
+ try {
103
+ // Get all keys matching a pattern using SCAN (avoids blocking Redis like KEYS does)
104
+ const keys = await client.getKeys('user:*');
105
+ console.log('Matching keys:', keys);
106
+
107
+ // Delete all matching keys using UNLINK
108
+ const deleted = await client.deleteKeys('cache:*');
109
+ console.log(`Deleted ${deleted} keys`);
110
+ } finally {
111
+ await redisPool.release(client);
112
+ }
113
+ }
114
+ ```
115
+
116
+ #### 📚 API Reference
117
+
118
+ ##### `createRedisPool(config: RedisConfig): ManagedRedisPool`
119
+
120
+ Creates a managed Redis pool.
121
+
122
+ | Property | Type | Default | Description |
123
+ | ---------------- | ------- | -------- | -------------------------------------- |
124
+ | `url` | string | required | Redis connection URL. |
125
+ | `db` | number | 0 | Default Redis database index. |
126
+ | `connectTimeout` | number | 5000 | Connection timeout in milliseconds. |
127
+ | `max` | number | 10 | Maximum number of clients in the pool. |
128
+ | `min` | number | 0 | Minimum number of clients in the pool. |
129
+ | `enableTls` | boolean | false | Enable TLS for Redis connection. |
130
+
131
+ ##### `ManagedRedisPool` Methods
132
+
133
+ | Method | Signature | Description |
134
+ | ------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------- |
135
+ | `acquire(dbIndex?: number)` | `Promise<ManagedRedisClient>` | Acquire a Redis client from the pool. Optionally switch to a different database index. |
136
+ | `release(client: ManagedRedisClient)` | `Promise<void>` | Release the Redis client back to the pool. |
137
+ | `destroy()` | `Promise<void>` | Drain and clear all connections in the pool. |
138
+
139
+ ##### `ManagedRedisClient` Methods
140
+
141
+ Extends the standard `ioredis` `Redis` client with additional helpers:
142
+
143
+ | Method | Signature | Description |
144
+ | ----------------------------- | ------------------- | ----------------------------------------------------------------------------------------------- |
145
+ | `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 keys. |
147
+
148
+ ---
149
+ #### 💡 Notes
150
+
151
+ * Always release clients back to the pool using `release(client)` to avoid connection leaks.
152
+ * Use `acquire()` and `release()` inside `try/finally` blocks for safe resource management.
153
+
154
+ #### 📜 License
155
+
156
+ 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 './redis-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("./redis-pooling"), exports);
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,kDAAgC"}
@@ -0,0 +1,27 @@
1
+ import { Redis } from 'ioredis';
2
+ export interface RedisConfig {
3
+ url: string;
4
+ db?: number;
5
+ connectTimeout?: number;
6
+ max?: number;
7
+ min?: number;
8
+ testOnBorrow?: boolean;
9
+ enableTls?: boolean;
10
+ }
11
+ export interface ManagedRedisPool {
12
+ acquire(dbIndex?: number): Promise<Redis>;
13
+ release(client?: Redis): Promise<void>;
14
+ destroy(timeoutMs?: number): Promise<void>;
15
+ }
16
+ export interface ManagedRedisClient extends Redis {
17
+ getKeys(pattern: string): Promise<string[]>;
18
+ deleteKeys(pattern: string): Promise<number>;
19
+ _originalDbIndex?: number;
20
+ }
21
+ /**
22
+ * Redisコネクションプーリングを生成する
23
+ *
24
+ * @param {RedisConfig} config
25
+ * @returns {ManagedRedisPool}
26
+ */
27
+ export declare const createRedisPool: (config: RedisConfig) => ManagedRedisPool;
@@ -0,0 +1,287 @@
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createRedisPool = void 0;
37
+ const ioredis_1 = require("ioredis");
38
+ const genericPool = __importStar(require("generic-pool"));
39
+ const logHeader = '[RedisPooling]';
40
+ const DEFAULT_CONNECT_TIMEOUT = 5000;
41
+ const DEFAULT_MAX_POOLING_SIZE = 10;
42
+ const DEFAULT_MIN_POOLING_SIZE = 0;
43
+ const REDIS_PING_TIMEOUT_MS = 3000; // 3秒
44
+ /**
45
+ * Redisコネクションプーリングを生成する
46
+ *
47
+ * @param {RedisConfig} config
48
+ * @returns {ManagedRedisPool}
49
+ */
50
+ const createRedisPool = (config) => {
51
+ const url = config.url;
52
+ const db = config.db ?? 0;
53
+ const connectTimeout = config.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
54
+ const max = config.max ?? DEFAULT_MAX_POOLING_SIZE;
55
+ const min = config.min ?? DEFAULT_MIN_POOLING_SIZE;
56
+ const testOnBorrow = config.testOnBorrow ?? true;
57
+ if (!url) {
58
+ throw new Error(`${logHeader} Redis connection url is required.`);
59
+ }
60
+ // dbIndexごとのプール管理
61
+ const poolMap = new Map();
62
+ const createSingleDbPool = (dbIndex) => {
63
+ const factory = {
64
+ create: async () => {
65
+ let client;
66
+ try {
67
+ client = new ioredis_1.Redis(url, {
68
+ db: dbIndex,
69
+ connectTimeout: connectTimeout,
70
+ keepAlive: 1,
71
+ enableOfflineQueue: true,
72
+ tls: config.enableTls ? { rejectUnauthorized: false } : undefined,
73
+ retryStrategy: (times) => {
74
+ const delay = Math.min(times * 50, 1000);
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;
83
+ }
84
+ });
85
+ }
86
+ catch (error) {
87
+ console.error(error);
88
+ throw error;
89
+ }
90
+ client._originalDbIndex = dbIndex;
91
+ client.on('error', error => {
92
+ process.emitWarning(`${logHeader} detected error (on error). ${error.message}`);
93
+ });
94
+ // カスタムメソッド START
95
+ /**
96
+ * 指定されたパターンに一致するキーを Redis から全て取得する
97
+ *
98
+ * @param {string} pattern
99
+ * @returns {Promise<string[]>}
100
+ */
101
+ client.getKeys = async function (pattern) {
102
+ const allKeys = [];
103
+ const stream = this.scanStream({
104
+ match: pattern,
105
+ count: 1000 // ioredisが1度にスキャンする件数の目安。1000件を超えるデータがあっても全件返却される
106
+ });
107
+ return new Promise((resolve, reject) => {
108
+ stream.on('data', (keys) => {
109
+ if (keys.length) {
110
+ allKeys.push(...keys);
111
+ }
112
+ });
113
+ stream.on('end', () => resolve(allKeys));
114
+ stream.on('error', (err) => {
115
+ console.error(logHeader, `Error during getKeys scan for pattern '${pattern}'.`, err);
116
+ reject(err);
117
+ });
118
+ });
119
+ };
120
+ /**
121
+ * 指定されたパターンに一致するキーを Redis から全て削除する (UNLINKを使用)
122
+ *
123
+ * @param {string} pattern
124
+ * @returns {Promise<number>}
125
+ */
126
+ client.deleteKeys = async function (pattern) {
127
+ let deletedCount = 0;
128
+ const stream = this.scanStream({
129
+ match: pattern,
130
+ count: 1000 // 1度にスキャンする件数
131
+ });
132
+ return new Promise((resolve, reject) => {
133
+ const tasks = [];
134
+ stream.on('data', async (keys) => {
135
+ if (keys.length) {
136
+ tasks.push((async () => {
137
+ try {
138
+ // UNLINK を使用し、現在のインスタンス (this) で実行
139
+ const unlinkResult = await this.unlink(...keys);
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
+ }
146
+ })());
147
+ }
148
+ });
149
+ stream.on('end', async () => {
150
+ await Promise.all(tasks);
151
+ resolve(deletedCount);
152
+ });
153
+ stream.on('error', (err) => {
154
+ console.error(logHeader, `Error during deleteKeys scan for pattern '${pattern}'.`, err);
155
+ reject(err);
156
+ });
157
+ });
158
+ };
159
+ // カスタムメソッド END
160
+ // 接続が不安定な場合は以下のロジックが生きるかもしれないので残しておく
161
+ // 動作確認した限りでは以下のロジックはなくても問題なく動作する
162
+ // await new Promise<void>((resolve, reject) => {
163
+ // const onReady = () => {
164
+ // cleanup();
165
+ // resolve();
166
+ // };
167
+ // const onError = (error: Error) => {
168
+ // cleanup();
169
+ // reject(error);
170
+ // };
171
+ // const cleanup = () => {
172
+ // client.off('ready', onReady);
173
+ // client.off('error', onError);
174
+ // };
175
+ // client.once('ready', onReady);
176
+ // client.once('error', onError);
177
+ // });
178
+ return client;
179
+ },
180
+ destroy: async (client) => {
181
+ try {
182
+ await client.quit();
183
+ console.debug(logHeader, 'client quit');
184
+ }
185
+ catch (error) {
186
+ client.disconnect();
187
+ console.debug(logHeader, 'client disconnected');
188
+ }
189
+ },
190
+ validate: async (client) => {
191
+ try {
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
+ }
207
+ }
208
+ };
209
+ return genericPool.createPool(factory, {
210
+ max,
211
+ min,
212
+ testOnBorrow
213
+ });
214
+ };
215
+ const getPool = (dbIndex) => {
216
+ let pool = poolMap.get(dbIndex);
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;
287
+ //# sourceMappingURL=redis-pooling.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-pooling.js","sourceRoot":"","sources":["../src/redis-pooling.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@digitalwalletcorp/redis-pooling",
3
+ "version": "1.0.0",
4
+ "description": "This is a library for redis connection pooling",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc --project tsconfig.build.json",
9
+ "version:patch": "npm version patch",
10
+ "version:minor": "npm version minor",
11
+ "version:major": "npm version major",
12
+ "prepack": "rm -rf lib && npm run build",
13
+ "pack": "rm -rf dest && mkdir -p dest && npm pack --pack-destination=dest",
14
+ "publish:npm": "bash ../bin/publish.sh",
15
+ "test:unit": "NODE_OPTIONS=--max-old-space-size=2048 npx jest --runInBand --logHeapUsage --detectOpenHandles --forceExit --errorOnDeprecated --coverage --silent --unhandled-rejections=strict"
16
+ },
17
+ "keywords": [
18
+ "redis",
19
+ "pooling"
20
+ ],
21
+ "author": "satoshi kotaki",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/digitalwalletcorp/redis-pooling.git"
26
+ },
27
+ "homepage": "https://github.com/digitalwalletcorp/redis-pooling",
28
+ "bugs": {
29
+ "url": "https://github.com/digitalwalletcorp/redis-pooling/issues"
30
+ },
31
+ "files": [
32
+ "lib/**/*",
33
+ "src/**/*"
34
+ ],
35
+ "devDependencies": {
36
+ "@types/ioredis-mock": "^8.2.6",
37
+ "@types/jest": "^29.5.14",
38
+ "ioredis-mock": "^8.13.1",
39
+ "jest": "^29.7.0",
40
+ "ts-jest": "^29.3.4",
41
+ "ts-node": "^10.9.2",
42
+ "typescript": "^5.8.3"
43
+ },
44
+ "dependencies": {
45
+ "generic-pool": "^3.9.0",
46
+ "ioredis": "^5.9.2"
47
+ }
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './redis-pooling';
@@ -0,0 +1,281 @@
1
+ import { Redis } from 'ioredis';
2
+ import * as genericPool from 'generic-pool';
3
+
4
+ export interface RedisConfig {
5
+ url: string;
6
+ db?: number;
7
+ connectTimeout?: number;
8
+ max?: number;
9
+ min?: number;
10
+ testOnBorrow?: boolean;
11
+ enableTls?: boolean;
12
+ }
13
+
14
+ export interface ManagedRedisPool {
15
+ acquire(dbIndex?: number): Promise<Redis>;
16
+ release(client?: Redis): Promise<void>;
17
+ destroy(timeoutMs?: number): Promise<void>;
18
+ }
19
+
20
+ export interface ManagedRedisClient extends Redis {
21
+ getKeys(pattern: string): Promise<string[]>;
22
+ deleteKeys(pattern: string): Promise<number>;
23
+ _originalDbIndex?: number; // 内部状態管理用変数
24
+ }
25
+
26
+ const logHeader = '[RedisPooling]';
27
+ const DEFAULT_CONNECT_TIMEOUT = 5000;
28
+ const DEFAULT_MAX_POOLING_SIZE = 10;
29
+ const DEFAULT_MIN_POOLING_SIZE = 0;
30
+ const REDIS_PING_TIMEOUT_MS = 3000; // 3秒
31
+
32
+ /**
33
+ * Redisコネクションプーリングを生成する
34
+ *
35
+ * @param {RedisConfig} config
36
+ * @returns {ManagedRedisPool}
37
+ */
38
+ export const createRedisPool = (config: RedisConfig): ManagedRedisPool => {
39
+
40
+ const url = config.url;
41
+ const db = config.db ?? 0;
42
+ const connectTimeout = config.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
43
+ const max = config.max ?? DEFAULT_MAX_POOLING_SIZE;
44
+ const min = config.min ?? DEFAULT_MIN_POOLING_SIZE;
45
+ const testOnBorrow = config.testOnBorrow ?? true;
46
+
47
+ if (!url) {
48
+ throw new Error(`${logHeader} Redis connection url is required.`);
49
+ }
50
+
51
+ // dbIndexごとのプール管理
52
+ const poolMap = new Map<number, genericPool.Pool<ManagedRedisClient>>();
53
+
54
+ const createSingleDbPool = (dbIndex: number): genericPool.Pool<ManagedRedisClient> => {
55
+ const factory: genericPool.Factory<ManagedRedisClient> = {
56
+ create: async () => {
57
+ let client;
58
+ try {
59
+ client = new Redis(url, {
60
+ db: dbIndex,
61
+ connectTimeout: connectTimeout,
62
+ keepAlive: 1,
63
+ enableOfflineQueue: true,
64
+ tls: config.enableTls ? { rejectUnauthorized: false } : undefined,
65
+ retryStrategy: (times) => {
66
+ const delay = Math.min(times * 50, 1000);
67
+ if (process.env.NODE_ENV !== 'test') {
68
+ console.debug(logHeader, `retry strategy called ${times} times. delaying ${delay}ms`);
69
+ }
70
+ return delay;
71
+ },
72
+ reconnectOnError: (error) => {
73
+ process.emitWarning(`${logHeader} detected error (on reconnect). ${error.message}`);
74
+ return true;
75
+ }
76
+ }) as ManagedRedisClient;
77
+ } catch (error) {
78
+ console.error(error);
79
+ throw error;
80
+ }
81
+
82
+ client._originalDbIndex = dbIndex;
83
+ client.on('error', error => {
84
+ process.emitWarning(`${logHeader} detected error (on error). ${error.message}`);
85
+ });
86
+
87
+ // カスタムメソッド START
88
+
89
+ /**
90
+ * 指定されたパターンに一致するキーを Redis から全て取得する
91
+ *
92
+ * @param {string} pattern
93
+ * @returns {Promise<string[]>}
94
+ */
95
+ client.getKeys = async function(pattern: string): Promise<string[]> {
96
+ const allKeys: string[] = [];
97
+ const stream = this.scanStream({
98
+ match: pattern,
99
+ count: 1000 // ioredisが1度にスキャンする件数の目安。1000件を超えるデータがあっても全件返却される
100
+ });
101
+
102
+ return new Promise((resolve, reject) => {
103
+ stream.on('data', (keys: string[]) => {
104
+ if (keys.length) {
105
+ allKeys.push(...keys);
106
+ }
107
+ });
108
+ stream.on('end', () => resolve(allKeys));
109
+ stream.on('error', (err: Error) => {
110
+ console.error(logHeader, `Error during getKeys scan for pattern '${pattern}'.`, err);
111
+ reject(err);
112
+ });
113
+ });
114
+ };
115
+ /**
116
+ * 指定されたパターンに一致するキーを Redis から全て削除する (UNLINKを使用)
117
+ *
118
+ * @param {string} pattern
119
+ * @returns {Promise<number>}
120
+ */
121
+ client.deleteKeys = async function(pattern: string): Promise<number> {
122
+ let deletedCount = 0;
123
+ const stream = this.scanStream({
124
+ match: pattern,
125
+ count: 1000 // 1度にスキャンする件数
126
+ });
127
+
128
+ return new Promise((resolve, reject) => {
129
+ const tasks: Promise<void>[] = [];
130
+ stream.on('data', async (keys: string[]) => {
131
+ if (keys.length) {
132
+ tasks.push((async () => {
133
+ try {
134
+ // UNLINK を使用し、現在のインスタンス (this) で実行
135
+ const unlinkResult = await this.unlink(...keys);
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
+ }
141
+ })());
142
+ }
143
+ });
144
+ stream.on('end', async () => {
145
+ await Promise.all(tasks);
146
+ resolve(deletedCount);
147
+ });
148
+ stream.on('error', (err: Error) => {
149
+ console.error(logHeader, `Error during deleteKeys scan for pattern '${pattern}'.`, err);
150
+ reject(err);
151
+ });
152
+ });
153
+ };
154
+ // カスタムメソッド END
155
+
156
+ // 接続が不安定な場合は以下のロジックが生きるかもしれないので残しておく
157
+ // 動作確認した限りでは以下のロジックはなくても問題なく動作する
158
+ // await new Promise<void>((resolve, reject) => {
159
+ // const onReady = () => {
160
+ // cleanup();
161
+ // resolve();
162
+ // };
163
+ // const onError = (error: Error) => {
164
+ // cleanup();
165
+ // reject(error);
166
+ // };
167
+ // const cleanup = () => {
168
+ // client.off('ready', onReady);
169
+ // client.off('error', onError);
170
+ // };
171
+ // client.once('ready', onReady);
172
+ // client.once('error', onError);
173
+ // });
174
+
175
+ return client;
176
+ },
177
+ destroy: async (client: Redis) => {
178
+ try {
179
+ await client.quit();
180
+ console.debug(logHeader, 'client quit');
181
+ } catch (error) {
182
+ client.disconnect();
183
+ console.debug(logHeader, 'client disconnected');
184
+ }
185
+ },
186
+ validate: async (client: Redis) => {
187
+ try {
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
+ }
202
+ }
203
+ };
204
+
205
+ return genericPool.createPool(factory, {
206
+ max,
207
+ min,
208
+ testOnBorrow
209
+ });
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
+ }
281
+ };