Skip to content

Commit a92cb83

Browse files
committed
add a version number to the cost parameters for licenses
- this is just the first step in util; to really use this, we need to also make it so the version number is encoded in PurchaseInfo when the purchase is made. - need to write some unit tests -- this is exactly the sort of thing where they are easy and useful
1 parent 4e2f8c0 commit a92cb83

File tree

5 files changed

+138
-82
lines changed

5 files changed

+138
-82
lines changed

src/packages/frontend/site-licenses/purchase/quota-editor.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import { KUCALC_ON_PREMISES } from "@cocalc/util/db-schema/site-defaults";
4747
import {
4848
COSTS,
4949
CostMap,
50-
GCE_COSTS,
5150
} from "@cocalc/util/licenses/purchase/consts";
5251
import { User } from "@cocalc/util/licenses/purchase/types";
5352
import { money } from "@cocalc/util/licenses/purchase/utils";
@@ -112,7 +111,7 @@ export const QuotaEditor: React.FC<Props> = (props: Props) => {
112111
return (
113112
(quota.member ? COSTS.custom_cost.member : 1) *
114113
(quota.always_running ? COSTS.custom_cost.always_running : 1) *
115-
(quota.member && quota.always_running ? GCE_COSTS.non_pre_factor : 1)
114+
(quota.member && quota.always_running ? COSTS.gce.non_pre_factor : 1)
116115
);
117116
}, [quota]);
118117

@@ -724,7 +723,7 @@ export const QuotaEditor: React.FC<Props> = (props: Props) => {
724723
<>
725724
<b>
726725
longer idle time increases price by up to{" "}
727-
{COSTS.custom_cost.always_running * GCE_COSTS.non_pre_factor}{" "}
726+
{COSTS.custom_cost.always_running * COSTS.gce.non_pre_factor}{" "}
728727
times
729728
</b>{" "}
730729
{render_explanation(`If you leave your project alone, it will be shut down the latest

src/packages/util/licenses/purchase/compute-cost.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
LicenseIdleTimeouts,
99
requiresMemberhosting,
1010
} from "@cocalc/util/consts/site-license";
11-
import { BASIC, COSTS, GCE_COSTS, MAX, STANDARD } from "./consts";
11+
import { BASIC, getCosts, MAX, STANDARD } from "./consts";
1212
import { dedicatedPrice } from "./dedicated-price";
1313
import { Cost, PurchaseInfo } from "./types";
1414

@@ -22,6 +22,7 @@ export function compute_cost(info: PurchaseInfo): Cost {
2222
}
2323

2424
let {
25+
version,
2526
quantity,
2627
user,
2728
upgrade,
@@ -35,8 +36,6 @@ export function compute_cost(info: PurchaseInfo): Cost {
3536
custom_uptime,
3637
} = info;
3738

38-
// at this point, we assume the start/end dates are already
39-
// set to the start/end time of a day in the user's timezone.
4039
const start = info.start ? new Date(info.start) : undefined;
4140
const end = info.end ? new Date(info.end) : undefined;
4241

@@ -45,7 +44,7 @@ export function compute_cost(info: PurchaseInfo): Cost {
4544
throw new Error(`unknown user ${user}`);
4645
}
4746

48-
// this is set in the next if/else block
47+
// custom_always_running is set in the next if/else block
4948
let custom_always_running = false;
5049
if (upgrade == "standard") {
5150
// set custom_* to what they would be:
@@ -77,6 +76,8 @@ export function compute_cost(info: PurchaseInfo): Cost {
7776
custom_member = true;
7877
}
7978

79+
const COSTS = getCosts(version);
80+
8081
// We compute the cost for one project for one month.
8182
// First we add the cost for RAM and CPU.
8283
let cost_per_project_per_month =
@@ -93,7 +94,7 @@ export function compute_cost(info: PurchaseInfo): Cost {
9394
// always on non-member means it gets restarted whenever the
9495
// pre-empt gets killed, which is still potentially very useful
9596
// for long-running computations that can be checkpointed and started.
96-
cost_per_project_per_month *= GCE_COSTS.non_pre_factor;
97+
cost_per_project_per_month *= COSTS.gce.non_pre_factor;
9798
}
9899
} else {
99100
// multiply by the idle_timeout factor

src/packages/util/licenses/purchase/consts.ts

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,49 +5,13 @@
55

66
import { CustomUpgrades, Subscription, User } from "./types";
77

8-
// discount is the number we multiply the price by:
9-
10-
// TODO: move the actual **data** that defines this cost map to the database
11-
// and admin site settings. It must be something that we can change at any time,
12-
// and that somebody else selling cocalc would set differently.
13-
14-
// See https://cloud.google.com/compute/vm-instance-pricing#e2_custommachinetypepricing
15-
// for the monthly GCE prices
16-
export const GCE_COSTS = {
17-
ram: 0.67, // for pre-emptibles
18-
cpu: 5, // for pre-emptibles
19-
disk: 0.04, // per GB/month
20-
non_pre_factor: 3.5, // Roughly Google's factor for non-preemptible's
21-
} as const;
8+
import costVersions from "./cost-versions";
9+
10+
// version for licenses before we were using versioning; this is what
11+
// is used when the version is not defined.
12+
const FALLBACK_VERSION = "1";
2213

23-
// Our price = GCE price times this. We charge LESS than Google VM's, due to our gamble
24-
// on having multiple users on a node at once.
25-
// 2022-06: price increase "version 2", from 0.75 → 0.8 to compensate for 15% higher GCE prices
26-
// and there is also a minimum of 3gb storage (the free base quota) now.
27-
const COST_MULTIPLIER = 0.8;
28-
// We gamble that projects are packed at least twice as densely on non-member
29-
// nodes (it's often worse).
30-
const NONMEMBER_DENSITY = 2;
31-
// Changing this doesn't change the actual academic prices --
32-
// it just changes the *business* prices.
33-
const ACADEMIC_DISCOUNT = 0.6;
34-
// Disk factor is based on how many copies of user data we have, plus guesses about
35-
// bandwidth to transfer data around (to/from cloud storage, backblaze, etc.).
36-
// 10 since we have about that many copies of user data, plus snapshots, and
37-
// we store their data long after they stop paying...
38-
const DISK_FACTOR = 10;
39-
40-
// These are based on what we observe in practice, what works well,
41-
// and what is configured in our backend autoscalers. This only
42-
// impacts the cost of dedicated cpu and RAM.
43-
const RAM_OVERCOMMIT = 5;
44-
const CPU_OVERCOMMIT = 10;
45-
46-
// Extra charge if project will always be on. Really we are gambling that
47-
// projects that are not always on, are off much of the time (at least 50%).
48-
// We use this factor since a 50-simultaneous active projects license could
49-
// easily be used about half of the time during a week in a large class.
50-
const ALWAYS_RUNNING_FACTOR = 2;
14+
const CURRENT_VERSION = "1";
5115

5216
// Another gamble implicit in this is that pre's are available. When they
5317
// aren't, cocalc.com switches to uses MUCH more expensive non-preemptibles.
@@ -62,23 +26,8 @@ export interface CostMap {
6226
member: number;
6327
}
6428

65-
export const CUSTOM_COST: CostMap = {
66-
ram:
67-
(COST_MULTIPLIER * GCE_COSTS.ram) / ACADEMIC_DISCOUNT / NONMEMBER_DENSITY,
68-
dedicated_ram:
69-
(RAM_OVERCOMMIT * (COST_MULTIPLIER * GCE_COSTS.ram)) /
70-
ACADEMIC_DISCOUNT /
71-
NONMEMBER_DENSITY,
72-
cpu:
73-
(COST_MULTIPLIER * GCE_COSTS.cpu) / ACADEMIC_DISCOUNT / NONMEMBER_DENSITY,
74-
dedicated_cpu:
75-
(CPU_OVERCOMMIT * (COST_MULTIPLIER * GCE_COSTS.cpu)) /
76-
ACADEMIC_DISCOUNT /
77-
NONMEMBER_DENSITY,
78-
disk: (DISK_FACTOR * COST_MULTIPLIER * GCE_COSTS.disk) / ACADEMIC_DISCOUNT,
79-
always_running: ALWAYS_RUNNING_FACTOR,
80-
member: NONMEMBER_DENSITY,
81-
} as const;
29+
// BASIC, STANDARD and MAX have nothing to do with defining pricing. They
30+
// are just some presets that might have been used at some point (?).
8231

8332
export const BASIC: CostMap = {
8433
ram: 1,
@@ -112,36 +61,88 @@ export const MAX: CostMap = {
11261

11362
export const MIN_QUOTE = 100;
11463

64+
interface GoogleComputeEngine {
65+
ram: number;
66+
cpu: number;
67+
disk: number;
68+
non_pre_factor: number;
69+
}
70+
11571
interface CostsStructure {
72+
version: string;
73+
74+
// these are critical to defining and computing the cost of a license
11675
user_discount: { [user in User]: number };
11776
sub_discount: { [sub in Subscription]: number };
118-
min_quote: number;
11977
custom_cost: { [key in CustomUpgrades]: number };
12078
custom_max: { [key in CustomUpgrades]: number };
79+
gce: GoogleComputeEngine;
80+
81+
// not even sure if any of this is ever used anymore -- it's generic.
82+
min_quote: number;
12183
basic: { [key in CustomUpgrades]: number };
12284
standard: { [key in CustomUpgrades]: number };
12385
max: { [key in CustomUpgrades]: number };
12486
}
12587

126-
export const COSTS: CostsStructure = {
127-
user_discount: { academic: ACADEMIC_DISCOUNT, business: 1 },
128-
sub_discount: { no: 1, monthly: 0.9, yearly: 0.85 },
129-
min_quote: MIN_QUOTE,
130-
custom_cost: CUSTOM_COST,
131-
custom_max: MAX,
132-
basic: BASIC,
133-
standard: STANDARD,
134-
max: MAX,
135-
} as const;
88+
export function getCosts(version = FALLBACK_VERSION): CostsStructure {
89+
const {
90+
SUB_DISCOUNT,
91+
GCE_COSTS,
92+
COST_MULTIPLIER,
93+
NONMEMBER_DENSITY,
94+
ACADEMIC_DISCOUNT,
95+
DISK_FACTOR,
96+
RAM_OVERCOMMIT,
97+
CPU_OVERCOMMIT,
98+
ALWAYS_RUNNING_FACTOR,
99+
} = costVersions[version];
100+
101+
const CUSTOM_COST: CostMap = {
102+
ram:
103+
(COST_MULTIPLIER * GCE_COSTS.ram) / ACADEMIC_DISCOUNT / NONMEMBER_DENSITY,
104+
dedicated_ram:
105+
(RAM_OVERCOMMIT * (COST_MULTIPLIER * GCE_COSTS.ram)) /
106+
ACADEMIC_DISCOUNT /
107+
NONMEMBER_DENSITY,
108+
cpu:
109+
(COST_MULTIPLIER * GCE_COSTS.cpu) / ACADEMIC_DISCOUNT / NONMEMBER_DENSITY,
110+
dedicated_cpu:
111+
(CPU_OVERCOMMIT * (COST_MULTIPLIER * GCE_COSTS.cpu)) /
112+
ACADEMIC_DISCOUNT /
113+
NONMEMBER_DENSITY,
114+
disk: (DISK_FACTOR * COST_MULTIPLIER * GCE_COSTS.disk) / ACADEMIC_DISCOUNT,
115+
always_running: ALWAYS_RUNNING_FACTOR,
116+
member: NONMEMBER_DENSITY,
117+
} as const;
118+
119+
return {
120+
version,
121+
122+
user_discount: { academic: ACADEMIC_DISCOUNT, business: 1 },
123+
sub_discount: SUB_DISCOUNT,
124+
custom_cost: CUSTOM_COST,
125+
custom_max: MAX,
126+
gce: GCE_COSTS,
127+
128+
min_quote: MIN_QUOTE,
129+
basic: BASIC,
130+
standard: STANDARD,
131+
max: MAX,
132+
} as const;
133+
}
134+
135+
const COSTS = getCosts(CURRENT_VERSION);
136+
export { COSTS };
136137

137138
export const discount_pct = Math.round(
138-
(1 - COSTS.user_discount["academic"]) * 100
139+
(1 - COSTS.user_discount["academic"]) * 100,
139140
);
140141

141142
export const discount_monthly_pct = Math.round(
142-
(1 - COSTS.sub_discount["monthly"]) * 100
143+
(1 - COSTS.sub_discount["monthly"]) * 100,
143144
);
144145

145146
export const discount_yearly_pct = Math.round(
146-
(1 - COSTS.sub_discount["yearly"]) * 100
147+
(1 - COSTS.sub_discount["yearly"]) * 100,
147148
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// We hard code the prices for licenses (and all versions of them) because they must
2+
// remain with the system forever, since we always want to be able to easily compute
3+
// the value of any existing licenses. For pay-as-you-go, on the other hand, the charges
4+
// are always short live and ephemeral, and the parameters for them are in the database.
5+
6+
const COST = {
7+
1: {
8+
// Subscription discount
9+
SUB_DISCOUNT: { no: 1, monthly: 0.9, yearly: 0.85 },
10+
// See https://cloud.google.com/compute/vm-instance-pricing#e2_custommachinetypepricing
11+
// for the monthly GCE prices
12+
GCE_COSTS: {
13+
ram: 0.67, // for pre-emptibles
14+
cpu: 5, // for pre-emptibles
15+
disk: 0.04, // per GB/month
16+
non_pre_factor: 3.5, // Roughly Google's factor for non-preemptible's
17+
},
18+
19+
// Our price = GCE price times this. We charge LESS than Google VM's, due to our gamble
20+
// on having multiple users on a node at once.
21+
// 2022-06: price increase "version 2", from 0.75 → 0.8 to compensate for 15% higher GCE prices
22+
// and there is also a minimum of 3gb storage (the free base quota) now.
23+
COST_MULTIPLIER: 0.8,
24+
// We gamble that projects are packed at least twice as densely on non-member
25+
// nodes (it's often worse).
26+
NONMEMBER_DENSITY: 2,
27+
// Changing this doesn't change the actual academic prices --
28+
// it just changes the *business* prices.
29+
ACADEMIC_DISCOUNT: 0.6,
30+
// Disk factor is based on how many copies of user data we have, plus guesses about
31+
// bandwidth to transfer data around (to/from cloud storage, backblaze, etc.).
32+
// 10 since we have about that many copies of user data, plus snapshots, and
33+
// we store their data long after they stop paying...
34+
DISK_FACTOR: 10,
35+
36+
// These are based on what we observe in practice, what works well,
37+
// and what is configured in our backend autoscalers. This only
38+
// impacts the cost of dedicated cpu and RAM.
39+
RAM_OVERCOMMIT: 5,
40+
CPU_OVERCOMMIT: 10,
41+
42+
// Extra charge if project will always be on. Really we are gambling that
43+
// projects that are not always on, are off much of the time (at least 50%).
44+
// We use this factor since a 50-simultaneous active projects license could
45+
// easily be used about half of the time during a week in a large class.
46+
ALWAYS_RUNNING_FACTOR: 2,
47+
},
48+
} as const;
49+
50+
export default COST;

src/packages/util/licenses/purchase/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ import type { Uptime } from "@cocalc/util/consts/site-license";
5454
import type { DedicatedDisk, DedicatedVM } from "@cocalc/util/types/dedicated";
5555
import type { CustomDescription, Period } from "../../upgrades/shopping";
5656

57+
interface Version {
58+
version?: string; // it's just a string with no special interpretation.
59+
}
60+
5761
interface PurchaseInfoQuota0 {
5862
type: "quota";
5963
user: User;
@@ -77,7 +81,8 @@ interface PurchaseInfoQuota0 {
7781
run_limit?: number;
7882
}
7983

80-
export type PurchaseInfoQuota = PurchaseInfoQuota0 &
84+
export type PurchaseInfoQuota = Version &
85+
PurchaseInfoQuota0 &
8186
CustomDescription &
8287
StartEndDates;
8388

0 commit comments

Comments
 (0)