Skip to content

Commit

Permalink
fix(server): billing for all apps including stopped apps (#1781)
Browse files Browse the repository at this point in the history
  • Loading branch information
0fatal authored Feb 22, 2024
1 parent 2364f57 commit 6cb7e57
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 126 deletions.
9 changes: 9 additions & 0 deletions server/src/application/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
Expand Down Expand Up @@ -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
}
}
220 changes: 105 additions & 115 deletions server/src/billing/billing-creation-task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export class BillingCreationTaskService {
latestBillingTime: {
$lt: new Date(Date.now() - 1000 * this.billingInterval),
},
state: ApplicationState.Running,
},
{ $set: { billingLockedAt: new Date() } },
)
Expand All @@ -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>('Application').updateOne(
{ appid: app.appid },
{
$set: {
billingLockedAt: TASK_LOCK_INIT_TIME,
latestBillingTime: billingTime,
},
},
)
} else {
await db.collection<Application>('Application').updateOne(
{ appid: app.appid },
{
$set: {
latestBillingTime: billingTime,
},
},
)
}
} catch (err) {
this.logger.error(
'handleApplicationBillingCreating error',
Expand All @@ -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,
)
Expand All @@ -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
Expand All @@ -168,59 +142,107 @@ export class BillingCreationTaskService {
const startAt = new Date(
nextMeteringTime.getTime() - 1000 * this.billingInterval,
)
const inserted = await db
.collection<ApplicationBilling>('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>('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>('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>('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(
Expand All @@ -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>('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
}
}
11 changes: 7 additions & 4 deletions server/src/billing/billing-payment-task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class BillingPaymentTaskService {

// stop application if balance is not enough
if (newBalance < 0) {
await db.collection<Application>('Application').updateOne(
const res = await db.collection<Application>('Application').updateOne(
{ appid: billing.appid, state: ApplicationState.Running },
{
$set: {
Expand All @@ -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) {
Expand Down
6 changes: 0 additions & 6 deletions server/src/billing/billing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion server/src/instance/instance-task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@ export class InstanceTaskService {
state: toState,
phase: ApplicationPhase.Started,
lockedAt: TASK_LOCK_INIT_TIME,
latestBillingTime: this.getHourTime(),
updatedAt: new Date(),
},
},
Expand Down

0 comments on commit 6cb7e57

Please sign in to comment.