mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
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:
parent
8097d1ab84
commit
17cf837ba9
103
package-lock.json
generated
103
package-lock.json
generated
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
166
src/cache/RedisQueryResultCache.ts
vendored
166
src/cache/RedisQueryResultCache.ts
vendored
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user