DiskManagement.utils.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import {
  2. COMPUTE_BASELINE_IOPS,
  3. COMPUTE_MAX_IOPS,
  4. computeInstanceAddonVariantIdSchema,
  5. } from 'shared-data'
  6. import {
  7. ComputeInstanceAddonVariantId,
  8. ComputeInstanceSize,
  9. InfraInstanceSize,
  10. } from './DiskManagement.types'
  11. import { DISK_LIMITS, DISK_PRICING, DiskType, PLAN_DETAILS } from './ui/DiskManagement.constants'
  12. import { ProjectDetail } from '@/data/projects/project-detail-query'
  13. import { PlanId, ProjectAddonVariantMeta } from '@/data/subscriptions/types'
  14. import { INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from '@/lib/constants'
  15. // Included disk size only applies to primary, not replicas
  16. export const calculateDiskSizePrice = ({
  17. planId,
  18. oldSize,
  19. oldStorageType,
  20. newSize,
  21. newStorageType,
  22. numReplicas = 0,
  23. }: {
  24. planId: string
  25. oldSize: number
  26. oldStorageType: DiskType
  27. newSize: number
  28. newStorageType: DiskType
  29. numReplicas?: number
  30. }) => {
  31. const oldPricePerGB = DISK_PRICING[oldStorageType]?.storage ?? 0
  32. const newPricePerGB = DISK_PRICING[newStorageType]?.storage ?? 0
  33. const { includedDiskGB } = PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS]
  34. const oldPrice = Math.max(oldSize - includedDiskGB[oldStorageType], 0) * oldPricePerGB
  35. const oldPriceReplica = oldSize * 1.25 * oldPricePerGB
  36. const newPrice = Math.max(newSize - includedDiskGB[newStorageType], 0) * newPricePerGB
  37. const newPriceReplica = newSize * 1.25 * newPricePerGB
  38. return {
  39. oldPrice: (oldPrice + numReplicas * oldPriceReplica).toFixed(2),
  40. newPrice: (newPrice + numReplicas * newPriceReplica).toFixed(2),
  41. }
  42. }
  43. export const calculateComputeSizePrice = ({
  44. availableOptions,
  45. oldComputeSize,
  46. newComputeSize,
  47. plan,
  48. }: {
  49. availableOptions: {
  50. identifier: string
  51. price: number
  52. }[]
  53. oldComputeSize: string
  54. newComputeSize: string
  55. plan: PlanId
  56. }) => {
  57. let _oldComputeSize = oldComputeSize
  58. if (plan !== 'free' && oldComputeSize === 'ci_nano') {
  59. /**
  60. * override the old compute size to micro if the plan is not free
  61. * this is to handle the case in which nano compute is a paid entity
  62. */
  63. _oldComputeSize = 'ci_micro'
  64. }
  65. const oldPrice = availableOptions?.find((x) => x.identifier === _oldComputeSize)?.price ?? 0
  66. const newPrice = availableOptions?.find((x) => x.identifier === newComputeSize)?.price ?? 0
  67. const oldPriceMonthly = oldPrice * 720
  68. const newPriceMonthly = newPrice * 720
  69. return {
  70. oldPrice: oldPriceMonthly.toFixed(2),
  71. newPrice: newPriceMonthly.toFixed(2),
  72. }
  73. }
  74. // Included IOPS applies to both primary and replicas
  75. export const calculateIOPSPrice = ({
  76. oldStorageType,
  77. oldProvisionedIOPS,
  78. newStorageType,
  79. newProvisionedIOPS,
  80. numReplicas = 0,
  81. }: {
  82. oldStorageType: DiskType
  83. oldProvisionedIOPS: number
  84. newStorageType: DiskType
  85. newProvisionedIOPS: number
  86. numReplicas?: number
  87. }) => {
  88. if (newStorageType === DiskType.GP3) {
  89. const oldChargeableIOPS = Math.max(
  90. 0,
  91. oldProvisionedIOPS - DISK_LIMITS[DiskType.GP3].includedIops
  92. )
  93. const newChargeableIOPS = Math.max(
  94. 0,
  95. newProvisionedIOPS - DISK_LIMITS[DiskType.GP3].includedIops
  96. )
  97. const oldPrice = oldChargeableIOPS * (DISK_PRICING[oldStorageType]?.iops ?? 0)
  98. const newPrice = newChargeableIOPS * (DISK_PRICING[newStorageType]?.iops ?? 0)
  99. return {
  100. oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2),
  101. newPrice: (newPrice * (1 + numReplicas)).toFixed(2),
  102. }
  103. } else {
  104. const oldPrice =
  105. oldStorageType === 'gp3'
  106. ? (oldProvisionedIOPS - DISK_LIMITS[oldStorageType].includedIops) *
  107. DISK_PRICING[oldStorageType].iops
  108. : oldProvisionedIOPS * (DISK_PRICING[oldStorageType]?.iops ?? 0)
  109. const newPrice = newProvisionedIOPS * (DISK_PRICING[newStorageType]?.iops ?? 0)
  110. return {
  111. oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2),
  112. newPrice: (newPrice * (1 + numReplicas)).toFixed(2),
  113. }
  114. }
  115. }
  116. // This is only applicable for GP3 storage type, no need to consider IO2 at all
  117. // Also assumes that disk size is > 400 GB (separate requirement to update throughput)
  118. // Also, included throughput applies to both primary and replicas
  119. export const calculateThroughputPrice = ({
  120. storageType,
  121. newThroughput,
  122. oldThroughput,
  123. numReplicas = 0,
  124. }: {
  125. storageType: DiskType
  126. newThroughput: number
  127. oldThroughput: number
  128. numReplicas?: number
  129. }) => {
  130. if (storageType === DiskType.GP3 && newThroughput) {
  131. const oldChargeableThroughput = Math.max(
  132. 0,
  133. oldThroughput - DISK_LIMITS[DiskType.GP3].includedThroughput
  134. )
  135. const newChargeableThroughput = Math.max(
  136. 0,
  137. newThroughput - DISK_LIMITS[DiskType.GP3].includedThroughput
  138. )
  139. const oldPrice = oldChargeableThroughput * DISK_PRICING[DiskType.GP3].throughput
  140. const newPrice = newChargeableThroughput * DISK_PRICING[DiskType.GP3].throughput
  141. return {
  142. oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2),
  143. newPrice: (newPrice * (1 + numReplicas)).toFixed(2),
  144. }
  145. }
  146. return { oldPrice: '0.00', newPrice: '0.00' }
  147. }
  148. export type ComputeAddonVariant = {
  149. identifier: ComputeInstanceAddonVariantId
  150. name: string
  151. price_description: string
  152. price: number
  153. price_interval: 'hourly' | 'monthly'
  154. price_type: string
  155. meta?: ProjectAddonVariantMeta
  156. }
  157. type AvailableAddon = {
  158. type: string
  159. variants: Array<{
  160. identifier: string
  161. name: string
  162. price_description: string
  163. price: number
  164. price_interval: 'hourly' | 'monthly'
  165. price_type: string
  166. meta?: unknown
  167. }>
  168. }
  169. const isProjectAddonVariantMeta = (meta: unknown): meta is ProjectAddonVariantMeta => {
  170. if (typeof meta !== 'object' || meta === null) return false
  171. const obj = meta as Record<string, unknown>
  172. // Validate supported_cloud_providers is an array if present (used at line 200)
  173. if ('supported_cloud_providers' in obj && !Array.isArray(obj.supported_cloud_providers)) {
  174. return false
  175. }
  176. // Check for at least one expected property to ensure it's likely a real ProjectAddonVariantMeta
  177. const hasExpectedProperty =
  178. 'cpu_cores' in obj ||
  179. 'memory_gb' in obj ||
  180. 'cpu_dedicated' in obj ||
  181. 'baseline_disk_io_mbs' in obj ||
  182. 'max_disk_io_mbs' in obj ||
  183. 'connections_direct' in obj ||
  184. 'connections_pooler' in obj ||
  185. 'backup_duration_days' in obj ||
  186. 'supported_cloud_providers' in obj
  187. return hasExpectedProperty
  188. }
  189. export function getAvailableComputeOptions(
  190. availableAddons: AvailableAddon[],
  191. projectCloudProvider?: string
  192. ) {
  193. const computeAddon = availableAddons.find((addon) => addon.type === 'compute_instance')
  194. const computeOptions: ComputeAddonVariant[] =
  195. computeAddon?.variants.flatMap((option) => {
  196. const parsedId = computeInstanceAddonVariantIdSchema.safeParse(option.identifier)
  197. if (!parsedId.success) return []
  198. if (projectCloudProvider && isProjectAddonVariantMeta(option.meta)) {
  199. const isSupported =
  200. !option.meta.supported_cloud_providers ||
  201. option.meta.supported_cloud_providers.includes(projectCloudProvider)
  202. if (!isSupported) return []
  203. }
  204. return [
  205. {
  206. ...option,
  207. identifier: parsedId.data,
  208. meta: isProjectAddonVariantMeta(option.meta) ? option.meta : undefined,
  209. },
  210. ]
  211. }) ?? []
  212. function hasMicroOptionFromApi() {
  213. return (computeAddon?.variants ?? []).some((variant) => variant.identifier === 'ci_micro')
  214. }
  215. // Backwards comp until API is deployed
  216. if (!hasMicroOptionFromApi()) {
  217. // Unshift to push to start of array
  218. computeOptions.unshift({
  219. identifier: 'ci_micro',
  220. name: 'Micro',
  221. price_description: '$0.01344/hour (~$10/month)',
  222. price: 0.01344,
  223. price_interval: 'hourly',
  224. price_type: 'usage',
  225. meta: {
  226. cpu_cores: INSTANCE_MICRO_SPECS.cpu_cores,
  227. cpu_dedicated: INSTANCE_MICRO_SPECS.cpu_dedicated,
  228. memory_gb: INSTANCE_MICRO_SPECS.memory_gb,
  229. baseline_disk_io_mbs: INSTANCE_MICRO_SPECS.baseline_disk_io_mbs,
  230. max_disk_io_mbs: INSTANCE_MICRO_SPECS.max_disk_io_mbs,
  231. connections_direct: INSTANCE_MICRO_SPECS.connections_direct,
  232. connections_pooler: INSTANCE_MICRO_SPECS.connections_pooler,
  233. } as ProjectAddonVariantMeta,
  234. })
  235. }
  236. computeOptions.unshift({
  237. identifier: 'ci_nano',
  238. name: 'Nano',
  239. price_description: '$0/hour (~$0/month)',
  240. price: 0,
  241. price_interval: 'hourly',
  242. price_type: 'usage',
  243. // @ts-ignore API types it as Record<string, never>
  244. meta: {
  245. cpu_cores: INSTANCE_NANO_SPECS.cpu_cores,
  246. cpu_dedicated: INSTANCE_NANO_SPECS.cpu_dedicated,
  247. memory_gb: INSTANCE_NANO_SPECS.memory_gb,
  248. baseline_disk_io_mbs: INSTANCE_NANO_SPECS.baseline_disk_io_mbs,
  249. max_disk_io_mbs: INSTANCE_NANO_SPECS.max_disk_io_mbs,
  250. connections_direct: INSTANCE_NANO_SPECS.connections_direct,
  251. connections_pooler: INSTANCE_NANO_SPECS.connections_pooler,
  252. } as ProjectAddonVariantMeta,
  253. })
  254. return computeOptions
  255. }
  256. export const calculateMaxIopsAllowedForDiskSizeWithGp3 = (totalSize: number) => {
  257. return Math.max(3000, Math.min(500 * totalSize, 16000))
  258. }
  259. export const calculateDiskSizeRequiredForIopsWithGp3 = (iops: number) => {
  260. return Math.max(1, Math.ceil(iops / 500))
  261. }
  262. export const calculateMaxIopsAllowedForDiskSizeWithio2 = (totalSize: number) => {
  263. return Math.min(500 * totalSize, 256000)
  264. }
  265. export const calculateDiskSizeRequiredForIopsWithIo2 = (iops: number) => {
  266. return Math.max(4, Math.ceil(iops / 1000))
  267. }
  268. export const calculateMaxThroughput = (iops: number) => {
  269. return Math.min(0.256 * iops, 1000)
  270. }
  271. export const calculateIopsRequiredForThroughput = (throughput: number) => {
  272. return Math.max(125, Math.ceil(throughput / 0.256))
  273. }
  274. export const calculateBaselineIopsForComputeSize = (computeSize: string): number => {
  275. const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
  276. if (!parsed.success) return 0
  277. return COMPUTE_BASELINE_IOPS[parsed.data] ?? 0
  278. }
  279. export const calculateMaxIopsForComputeSize = (computeSize: string): number => {
  280. const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
  281. if (!parsed.success) return 0
  282. return COMPUTE_MAX_IOPS[parsed.data] ?? 0
  283. }
  284. export const calculateComputeSizeRequiredForIops = (
  285. iops: number
  286. ): ComputeInstanceAddonVariantId | undefined => {
  287. type ComputeSizeMax = { size: ComputeInstanceAddonVariantId; maxIops: number }
  288. const computeSizes: ComputeSizeMax[] = Object.entries(COMPUTE_MAX_IOPS)
  289. .map((entry) => {
  290. const [size, maxIops] = entry
  291. const parsedSize = computeInstanceAddonVariantIdSchema.safeParse(size)
  292. if (!parsedSize.success) return undefined
  293. return { size: parsedSize.data, maxIops: Number(maxIops) }
  294. })
  295. .filter((value): value is ComputeSizeMax => value !== undefined)
  296. .sort((a, b) => a.maxIops - b.maxIops)
  297. for (const { size, maxIops } of computeSizes) {
  298. if (iops <= maxIops) {
  299. return size
  300. }
  301. }
  302. const fallbackSize = computeSizes[computeSizes.length - 1]?.size
  303. if (!fallbackSize) return undefined
  304. return fallbackSize
  305. }
  306. export const calculateDiskSizeRequiredForIops = (provisionedIOPS: number): number | undefined => {
  307. if (!provisionedIOPS) {
  308. console.error('IOPS is required')
  309. return undefined
  310. }
  311. if (isNaN(provisionedIOPS) || provisionedIOPS < 0) {
  312. console.error('IOPS must be a non-negative number')
  313. return undefined
  314. }
  315. if (provisionedIOPS > 256000) {
  316. console.error('Maximum allowed IOPS is 256000')
  317. return undefined
  318. }
  319. return Math.max(1, Math.ceil(provisionedIOPS / 1000))
  320. }
  321. export const formatComputeName = (compute: string) => {
  322. return compute.toUpperCase().replace('CI_', '')
  323. }
  324. const infraToAddonVariant: Record<InfraInstanceSize, ComputeInstanceAddonVariantId> = {
  325. pico: 'ci_nano',
  326. nano: 'ci_nano',
  327. micro: 'ci_micro',
  328. small: 'ci_small',
  329. medium: 'ci_medium',
  330. large: 'ci_large',
  331. xlarge: 'ci_xlarge',
  332. '2xlarge': 'ci_2xlarge',
  333. '4xlarge': 'ci_4xlarge',
  334. '8xlarge': 'ci_8xlarge',
  335. '12xlarge': 'ci_12xlarge',
  336. '16xlarge': 'ci_16xlarge',
  337. '24xlarge': 'ci_24xlarge',
  338. '24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory',
  339. '24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu',
  340. '24xlarge_high_memory': 'ci_24xlarge_high_memory',
  341. '48xlarge': 'ci_48xlarge',
  342. '48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory',
  343. '48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu',
  344. '48xlarge_high_memory': 'ci_48xlarge_high_memory',
  345. }
  346. const isInfraInstanceSize = (value: string): value is InfraInstanceSize =>
  347. Object.prototype.hasOwnProperty.call(infraToAddonVariant, value)
  348. export const mapComputeSizeNameToAddonVariantId = (
  349. computeSize: ProjectDetail['infra_compute_size']
  350. ): ComputeInstanceAddonVariantId => {
  351. const fallback: InfraInstanceSize = 'nano'
  352. const matchedSize = computeSize && isInfraInstanceSize(computeSize) ? computeSize : undefined
  353. const sizeKey = matchedSize ?? fallback
  354. return infraToAddonVariant[sizeKey]
  355. }
  356. const addonVariantToComputeSize: Record<ComputeInstanceAddonVariantId, ComputeInstanceSize> = {
  357. ci_nano: 'Nano',
  358. ci_micro: 'Micro',
  359. ci_small: 'Small',
  360. ci_medium: 'Medium',
  361. ci_large: 'Large',
  362. ci_xlarge: 'XL',
  363. ci_2xlarge: '2XL',
  364. ci_4xlarge: '4XL',
  365. ci_8xlarge: '8XL',
  366. ci_12xlarge: '12XL',
  367. ci_16xlarge: '16XL',
  368. ci_24xlarge: '24XL',
  369. ci_24xlarge_optimized_memory: '24XL - Optimized Memory',
  370. ci_24xlarge_optimized_cpu: '24XL - Optimized CPU',
  371. ci_24xlarge_high_memory: '24XL - High Memory',
  372. ci_48xlarge: '48XL',
  373. ci_48xlarge_optimized_memory: '48XL - Optimized Memory',
  374. ci_48xlarge_optimized_cpu: '48XL - Optimized CPU',
  375. ci_48xlarge_high_memory: '48XL - High Memory',
  376. }
  377. export const mapAddOnVariantIdToComputeSize = (
  378. addonVariantId: ComputeInstanceAddonVariantId = 'ci_nano'
  379. ): ComputeInstanceSize => {
  380. const parsed = computeInstanceAddonVariantIdSchema.safeParse(addonVariantId)
  381. if (!parsed.success) return addonVariantToComputeSize.ci_nano
  382. return addonVariantToComputeSize[parsed.data]
  383. }
  384. export const formatNumber = (num: number): string => {
  385. return num.toLocaleString('en-US')
  386. }
  387. export const showMicroUpgrade = (plan: PlanId, infraComputeSize: InfraInstanceSize): boolean => {
  388. return plan !== 'free' && infraComputeSize === 'nano'
  389. }
  390. export function hasBurstableIO(): boolean { return false; }