Skip to content

Commit 6ccb642

Browse files
Add schedule on budget incurred expenses rate update
1 parent 459fc46 commit 6ccb642

5 files changed

+880
-5
lines changed

lib/core-resources.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@ import {
1616
StartingPosition,
1717
} from "aws-cdk-lib/aws-lambda";
1818
import path from "path";
19+
import { Revantios } from "./revantios";
20+
import { Role } from "aws-cdk-lib/aws-iam";
1921

2022
const ENV_VARIABLE_REVANT_COST_TABLE_NAME = "REVANT_COST_TABLE_NAME";
23+
const ENV_VARIABLE_REVANT_COST_LIMIT_PREFIX = "REVANT_COST_LIMIT";
24+
const ENV_VARIABLE_REVANT_SCHEDULE_ROLE_ARN = "REVANT_SCHEDULE_ROLE_ARN";
2125
const DYNAMODB_INCURRED_EXPENSES_RATE_ATTRIBUTE_NAME = "incurredExpensesRate";
2226

2327
export class CoreRessources extends Construct {
2428
public dynamoDBTable: Table;
29+
2530
private _lambdaCommonResources?: LambdaCommonResources;
2631
private _ec2CommonResources?: EC2CommonResources;
32+
33+
private updateAccruedExpensesWithCurrentIncurredExpensesRate: NodejsFunction;
34+
2735
static instance: CoreRessources;
2836

2937
private constructor(scope: Construct) {
@@ -37,7 +45,7 @@ export class CoreRessources extends Construct {
3745
billingMode: BillingMode.PAY_PER_REQUEST,
3846
stream: StreamViewType.NEW_AND_OLD_IMAGES,
3947
});
40-
const updateAccruedExpensesWithCurrentIncurredExpensesRate =
48+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate =
4149
new NodejsFunction(
4250
this,
4351
"UpdateAccruedExpensesWithCurrentIncurredExpensesRateFunction",
@@ -49,14 +57,14 @@ export class CoreRessources extends Construct {
4957
}
5058
);
5159
this.dynamoDBTable.grant(
52-
updateAccruedExpensesWithCurrentIncurredExpensesRate,
60+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate,
5361
"dynamodb:UpdateItem"
5462
);
55-
updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
63+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
5664
ENV_VARIABLE_REVANT_COST_TABLE_NAME,
5765
this.dynamoDBTable.tableName
5866
);
59-
updateAccruedExpensesWithCurrentIncurredExpensesRate.addEventSource(
67+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEventSource(
6068
new DynamoEventSource(this.dynamoDBTable, {
6169
startingPosition: StartingPosition.LATEST,
6270
reportBatchItemFailures: true,
@@ -74,8 +82,23 @@ export class CoreRessources extends Construct {
7482
],
7583
})
7684
);
85+
86+
const scheduleRole = new Role(this, "ScheduleRole", {
87+
assumedBy:
88+
})
89+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
90+
ENV_VARIABLE_REVANT_SCHEDULE_ROLE_ARN,
91+
this.dynamoDBTable.tableName
92+
);
7793
}
7894

95+
public registerBudget = (address: string, budget: number) => {
96+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
97+
[ENV_VARIABLE_REVANT_COST_LIMIT_PREFIX, address].join("_"),
98+
Revantios.fromCents(budget).toString()
99+
);
100+
};
101+
79102
public get lambdaCommonResources() {
80103
if (this._lambdaCommonResources === undefined) {
81104
this._lambdaCommonResources = new LambdaCommonResources(this);

lib/cost-limit.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Aspects, IAspect } from "aws-cdk-lib";
22
import { IConstruct } from "constructs";
33
import { Function } from "./services/lambda";
44
import { Instance } from "./services/ec2";
5+
import { CoreRessources } from "./core-resources";
56

67
export type CostLimitProps = {
78
/**
@@ -28,6 +29,7 @@ export class CostLimit implements IAspect {
2829
) as this | undefined;
2930
if (nodeWithCostLimitAspect !== undefined) {
3031
this.address = node.node.addr;
32+
CoreRessources.getInstance(node).registerBudget(this.address, this.budget);
3133
}
3234

3335
CostLimitedConstructs.map((CostLimitedConstruct) => {

lib/functions/updateAccruedExpensesWithCurrentIncurredExpensesRate.ts

+66-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import { DynamoDBStreamHandler } from "aws-lambda";
22
import { unmarshall } from "@aws-sdk/util-dynamodb";
33
import { AttributeValue, DynamoDBClient } from "@aws-sdk/client-dynamodb";
44
import { DynamoDBDocumentClient, UpdateCommand } from "@aws-sdk/lib-dynamodb";
5+
import {
6+
SchedulerClient,
7+
CreateScheduleCommand,
8+
} from "@aws-sdk/client-scheduler";
59

610
const ENV_VARIABLE_REVANT_COST_TABLE_NAME = "REVANT_COST_TABLE_NAME";
11+
const ENV_VARIABLE_REVANT_COST_LIMIT_PREFIX = "REVANT_COST_LIMIT";
12+
const ENV_VARIABLE_REVANT_SCHEDULE_ROLE_ARN = "REVANT_SCHEDULE_ROLE_ARN";
713

814
const DYNAMODB_ACCRUED_EXPENSES_ATTRIBUTE_NAME = "accruedExpenses";
915
const DYNAMODB_INCURRED_EXPENSES_RATE_ATTRIBUTE_NAME = "incurredExpensesRate";
@@ -23,6 +29,7 @@ type BudgetUpdateOperation = {
2329

2430
const dynamoDBClient = new DynamoDBClient({});
2531
const dynamoDBDocumentClient = DynamoDBDocumentClient.from(dynamoDBClient);
32+
const schedulerClient = new SchedulerClient({});
2633

2734
const isBudgetUpdateOperation = ({
2835
oldBudget,
@@ -41,6 +48,25 @@ const calculateNewAccruedExpenses = ({
4148
1000
4249
) * oldBudget[DYNAMODB_INCURRED_EXPENSES_RATE_ATTRIBUTE_NAME];
4350

51+
const calculateBudgetReachedEstimatedDate = ({
52+
accruedExpenses,
53+
incurredExpensesRate,
54+
updatedAt,
55+
budget,
56+
}: {
57+
accruedExpenses: number;
58+
incurredExpensesRate: number;
59+
updatedAt: Date;
60+
budget: number;
61+
}): Date => {
62+
const budgetReachedDate = new Date(updatedAt);
63+
budgetReachedDate.setSeconds(
64+
budgetReachedDate.getSeconds() +
65+
(budget - accruedExpenses) / incurredExpensesRate
66+
);
67+
return budgetReachedDate;
68+
};
69+
4470
export const handler: DynamoDBStreamHandler = async ({ Records }) => {
4571
console.log(`${Records.length} records received`);
4672
const budgetUpdatesOperations = Records.map((record) => ({
@@ -56,12 +82,21 @@ export const handler: DynamoDBStreamHandler = async ({ Records }) => {
5682
`${budgetUpdatesOperations.length} budget update operations received`
5783
);
5884

85+
const budgets = Object.fromEntries(
86+
Object.entries(process.env)
87+
.filter(([key]) => key.startsWith(ENV_VARIABLE_REVANT_COST_LIMIT_PREFIX))
88+
.map(([key, value]) => [
89+
key.slice(ENV_VARIABLE_REVANT_COST_LIMIT_PREFIX.length + 1),
90+
Number(value),
91+
])
92+
);
93+
5994
const failedUpdateIds: { itemIdentifier: string }[] = [];
6095
await Promise.all(
6196
budgetUpdatesOperations.map(
6297
async ({ itemIdentifier, oldBudget, newBudget }) => {
6398
try {
64-
await dynamoDBDocumentClient.send(
99+
const { Attributes } = await dynamoDBDocumentClient.send(
65100
new UpdateCommand({
66101
TableName: process.env[ENV_VARIABLE_REVANT_COST_TABLE_NAME],
67102
Key: { PK: oldBudget.PK },
@@ -75,8 +110,38 @@ export const handler: DynamoDBStreamHandler = async ({ Records }) => {
75110
newBudget,
76111
}),
77112
},
113+
ReturnValues: "ALL_NEW",
78114
})
79115
);
116+
if (Attributes === undefined) {
117+
console.error("Did not get any updated budget from DynamDB");
118+
return;
119+
}
120+
121+
const address = oldBudget.PK.split("#")[1];
122+
const budget = Number(budgets[address);
123+
const budgetReachedDate = calculateBudgetReachedEstimatedDate({
124+
accruedExpenses: Attributes[
125+
DYNAMODB_ACCRUED_EXPENSES_ATTRIBUTE_NAME
126+
] as number,
127+
incurredExpensesRate: Attributes[
128+
DYNAMODB_INCURRED_EXPENSES_RATE_ATTRIBUTE_NAME
129+
] as number,
130+
updatedAt: new Date(
131+
Attributes[DYNAMODB_LAST_UPDATE_ATTRIBUTE_NAME]
132+
),
133+
budget,
134+
});
135+
await schedulerClient.send(new CreateScheduleCommand({
136+
Name: address,
137+
ScheduleExpression: `at(${budgetReachedDate.toISOString().split('.')[0]})`,
138+
Target: {
139+
RoleArn: process.env[ENV_VARIABLE_REVANT_SCHEDULE_ROLE_ARN]
140+
},
141+
FlexibleTimeWindow: {
142+
Mode: "OFF"
143+
}
144+
}));
80145
} catch (error) {
81146
failedUpdateIds.push({ itemIdentifier });
82147
}

0 commit comments

Comments
 (0)