Skip to content

Commit b69612b

Browse files
Add schedule on budget incurred expenses rate update
1 parent bb8e240 commit b69612b

File tree

5 files changed

+890
-5
lines changed

5 files changed

+890
-5
lines changed

lib/core-resources.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,24 @@ import {
1616
StartingPosition,
1717
} from "aws-cdk-lib/aws-lambda";
1818
import path from "path";
19+
import { Revantios } from "./revantios";
20+
import { Role, ServicePrincipal } 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";
25+
const ENV_VARIABLE_REVANT_SCHEDULE_FUNCTION_ARN =
26+
"REVANT_SCHEDULE_FUNCTION_ARN";
2127
const DYNAMODB_INCURRED_EXPENSES_RATE_ATTRIBUTE_NAME = "incurredExpensesRate";
2228

2329
export class CoreRessources extends Construct {
2430
public dynamoDBTable: Table;
31+
2532
private _lambdaCommonResources?: LambdaCommonResources;
2633
private _ec2CommonResources?: EC2CommonResources;
34+
35+
private updateAccruedExpensesWithCurrentIncurredExpensesRate: NodejsFunction;
36+
2737
static instance: CoreRessources;
2838

2939
private constructor(scope: Construct) {
@@ -37,7 +47,7 @@ export class CoreRessources extends Construct {
3747
billingMode: BillingMode.PAY_PER_REQUEST,
3848
stream: StreamViewType.NEW_AND_OLD_IMAGES,
3949
});
40-
const updateAccruedExpensesWithCurrentIncurredExpensesRate =
50+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate =
4151
new NodejsFunction(
4252
this,
4353
"UpdateAccruedExpensesWithCurrentIncurredExpensesRateFunction",
@@ -49,14 +59,14 @@ export class CoreRessources extends Construct {
4959
}
5060
);
5161
this.dynamoDBTable.grant(
52-
updateAccruedExpensesWithCurrentIncurredExpensesRate,
62+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate,
5363
"dynamodb:UpdateItem"
5464
);
55-
updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
65+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
5666
ENV_VARIABLE_REVANT_COST_TABLE_NAME,
5767
this.dynamoDBTable.tableName
5868
);
59-
updateAccruedExpensesWithCurrentIncurredExpensesRate.addEventSource(
69+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEventSource(
6070
new DynamoEventSource(this.dynamoDBTable, {
6171
startingPosition: StartingPosition.LATEST,
6272
reportBatchItemFailures: true,
@@ -74,8 +84,29 @@ export class CoreRessources extends Construct {
7484
],
7585
})
7686
);
87+
88+
const scheduleRole = new Role(this, "ScheduleRole", {});
89+
scheduleRole.grantAssumeRole(
90+
new ServicePrincipal("scheduler.amazonaws.com")
91+
);
92+
93+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
94+
ENV_VARIABLE_REVANT_SCHEDULE_ROLE_ARN,
95+
scheduleRole.roleArn
96+
);
97+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
98+
ENV_VARIABLE_REVANT_SCHEDULE_FUNCTION_ARN,
99+
"test-arn"
100+
);
77101
}
78102

103+
public registerBudget = (address: string, budget: number) => {
104+
this.updateAccruedExpensesWithCurrentIncurredExpensesRate.addEnvironment(
105+
[ENV_VARIABLE_REVANT_COST_LIMIT_PREFIX, address].join("_"),
106+
Revantios.fromCents(budget).toString()
107+
);
108+
};
109+
79110
public get lambdaCommonResources() {
80111
if (this._lambdaCommonResources === undefined) {
81112
this._lambdaCommonResources = new LambdaCommonResources(this);

lib/cost-limit.ts

Lines changed: 2 additions & 0 deletions
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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@ 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";
13+
const ENV_VARIABLE_REVANT_SCHEDULE_FUNCTION_ARN = "REVANT_SCHEDULE_FUNCTION_ARN";
714

815
const DYNAMODB_ACCRUED_EXPENSES_ATTRIBUTE_NAME = "accruedExpenses";
916
const DYNAMODB_INCURRED_EXPENSES_RATE_ATTRIBUTE_NAME = "incurredExpensesRate";
@@ -23,6 +30,7 @@ type BudgetUpdateOperation = {
2330

2431
const dynamoDBClient = new DynamoDBClient({});
2532
const dynamoDBDocumentClient = DynamoDBDocumentClient.from(dynamoDBClient);
33+
const schedulerClient = new SchedulerClient({});
2634

2735
const isBudgetUpdateOperation = ({
2836
oldBudget,
@@ -41,6 +49,25 @@ const calculateNewAccruedExpenses = ({
4149
1000
4250
) * oldBudget[DYNAMODB_INCURRED_EXPENSES_RATE_ATTRIBUTE_NAME];
4351

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

86+
const budgets = Object.fromEntries(
87+
Object.entries(process.env)
88+
.filter(([key]) => key.startsWith(ENV_VARIABLE_REVANT_COST_LIMIT_PREFIX))
89+
.map(([key, value]) => [
90+
key.slice(ENV_VARIABLE_REVANT_COST_LIMIT_PREFIX.length + 1),
91+
Number(value),
92+
])
93+
);
94+
5995
const failedUpdateIds: { itemIdentifier: string }[] = [];
6096
await Promise.all(
6197
budgetUpdatesOperations.map(
6298
async ({ itemIdentifier, oldBudget, newBudget }) => {
6399
try {
64-
await dynamoDBDocumentClient.send(
100+
const { Attributes } = await dynamoDBDocumentClient.send(
65101
new UpdateCommand({
66102
TableName: process.env[ENV_VARIABLE_REVANT_COST_TABLE_NAME],
67103
Key: { PK: oldBudget.PK },
@@ -75,8 +111,39 @@ export const handler: DynamoDBStreamHandler = async ({ Records }) => {
75111
newBudget,
76112
}),
77113
},
114+
ReturnValues: "ALL_NEW",
78115
})
79116
);
117+
if (Attributes === undefined) {
118+
console.error("Did not get any updated budget from DynamDB");
119+
return;
120+
}
121+
122+
const address = oldBudget.PK.split("#")[1];
123+
const budget = budgets[address];
124+
const budgetReachedDate = calculateBudgetReachedEstimatedDate({
125+
accruedExpenses: Attributes[
126+
DYNAMODB_ACCRUED_EXPENSES_ATTRIBUTE_NAME
127+
] as number,
128+
incurredExpensesRate: Attributes[
129+
DYNAMODB_INCURRED_EXPENSES_RATE_ATTRIBUTE_NAME
130+
] as number,
131+
updatedAt: new Date(
132+
Attributes[DYNAMODB_LAST_UPDATE_ATTRIBUTE_NAME]
133+
),
134+
budget,
135+
});
136+
await schedulerClient.send(new CreateScheduleCommand({
137+
Name: address,
138+
ScheduleExpression: `at(${budgetReachedDate.toISOString().split('.')[0]})`,
139+
Target: {
140+
RoleArn: process.env[ENV_VARIABLE_REVANT_SCHEDULE_ROLE_ARN],
141+
Arn: process.env[ENV_VARIABLE_REVANT_SCHEDULE_FUNCTION_ARN],
142+
},
143+
FlexibleTimeWindow: {
144+
Mode: "OFF"
145+
}
146+
}));
80147
} catch (error) {
81148
failedUpdateIds.push({ itemIdentifier });
82149
}

0 commit comments

Comments
 (0)