backend: implement health check for storage providers and integrate into service initialization

This commit is contained in:
Carlos Santos 2025-07-15 16:24:55 +02:00
parent da26614033
commit 385dab5710
7 changed files with 138 additions and 1 deletions

View File

@ -90,5 +90,11 @@ const configureStorage = (storageMode: string) => {
export const initializeEagerServices = async () => {
// Force the creation of services that need to be initialized at startup
container.get(RecordingService);
await container.get(MeetStorageService).initializeGlobalPreferences();
// Perform comprehensive health checks before initializing other services
const storageService = container.get(MeetStorageService);
await storageService.checkStartupHealth();
// Initialize global preferences after health checks pass
await storageService.initializeGlobalPreferences();
};

View File

@ -148,4 +148,24 @@ export class ABSStorageProvider implements StorageProvider {
throw error;
}
}
/**
* Performs a health check on the Azure Blob Storage provider.
*/
async checkHealth(): Promise<{ accessible: boolean; bucketExists?: boolean; containerExists?: boolean }> {
try {
this.logger.debug('Performing ABS storage health check');
const healthResult = await this.azureBlobService.checkHealth();
return {
accessible: healthResult.accessible,
containerExists: healthResult.containerExists
};
} catch (error) {
this.logger.error(`ABS storage health check failed: ${error}`);
return {
accessible: false,
containerExists: false
};
}
}
}

View File

@ -287,4 +287,26 @@ export class ABSService {
return `${prefix}/${name}`;
}
/**
* Health check for Azure Blob Storage service and container accessibility.
* Verifies both service connectivity and container existence.
*/
async checkHealth(): Promise<{ accessible: boolean; containerExists: boolean }> {
try {
// Check if we can access the container by checking if it exists
const exists = await this.containerClient.exists();
if (exists) {
this.logger.verbose(`ABS health check: service accessible and container '${MEET_AZURE_CONTAINER_NAME}' exists`);
return { accessible: true, containerExists: true };
} else {
this.logger.error(`ABS container '${MEET_AZURE_CONTAINER_NAME}' does not exist`);
return { accessible: true, containerExists: false };
}
} catch (error: any) {
this.logger.error(`ABS health check failed: ${error.message}`);
return { accessible: false, containerExists: false };
}
}
}

View File

@ -137,4 +137,24 @@ export class S3StorageProvider implements StorageProvider {
throw error;
}
}
/**
* Performs a health check on the S3 storage provider.
*/
async checkHealth(): Promise<{ accessible: boolean; bucketExists?: boolean; containerExists?: boolean }> {
try {
this.logger.debug('Performing S3 storage health check');
const healthResult = await this.s3Service.checkHealth();
return {
accessible: healthResult.accessible,
bucketExists: healthResult.bucketExists
};
} catch (error) {
this.logger.error(`S3 storage health check failed: ${error}`);
return {
accessible: false,
bucketExists: false
};
}
}
}

View File

@ -233,6 +233,37 @@ export class S3Service {
this.logger.info('S3 client destroyed');
}
/**
* Health check for S3 service and bucket accessibility.
* Verifies both service connectivity and bucket existence.
*/
async checkHealth(): Promise<{ accessible: boolean; bucketExists: boolean }> {
try {
// Check if we can access the S3 service by listing objects with a small limit
await this.run(
new ListObjectsV2Command({
Bucket: MEET_S3_BUCKET,
MaxKeys: 1
})
);
// If we reach here, both service and bucket are accessible
this.logger.verbose(`S3 health check: service accessible and bucket '${MEET_S3_BUCKET}' exists`);
return { accessible: true, bucketExists: true };
} catch (error: any) {
this.logger.error(`S3 health check failed: ${error.message}`);
// Check if it's a bucket-specific error
if (error.name === 'NoSuchBucket') {
this.logger.error(`S3 bucket '${MEET_S3_BUCKET}' does not exist`);
return { accessible: true, bucketExists: false };
}
// Service is not accessible
return { accessible: false, bucketExists: false };
}
}
/**
* Constructs the full key for an S3 object by ensuring it includes the specified sub-bucket prefix.
* If the provided name already starts with the prefix, it is returned as-is.

View File

@ -93,6 +93,14 @@ export interface StorageProvider {
* @returns A promise that resolves to a readable stream of the object content
*/
getObjectAsStream(key: string, range?: { start: number; end: number }): Promise<Readable>;
/**
* Performs a health check on the storage provider.
* Verifies both service connectivity and container/bucket existence.
*
* @returns A promise that resolves to an object indicating accessibility and container/bucket existence
*/
checkHealth(): Promise<{ accessible: boolean; bucketExists?: boolean; containerExists?: boolean }>;
}
/**

View File

@ -67,6 +67,36 @@ export class MeetStorageService<
this.keyBuilder = keyBuilder;
}
/**
* Performs a health check on the storage system.
* Verifies both service connectivity and container/bucket existence.
* Terminates the process if storage is not accessible.
*/
async checkStartupHealth(): Promise<void> {
try {
this.logger.verbose('Performing storage health check...');
// Get the underlying storage service to perform health check
const isHealthy = await this.storageProvider.checkHealth();
if (!isHealthy.accessible) {
this.logger.error('Storage service is not accessible. Terminating process...');
process.exit(1);
}
if (!isHealthy.bucketExists && !isHealthy.containerExists) {
this.logger.error('Storage bucket/container does not exist. Terminating process...');
process.exit(1);
}
this.logger.verbose('Storage health check passed successfully');
} catch (error) {
this.logger.error('Storage health check failed:', error);
this.logger.error('Terminating process due to storage health check failure...');
process.exit(1);
}
}
// ==========================================
// GLOBAL PREFERENCES DOMAIN LOGIC
// ==========================================