openvidu/backend/src/services/redis.service.ts

231 lines
6.4 KiB
TypeScript

import { inject, injectable } from '../config/dependency-injector.config.js';
import * as config from '../environment.js';
import { Redis, RedisOptions, SentinelAddress } from 'ioredis';
import {
REDIS_DB,
REDIS_HOST,
REDIS_PASSWORD,
REDIS_PORT,
REDIS_SENTINEL_MASTER_NAME,
REDIS_SENTINEL_HOST_LIST,
REDIS_SENTINEL_PASSWORD,
REDIS_USERNAME
} from '../environment.js';
import { internalError } from '../models/error.model.js';
import { LoggerService } from './logger.service.js';
import { EventEmitter } from 'events';
import Redlock from 'redlock';
@injectable()
export class RedisService {
protected readonly DEFAULT_TTL: number = 32 * 60 * 60 * 24; // 32 days
protected redis: Redis;
protected isConnected = false;
public events: EventEmitter;
constructor(@inject(LoggerService) protected logger: LoggerService) {
this.events = new EventEmitter();
const redisOptions = this.loadRedisConfig();
this.redis = new Redis(redisOptions);
this.redis.on('connect', () => {
if (!this.isConnected) {
this.logger.verbose('Connected to Redis');
} else {
this.logger.verbose('Reconnected to Redis');
}
this.isConnected = true;
this.events.emit('redisConnected');
});
this.redis.on('error', (e) => this.logger.error('Error Redis', e));
this.redis.on('end', () => {
this.isConnected = false;
this.logger.warn('Redis disconnected');
});
}
createRedlock(retryCount = -1, retryDelay = 200) {
return new Redlock([this.redis], {
driftFactor: 0.01,
retryCount,
retryDelay,
retryJitter: 200 // Random variation in the time between retries.
});
}
public onReady(callback: () => void) {
if (this.isConnected) {
callback();
}
this.events.on('redisConnected', callback);
}
/**
* Retrieves all keys from Redis that match the specified pattern.
*
* @param pattern - The pattern to match against Redis keys.
* @returns A promise that resolves to an array of matching keys.
* @throws {internalRecordingError} If there is an error retrieving keys from Redis.
*/
async getKeys(pattern: string): Promise<string[]> {
let cursor = '0';
const keys: Set<string> = new Set();
do {
const [nextCursor, partialKeys] = await this.redis.scan(cursor, 'MATCH', pattern);
partialKeys.forEach((key) => keys.add(key));
cursor = nextCursor;
} while (cursor !== '0');
return Array.from(keys);
}
/**
* Checks if a given key exists in the Redis store.
*
* @param {string} key - The key to check for existence.
* @returns {Promise<boolean>} - A promise that resolves to `true` if the key exists, otherwise `false`.
*/
async exists(key: string): Promise<boolean> {
const result = await this.get(key);
return !!result;
}
get(key: string, hashKey?: string): Promise<string | null> {
try {
if (hashKey) {
return this.redis.hget(key, hashKey);
} else {
return this.redis.get(key);
}
} catch (error) {
this.logger.error('Error getting value from Redis', error);
throw internalError(error);
}
}
// getAll(key: string): Promise<Record<string, string>> {
// try {
// return this.redis.hgetall(key);
// } catch (error) {
// this.logger.error('Error getting value from Redis', error);
// throw internalError(error);
// }
// }
// getDel(key: string): Promise<string | null> {
// try {
// return this.redis.getdel(key);
// } catch (error) {
// this.logger.error('Error getting and deleting value from Redis', error);
// throw internalError(error);
// }
// }
/**
* Sets a value in Redis with an optional TTL (time-to-live).
*
* @param {string} key - The key under which the value will be stored.
* @param {any} value - The value to be stored. Can be a string, number, boolean, or object.
* @param {boolean} [withTTL=true] - Whether to set a TTL for the key. Defaults to true.
* @returns {Promise<string>} - A promise that resolves to 'OK' if the operation is successful.
* @throws {Error} - Throws an error if the value type is invalid or if there is an issue setting the value in Redis.
*/
async set(key: string, value: any, withTTL = true): Promise<string> {
try {
const valueType = typeof value;
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
if (withTTL) {
await this.redis.set(key, value, 'EX', this.DEFAULT_TTL);
} else {
await this.redis.set(key, value);
}
} else if (valueType === 'object') {
await this.redis.hmset(key, value);
if (withTTL) await this.redis.expire(key, this.DEFAULT_TTL);
} else {
throw new Error('Invalid value type');
}
return 'OK';
} catch (error) {
this.logger.error('Error setting value in Redis', error);
throw error;
}
}
/**
* Deletes a key from Redis.
* @param key - The key to delete.
* @param hashKey - The hash key to delete. If provided, it will delete the hash key from the hash stored at the given key.
* @returns A promise that resolves to the number of keys deleted.
*/
delete(key: string, hashKey?: string): Promise<number> {
try {
if (hashKey) {
return this.redis.hdel(key, hashKey);
} else {
return this.redis.del(key);
}
} catch (error) {
throw internalError(`Error deleting key from Redis ${error}`);
}
}
quit() {
this.redis.quit();
}
async checkHealth() {
return (await this.redis.ping()) === 'PONG';
}
private loadRedisConfig(): RedisOptions {
// Check if openviduCall module is enabled. If not, exit the process
config.checkModuleEnabled();
//Check if Redis Sentinel is configured
if (REDIS_SENTINEL_HOST_LIST) {
const sentinels: Array<SentinelAddress> = [];
const sentinelHosts = REDIS_SENTINEL_HOST_LIST.split(',');
sentinelHosts.forEach((host) => {
const rawHost = host.split(':');
if (rawHost.length !== 2) {
throw new Error('The Redis Sentinel host list is required');
}
const hostName = rawHost[0];
const port = parseInt(rawHost[1]);
sentinels.push({ host: hostName, port });
});
if (!REDIS_SENTINEL_PASSWORD) throw new Error('The Redis Sentinel password is required');
this.logger.verbose('Using Redis Sentinel');
return {
sentinels,
sentinelPassword: REDIS_SENTINEL_PASSWORD,
username: REDIS_USERNAME,
password: REDIS_PASSWORD,
name: REDIS_SENTINEL_MASTER_NAME,
db: Number(REDIS_DB)
};
} else {
this.logger.verbose('Using Redis standalone');
return {
port: Number(REDIS_PORT),
host: REDIS_HOST,
username: REDIS_USERNAME,
password: REDIS_PASSWORD,
db: Number(REDIS_DB)
};
}
}
}