From 6cb7e576c6e926735a2d7d8fbdcbca490ba3ce05 Mon Sep 17 00:00:00 2001 From: 0fatal <72899968+0fatal@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:11:08 +0800 Subject: [PATCH] fix(server): billing for all apps including stopped apps (#1781) --- server/src/application/application.service.ts | 9 + .../billing/billing-creation-task.service.ts | 220 +++++++++--------- .../billing/billing-payment-task.service.ts | 11 +- server/src/billing/billing.service.ts | 6 - server/src/instance/instance-task.service.ts | 1 - 5 files changed, 121 insertions(+), 126 deletions(-) diff --git a/server/src/application/application.service.ts b/server/src/application/application.service.ts index a7bb92edf0..37812a57d6 100644 --- a/server/src/application/application.service.ts +++ b/server/src/application/application.service.ts @@ -116,6 +116,7 @@ export class ApplicationService { regionId: new ObjectId(dto.regionId), runtimeId: new ObjectId(dto.runtimeId), billingLockedAt: TASK_LOCK_INIT_TIME, + latestBillingTime: this.getHourTime(), createdAt: new Date(), updatedAt: new Date(), }, @@ -499,4 +500,12 @@ export class ApplicationService { } return autoscaling } + + private getHourTime() { + const latestTime = new Date() + latestTime.setMinutes(0) + latestTime.setSeconds(0) + latestTime.setMilliseconds(0) + return latestTime + } } diff --git a/server/src/billing/billing-creation-task.service.ts b/server/src/billing/billing-creation-task.service.ts index 08b55ea64e..90d394bae8 100644 --- a/server/src/billing/billing-creation-task.service.ts +++ b/server/src/billing/billing-creation-task.service.ts @@ -62,7 +62,6 @@ export class BillingCreationTaskService { latestBillingTime: { $lt: new Date(Date.now() - 1000 * this.billingInterval), }, - state: ApplicationState.Running, }, { $set: { billingLockedAt: new Date() } }, ) @@ -81,32 +80,6 @@ export class BillingCreationTaskService { this.logger.warn(`No billing time found for application: ${app.appid}`) return } - - // unlock billing if billing time is not the latest - if (Date.now() - billingTime.getTime() > 1000 * this.billingInterval) { - this.logger.warn( - `Unlocking billing for application: ${app.appid} since billing time is not the latest`, - ) - - await db.collection('Application').updateOne( - { appid: app.appid }, - { - $set: { - billingLockedAt: TASK_LOCK_INIT_TIME, - latestBillingTime: billingTime, - }, - }, - ) - } else { - await db.collection('Application').updateOne( - { appid: app.appid }, - { - $set: { - latestBillingTime: billingTime, - }, - }, - ) - } } catch (err) { this.logger.error( 'handleApplicationBillingCreating error', @@ -122,13 +95,9 @@ export class BillingCreationTaskService { this.logger.debug(`Start creating billing for application: ${app.appid}`) const appid = app.appid - const db = SystemDatabase.db // determine latest billing time & next metering time - const latestBillingTime = - app.latestBillingTime > TASK_LOCK_INIT_TIME - ? app.latestBillingTime - : await this.getLatestBillingTime(appid) + const latestBillingTime = app.latestBillingTime const nextMeteringTime = new Date( latestBillingTime.getTime() + 1000 * this.billingInterval, ) @@ -142,9 +111,14 @@ export class BillingCreationTaskService { app, nextMeteringTime, ) - if (!meteringData) { - this.logger.warn(`No metering data found for application: ${appid}`) - return nextMeteringTime + if (meteringData.cpu === 0 && meteringData.memory === 0) { + if ( + [ApplicationState.Running, ApplicationState.Restarting].includes( + app.state, + ) + ) { + this.logger.warn(`No metering data found for application: ${appid}`) + } } // get application bundle @@ -168,59 +142,107 @@ export class BillingCreationTaskService { const startAt = new Date( nextMeteringTime.getTime() - 1000 * this.billingInterval, ) - const inserted = await db - .collection('ApplicationBilling') - .insertOne({ - appid, - state: - priceResult.total === 0 - ? ApplicationBillingState.Done - : ApplicationBillingState.Pending, - amount: priceResult.total, - detail: { - cpu: { - usage: priceInput.cpu, - amount: priceResult.cpu, - }, - memory: { - usage: priceInput.memory, - amount: priceResult.memory, - }, - databaseCapacity: { - usage: priceInput.databaseCapacity, - amount: priceResult.databaseCapacity, - }, - storageCapacity: { - usage: priceInput.storageCapacity, - amount: priceResult.storageCapacity, + + const db = SystemDatabase.db + const client = SystemDatabase.client + const session = client.startSession() + session.startTransaction() + + try { + const inserted = await db + .collection('ApplicationBilling') + .insertOne( + { + appid, + state: + priceResult.total === 0 + ? ApplicationBillingState.Done + : ApplicationBillingState.Pending, + amount: priceResult.total, + detail: { + cpu: { + usage: priceInput.cpu, + amount: priceResult.cpu, + }, + memory: { + usage: priceInput.memory, + amount: priceResult.memory, + }, + databaseCapacity: { + usage: priceInput.databaseCapacity, + amount: priceResult.databaseCapacity, + }, + storageCapacity: { + usage: priceInput.storageCapacity, + amount: priceResult.storageCapacity, + }, + dedicatedDatabaseCPU: { + usage: priceInput.dedicatedDatabase.cpu, + amount: priceResult.dedicatedDatabase.cpu, + }, + dedicatedDatabaseMemory: { + usage: priceInput.dedicatedDatabase.memory, + amount: priceResult.dedicatedDatabase.memory, + }, + dedicatedDatabaseCapacity: { + usage: priceInput.dedicatedDatabase.capacity, + amount: priceResult.dedicatedDatabase.capacity, + }, + }, + startAt: startAt, + endAt: nextMeteringTime, + lockedAt: TASK_LOCK_INIT_TIME, + createdAt: new Date(), + updatedAt: new Date(), + createdBy: app.createdBy, }, - dedicatedDatabaseCPU: { - usage: priceInput.dedicatedDatabase.cpu, - amount: priceResult.dedicatedDatabase.cpu, + { + session, }, - dedicatedDatabaseMemory: { - usage: priceInput.dedicatedDatabase.memory, - amount: priceResult.dedicatedDatabase.memory, + ) + + const billingTime = nextMeteringTime + // unlock billing if billing time is not the latest + if (Date.now() - billingTime.getTime() > 1000 * this.billingInterval) { + this.logger.warn( + `Unlocking billing for application: ${app.appid} since billing time is not the latest`, + ) + + await db.collection('Application').updateOne( + { appid: app.appid }, + { + $set: { + billingLockedAt: TASK_LOCK_INIT_TIME, + latestBillingTime: billingTime, + }, }, - dedicatedDatabaseCapacity: { - usage: priceInput.dedicatedDatabase.capacity, - amount: priceResult.dedicatedDatabase.capacity, + { session }, + ) + } else { + await db.collection('Application').updateOne( + { appid: app.appid }, + { + $set: { + latestBillingTime: billingTime, + }, }, - }, - startAt: startAt, - endAt: nextMeteringTime, - lockedAt: TASK_LOCK_INIT_TIME, - createdAt: new Date(), - updatedAt: new Date(), - createdBy: app.createdBy, - }) + { session }, + ) + } + await session.commitTransaction() - this.logger.log( - `Billing creation complete for application: ${appid} from ${startAt.toISOString()} to ${nextMeteringTime.toISOString()} for billing ${ - inserted.insertedId - }`, - ) - return nextMeteringTime + this.logger.log( + `Billing creation complete for application: ${appid} from ${startAt.toISOString()} to ${nextMeteringTime.toISOString()} for billing ${ + inserted.insertedId + }`, + ) + return billingTime + } catch (err) { + await session.abortTransaction() + throw err + } finally { + session.endSession() + } } private buildCalculatePriceInput( @@ -245,36 +267,4 @@ export class BillingCreationTaskService { return dto } - - private async getLatestBillingTime(appid: string) { - const db = SystemDatabase.db - - // get latest billing - // TODO: perf issue? - const latestBilling = await db - .collection('ApplicationBilling') - .findOne({ appid }, { sort: { endAt: -1 } }) - - if (latestBilling) { - this.logger.debug(`Found latest billing record for application: ${appid}`) - return latestBilling.endAt - } - - this.logger.debug( - `No previous billing record, setting latest time to last hour for application: ${appid}`, - ) - - const latestTime = this.getHourTime() - latestTime.setHours(latestTime.getHours() - 1) - - return latestTime - } - - private getHourTime() { - const latestTime = new Date() - latestTime.setMinutes(0) - latestTime.setSeconds(0) - latestTime.setMilliseconds(0) - return latestTime - } } diff --git a/server/src/billing/billing-payment-task.service.ts b/server/src/billing/billing-payment-task.service.ts index b22767037a..eee1f047b5 100644 --- a/server/src/billing/billing-payment-task.service.ts +++ b/server/src/billing/billing-payment-task.service.ts @@ -127,7 +127,7 @@ export class BillingPaymentTaskService { // stop application if balance is not enough if (newBalance < 0) { - await db.collection('Application').updateOne( + const res = await db.collection('Application').updateOne( { appid: billing.appid, state: ApplicationState.Running }, { $set: { @@ -137,9 +137,12 @@ export class BillingPaymentTaskService { }, { session }, ) - this.logger.warn( - `Application ${billing.appid} stopped due to insufficient balance`, - ) + + if (res.modifiedCount > 0) { + this.logger.warn( + `Application ${billing.appid} stopped due to insufficient balance`, + ) + } } }) } catch (error) { diff --git a/server/src/billing/billing.service.ts b/server/src/billing/billing.service.ts index 04a3493234..4782344147 100644 --- a/server/src/billing/billing.service.ts +++ b/server/src/billing/billing.service.ts @@ -295,15 +295,9 @@ export class BillingService { .then((res) => res.result[0]) .then((res) => Number(res.value.value)) - let error = false - const [cpu, memory] = await Promise.all([cpuTask, memoryTask]).catch(() => { - error = true return [0, 0] }) - if (error) { - return null - } return { cpu, diff --git a/server/src/instance/instance-task.service.ts b/server/src/instance/instance-task.service.ts index 2d2820887a..8903796f9e 100644 --- a/server/src/instance/instance-task.service.ts +++ b/server/src/instance/instance-task.service.ts @@ -220,7 +220,6 @@ export class InstanceTaskService { state: toState, phase: ApplicationPhase.Started, lockedAt: TASK_LOCK_INIT_TIME, - latestBillingTime: this.getHourTime(), updatedAt: new Date(), }, },