import { createLogger } from "@/logger.builder"
import Redis from "ioredis"
* Configuration options for Redis connection.
export type RedisConfiguration = {
/** The Redis connection URL */
/** Optional name for the Redis instance (used for logging) */
/** Maximum delay between retry attempts in milliseconds */
/** Maximum number of retry attempts */
/** Connection timeout in milliseconds */
connectionTimeout?: number
/** Command execution timeout in milliseconds */
* Creates a new Redis client instance based on the provided configuration.
* Supports both standard Redis and Redis Sentinel connections.
* @param options - The Redis configuration options
* @param options.redisUrl - The Redis connection URL
* @param options.name - Optional name for the Redis instance
* @param options.maxRetryDelay - Maximum delay between retry attempts
* @param options.maxRetries - Maximum number of retry attempts
* @param options.connectionTimeout - Connection timeout
* @param options.commandTimeout - Command execution timeout
* @returns A configured Redis client instance
export const redisFactory = ({
}: RedisConfiguration): Redis => {
const logger = createLogger(name ?? "redis")
if (redisUrl.startsWith("redis+sentinel://")) {
const [credentialsAndHost, masterSet] = redisUrl.split("//")[1].split("/")
let credentials: string | undefined
let hostInfo: string | undefined
let port: string | undefined
if (credentialsAndHost.includes("@")) {
;[credentials, hostInfo] = credentialsAndHost.split("@")
;[hostInfo, port] = credentialsAndHost.split(":")
let username: string | undefined
let password: string | undefined
if (credentials?.includes(":")) {
;[username, password] = credentials.split(":")
;[host, port] = hostInfo.split(":")
;[host, port] = credentialsAndHost.split(":")
redis = redisSentinelFactory({
port: Number.parseInt(port),
redis = new Redis(redisUrl, {
retryStrategy: (times) => {
if (times > (maxRetries ?? 10)) {
const maxDelay = maxRetryDelay ?? 2000
const delay = Math.min(times * 50, maxDelay)
reconnectOnError: (err) => {
const targetError = "READONLY"
return err.message.includes(targetError)
maxRetriesPerRequest: maxRetries ?? 10,
connectTimeout: connectionTimeout ?? 20000,
commandTimeout: commandTimeout ?? 5000,
redis.on("connect", () => {
logger.info("Connected to Redis server")
redis.on("ready", () => {
logger.info("Redis client is ready to handle commands")
redis.on("error", (error) => {
logger.error(`Redis failed with ${error.message}`, { error })
redis.on("close", () => {
logger.info("Connection to Redis server was closed")
redis.on("reconnecting", (timeToReconnect: number) => {
logger.info(`Reconnecting to Redis in ${timeToReconnect}ms...`)
logger.info("Redis client connection ended")
// Sentinel-specific events
redis.on("+node", (node) => {
logger.debug("New node discovered:", node)
redis.on("-node", (node) => {
logger.debug("Node removed:", node)
redis.on("+sdown", (node) => {
logger.warn("Node is subjectively down:", node)
redis.on("-sdown", (node) => {
logger.warn("Node is subjectively up:", node)
redis.on("+failover-start", () => {
logger.warn("Failover process has started")
redis.on("+failover-end", () => {
logger.warn("Failover process has completed")
* Creates a Redis client configured for Sentinel mode.
* @param options - The Redis Sentinel configuration options
* @param options.sentinelUrl - The URL of the Sentinel server
* @param options.port - The port number for the Sentinel server
* @param options.username - Optional username for authentication
* @param options.password - Optional password for authentication
* @param options.setName - The name of the Sentinel set
* @param options.maxRetryDelay - Maximum delay between retry attempts
* @param options.maxRetries - Maximum number of retry attempts
* @param options.connectionTimeout - Connection timeout
* @param options.commandTimeout - Command execution timeout
* @returns A configured Redis client instance in Sentinel mode
export const redisSentinelFactory = (options: {
connectionTimeout?: number
setName: options.setName ?? "mymaster",
sentinels: [{ host: configuration.sentinelUrl, port: configuration.port ?? 26379 }],
retryStrategy: (times) => {
if (times > (configuration.maxRetries ?? 10)) {
const maxDelay = configuration.maxRetryDelay ?? 2000
const delay = Math.min(times * 50, maxDelay)
reconnectOnError: (err) => {
const targetError = "READONLY"
return err.message.includes(targetError)
maxRetriesPerRequest: configuration.maxRetries ?? 10,
connectTimeout: configuration.connectionTimeout ?? 20000,
commandTimeout: configuration.commandTimeout ?? 5000,
...(configuration.password && { sentinelPassword: configuration.password }),
...(configuration.password && { password: configuration.password }),
...(configuration.username && { username: configuration.username }),
name: configuration.setName,
* Stores Redis client instances for reuse
const redisPool = new Map<string, Redis>()
* Stores Redis configurations for each connection URL
const redisConfigurationCache = new Map<string, RedisConfiguration>()
const poolLogger = createLogger("redis-pool-factory")
* Configures a Redis instance with the specified options.
* If an instance with the same URL already exists, it won't be reconfigured unless override is true.
* @param options - The Redis configuration options
* @param override - Whether to override existing configuration
export const configureRedisInstance = (options: RedisConfiguration, override?: boolean) => {
if (redisConfigurationCache.has(options.redisUrl) && !override) {
poolLogger.warn("Redis configuration already exists", { redisUrl: options.redisUrl })
redisConfigurationCache.set(options.redisUrl, options)
* Retrieves a Redis instance for the specified URL.
* If an instance doesn't exist, it creates one using the cached configuration.
* @param options - Object containing the Redis URL
* @param options.redisUrl - The Redis connection URL
* @returns A Redis client instance
* @throws Error if no configuration exists for the specified URL
export const getRedisInstance = (options: { redisUrl: string }) => {
let redis = redisPool.get(options.redisUrl)
const configuration = redisConfigurationCache.get(options.redisUrl)
poolLogger.error("Redis configuration not found", { redisUrl: options.redisUrl })
throw new Error("Redis configuration not found")
redis = redisFactory(configuration)
redisPool.set(options.redisUrl, redis)