backend: Refactor storage service operations with global preferences
This commit is contained in:
parent
12ef04964c
commit
2c65ec1da8
@ -20,7 +20,7 @@ import {
|
||||
UserService
|
||||
} from '../services/index.js';
|
||||
|
||||
const container: Container = new Container();
|
||||
export const container: Container = new Container();
|
||||
|
||||
/**
|
||||
* Registers all necessary dependencies in the container.
|
||||
@ -30,7 +30,7 @@ const container: Container = new Container();
|
||||
* available for injection throughout the application.
|
||||
*
|
||||
*/
|
||||
const registerDependencies = () => {
|
||||
export const registerDependencies = () => {
|
||||
console.log('Registering CE dependencies');
|
||||
container.bind(SystemEventService).toSelf().inSingletonScope();
|
||||
container.bind(MutexService).toSelf().inSingletonScope();
|
||||
@ -52,14 +52,12 @@ const registerDependencies = () => {
|
||||
|
||||
container.bind(S3Storage).toSelf().inSingletonScope();
|
||||
container.bind(StorageFactory).toSelf().inSingletonScope();
|
||||
|
||||
initializeEagerServices();
|
||||
};
|
||||
|
||||
const initializeEagerServices = () => {
|
||||
export const initializeEagerServices = async () => {
|
||||
// Force the creation of services that need to be initialized at startup
|
||||
container.get(RecordingService);
|
||||
await container.get(MeetStorageService).buildAndSaveDefaultPreferences();
|
||||
};
|
||||
|
||||
export { injectable, inject } from 'inversify';
|
||||
export { container, registerDependencies };
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
export const enum RedisKeyPrefix {
|
||||
BASE = 'ov_meet:'
|
||||
}
|
||||
|
||||
export const enum RedisKeyName {
|
||||
GLOBAL_PREFERENCES = `${RedisKeyPrefix.BASE}global_preferences`
|
||||
}
|
||||
|
||||
export const enum RedisLockPrefix {
|
||||
BASE = 'ov_meet_lock:',
|
||||
REGISTRY = 'ov_meet_lock_registry:'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import express, { Request, Response, Express } from 'express';
|
||||
import cors from 'cors';
|
||||
import chalk from 'chalk';
|
||||
import { registerDependencies, container } from './config/dependency-injector.config.js';
|
||||
import { registerDependencies, initializeEagerServices } from './config/dependency-injector.config.js';
|
||||
import {
|
||||
SERVER_PORT,
|
||||
SERVER_CORS_ORIGIN,
|
||||
@ -25,7 +25,6 @@ import {
|
||||
recordingRouter,
|
||||
roomRouter
|
||||
} from './routes/index.js';
|
||||
import { MeetStorageService } from './services/index.js';
|
||||
import { internalParticipantsRouter } from './routes/participants.routes.js';
|
||||
import cookieParser from 'cookie-parser';
|
||||
|
||||
@ -75,12 +74,6 @@ const createApp = () => {
|
||||
return app;
|
||||
};
|
||||
|
||||
const initializeGlobalPreferences = async () => {
|
||||
const globalPreferencesService = container.get(MeetStorageService);
|
||||
// TODO: This should be invoked in the constructor of the service
|
||||
await globalPreferencesService.ensurePreferencesInitialized();
|
||||
};
|
||||
|
||||
const startServer = (app: express.Application) => {
|
||||
app.listen(SERVER_PORT, async () => {
|
||||
console.log(' ');
|
||||
@ -92,7 +85,6 @@ const startServer = (app: express.Application) => {
|
||||
chalk.cyanBright(`http://localhost:${SERVER_PORT}${MEET_API_BASE_PATH_V1}/docs`)
|
||||
);
|
||||
logEnvVars();
|
||||
await Promise.all([initializeGlobalPreferences()]);
|
||||
});
|
||||
};
|
||||
|
||||
@ -118,6 +110,7 @@ if (isMainModule()) {
|
||||
registerDependencies();
|
||||
const app = createApp();
|
||||
startServer(app);
|
||||
await initializeEagerServices();
|
||||
}
|
||||
|
||||
export { registerDependencies, createApp, initializeGlobalPreferences };
|
||||
export { registerDependencies, createApp };
|
||||
|
||||
@ -95,7 +95,7 @@ export class S3Service {
|
||||
Body: JSON.stringify(body)
|
||||
});
|
||||
const result = await this.retryOperation<PutObjectCommandOutput>(() => this.run(command));
|
||||
this.logger.info(`S3: successfully saved object '${fullKey}' in bucket '${bucket}'`);
|
||||
this.logger.verbose(`S3: successfully saved object '${fullKey}' in bucket '${bucket}'`);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`S3: error saving object '${fullKey}' in bucket '${bucket}': ${error}`);
|
||||
@ -175,9 +175,9 @@ export class S3Service {
|
||||
searchPattern = '',
|
||||
bucket: string = MEET_S3_BUCKET
|
||||
): Promise<ListObjectsV2CommandOutput> {
|
||||
// Se construye el prefijo completo combinando el subbucket y el additionalPrefix.
|
||||
// Ejemplo: si s3Subbucket es "recordings" y additionalPrefix es ".metadata/",
|
||||
// se listarán los objetos con key que empiece por "recordings/.metadata/".
|
||||
// The complete prefix is constructed by combining the subbucket and the additionalPrefix.
|
||||
// Example: if s3Subbucket is "recordings" and additionalPrefix is ".metadata/",
|
||||
// it will list objects with keys that start with "recordings/.metadata/".
|
||||
const basePrefix = this.getFullKey(additionalPrefix);
|
||||
this.logger.verbose(`S3 listObjectsPaginated: listing objects with prefix "${basePrefix}"`);
|
||||
|
||||
@ -191,7 +191,8 @@ export class S3Service {
|
||||
try {
|
||||
const response: ListObjectsV2CommandOutput = await this.s3.send(command);
|
||||
|
||||
// Si se ha proporcionado searchPattern, se filtran los resultados.
|
||||
// If searchPattern is provided, filter the results.
|
||||
|
||||
if (searchPattern) {
|
||||
const regex = new RegExp(searchPattern);
|
||||
response.Contents = (response.Contents || []).filter((item) => item.Key && regex.test(item.Key));
|
||||
|
||||
@ -5,8 +5,8 @@ import { LoggerService } from '../../logger.service.js';
|
||||
import { RedisService } from '../../redis.service.js';
|
||||
import { OpenViduMeetError } from '../../../models/error.model.js';
|
||||
import { inject, injectable } from '../../../config/dependency-injector.config.js';
|
||||
import { MEET_S3_ROOMS_PREFIX } from '../../../environment.js';
|
||||
|
||||
import { MEET_S3_ROOMS_PREFIX, MEET_S3_SUBBUCKET } from '../../../environment.js';
|
||||
import { RedisKeyName } from '../../../models/redis.model.js';
|
||||
|
||||
/**
|
||||
* Implementation of the StorageProvider interface using AWS S3 for persistent storage
|
||||
@ -29,43 +29,75 @@ import { MEET_S3_ROOMS_PREFIX } from '../../../environment.js';
|
||||
export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extends MeetRoom = MeetRoom>
|
||||
implements StorageProvider
|
||||
{
|
||||
protected readonly GLOBAL_PREFERENCES_KEY = 'openvidu-meet-preferences';
|
||||
protected readonly S3_GLOBAL_PREFERENCES_KEY = `${MEET_S3_SUBBUCKET}/global-preferences.json`;
|
||||
constructor(
|
||||
@inject(LoggerService) protected logger: LoggerService,
|
||||
@inject(S3Service) protected s3Service: S3Service,
|
||||
@inject(RedisService) protected redisService: RedisService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initializes global preferences. If no preferences exist, persists the provided defaults.
|
||||
* If preferences exist but belong to a different project, they are replaced.
|
||||
*
|
||||
* @param defaultPreferences - The default preferences to initialize with.
|
||||
*/
|
||||
async initialize(defaultPreferences: G): Promise<void> {
|
||||
const existingPreferences = await this.getGlobalPreferences();
|
||||
try {
|
||||
const existingPreferences = await this.getGlobalPreferences();
|
||||
|
||||
if (existingPreferences) {
|
||||
if (existingPreferences.projectId !== defaultPreferences.projectId) {
|
||||
if (!existingPreferences) {
|
||||
this.logger.info('No existing preferences found. Saving default preferences to S3.');
|
||||
await this.saveGlobalPreferences(defaultPreferences);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.verbose('Global preferences found. Checking project association...');
|
||||
const isDifferentProject = existingPreferences.projectId !== defaultPreferences.projectId;
|
||||
|
||||
if (isDifferentProject) {
|
||||
this.logger.warn(
|
||||
`Existing preferences are associated with a different project (Project ID: ${existingPreferences.projectId}). Replacing them with the default preferences for the current project.`
|
||||
`Existing global preferences belong to project [${existingPreferences.projectId}], ` +
|
||||
`which differs from current project [${defaultPreferences.projectId}]. Replacing preferences.`
|
||||
);
|
||||
|
||||
await this.saveGlobalPreferences(defaultPreferences);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.logger.info('Saving default preferences to S3');
|
||||
await this.saveGlobalPreferences(defaultPreferences);
|
||||
|
||||
this.logger.verbose(
|
||||
'Global preferences for the current project are already initialized. No action needed.'
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Error during global preferences initialization:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the global preferences.
|
||||
* First attempts to retrieve from Redis; if not available, falls back to S3.
|
||||
* If fetched from S3, caches the result in Redis.
|
||||
*
|
||||
* @returns A promise that resolves to the global preferences or null if not found.
|
||||
*/
|
||||
async getGlobalPreferences(): Promise<G | null> {
|
||||
try {
|
||||
let preferences: G | null = await this.getFromRedis<G>(this.GLOBAL_PREFERENCES_KEY);
|
||||
// Try to get preferences from Redis cache
|
||||
let preferences: G | null = await this.getFromRedis<G>(RedisKeyName.GLOBAL_PREFERENCES);
|
||||
|
||||
if (!preferences) {
|
||||
// Fallback to fetching from S3 if Redis doesn't have it
|
||||
this.logger.debug('Preferences not found in Redis. Fetching from S3...');
|
||||
preferences = await this.getFromS3<G>(`${this.GLOBAL_PREFERENCES_KEY}.json`);
|
||||
this.logger.debug('Global preferences not found in Redis. Fetching from S3...');
|
||||
preferences = await this.getFromS3<G>(this.S3_GLOBAL_PREFERENCES_KEY);
|
||||
|
||||
if (preferences) {
|
||||
// TODO: Use a key prefix for Redis
|
||||
await this.redisService.set(this.GLOBAL_PREFERENCES_KEY, JSON.stringify(preferences), false);
|
||||
this.logger.verbose('Fetched global preferences from S3. Caching them in Redis.');
|
||||
const redisPayload = JSON.stringify(preferences);
|
||||
await this.redisService.set(RedisKeyName.GLOBAL_PREFERENCES, redisPayload, false);
|
||||
} else {
|
||||
this.logger.warn('No global preferences found in S3.');
|
||||
}
|
||||
} else {
|
||||
this.logger.verbose('Global preferences retrieved from Redis.');
|
||||
}
|
||||
|
||||
return preferences;
|
||||
@ -75,16 +107,26 @@ export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extend
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the global preferences to both S3 and Redis in parallel.
|
||||
* Uses Promise.all to execute both operations concurrently.
|
||||
*
|
||||
* @param preferences - Global preferences to store.
|
||||
* @returns The saved preferences.
|
||||
* @throws Rethrows any error if saving fails.
|
||||
*/
|
||||
async saveGlobalPreferences(preferences: G): Promise<G> {
|
||||
try {
|
||||
const redisPayload = JSON.stringify(preferences);
|
||||
|
||||
await Promise.all([
|
||||
this.s3Service.saveObject(`${this.GLOBAL_PREFERENCES_KEY}.json`, preferences),
|
||||
// TODO: Use a key prefix for Redis
|
||||
this.redisService.set(this.GLOBAL_PREFERENCES_KEY, JSON.stringify(preferences), false)
|
||||
this.s3Service.saveObject(this.S3_GLOBAL_PREFERENCES_KEY, preferences),
|
||||
this.redisService.set(RedisKeyName.GLOBAL_PREFERENCES, redisPayload, false)
|
||||
]);
|
||||
this.logger.info('Global preferences saved successfully');
|
||||
return preferences;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'Error saving preferences');
|
||||
this.handleError(error, 'Error saving global preferences');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -233,27 +275,49 @@ export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extend
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an object of type U from Redis by the given key.
|
||||
* Returns null if the key is not found or an error occurs.
|
||||
*
|
||||
* @param key - The Redis key to fetch.
|
||||
* @returns A promise that resolves to an object of type U or null.
|
||||
*/
|
||||
protected async getFromRedis<U>(key: string): Promise<U | null> {
|
||||
let response: string | null = null;
|
||||
try {
|
||||
const response = await this.redisService.get(key);
|
||||
|
||||
response = await this.redisService.get(key);
|
||||
if (response) {
|
||||
return JSON.parse(response) as U;
|
||||
}
|
||||
|
||||
if (response) {
|
||||
return JSON.parse(response) as U;
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error fetching from Redis for key ${key}: ${error}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an object of type U from S3 at the specified path.
|
||||
* Returns null if the object is not found.
|
||||
*
|
||||
* @param path - The S3 key or path to fetch.
|
||||
* @returns A promise that resolves to an object of type U or null.
|
||||
*/
|
||||
protected async getFromS3<U>(path: string): Promise<U | null> {
|
||||
const response = await this.s3Service.getObjectAsJson(path);
|
||||
try {
|
||||
const response = await this.s3Service.getObjectAsJson(path);
|
||||
|
||||
if (response) {
|
||||
this.logger.verbose(`Object found in S3 at path: ${path}`);
|
||||
return response as U;
|
||||
if (response) {
|
||||
this.logger.verbose(`Object found in S3 at path: ${path}`);
|
||||
return response as U;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error fetching from S3 for path ${path}: ${error}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected handleError(error: any, message: string) {
|
||||
|
||||
@ -30,10 +30,11 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
||||
* Initializes default preferences if not already initialized.
|
||||
* @returns {Promise<G>} Default global preferences.
|
||||
*/
|
||||
async ensurePreferencesInitialized(): Promise<G> {
|
||||
async buildAndSaveDefaultPreferences(): Promise<G> {
|
||||
const preferences = await this.getDefaultPreferences();
|
||||
|
||||
try {
|
||||
this.logger.verbose('Initializing global preferences with default values');
|
||||
await this.storageProvider.initialize(preferences);
|
||||
return preferences as G;
|
||||
} catch (error) {
|
||||
@ -51,7 +52,7 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
||||
|
||||
if (preferences) return preferences as G;
|
||||
|
||||
return await this.ensurePreferencesInitialized();
|
||||
return await this.buildAndSaveDefaultPreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,12 +118,12 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Move validation to the controller layer
|
||||
* Updates room preferences in storage.
|
||||
* @param {RoomPreferences} roomPreferences
|
||||
* @returns {Promise<GlobalPreferences>}
|
||||
*/
|
||||
async updateOpenViduRoomPreferences(roomId: string, roomPreferences: MeetRoomPreferences): Promise<R> {
|
||||
// TODO: Move validation to the controller layer
|
||||
this.validateRoomPreferences(roomPreferences);
|
||||
|
||||
const openviduRoom = await this.getOpenViduRoom(roomId);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user