diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index e5a3ddd47c71bbc4ddfabfb48d9cc134a38d481f..29d052ad49999699842682e57ad45363ff4f4a6e 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -1084,6 +1084,10 @@ For TLS/SSL-enabled connections, use rediss prefix Example URL structure: `rediss://[[username]:[password]]@localhost:6379/0`. +Renovate also supports connecting to Redis clusters as well. In order to connect to a cluster, provide the connection string using the `redis+cluster` or `rediss+cluster` schema as appropriate. + +Example URL structure: `redis+cluster://[[username]:[password]]@redis.cluster.local:6379/0` + ## reportPath `reportPath` describes the location where the report is written to. diff --git a/lib/util/cache/package/redis.spec.ts b/lib/util/cache/package/redis.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e43db0768de9e158ec0ab1c61b06a5d1cec80743 --- /dev/null +++ b/lib/util/cache/package/redis.spec.ts @@ -0,0 +1,29 @@ +import { normalizeRedisUrl } from './redis'; + +describe('util/cache/package/redis', () => { + describe('normalizeRedisUrl', () => { + it('leaves standard Redis URL alone', () => { + const url = 'redis://user:password@localhost:6379'; + expect(normalizeRedisUrl(url)).toBe(url); + }); + + it('leaves secure Redis URL alone', () => { + const url = 'rediss://user:password@localhost:6379'; + expect(normalizeRedisUrl(url)).toBe(url); + }); + + it('rewrites standard Redis Cluster URL', () => { + const url = 'redis+cluster://user:password@localhost:6379'; + expect(normalizeRedisUrl(url)).toBe( + 'redis://user:password@localhost:6379', + ); + }); + + it('rewrites secure Redis Cluster URL', () => { + const url = 'rediss+cluster://user:password@localhost:6379'; + expect(normalizeRedisUrl(url)).toBe( + 'rediss://user:password@localhost:6379', + ); + }); + }); +}); diff --git a/lib/util/cache/package/redis.ts b/lib/util/cache/package/redis.ts index 259103b1245c951c4e6cbd70278c78095173675c..24c36a8d4ab6c4c21184179c2eb615754d5b9e54 100644 --- a/lib/util/cache/package/redis.ts +++ b/lib/util/cache/package/redis.ts @@ -1,17 +1,25 @@ /* istanbul ignore file */ import { DateTime } from 'luxon'; -import { createClient } from 'redis'; +import { createClient, createCluster } from 'redis'; import { logger } from '../../../logger'; import { compressToBase64, decompressFromBase64 } from '../../compress'; +import { regEx } from '../../regex'; import type { PackageCacheNamespace } from './types'; -let client: ReturnType<typeof createClient> | undefined; +let client: + | ReturnType<typeof createClient> + | ReturnType<typeof createCluster> + | undefined; let rprefix: string | undefined; function getKey(namespace: PackageCacheNamespace, key: string): string { return `${rprefix}${namespace}-${key}`; } +export function normalizeRedisUrl(url: string): string { + return url.replace(regEx(/^(rediss?)\+cluster:\/\//), '$1://'); +} + export async function end(): Promise<void> { try { // https://github.com/redis/node-redis#disconnecting @@ -94,16 +102,28 @@ export async function init( } rprefix = prefix ?? ''; logger.debug('Redis cache init'); - client = createClient({ - url, + + const rewrittenUrl = normalizeRedisUrl(url); + // If any replacement was made, it means the regex matched and we are in clustered mode + const clusteredMode = rewrittenUrl.length !== url.length; + + const config = { + url: rewrittenUrl, socket: { - reconnectStrategy: (retries) => { + reconnectStrategy: (retries: number) => { // Reconnect after this time return Math.min(retries * 100, 3000); }, }, pingInterval: 30000, // 30s - }); + }; + if (clusteredMode) { + client = createCluster({ + rootNodes: [config], + }); + } else { + client = createClient(config); + } await client.connect(); logger.debug('Redis cache connected'); }