feat(11528): add Redis 5.x support with backward compatibility wite peer dependency to allow (#11585)

* feat: add Redis 5 support to cache implementation

- Add version detection for Redis client to handle API differences
- Support Redis 5 Promise-based API while maintaining backward compatibility
- Update methods to use appropriate API based on Redis version
- Add tests for Redis 5 compatibility

Redis 5 introduced Promise-based API as default, replacing the callback-based
API. This change detects the Redis version and uses the appropriate API calls
to ensure compatibility with Redis 3, 4, and 5.

Closes #11528

* feat: add Redis 5 support to cache implementation

Implement automatic version detection for Redis client libraries to support
Redis 3, 4, and 5 seamlessly. The implementation uses runtime API testing
to determine the appropriate Redis client behavior without breaking existing
functionality.

Changes include:
  - Dynamic Redis version detection based on client API characteristics
  - Promise-based API support for Redis 5.x
  - Backward compatibility with Redis 3.x and 4.x callback-based APIs
  - Safe fallback mechanism defaulting to Redis 3 behavior
  - Updated peer dependency to include Redis 5.x versions

The cache implementation now automatically adapts to the installed Redis version, ensuring optimal performance and compatibility across all supported Redis client versions while maintaining full backward compatibility.

* fix: delete wrong migration guide

* feat: add package-lock.json

* refactor: optimize Redis client creation to reduce memory usage
Eliminate unnecessary Redis client recreation by using explicit tempClient
variable management, reducing potential client instances while maintaining
full Redis 3/4/5 compatibility and accurate version detection.

* refactor: improve Redis version detection to avoid cache pollution
Replace test key creation method with client method signature analysis
to prevent potential cache pollution and improve performance.

* style: apply Prettier formatting to RedisQueryResultCache.ts
This commit is contained in:
Park Jin Woong 2025-08-02 00:30:41 +09:00 committed by GitHub
parent 8097d1ab84
commit 17cf837ba9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 175 additions and 98 deletions

103
package-lock.json generated
View File

@ -71,7 +71,7 @@
"pg-query-stream": "^4.8.1",
"pkg-pr-new": "^0.0.43",
"prettier": "^2.8.8",
"redis": "^4.7.0",
"redis": "^5.7.0",
"reflect-metadata": "^0.2.2",
"remap-istanbul": "^0.13.0",
"rimraf": "^5.0.10",
@ -104,7 +104,7 @@
"pg": "^8.5.1",
"pg-native": "^3.0.0",
"pg-query-stream": "^4.0.0",
"redis": "^3.1.1 || ^4.0.0",
"redis": "^3.1.1 || ^4.0.0 || ^5.0.14",
"reflect-metadata": "^0.1.14 || ^0.2.0",
"sql.js": "^1.4.0",
"sqlite3": "^5.0.3",
@ -2424,66 +2424,68 @@
}
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.7.0.tgz",
"integrity": "sha512-KtBHDH2Aw1BxYDQd87PJsdEmZcpMbD4oPzdBwB4IvSRmMovukO2NNGi5vpCHhCoicS83zu7cjX1fw79uFBZFJA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^1.0.0"
"@redis/client": "^5.7.0"
}
},
"node_modules/@redis/client": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz",
"integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.7.0.tgz",
"integrity": "sha512-YV3Knspdj9k6H6s4v8QRcj1WBxHt40vtPmszLKGwRUOUpUOLWSlI9oCUjprMDcQNzgSCXGXYdL/Aj6nT2+Ub0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
"cluster-key-slot": "1.1.2"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/graph": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz",
"integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==",
"dev": true,
"peerDependencies": {
"@redis/client": "^1.0.0"
"node": ">= 18"
}
},
"node_modules/@redis/json": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz",
"integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.7.0.tgz",
"integrity": "sha512-VP3wtse1PSB/UjZAV1lWyDrWrrZcwi/cjb3L0lIarcIJ+EbHliB2QPml0Bvjz8F8F0eDJRtChJVXFc+jhGxCtA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^1.0.0"
"@redis/client": "^5.7.0"
}
},
"node_modules/@redis/search": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz",
"integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.7.0.tgz",
"integrity": "sha512-dDZIq8pZJnT+kZ9xRlLLi2Rvkd792z9eh31QRIwPr5wXjAXeaQ+Nf65em6dLpsxZ60MmhwDwLrBPJpYVjKPBPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^1.0.0"
"@redis/client": "^5.7.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz",
"integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.7.0.tgz",
"integrity": "sha512-AJTF9sz3y1MJAukgQW4Jw8zt8qGOE3+1d87pufOP35zsFBlHipGscpctoXiNMebfy0114y/FjSprr65LjbJQSQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^1.0.0"
"@redis/client": "^5.7.0"
}
},
"node_modules/@sap/hana-client": {
@ -5369,6 +5371,7 @@
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
@ -7187,15 +7190,6 @@
"is-property": "^1.0.2"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -13273,21 +13267,20 @@
}
},
"node_modules/redis": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz",
"integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-5.7.0.tgz",
"integrity": "sha512-ZRbiWYBUYdDTopodRjCVwwCLThrkciPW3bOrkdMCW3nYEelBwUGN6SovmACDsiLUB7mnU3mXnaI5f0W7bDcwng==",
"dev": true,
"license": "MIT",
"workspaces": [
"./packages/*"
],
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.6.0",
"@redis/graph": "1.1.1",
"@redis/json": "1.0.7",
"@redis/search": "1.2.0",
"@redis/time-series": "1.1.0"
"@redis/bloom": "5.7.0",
"@redis/client": "5.7.0",
"@redis/json": "5.7.0",
"@redis/search": "5.7.0",
"@redis/time-series": "5.7.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/reflect-metadata": {

View File

@ -149,7 +149,7 @@
"pg-query-stream": "^4.8.1",
"pkg-pr-new": "^0.0.43",
"prettier": "^2.8.8",
"redis": "^4.7.0",
"redis": "^5.7.0",
"reflect-metadata": "^0.2.2",
"remap-istanbul": "^0.13.0",
"rimraf": "^5.0.10",
@ -176,7 +176,7 @@
"pg": "^8.5.1",
"pg-native": "^3.0.0",
"pg-query-stream": "^4.0.0",
"redis": "^3.1.1 || ^4.0.0",
"redis": "^3.1.1 || ^4.0.0 || ^5.0.14",
"reflect-metadata": "^0.1.14 || ^0.2.0",
"sql.js": "^1.4.0",
"sqlite3": "^5.0.3",

View File

@ -28,6 +28,11 @@ export class RedisQueryResultCache implements QueryResultCache {
*/
protected clientType: "redis" | "ioredis" | "ioredis/cluster"
/**
* Redis major version number
*/
protected redisMajorVersion: number | undefined
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -50,10 +55,24 @@ export class RedisQueryResultCache implements QueryResultCache {
async connect(): Promise<void> {
const cacheOptions: any = this.connection.options.cache
if (this.clientType === "redis") {
this.client = this.redis.createClient({
const clientOptions = {
...cacheOptions?.options,
legacyMode: true,
})
}
// Create initial client to test Redis version
let tempClient = this.redis.createClient(clientOptions)
const isRedis4Plus = typeof tempClient.connect === "function"
if (isRedis4Plus) {
// Redis 4+ detected, recreate with legacyMode for Redis 4.x
// (Redis 5 will ignore legacyMode if not needed)
clientOptions.legacyMode = true
tempClient = this.redis.createClient(clientOptions)
}
// Set as the main client
this.client = tempClient
if (
typeof this.connection.options.cache === "object" &&
this.connection.options.cache.ignoreErrors
@ -62,9 +81,14 @@ export class RedisQueryResultCache implements QueryResultCache {
this.connection.logger.log("warn", err)
})
}
if ("connect" in this.client) {
// Connect if Redis 4+
if (typeof this.client.connect === "function") {
await this.client.connect()
}
// Detect precise version after connection is established
this.detectRedisVersion()
} else if (this.clientType === "ioredis") {
if (cacheOptions && cacheOptions.port) {
if (cacheOptions.options) {
@ -108,6 +132,14 @@ export class RedisQueryResultCache implements QueryResultCache {
* Disconnects the connection
*/
async disconnect(): Promise<void> {
if (this.isRedis5OrHigher()) {
// Redis 5+ uses quit() that returns a Promise
await this.client.quit()
this.client = undefined
return
}
// Redis 3/4 callback style
return new Promise<void>((ok, fail) => {
this.client.quit((err: any, result: any) => {
if (err) return fail(err)
@ -131,20 +163,22 @@ export class RedisQueryResultCache implements QueryResultCache {
options: QueryResultCacheOptions,
queryRunner?: QueryRunner,
): Promise<QueryResultCacheOptions | undefined> {
const key = options.identifier || options.query
if (!key) return Promise.resolve(undefined)
if (this.isRedis5OrHigher()) {
// Redis 5+ Promise-based API
return this.client.get(key).then((result: any) => {
return result ? JSON.parse(result) : undefined
})
}
// Redis 3/4 callback-based API
return new Promise<QueryResultCacheOptions | undefined>((ok, fail) => {
if (options.identifier) {
this.client.get(options.identifier, (err: any, result: any) => {
if (err) return fail(err)
ok(JSON.parse(result))
})
} else if (options.query) {
this.client.get(options.query, (err: any, result: any) => {
if (err) return fail(err)
ok(JSON.parse(result))
})
} else {
ok(undefined)
}
this.client.get(key, (err: any, result: any) => {
if (err) return fail(err)
ok(result ? JSON.parse(result) : undefined)
})
})
}
@ -163,30 +197,32 @@ export class RedisQueryResultCache implements QueryResultCache {
savedCache: QueryResultCacheOptions,
queryRunner?: QueryRunner,
): Promise<void> {
const key = options.identifier || options.query
if (!key) return
const value = JSON.stringify(options)
const duration = options.duration
if (this.isRedis5OrHigher()) {
// Redis 5+ Promise-based API with PX option
await this.client.set(key, value, {
PX: duration,
})
return
}
// Redis 3/4 callback-based API
return new Promise<void>((ok, fail) => {
if (options.identifier) {
this.client.set(
options.identifier,
JSON.stringify(options),
"PX",
options.duration,
(err: any, result: any) => {
if (err) return fail(err)
ok()
},
)
} else if (options.query) {
this.client.set(
options.query,
JSON.stringify(options),
"PX",
options.duration,
(err: any, result: any) => {
if (err) return fail(err)
ok()
},
)
}
this.client.set(
key,
value,
"PX",
duration,
(err: any, result: any) => {
if (err) return fail(err)
ok()
},
)
})
}
@ -194,6 +230,13 @@ export class RedisQueryResultCache implements QueryResultCache {
* Clears everything stored in the cache.
*/
async clear(queryRunner?: QueryRunner): Promise<void> {
if (this.isRedis5OrHigher()) {
// Redis 5+ Promise-based API
await this.client.flushDb()
return
}
// Redis 3/4 callback-based API
return new Promise<void>((ok, fail) => {
this.client.flushdb((err: any, result: any) => {
if (err) return fail(err)
@ -223,7 +266,14 @@ export class RedisQueryResultCache implements QueryResultCache {
/**
* Removes a single key from redis database.
*/
protected deleteKey(key: string): Promise<void> {
protected async deleteKey(key: string): Promise<void> {
if (this.isRedis5OrHigher()) {
// Redis 5+ Promise-based API
await this.client.del(key)
return
}
// Redis 3/4 callback-based API
return new Promise<void>((ok, fail) => {
this.client.del(key, (err: any, result: any) => {
if (err) return fail(err)
@ -248,4 +298,38 @@ export class RedisQueryResultCache implements QueryResultCache {
)
}
}
/**
* Detects the Redis version based on the connected client's API characteristics
* without creating test keys in the database
*/
private detectRedisVersion(): void {
if (this.clientType !== "redis") return
try {
// Detect version by examining the client's method signatures
// This avoids creating test keys in the database
const setMethod = this.client.set
if (setMethod && setMethod.length <= 3) {
// Redis 5+ set method accepts fewer parameters (key, value, options)
this.redisMajorVersion = 5
} else {
// Redis 3/4 set method requires more parameters (key, value, flag, duration, callback)
this.redisMajorVersion = 3
}
} catch {
// Default to Redis 3/4 for maximum compatibility
this.redisMajorVersion = 3
}
}
/**
* Checks if Redis version is 5.x or higher
*/
private isRedis5OrHigher(): boolean {
if (this.clientType !== "redis") return false
return (
this.redisMajorVersion !== undefined && this.redisMajorVersion >= 5
)
}
}