From 5b392df5d060dd1b87ae834f4de0a9951c976328 Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Fri, 25 Aug 2023 05:57:48 +0000 Subject: [PATCH 1/5] init --- server/src/user/entities/user-quota.ts | 11 +++++++++++ server/src/user/quota.service.ts.service.ts | 4 ++++ server/src/user/user.module.ts | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 server/src/user/quota.service.ts.service.ts diff --git a/server/src/user/entities/user-quota.ts b/server/src/user/entities/user-quota.ts index b3fc125bae..2921f0730c 100644 --- a/server/src/user/entities/user-quota.ts +++ b/server/src/user/entities/user-quota.ts @@ -1,6 +1,11 @@ import { ApiProperty } from '@nestjs/swagger' import { ObjectId } from 'mongodb' +type LimitOfDatabaseSyncCount = { + countLimit: number + timePeriodInSeconds: number +} + export class UserQuota { @ApiProperty({ type: String }) _id?: ObjectId @@ -17,6 +22,12 @@ export class UserQuota { @ApiProperty() limitCountOfApplication: number + @ApiProperty({ + description: 'Limits of database synchronization count and time period.', + type: { countLimit: Number, timePeriodInSeconds: Number }, + }) + limitOfDatabaseSyncCount: LimitOfDatabaseSyncCount + @ApiProperty() createdAt: Date diff --git a/server/src/user/quota.service.ts.service.ts b/server/src/user/quota.service.ts.service.ts new file mode 100644 index 0000000000..014abf4e6a --- /dev/null +++ b/server/src/user/quota.service.ts.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common' + +@Injectable() +export class QuotaServiceTsService {} diff --git a/server/src/user/user.module.ts b/server/src/user/user.module.ts index 1af6522d3a..18b5bfdcb1 100644 --- a/server/src/user/user.module.ts +++ b/server/src/user/user.module.ts @@ -3,9 +3,10 @@ import { UserService } from './user.service' import { PatService } from './pat.service' import { PatController } from './pat.controller' import { UserController } from './user.controller' +import { QuotaServiceTsService } from './quota.service.ts.service' @Module({ - providers: [UserService, PatService], + providers: [UserService, PatService, QuotaServiceTsService], exports: [UserService], controllers: [PatController, UserController], }) From 8d22ca713b2502056e75f75ac61523236275649c Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Fri, 25 Aug 2023 11:22:05 +0000 Subject: [PATCH 2/5] add quota --- .../src/application/application.controller.ts | 19 ++++- server/src/application/application.module.ts | 4 + server/src/initializer/initializer.service.ts | 39 ++++++++++ server/src/user/quota.service.ts | 78 +++++++++++++++++++ server/src/user/quota.service.ts.service.ts | 4 - server/src/user/user.controller.ts | 18 ++++- server/src/user/user.module.ts | 12 ++- 7 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 server/src/user/quota.service.ts delete mode 100644 server/src/user/quota.service.ts.service.ts diff --git a/server/src/application/application.controller.ts b/server/src/application/application.controller.ts index 1d22082a32..c1e0ab7e3f 100644 --- a/server/src/application/application.controller.ts +++ b/server/src/application/application.controller.ts @@ -55,6 +55,7 @@ import { User } from 'src/user/entities/user' import { GroupWithRole } from 'src/group/entities/group' import { isEqual } from 'lodash' import { InstanceService } from 'src/instance/instance.service' +import { QuotaService } from 'src/user/quota.service' @ApiTags('Application') @Controller('applications') @@ -71,6 +72,7 @@ export class ApplicationController { private readonly account: AccountService, private readonly resource: ResourceService, private readonly runtimeDomain: RuntimeDomainService, + private readonly quotaServiceTsService: QuotaService, ) {} /** @@ -118,9 +120,11 @@ export class ApplicationController { } // one user can only have 20 applications in one region - const count = await this.application.countByUser(user._id) - if (count > 20) { - return ResponseUtil.error(`too many applications, limit is 20`) + const limitResource = await this.quotaServiceTsService.resourceLimit( + user._id, + ) + if (limitResource) { + return ResponseUtil.error(`resource limit`) } // check account balance @@ -311,6 +315,7 @@ export class ApplicationController { @Param('appid') appid: string, @Body() dto: UpdateApplicationBundleDto, @InjectApplication() app: ApplicationWithRelations, + @InjectUser() user: User, ) { const error = dto.autoscaling.validate() if (error) { @@ -341,6 +346,14 @@ export class ApplicationController { return ResponseUtil.error('invalid resource specification') } + // check resource limit + const limitResource = await this.quotaServiceTsService.resourceLimit( + user._id, + ) + if (limitResource) { + return ResponseUtil.error(`resource limit`) + } + const doc = await this.application.updateBundle(appid, dto, isTrialTier) // restart running application if cpu or memory changed diff --git a/server/src/application/application.module.ts b/server/src/application/application.module.ts index c24670a3e7..3420cafda8 100644 --- a/server/src/application/application.module.ts +++ b/server/src/application/application.module.ts @@ -18,6 +18,8 @@ import { BundleService } from './bundle.service' import { ResourceService } from 'src/billing/resource.service' import { FunctionRecycleBinService } from 'src/recycle-bin/cloud-function/function-recycle-bin.service' import { HttpModule } from '@nestjs/axios' +import { QuotaService } from 'src/user/quota.service' +import { SettingService } from 'src/setting/setting.service' @Module({ imports: [ @@ -41,6 +43,8 @@ import { HttpModule } from '@nestjs/axios' WebsiteService, BundleService, ResourceService, + QuotaService, + SettingService, ], exports: [ ApplicationService, diff --git a/server/src/initializer/initializer.service.ts b/server/src/initializer/initializer.service.ts index ab984d0fea..292e63c848 100644 --- a/server/src/initializer/initializer.service.ts +++ b/server/src/initializer/initializer.service.ts @@ -12,6 +12,7 @@ import { AuthProvider, AuthProviderState, } from 'src/authentication/entities/auth-provider' +import { Setting } from 'src/setting/entities/setting' @Injectable() export class InitializerService { @@ -322,4 +323,42 @@ export class InitializerService { this.logger.verbose('Created default resource templates') } + + // create default settings + async createDefaultSettings() { + // check if exists + const existed = await this.db + .collection('Setting') + .countDocuments() + + if (existed) { + this.logger.debug('default settings already exists') + return + } + + await this.db.collection('Setting').insertOne({ + public: false, + key: 'resource_limit', + value: 'default', + desc: 'resource limit of user', + metadata: { + limitOfCPU: 2000, + limitOfMemory: 2000, + limitCountOfApplication: 20, + limitOfDatabaseSyncCount: { + countLimit: 10, + timePeriodInSeconds: 86400, + }, + }, + }) + + await this.db.collection('Setting').insertOne({ + public: true, + key: 'invitation_profit', + value: '100', + desc: 'Set up invitation rebate', + }) + + this.logger.verbose('Created default settings') + } } diff --git a/server/src/user/quota.service.ts b/server/src/user/quota.service.ts new file mode 100644 index 0000000000..451b750596 --- /dev/null +++ b/server/src/user/quota.service.ts @@ -0,0 +1,78 @@ +import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common' +import { ObjectId } from 'mongodb' +import { ApplicationService } from 'src/application/application.service' +import { ApplicationWithRelations } from 'src/application/entities/application' +import { SystemDatabase } from 'src/system-database' +import { UserQuota } from './entities/user-quota' +import { SettingService } from 'src/setting/setting.service' + +@Injectable() +export class QuotaService { + private readonly logger = new Logger(QuotaService.name) + constructor( + @Inject(forwardRef(() => ApplicationService)) + private readonly applicationService: ApplicationService, + private readonly settingService: SettingService, + ) {} + + async resourceLimit(uid: ObjectId) { + const client = SystemDatabase.client + const db = client.db() + + const defaultUserQuotaSetting = await this.settingService.findOne( + 'resource_limit', + ) + + if (!defaultUserQuotaSetting) { + return true + } + + const defaultUserQuota: UserQuota = { + uid, + limitOfCPU: defaultUserQuotaSetting.metadata.limitOfCPU, + limitOfMemory: defaultUserQuotaSetting.metadata.limitOfMemory, + limitCountOfApplication: + defaultUserQuotaSetting.metadata.limitCountOfApplication, + limitOfDatabaseSyncCount: { + countLimit: + defaultUserQuotaSetting.metadata.limitOfDatabaseSyncCount.countLimit, + timePeriodInSeconds: + defaultUserQuotaSetting.metadata.limitOfDatabaseSyncCount + .timePeriodInSeconds, + }, + createdAt: new Date(), + updatedAt: new Date(), + } + + const userQuota: UserQuota = await db + .collection('UserQuota') + .findOne({ uid }) + + if (!userQuota) { + await db.collection('UserQuota').insertOne(defaultUserQuota) + } + + const allApplications: ApplicationWithRelations[] = + await this.applicationService.findAllByUser(uid) + + let totalLimitCPU = 0 + let totalLimitMemory = 0 + + for (const app of allApplications) { + if (app.bundle && app.bundle.resource) { + totalLimitCPU += app.bundle.resource.limitCPU + totalLimitMemory += app.bundle.resource.limitMemory + } + } + + if ( + totalLimitCPU > userQuota.limitOfCPU || + totalLimitMemory > userQuota.limitOfMemory || + allApplications.length > userQuota.limitCountOfApplication + ) { + return true + } + + return false + } +} diff --git a/server/src/user/quota.service.ts.service.ts b/server/src/user/quota.service.ts.service.ts deleted file mode 100644 index 014abf4e6a..0000000000 --- a/server/src/user/quota.service.ts.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common' - -@Injectable() -export class QuotaServiceTsService {} diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index ad22cf90fb..041e207fc3 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -22,7 +22,11 @@ import { import { UserService } from './user.service' import { FileInterceptor } from '@nestjs/platform-express' import { IRequest, IResponse } from 'src/utils/interface' -import { ApiResponseObject, ResponseUtil } from 'src/utils/response' +import { + ApiResponseObject, + ResponseUtil, + ApiResponseString, +} from 'src/utils/response' import { JwtAuthGuard } from 'src/authentication/jwt.auth.guard' import { UserWithProfile } from './entities/user' import { SmsService } from 'src/authentication/phone/sms.service' @@ -34,6 +38,7 @@ import { ObjectId } from 'mongodb' import { EmailService } from 'src/authentication/email/email.service' import { EmailVerifyCodeType } from 'src/authentication/entities/email-verify-code' import { BindEmailDto } from './dto/bind-email.dto' +import { QuotaService } from './quota.service' @ApiTags('User') @ApiBearerAuth('Authorization') @@ -43,6 +48,7 @@ export class UserController { private readonly userService: UserService, private readonly smsService: SmsService, private readonly emailService: EmailService, + private readonly quotaServiceTsService: QuotaService, ) {} /** @@ -222,4 +228,14 @@ export class UserController { const user = request.user return ResponseUtil.ok(user) } + + @UseGuards(JwtAuthGuard) + @Get('test') + @ApiResponseString() + @ApiBearerAuth('Authorization') + async test(@Req() request: IRequest) { + const uid = request.user._id + this.quotaServiceTsService.resourceLimit(uid) + return ResponseUtil.ok('ww') + } } diff --git a/server/src/user/user.module.ts b/server/src/user/user.module.ts index 18b5bfdcb1..ed1aaf7bae 100644 --- a/server/src/user/user.module.ts +++ b/server/src/user/user.module.ts @@ -3,10 +3,18 @@ import { UserService } from './user.service' import { PatService } from './pat.service' import { PatController } from './pat.controller' import { UserController } from './user.controller' -import { QuotaServiceTsService } from './quota.service.ts.service' +import { QuotaService } from './quota.service' +import { ApplicationService } from 'src/application/application.service' +import { SettingService } from 'src/setting/setting.service' @Module({ - providers: [UserService, PatService, QuotaServiceTsService], + providers: [ + UserService, + PatService, + QuotaService, + ApplicationService, + SettingService, + ], exports: [UserService], controllers: [PatController, UserController], }) From eee5ec7a7f0d6830aef5efe9a5b35ea1cc6f74f3 Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Fri, 25 Aug 2023 11:23:32 +0000 Subject: [PATCH 3/5] chore --- server/src/user/quota.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/user/quota.service.ts b/server/src/user/quota.service.ts index 451b750596..75c41d2b8e 100644 --- a/server/src/user/quota.service.ts +++ b/server/src/user/quota.service.ts @@ -75,4 +75,7 @@ export class QuotaService { return false } + async databaseSyncLimit(uid: ObjectId) { + return false + } } From 2800bb0908006bacb655cd3fa329f8bd0a76ead0 Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Mon, 28 Aug 2023 04:19:12 +0000 Subject: [PATCH 4/5] add database sync limit --- .../src/application/application.controller.ts | 8 +- server/src/database/database.controller.ts | 33 ++++++- server/src/database/database.module.ts | 4 + server/src/database/database.service.ts | 11 ++- server/src/database/entities/database-sync.ts | 6 ++ server/src/initializer/initializer.service.ts | 18 +++- server/src/user/quota.service.ts | 98 +++++++++++++------ server/src/user/user.controller.ts | 10 -- 8 files changed, 136 insertions(+), 52 deletions(-) create mode 100644 server/src/database/entities/database-sync.ts diff --git a/server/src/application/application.controller.ts b/server/src/application/application.controller.ts index c1e0ab7e3f..89feed7255 100644 --- a/server/src/application/application.controller.ts +++ b/server/src/application/application.controller.ts @@ -119,12 +119,12 @@ export class ApplicationController { } } - // one user can only have 20 applications in one region + // check if a user exceeds the resource limit in a region const limitResource = await this.quotaServiceTsService.resourceLimit( user._id, ) if (limitResource) { - return ResponseUtil.error(`resource limit`) + return ResponseUtil.error(`exceed resource limit`) } // check account balance @@ -346,12 +346,12 @@ export class ApplicationController { return ResponseUtil.error('invalid resource specification') } - // check resource limit + // check if a user exceeds the resource limit in a region const limitResource = await this.quotaServiceTsService.resourceLimit( user._id, ) if (limitResource) { - return ResponseUtil.error(`resource limit`) + return ResponseUtil.error(`exceed resource limit`) } const doc = await this.application.updateBundle(appid, dto, isTrialTier) diff --git a/server/src/database/database.controller.ts b/server/src/database/database.controller.ts index b7ff49279f..54be7b033f 100644 --- a/server/src/database/database.controller.ts +++ b/server/src/database/database.controller.ts @@ -32,6 +32,9 @@ import { unlink, writeFile } from 'node:fs/promises' import * as os from 'os' import { ResponseUtil } from 'src/utils/response' import { ImportDatabaseDto } from './dto/import-database.dto' +import { InjectUser } from 'src/utils/decorator' +import { User } from 'src/user/entities/user' +import { QuotaService } from 'src/user/quota.service' @ApiTags('Database') @ApiBearerAuth('Authorization') @@ -39,7 +42,10 @@ import { ImportDatabaseDto } from './dto/import-database.dto' export class DatabaseController { private readonly logger = new Logger(DatabaseController.name) - constructor(private readonly dbService: DatabaseService) {} + constructor( + private readonly dbService: DatabaseService, + private readonly quotaService: QuotaService, + ) {} /** * The database proxy for database management @@ -91,7 +97,15 @@ export class DatabaseController { async exportDatabase( @Param('appid') appid: string, @Res({ passthrough: true }) res: IResponse, + @InjectUser() user: User, ) { + // Check if user data import and export is out of limits + const databaseSyncLimit = await this.quotaService.databaseSyncLimit( + user._id, + ) + if (databaseSyncLimit) { + return ResponseUtil.error('Database sync limit exceeded') + } const tempFilePath = path.join( os.tmpdir(), 'mongodb-data', @@ -104,7 +118,7 @@ export class DatabaseController { mkdirSync(path.dirname(tempFilePath), { recursive: true }) } - await this.dbService.exportDatabase(appid, tempFilePath) + await this.dbService.exportDatabase(appid, tempFilePath, user._id) const filename = path.basename(tempFilePath) res.set({ @@ -132,7 +146,15 @@ export class DatabaseController { @UploadedFile() file: Express.Multer.File, @Body('sourceAppid') sourceAppid: string, @Param('appid') appid: string, + @InjectUser() user: User, ) { + // Check if user data import and export is out of limits + const databaseSyncLimit = await this.quotaService.databaseSyncLimit( + user._id, + ) + if (databaseSyncLimit) { + return ResponseUtil.error('Database sync limit exceeded') + } // check if db is valid if (!/^[a-z0-9]{6}$/.test(sourceAppid)) { return ResponseUtil.error('Invalid source appid') @@ -156,7 +178,12 @@ export class DatabaseController { try { await writeFile(tempFilePath, file.buffer) - await this.dbService.importDatabase(appid, sourceAppid, tempFilePath) + await this.dbService.importDatabase( + appid, + sourceAppid, + tempFilePath, + user._id, + ) return ResponseUtil.ok({}) } finally { if (existsSync(tempFilePath)) await unlink(tempFilePath) diff --git a/server/src/database/database.module.ts b/server/src/database/database.module.ts index 6b0193a16e..73cb008d6a 100644 --- a/server/src/database/database.module.ts +++ b/server/src/database/database.module.ts @@ -12,6 +12,8 @@ import { ApplicationService } from 'src/application/application.service' import { BundleService } from 'src/application/bundle.service' import { DatabaseUsageLimitTaskService } from './database-usage-limit-task.service' import { DatabaseUsageCaptureTaskService } from './database-usage-capture-task.service' +import { QuotaService } from 'src/user/quota.service' +import { SettingService } from 'src/setting/setting.service' @Module({ imports: [], @@ -31,6 +33,8 @@ import { DatabaseUsageCaptureTaskService } from './database-usage-capture-task.s BundleService, DatabaseUsageCaptureTaskService, DatabaseUsageLimitTaskService, + SettingService, + QuotaService, ], exports: [ CollectionService, diff --git a/server/src/database/database.service.ts b/server/src/database/database.service.ts index 707955551d..6af83c1491 100644 --- a/server/src/database/database.service.ts +++ b/server/src/database/database.service.ts @@ -16,6 +16,8 @@ import { } from './entities/database' import { exec } from 'node:child_process' import { promisify } from 'node:util' +import { DatabaseSync } from './entities/database-sync' +import { ObjectId } from 'mongodb' const p_exec = promisify(exec) @@ -211,7 +213,7 @@ export class DatabaseService { } } - async exportDatabase(appid: string, filePath: string) { + async exportDatabase(appid: string, filePath: string, uid: ObjectId) { const region = await this.regionService.findByAppId(appid) const database = await this.findOne(appid) assert(database, 'Database not found') @@ -223,6 +225,9 @@ export class DatabaseService { await p_exec( `mongodump --uri='${connectionUri}' --gzip --archive=${filePath}`, ) + await this.db + .collection('DatabaseSync') + .insertOne({ uid, createdAt: new Date() }) } catch (error) { this.logger.error(`failed to export db ${appid}`, error) throw error @@ -233,6 +238,7 @@ export class DatabaseService { appid: string, dbName: string, filePath: string, + uid: ObjectId, ): Promise { const region = await this.regionService.findByAppId(appid) const database = await this.findOne(appid) @@ -245,6 +251,9 @@ export class DatabaseService { await p_exec( `mongorestore --uri='${connectionUri}' --gzip --archive='${filePath}' --nsFrom="${dbName}.*" --nsTo="${appid}.*" -v --nsInclude="${dbName}.*"`, ) + await this.db + .collection('DatabaseSync') + .insertOne({ uid, createdAt: new Date() }) } catch (error) { console.error(`failed to import db to ${appid}:`, error) throw error diff --git a/server/src/database/entities/database-sync.ts b/server/src/database/entities/database-sync.ts new file mode 100644 index 0000000000..6757432244 --- /dev/null +++ b/server/src/database/entities/database-sync.ts @@ -0,0 +1,6 @@ +import { ObjectId } from 'mongodb' + +export class DatabaseSync { + uid: ObjectId + createdAt: Date +} diff --git a/server/src/initializer/initializer.service.ts b/server/src/initializer/initializer.service.ts index 292e63c848..6ab21a79a7 100644 --- a/server/src/initializer/initializer.service.ts +++ b/server/src/initializer/initializer.service.ts @@ -25,6 +25,7 @@ export class InitializerService { await this.createDefaultAuthProvider() await this.createDefaultResourceOptions() await this.createDefaultResourceBundles() + await this.createDefaultSettings() } async createDefaultRegion() { @@ -342,8 +343,8 @@ export class InitializerService { value: 'default', desc: 'resource limit of user', metadata: { - limitOfCPU: 2000, - limitOfMemory: 2000, + limitOfCPU: 20000, + limitOfMemory: 20480, limitCountOfApplication: 20, limitOfDatabaseSyncCount: { countLimit: 10, @@ -355,10 +356,21 @@ export class InitializerService { await this.db.collection('Setting').insertOne({ public: true, key: 'invitation_profit', - value: '100', + value: '0', desc: 'Set up invitation rebate', }) + await this.db.collection('Setting').insertOne({ + public: true, + key: 'id_verify', + value: 'off', // on | off + desc: 'real name authentication', + metadata: { + message: '', + authenticateSite: '', + }, + }) + this.logger.verbose('Created default settings') } } diff --git a/server/src/user/quota.service.ts b/server/src/user/quota.service.ts index 75c41d2b8e..a4749252d2 100644 --- a/server/src/user/quota.service.ts +++ b/server/src/user/quota.service.ts @@ -5,10 +5,12 @@ import { ApplicationWithRelations } from 'src/application/entities/application' import { SystemDatabase } from 'src/system-database' import { UserQuota } from './entities/user-quota' import { SettingService } from 'src/setting/setting.service' +import { DatabaseSync } from 'src/database/entities/database-sync' @Injectable() export class QuotaService { private readonly logger = new Logger(QuotaService.name) + private db = SystemDatabase.db constructor( @Inject(forwardRef(() => ApplicationService)) private readonly applicationService: ApplicationService, @@ -16,15 +18,71 @@ export class QuotaService { ) {} async resourceLimit(uid: ObjectId) { - const client = SystemDatabase.client - const db = client.db() + const userQuota = await this.getUserQuota(uid) + if (!userQuota) { + return true + } + + const allApplications: ApplicationWithRelations[] = + await this.applicationService.findAllByUser(uid) + + let totalLimitCPU = 0 + let totalLimitMemory = 0 + + for (const app of allApplications) { + if (app.bundle && app.bundle.resource) { + totalLimitCPU += app.bundle.resource.limitCPU + totalLimitMemory += app.bundle.resource.limitMemory + } + } + + if ( + totalLimitCPU > userQuota.limitOfCPU || + totalLimitMemory > userQuota.limitOfMemory || + allApplications.length > userQuota.limitCountOfApplication + ) { + return true + } + + return false + } + + async databaseSyncLimit(uid: ObjectId) { + const userQuota = await this.getUserQuota(uid) + if (!userQuota) { + return true + } + // Calculate the time range for counting DatabaseSync documents + const currentTime = new Date() + const startTime = new Date( + currentTime.getTime() - + userQuota.limitOfDatabaseSyncCount.timePeriodInSeconds * 1000, + ) + const counts = await this.db + .collection('DatabaseSync') + .countDocuments({ + uid: uid, + createdAt: { + $gte: startTime, + $lte: currentTime, + }, + }) + + if (counts >= userQuota.limitOfDatabaseSyncCount.countLimit) { + return true // The user has exceeded their limit + } + + return false + } + + async getUserQuota(uid: ObjectId) { const defaultUserQuotaSetting = await this.settingService.findOne( 'resource_limit', ) if (!defaultUserQuotaSetting) { - return true + return null } const defaultUserQuota: UserQuota = { @@ -44,38 +102,16 @@ export class QuotaService { updatedAt: new Date(), } - const userQuota: UserQuota = await db + let userQuota: UserQuota = await this.db .collection('UserQuota') .findOne({ uid }) if (!userQuota) { - await db.collection('UserQuota').insertOne(defaultUserQuota) + await this.db + .collection('UserQuota') + .insertOne(defaultUserQuota) + userQuota = defaultUserQuota } - - const allApplications: ApplicationWithRelations[] = - await this.applicationService.findAllByUser(uid) - - let totalLimitCPU = 0 - let totalLimitMemory = 0 - - for (const app of allApplications) { - if (app.bundle && app.bundle.resource) { - totalLimitCPU += app.bundle.resource.limitCPU - totalLimitMemory += app.bundle.resource.limitMemory - } - } - - if ( - totalLimitCPU > userQuota.limitOfCPU || - totalLimitMemory > userQuota.limitOfMemory || - allApplications.length > userQuota.limitCountOfApplication - ) { - return true - } - - return false - } - async databaseSyncLimit(uid: ObjectId) { - return false + return userQuota } } diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index 041e207fc3..3aa62f4391 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -228,14 +228,4 @@ export class UserController { const user = request.user return ResponseUtil.ok(user) } - - @UseGuards(JwtAuthGuard) - @Get('test') - @ApiResponseString() - @ApiBearerAuth('Authorization') - async test(@Req() request: IRequest) { - const uid = request.user._id - this.quotaServiceTsService.resourceLimit(uid) - return ResponseUtil.ok('ww') - } } From 3cc94f898fee3c53bc657c99b65a8588ba3321a9 Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Mon, 4 Sep 2023 03:34:50 +0000 Subject: [PATCH 5/5] add some reminder message --- .../src/application/application.controller.ts | 9 +++-- server/src/database/database.service.ts | 6 ++-- ...tabase-sync.ts => database-sync-record.ts} | 2 +- server/src/user/quota.service.ts | 33 ++++++++++++------- 4 files changed, 32 insertions(+), 18 deletions(-) rename server/src/database/entities/{database-sync.ts => database-sync-record.ts} (67%) diff --git a/server/src/application/application.controller.ts b/server/src/application/application.controller.ts index 89feed7255..8fc9d26440 100644 --- a/server/src/application/application.controller.ts +++ b/server/src/application/application.controller.ts @@ -122,9 +122,11 @@ export class ApplicationController { // check if a user exceeds the resource limit in a region const limitResource = await this.quotaServiceTsService.resourceLimit( user._id, + dto.cpu, + dto.memory, ) if (limitResource) { - return ResponseUtil.error(`exceed resource limit`) + return ResponseUtil.error(limitResource) } // check account balance @@ -349,9 +351,12 @@ export class ApplicationController { // check if a user exceeds the resource limit in a region const limitResource = await this.quotaServiceTsService.resourceLimit( user._id, + dto.cpu, + dto.memory, + appid, ) if (limitResource) { - return ResponseUtil.error(`exceed resource limit`) + return ResponseUtil.error(limitResource) } const doc = await this.application.updateBundle(appid, dto, isTrialTier) diff --git a/server/src/database/database.service.ts b/server/src/database/database.service.ts index 6af83c1491..52b8a9a06f 100644 --- a/server/src/database/database.service.ts +++ b/server/src/database/database.service.ts @@ -16,7 +16,7 @@ import { } from './entities/database' import { exec } from 'node:child_process' import { promisify } from 'node:util' -import { DatabaseSync } from './entities/database-sync' +import { DatabaseSyncRecord } from './entities/database-sync-record' import { ObjectId } from 'mongodb' const p_exec = promisify(exec) @@ -226,7 +226,7 @@ export class DatabaseService { `mongodump --uri='${connectionUri}' --gzip --archive=${filePath}`, ) await this.db - .collection('DatabaseSync') + .collection('DatabaseSyncRecord') .insertOne({ uid, createdAt: new Date() }) } catch (error) { this.logger.error(`failed to export db ${appid}`, error) @@ -252,7 +252,7 @@ export class DatabaseService { `mongorestore --uri='${connectionUri}' --gzip --archive='${filePath}' --nsFrom="${dbName}.*" --nsTo="${appid}.*" -v --nsInclude="${dbName}.*"`, ) await this.db - .collection('DatabaseSync') + .collection('DatabaseSyncRecord') .insertOne({ uid, createdAt: new Date() }) } catch (error) { console.error(`failed to import db to ${appid}:`, error) diff --git a/server/src/database/entities/database-sync.ts b/server/src/database/entities/database-sync-record.ts similarity index 67% rename from server/src/database/entities/database-sync.ts rename to server/src/database/entities/database-sync-record.ts index 6757432244..ea7a09cf08 100644 --- a/server/src/database/entities/database-sync.ts +++ b/server/src/database/entities/database-sync-record.ts @@ -1,6 +1,6 @@ import { ObjectId } from 'mongodb' -export class DatabaseSync { +export class DatabaseSyncRecord { uid: ObjectId createdAt: Date } diff --git a/server/src/user/quota.service.ts b/server/src/user/quota.service.ts index a4749252d2..741624783e 100644 --- a/server/src/user/quota.service.ts +++ b/server/src/user/quota.service.ts @@ -5,7 +5,7 @@ import { ApplicationWithRelations } from 'src/application/entities/application' import { SystemDatabase } from 'src/system-database' import { UserQuota } from './entities/user-quota' import { SettingService } from 'src/setting/setting.service' -import { DatabaseSync } from 'src/database/entities/database-sync' +import { DatabaseSyncRecord } from 'src/database/entities/database-sync-record' @Injectable() export class QuotaService { @@ -17,10 +17,15 @@ export class QuotaService { private readonly settingService: SettingService, ) {} - async resourceLimit(uid: ObjectId) { + async resourceLimit( + uid: ObjectId, + cpu: number, + memory: number, + appid?: string, + ) { const userQuota = await this.getUserQuota(uid) if (!userQuota) { - return true + return 'user quota not found' } const allApplications: ApplicationWithRelations[] = @@ -30,21 +35,25 @@ export class QuotaService { let totalLimitMemory = 0 for (const app of allApplications) { - if (app.bundle && app.bundle.resource) { + if (app.bundle && app.bundle.resource && app.appid !== appid) { totalLimitCPU += app.bundle.resource.limitCPU totalLimitMemory += app.bundle.resource.limitMemory } } - if ( - totalLimitCPU > userQuota.limitOfCPU || - totalLimitMemory > userQuota.limitOfMemory || - allApplications.length > userQuota.limitCountOfApplication - ) { - return true + if (totalLimitCPU + cpu > userQuota.limitOfCPU) { + return 'cpu exceeds resource limit' } - return false + if (totalLimitMemory + memory > userQuota.limitOfMemory) { + return 'memory exceeds resource limit' + } + + if (allApplications.length > userQuota.limitCountOfApplication) { + return 'application counts exceeds resource limit' + } + + return null } async databaseSyncLimit(uid: ObjectId) { @@ -60,7 +69,7 @@ export class QuotaService { ) const counts = await this.db - .collection('DatabaseSync') + .collection('DatabaseSyncRecord') .countDocuments({ uid: uid, createdAt: {