Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions api-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ DB_DATABASE=
REDIS_HOST=
REDIS_PORT=

# Cloud (ovh or aws)
CLOUD_PROVIDER=

#OVH
OVH_KUBECONFIG_PATH=
OVH_CLUSTER_NAMESPACE=

OVH_IMAGE_GOOGLE=
OVH_IMAGE_TEAMS=
OVH_IMAGE_ZOOM=
OVH_IMAGE_PULL_SECRET=

#OVH bot env
OVH_ENVIRONMENT_NAME=
OVH_DEBUG=
OVH_HIGHLIGHT_PROJECT_ID=

# AWS
AWS_ACCESS_KEY=
Expand Down
2 changes: 2 additions & 0 deletions api-backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ pids

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

kubeconfig.yml
2 changes: 1 addition & 1 deletion api-backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use the official Node.js 20 image as a base image
FROM public.ecr.aws/docker/library/node:20.11.0
FROM public.ecr.aws/docker/library/node:22.15.1

# Set the working directory
WORKDIR /app
Expand Down
1 change: 1 addition & 0 deletions api-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@aws-sdk/s3-request-presigner": "^3.758.0",
"@grpc/grpc-js": "^1.12.6",
"@grpc/proto-loader": "^0.7.13",
"@kubernetes/client-node": "^1.3.0",
"@nestjs/bullmq": "^11.0.2",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^4.0.1",
Expand Down
4 changes: 4 additions & 0 deletions api-backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { AwsModule } from './aws/aws.module';
import { BullModule } from '@nestjs/bullmq';
import { CronJobModule } from './cron-job/cron-job.module';
import { GrpcModule } from './grpc/grpc.module';
import { OvhModule } from './ovh/ovh.module';
import { CloudModule } from './cloud/cloud.module';

@Module({
imports: [
Expand All @@ -38,6 +40,8 @@ import { GrpcModule } from './grpc/grpc.module';
AwsModule,
CronJobModule,
GrpcModule,
OvhModule,
CloudModule,
],
controllers: [AppController],
providers: [
Expand Down
37 changes: 25 additions & 12 deletions api-backend/src/aws/ecs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import {
ListTasksCommandOutput,
RunTaskCommand,
RunTaskCommandInput,
RunTaskCommandOutput,
StopTaskCommand,
StopTaskCommandInput,
StopTaskCommandOutput,
Task,
} from '@aws-sdk/client-ecs';

Expand All @@ -31,6 +29,10 @@ import { InjectQueue } from '@nestjs/bullmq';
import { Bot, ExecutionStatusLogEnum } from 'src/database/models/bot.model';
import { InjectModel } from '@nestjs/sequelize';
import { BotService } from 'src/bot/bot.service';
import {
ICloudService,
IRunTaskInput,
} from 'src/cloud/cloud-service.interface';
interface TaskInfo {
taskId?: string;
lastStatus?: string;
Expand All @@ -43,7 +45,7 @@ interface TaskInfo {
}

@Injectable()
export class ECSClientService {
export class ECSClientService implements ICloudService {
private readonly ecsClient: ECSClient;
private readonly clusterName: string;
private readonly launchType: LaunchType = 'FARGATE';
Expand Down Expand Up @@ -87,12 +89,12 @@ export class ECSClientService {
}
}

async runTask(
taskDefinition: string,
containerName: string,
command: string[],
async runTask({
containerName,
command,
taskDefinition,
taskCount = 1,
): Promise<RunTaskCommandOutput> {
}: IRunTaskInput): Promise<string> {
try {
const input: RunTaskCommandInput = {
cluster: this.clusterName,
Expand All @@ -118,22 +120,29 @@ export class ECSClientService {

const commandInstance = new RunTaskCommand(input);

return this.ecsClient.send(commandInstance);
const res = await this.ecsClient.send(commandInstance);

if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0))
throw new Error('Failed to start ECS task.');

const taskArn = res['tasks'][0]['taskArn'];

return this.getTaskIdByTaskArn(taskArn);
} catch (error) {
Logger.error(`Failed to run task: ${error}`);
throw error;
}
}

async stopTask(taskId: string): Promise<StopTaskCommandOutput> {
async stopTask(taskId: string): Promise<void> {
try {
const input: StopTaskCommandInput = {
cluster: this.clusterName,
task: taskId,
};
const command = new StopTaskCommand(input);

return this.ecsClient.send(command);
await this.ecsClient.send(command);
} catch (error) {
Logger.error(`Failed to stop task: ${error}`);
throw error;
Expand Down Expand Up @@ -242,7 +251,11 @@ export class ECSClientService {
const containerName = taskInfo?.containerOverrides?.[0]?.name;
const command = taskInfo.containerOverrides?.[0]?.command;

const res = await this.runTask(taskDefinition, containerName, command);
const res = await this.runTask({
taskDefinition,
containerName,
command,
});

if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0))
throw new Error('Failed to start ECS task.');
Expand Down
2 changes: 2 additions & 0 deletions api-backend/src/bot/bot.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { AwsModule } from 'src/aws/aws.module';
import { Bot } from 'src/database/models/bot.model';
import { BotController } from './bot.controller';
import { BotService } from './bot.service';
import { CloudModule } from 'src/cloud/cloud.module';
@Module({
imports: [
SequelizeModule.forFeature([Bot]),
forwardRef(() => AwsModule),
AuthModule,
CloudModule,
],
providers: [BotService],
controllers: [BotController],
Expand Down
89 changes: 6 additions & 83 deletions api-backend/src/bot/bot.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {
BadRequestException,
forwardRef,
Inject,
Injectable,
InternalServerErrorException,
NotFoundException,
Expand All @@ -12,7 +10,7 @@ import * as moment from 'moment-timezone';
import { lastValueFrom } from 'rxjs';
import { Op } from 'sequelize';
import { AwsService } from 'src/aws/aws.service';
import { ECSClientService } from 'src/aws/ecs.service';
import { CloudService } from 'src/cloud/cloud.service';
import { ApiKey } from 'src/database/models/api-key.model';
import {
Bot,
Expand All @@ -28,16 +26,14 @@ import {
TranscriptionLogResponse,
} from 'src/interfaces/proto-generated/transcript_management';
import { v4 as uuidv4 } from 'uuid';
import { TASK_STOPPED_ERROR_CODES } from 'src/constants/ecs';

@Injectable()
export class BotService {
constructor(
@InjectModel(Bot)
private botModel: typeof Bot,
private readonly awsService: AwsService,
@Inject(forwardRef(() => ECSClientService))
private readonly ecsService: ECSClientService,
private readonly cloudService: CloudService,
private readonly configService: ConfigService,
private readonly workerService: WorkerService,
) {}
Expand Down Expand Up @@ -199,7 +195,7 @@ export class BotService {
async leaveCall(botId: string): Promise<Bot> {
const bot = await this.botModel.findByPk(botId);
if (!bot) throw new NotFoundException('Bot not found');
await this.ecsService.stopTask(bot.taskId);
await this.cloudService.stopTask(bot.taskId);
await bot.update({ status: ExecutionStatusLogEnum.STOPPED });
await bot.reload();

Expand Down Expand Up @@ -254,29 +250,14 @@ export class BotService {
botName,
waitingTime = 8100,
) {
const taskDefinition = this.configService.get(
'aws.ecsTaskDefinitionGoogle',
);
const containerName = this.configService.get('aws.ecsContainerNameGoogle');
const command = [
'/bin/bash',
'-c',
`pulseaudio --start && python app.py "${meetingLink}" --bot-name "${botName}" --presigned-url-combined "${presignedUrls.tarUrl}" --presigned-url-audio "${presignedUrls.audioUrl}"`,
'--max-waiting-time',
waitingTime.toString(),
];
const res = await this.ecsService.runTask(
taskDefinition,
containerName,
command,
);

if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0))
throw new Error('Failed to start ECS task.');

const taskArn = res['tasks'][0]['taskArn'];
const splittedTaskArn = taskArn.split('/');
const taskId = splittedTaskArn[splittedTaskArn.length - 1];
const taskId = await this.cloudService.runGoogleBotTask(command);

return taskId;
}
Expand All @@ -287,27 +268,14 @@ export class BotService {
botName,
waitingTime = 8100,
) {
const taskDefinition = this.configService.get('aws.ecsTaskDefinitionTeams');
const containerName = this.configService.get('aws.ecsContainerNameTeams');
const command = [
'/bin/bash',
'-c',
`pulseaudio --start && python app.py "${meetingLink}" --bot-name "${botName}" --presigned-url-combined "${presignedUrls.tarUrl}" --presigned-url-audio "${presignedUrls.audioUrl}"`,
'--max-waiting-time',
waitingTime.toString(),
];
const res = await this.ecsService.runTask(
taskDefinition,
containerName,
command,
);

if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0))
throw new Error('Failed to start ECS task.');

const taskArn = res['tasks'][0]['taskArn'];
const splittedTaskArn = taskArn.split('/');
const taskId = splittedTaskArn[splittedTaskArn.length - 1];
const taskId = await this.cloudService.runTeemsBotTask(command);

return taskId;
}
Expand All @@ -318,8 +286,6 @@ export class BotService {
botName,
waitingTime = 8100,
) {
const taskDefinition = this.configService.get('aws.ecsTaskDefinitionZoom');
const containerName = this.configService.get('aws.ecsContainerNameZoom');
const command = [
'/bin/bash',
'-c',
Expand All @@ -330,18 +296,7 @@ export class BotService {
}" --max-waiting-time ${waitingTime.toString()}`,
];

const res = await this.ecsService.runTask(
taskDefinition,
containerName,
command,
);

if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0))
throw new Error('Failed to start ECS task.');

const taskArn = res['tasks'][0]['taskArn'];
const splittedTaskArn = taskArn.split('/');
const taskId = splittedTaskArn[splittedTaskArn.length - 1];
const taskId = await this.cloudService.runZoomBotTask(command);

return taskId;
}
Expand Down Expand Up @@ -392,38 +347,6 @@ export class BotService {
};
}

async findBotsWithNonErrorFailures(): Promise<Bot[]> {
const bots = await this.botModel.findAll({
where: {
status: { [Op.not]: ExecutionStatusLogEnum.FAILED },
taskId: { [Op.not]: null },
// Only get bots from the last 24 hours
createdAt: {
[Op.gte]: moment().subtract(24, 'hours').toDate(),
},
},
include: [{ model: ApiKey, attributes: ['userId'] }],
});

// Filter bots to only those that failed for non-error reasons
const results = [];
for (const bot of bots) {
try {
const taskInfo = await this.ecsService.healthCheckTask(bot.taskId);
if (
taskInfo?.lastStatus === 'STOPPED' &&
!TASK_STOPPED_ERROR_CODES.includes(taskInfo?.stopCode)
) {
results.push(bot);
}
} catch (error) {
console.error(`Error checking task status for bot ${bot.id}:`, error);
}
}

return results;
}

async triggerTranscriptGeneration(botId: string): Promise<void> {
const bot = await this.botModel.findByPk(botId, {
include: [{ model: ApiKey, attributes: ['userId'] }],
Expand Down
21 changes: 21 additions & 0 deletions api-backend/src/cloud/cloud-service.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface ICloudService {
runTask(input: IRunTaskInput): Promise<string>;
stopTask(taskId: string): Promise<void>;
syncTaskStatus(): Promise<void>;
}

export interface IRunTaskInput {
command: string[];
containerName?: string;
image?: string;
taskDefinition?: string;
taskCount?: number;
resourceRequests?: {
cpu: string;
memory: string;
};
resourceLimits?: {
cpu: string;
memory: string;
};
}
11 changes: 11 additions & 0 deletions api-backend/src/cloud/cloud.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AwsModule } from 'src/aws/aws.module';
import { OvhModule } from 'src/ovh/ovh.module';
import { CloudService } from './cloud.service';

@Module({
imports: [OvhModule, AwsModule],
providers: [CloudService],
exports: [CloudService],
})
export class CloudModule {}
18 changes: 18 additions & 0 deletions api-backend/src/cloud/cloud.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CloudService } from './cloud.service';

describe('CloudService', () => {
let service: CloudService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CloudService],
}).compile();

service = module.get<CloudService>(CloudService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Loading