Skip to content

Commit

Permalink
feat: custom resource pricing for scale page estimates (#1002)
Browse files Browse the repository at this point in the history
  • Loading branch information
neurosnap authored Dec 12, 2024
1 parent 3f1bdc2 commit 95764ee
Show file tree
Hide file tree
Showing 19 changed files with 232 additions and 198 deletions.
5 changes: 3 additions & 2 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,10 @@ export const cacheMinTimer = () => timer(60 * SECONDS);
export const cacheShortTimer = () => timer(5 * SECONDS);

function* apiErrorMdw(ctx: ApiCtx, next: Next) {
const config = yield* select(selectEnv);
yield* next();
if (!ctx.json.ok) {
console.error(ctx.json.error, ctx);
if (!ctx.json.ok && config.isDev && !config.isTest) {
console.warn(ctx.json.error, ctx);
}
}

Expand Down
12 changes: 7 additions & 5 deletions src/bootup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { fetchBillingDetail } from "@app/billing";
import {
emptyFilterProps,
fetchApps,
fetchCostRates,
fetchCostsByApps,
fetchCostsByDatabases,
fetchCostsByEnvironments,
Expand Down Expand Up @@ -76,11 +77,12 @@ function* onFetchRequiredData() {
}

function* onFetchCostData(orgId: string) {
yield* put(fetchCostsByStacks({ orgId: orgId }));
yield* put(fetchCostsByEnvironments({ orgId: orgId }));
yield* put(fetchCostsByApps({ orgId: orgId }));
yield* put(fetchCostsByDatabases({ orgId: orgId }));
yield* put(fetchCostsByServices({ orgId: orgId }));
yield* put(fetchCostsByStacks({ orgId }));
yield* put(fetchCostsByEnvironments({ orgId }));
yield* put(fetchCostsByApps({ orgId }));
yield* put(fetchCostsByDatabases({ orgId }));
yield* put(fetchCostsByServices({ orgId }));
yield* put(fetchCostRates({ orgId }));
}

function* onFetchResourceData() {
Expand Down
9 changes: 0 additions & 9 deletions src/deploy/container/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,71 +6,62 @@ export const CONTAINER_PROFILES: {
} = {
m4: {
name: "General Purpose (M) - Legacy",
costPerContainerGBHourInCents: 8,
cpuShare: 0.25 / GB,
minimumContainerSize: GB / 2,
maximumContainerSize: 240 * GB,
maximumContainerCount: 32,
},
m5: {
name: "General Purpose (M)",
costPerContainerGBHourInCents: 8,
cpuShare: 0.25 / GB,
minimumContainerSize: GB / 2,
maximumContainerSize: 368 * GB,
maximumContainerCount: 32,
},
m: {
name: "General Purpose (M)",
costPerContainerGBHourInCents: 8,
cpuShare: 0.25 / GB,
minimumContainerSize: GB / 2,
maximumContainerSize: 368 * GB,
maximumContainerCount: 32,
},
r4: {
name: "Memory Optimized (R) - Legacy",
costPerContainerGBHourInCents: 5,
cpuShare: 0.125 / GB,
minimumContainerSize: 4 * GB,
maximumContainerSize: 472 * GB,
maximumContainerCount: 32,
},
r5: {
name: "Memory Optimized (R)",
costPerContainerGBHourInCents: 5,
cpuShare: 0.125 / GB,
minimumContainerSize: 4 * GB,
maximumContainerSize: 752 * GB,
maximumContainerCount: 32,
},
r: {
name: "Memory Optimized (R)",
costPerContainerGBHourInCents: 5,
cpuShare: 0.125 / GB,
minimumContainerSize: 4 * GB,
maximumContainerSize: 752 * GB,
maximumContainerCount: 32,
},
c4: {
name: "Compute Optimized (C) - Legacy",
costPerContainerGBHourInCents: 10,
cpuShare: 0.5 / GB,
minimumContainerSize: 2 * GB,
maximumContainerSize: 58 * GB,
maximumContainerCount: 32,
},
c5: {
name: "Compute Optimized (C)",
costPerContainerGBHourInCents: 10,
cpuShare: 0.5 / GB,
minimumContainerSize: 2 * GB,
maximumContainerSize: 368 * GB,
maximumContainerCount: 32,
},
c: {
name: "Compute Optimized (C)",
costPerContainerGBHourInCents: 10,
cpuShare: 0.5 / GB,
minimumContainerSize: 2 * GB,
maximumContainerSize: 368 * GB,
Expand Down
81 changes: 30 additions & 51 deletions src/deploy/cost/calc.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { CONTAINER_PROFILES } from "../container";
import {
backupCostPerGBMonth,
diskCostPerGBMonth,
diskIopsCostPerMonth,
endpointCostPerHour,
estimateMonthlyCost,
hoursPerMonth,
stackCostPerMonth,
vpnTunnelCostPerMonth,
} from "./calc";
import { defaultCostRates, defaultDeployEndpoint } from "@app/schema";
import { estimateMonthlyCost, hoursPerMonth } from "./calc";

describe("estimateMonthlyCost", () => {
const rates = defaultCostRates();

it("should calculate the cost of containers", () => {
const monthlyCost = estimateMonthlyCost({
rates,
services: [
{
containerCount: 2,
Expand All @@ -28,61 +22,50 @@ describe("estimateMonthlyCost", () => {
});

expect(monthlyCost).toBeCloseTo(
((5 * CONTAINER_PROFILES.r5.costPerContainerGBHourInCents) / 100) *
hoursPerMonth,
5 * rates.r_class_gb_per_hour * hoursPerMonth,
);
});

it("should calculate the cost of disks", () => {
const monthlyCost = estimateMonthlyCost({
rates,
disks: [
{ size: 3, provisionedIops: 4000 },
{ size: 2, provisionedIops: 2000 },
],
});
expect(monthlyCost).toBeCloseTo(
5 * diskCostPerGBMonth + 1000 * diskIopsCostPerMonth,
5 * rates.disk_cost_gb_per_month + 1000 * rates.disk_iops_cost_per_month,
);
});

it("should calculate the cost of endpoints", () => {
const monthlyCost = estimateMonthlyCost({
endpoints: [{}, {}, {}],
rates,
endpoints: [
defaultDeployEndpoint(),
defaultDeployEndpoint(),
defaultDeployEndpoint(),
],
});

expect(monthlyCost).toBeCloseTo(3 * endpointCostPerHour * hoursPerMonth);
expect(monthlyCost).toBeCloseTo(
3 * rates.vhost_cost_per_hour * hoursPerMonth,
);
});

it("should calculate the cost of backups", () => {
const monthlyCost = estimateMonthlyCost({
rates,
backups: [{ size: 5 }, { size: 10 }],
});

expect(monthlyCost).toBeCloseTo(15 * backupCostPerGBMonth);
});

it("should calculate the cost of VPN tunnels", () => {
const monthlyCost = estimateMonthlyCost({
vpnTunnels: [{}, {}, {}, {}],
});

expect(monthlyCost).toBeCloseTo(4 * vpnTunnelCostPerMonth);
});

it("should calculate the cost of dedicated stacks", () => {
const monthlyCost = estimateMonthlyCost({
stacks: [
{ organizationId: "abc" },
{ organizationId: "123" },
{ organizationId: "" },
],
});

expect(monthlyCost).toBeCloseTo(2 * stackCostPerMonth);
expect(monthlyCost).toBeCloseTo(15 * rates.backup_cost_gb_per_month);
});

it("should calculate the cost of everything", () => {
const monthlyCost = estimateMonthlyCost({
rates,
services: [
{
containerCount: 2,
Expand All @@ -99,25 +82,21 @@ describe("estimateMonthlyCost", () => {
{ size: 3, provisionedIops: 4000 },
{ size: 2, provisionedIops: 2000 },
],
endpoints: [{}, {}, {}],
backups: [{ size: 5 }, { size: 10 }],
vpnTunnels: [{}, {}, {}, {}],
stacks: [
{ organizationId: "abc" },
{ organizationId: "123" },
{ organizationId: "" },
endpoints: [
defaultDeployEndpoint(),
defaultDeployEndpoint(),
defaultDeployEndpoint(),
],
backups: [{ size: 5 }, { size: 10 }],
});

let expectedHourly =
(5 * CONTAINER_PROFILES.r5.costPerContainerGBHourInCents) / 100;
expectedHourly += 3 * endpointCostPerHour;
let expectedHourly = 5 * rates.r_class_gb_per_hour;
expectedHourly += 3 * rates.vhost_cost_per_hour;

let expectedMonthly = expectedHourly * hoursPerMonth;
expectedMonthly += 5 * diskCostPerGBMonth + 1000 * diskIopsCostPerMonth;
expectedMonthly += 15 * backupCostPerGBMonth;
expectedMonthly += 4 * vpnTunnelCostPerMonth;
expectedMonthly += 2 * stackCostPerMonth;
expectedMonthly +=
5 * rates.disk_cost_gb_per_month + 1000 * rates.disk_iops_cost_per_month;
expectedMonthly += 15 * rates.backup_cost_gb_per_month;

expect(monthlyCost).toBeCloseTo(expectedMonthly, 5);
});
Expand Down
59 changes: 24 additions & 35 deletions src/deploy/cost/calc.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { DEFAULT_INSTANCE_CLASS } from "@app/schema";
import type {
DeployBackup,
DeployCostRates,
DeployDisk,
DeployService,
DeployStack,
InstanceClass,
} from "@app/types";
import { CONTAINER_PROFILES } from "../container";

export const hoursPerMonth = 730;
export const diskCostPerGBMonth = 0.2;
export const diskIopsCostPerMonth = 0.01;
export const endpointCostPerHour = 0.05;
export const backupCostPerGBMonth = 0.02;
export const vpnTunnelCostPerMonth = 99;
export const stackCostPerMonth = 499;

export const profileCostPerGBHour = (
rates: DeployCostRates,
instanceClass: InstanceClass,
) => {
let profileRate = rates.m_class_gb_per_hour;
if (instanceClass.startsWith("r")) {
profileRate = rates.r_class_gb_per_hour;
} else if (instanceClass.startsWith("c")) {
profileRate = rates.c_class_gb_per_hour;
}
return profileRate;
};

export type ServiceCostProps = Pick<
DeployService,
Expand All @@ -23,61 +28,45 @@ export type ServiceCostProps = Pick<
export type DiskCostProps = Pick<DeployDisk, "size" | "provisionedIops">;
export type EndpointCostProps = any;
export type BackupCostProps = Pick<DeployBackup, "size">;
export type VpnTunnelCostProps = any;
export type StackCostProps = Pick<DeployStack, "organizationId">;

export type EstimateMonthlyCostProps = {
rates: DeployCostRates;
services?: ServiceCostProps[];
disks?: DiskCostProps[];
endpoints?: EndpointCostProps[];
backups?: BackupCostProps[];
vpnTunnels?: VpnTunnelCostProps[];
stacks?: StackCostProps[];
};

export const containerProfileCostPerGBHour = (
profile: InstanceClass | undefined | null,
) =>
CONTAINER_PROFILES[profile || DEFAULT_INSTANCE_CLASS]
.costPerContainerGBHourInCents / 100;

export const estimateMonthlyCost = ({
rates,
services = [],
disks = [],
endpoints = [],
backups = [],
vpnTunnels: vpn_tunnels = [],
stacks = [],
}: EstimateMonthlyCostProps) => {
// Returns the monthly cost of all resources
// Hourly cost
let hourlyCost = 0;
let hourlyCost = 0.0;

for (const service of services) {
const profileRate = profileCostPerGBHour(rates, service.instanceClass);

hourlyCost +=
((service.containerCount * service.containerMemoryLimitMb) / 1024) *
containerProfileCostPerGBHour(service.instanceClass);
profileRate;
}

hourlyCost += endpoints.length * endpointCostPerHour;
hourlyCost += endpoints.length * rates.vhost_cost_per_hour;

// Monthly cost
let monthlyCost = hourlyCost * hoursPerMonth;

for (const disk of disks) {
monthlyCost += disk.size * diskCostPerGBMonth;
monthlyCost += disk.size * rates.disk_cost_gb_per_month;
monthlyCost +=
Math.max(disk.provisionedIops - 3000, 0) * diskIopsCostPerMonth;
Math.max(disk.provisionedIops - 3000, 0) * rates.disk_iops_cost_per_month;
}

for (const backup of backups) {
monthlyCost += backup.size * backupCostPerGBMonth;
monthlyCost += backup.size * rates.backup_cost_gb_per_month;
}

monthlyCost += vpn_tunnels.length * vpnTunnelCostPerMonth;
monthlyCost +=
stacks.filter((stack) => stack.organizationId !== "").length *
stackCostPerMonth;

return monthlyCost;
};
22 changes: 19 additions & 3 deletions src/deploy/cost/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { api, cacheLongTimer } from "@app/api";
import { defaultEntity } from "@app/hal";
import { schema } from "@app/schema";
import type { DeployCost } from "@app/types";
import type { DeployCost, DeployCostRates } from "@app/types";

export * from "./calc";

Expand Down Expand Up @@ -65,6 +65,22 @@ export const fetchCostsByServices = api.get<OrgIdProp>(
},
);

export const fetchCostRates = api.get<OrgIdProp, DeployCostRates>(
"/costs/:orgId/rates",
{
supervisor: cacheLongTimer(),
},
function* (ctx, next) {
yield* next();

if (!ctx.json.ok) {
return;
}

yield* schema.update(schema.costRates.set(ctx.json.value));
},
);

export const costEntities = {
cost: defaultEntity({
id: "cost",
Expand All @@ -73,8 +89,8 @@ export const costEntities = {
}),
};

export const formatCurrency = (num: number) =>
num.toLocaleString("en", {
export const formatCurrency = (num?: number) =>
(num || 0.0).toLocaleString("en", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
Expand Down
Loading

0 comments on commit 95764ee

Please sign in to comment.