compute sizes now a reccomendation

This commit is contained in:
Jonathan Summers-Muir committed 2024-10-25 18:01:38 +08:00
1 parent e4ac94567e
commit 35112eb85b
5 files changed
+286 -90

No files matched your search

@@ -0,0 +1,52 @@
import { AnimatePresence, motion } from 'framer-motion'
import { RotateCcw } from 'lucide-react'
import {
calculateComputeSizeRequiredForIops,
calculateMaxIopsAllowedForComputeSize,
} from './DiskManagement.utils'
import { Button, DialogSection, WarningIcon } from 'ui'
import { RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 } from './DiskManagement.constants'
import { ComputeBadge } from 'ui-patterns'
export function ComputeSizeReccomendationSection({
iops,
computeSize,
actions,
}: {
iops: number
computeSize: string
actions?: React.ReactNode
}) {
const compute = calculateComputeSizeRequiredForIops(iops)
const maxIOPSforComputeSize = calculateMaxIopsAllowedForComputeSize(computeSize)
const isVisible =
iops > maxIOPSforComputeSize && !RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(computeSize)
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.15 }}
>
<DialogSection className="bg-alternative text-sm text-foreground-light flex items-start gap-4 relative w-full border rounded-md">
<WarningIcon />
<div className="flex flex-col gap-2 flex-grow">
<div>
<p className="text-sm text-foreground">{compute} Compute size is recommended.</p>
<span className="text-sm text-foreground-light">
To achieve optimal IOPS performance use{' '}
<span className="text-foreground">{compute}</span> compute size.
</span>
</div>
{actions && <div>{actions}</div>}
</div>
</DialogSection>
</motion.div>
)}
</AnimatePresence>
)
}
@@ -1,5 +1,11 @@
import { components } from 'api-types'
import { DISK_LIMITS, DISK_PRICING, DiskType, PLAN_DETAILS } from './DiskManagement.constants'
import {
DISK_LIMITS,
DISK_PRICING,
DiskType,
PLAN_DETAILS,
COMPUTE_BASELINE_IOPS,
} from './DiskManagement.constants'
import { INSTANCE_MICRO_SPECS } from 'lib/constants'
import { ProjectAddonVariantMeta } from 'data/subscriptions/types'
import { useMemo } from 'react'
@@ -183,11 +189,58 @@ export function getAvailableComputeOptions(availableAddons: any[], projectCloudP
return computeOptions
}
export const calculateMaxIopsAllowed = (totalSize: number) => {
export const calculateMaxIopsAllowedForDiskSizeWithGp3 = (totalSize: number) => {
if (!totalSize) throw Error('Total size is required')
return Math.min(1000 * totalSize, 256000)
}
export const calculateDiskSizeRequiredForIopsWithGp3 = (iops: number) => {
if (!iops) throw Error('IOPS is required')
return Math.max(1, Math.ceil(iops / 1000))
}
export const calculateMaxIopsAllowedForDiskSizeWithio2 = (totalSize: number) => {
if (!totalSize) throw Error('Total size is required')
return Math.min(500 * totalSize, 16000)
}
export const calculateDiskSizeRequiredForIopsWithIo2 = (iops: number) => {
if (!iops) throw Error('IOPS is required')
return Math.max(4, Math.ceil(iops / 500))
}
export const calculateMaxThroughput = (iops: number) => {
if (!iops) throw Error('IOPS is required')
return Math.min(0.256 * iops, 1000)
}
export const calculateIopsRequiredForThroughput = (throughput: number) => {
if (!throughput) throw Error('Throughput is required')
return Math.max(125, Math.ceil(throughput / 0.256))
}
export const calculateMaxIopsAllowedForComputeSize = (computeSize: string): number => {
if (!computeSize) throw Error('Compute size is required')
return COMPUTE_BASELINE_IOPS[computeSize as keyof typeof COMPUTE_BASELINE_IOPS] || 0
}
export const calculateComputeSizeRequiredForIops = (iops: number) => {
if (iops === undefined || iops === null) {
throw new Error('IOPS is required')
}
const computeSizes = Object.entries(COMPUTE_BASELINE_IOPS).sort((a, b) => a[1] - b[1])
for (const [size, baselineIops] of computeSizes) {
if (iops <= baselineIops) {
return size.toUpperCase().replace('CI_', '')
}
}
// fallback to largest compute size
// this should never happen though :-/
return computeSizes[computeSizes.length - 1][0].toUpperCase().replace('CI_', '')
}
export const calculateDiskSizeRequiredForIops = (provisionedIOPS: number): number => {
if (provisionedIOPS === undefined || provisionedIOPS === null) {
throw new Error('Provisioned IOPS is required')
@@ -65,6 +65,7 @@ import {
} from './DiskManagement.constants'
import {
calculateComputeSizePrice,
calculateComputeSizeRequiredForIops,
calculateDiskSizePrice,
calculateIOPSPrice,
calculateThroughputPrice,
@@ -86,6 +87,7 @@ import { InputPostTab } from './InputPostTab'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import { InputResetButton } from './InputResetButton'
import { DiskManagementFormLoading } from './BillingMangementForm.loading'
import { ComputeSizeReccomendationSection } from './ComputeSizeReccomendationSection'
export function DiskManagementForm() {
const { project } = useProjectContext()
@@ -492,6 +494,8 @@ export function DiskManagementForm() {
shouldDirty: true,
shouldValidate: true,
})
trigger('provisionedIOPS')
trigger('throughput')
}}
defaultValue={field.value}
value={field.value}
@@ -620,7 +624,10 @@ export function DiskManagementForm() {
</InputPostTab>
<InputResetButton
isDirty={isAllocatedStorageDirty}
onClick={() => form.resetField('totalSize')}
onClick={() => {
form.resetField('totalSize')
trigger('provisionedIOPS')
}}
/>
</div>
</FormItemLayout>
@@ -724,14 +731,16 @@ export function DiskManagementForm() {
<FormField_Shadcn_
control={form.control}
name="provisionedIOPS"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="IOPS"
description={
<div className="flex flex-col gap-y-2">
<div className="flex items-center gap-x-2">
{watchedStorageType === 'io2' ? (
render={({ field }) => {
const reccomendedComputeSize = calculateComputeSizeRequiredForIops(watchedIOPS)
return (
<FormItemLayout
layout="horizontal"
label="IOPS"
description={
<div className="flex flex-col gap-y-2">
<div className="flex items-center gap-x-2">
{/* {watchedStorageType === 'io2' ? (
<>
<span>
IOPS must be{' '}
@@ -761,9 +770,27 @@ export function DiskManagementForm() {
lower
</InfoTooltip>
</>
)}
</div>
{!form.formState.errors.provisionedIOPS &&
)} */}
</div>
<ComputeSizeReccomendationSection
computeSize={watchedComputeSize}
iops={watchedIOPS}
actions={
<Button
type="default"
onClick={() => {
setValue(
'computeSize',
`ci_${reccomendedComputeSize.toLocaleLowerCase()}`
)
trigger('provisionedIOPS')
}}
>
Update to {reccomendedComputeSize}
</Button>
}
/>
{/* {!form.formState.errors.provisionedIOPS &&
field.value > maxIopsBasedOnCompute && (
<p>
Note: Final usable IOPS will be at{' '}
@@ -772,62 +799,65 @@ export function DiskManagementForm() {
</span>{' '}
based on your current compute size of {currentCompute?.name}
</p>
)}
)} */}
{!form.formState.errors.provisionedIOPS && (
<DiskManagementIOPSReadReplicas
isDirty={form.formState.dirtyFields.provisionedIOPS !== undefined}
oldIOPS={iops ?? 0}
newIOPS={field.value}
oldStorageType={form.formState.defaultValues?.storageType as DiskType}
newStorageType={form.getValues('storageType') as DiskType}
{!form.formState.errors.provisionedIOPS && (
<DiskManagementIOPSReadReplicas
isDirty={form.formState.dirtyFields.provisionedIOPS !== undefined}
oldIOPS={iops ?? 0}
newIOPS={field.value}
oldStorageType={
form.formState.defaultValues?.storageType as DiskType
}
newStorageType={form.getValues('storageType') as DiskType}
/>
)}
</div>
}
labelOptional={
<>
<BillingChangeBadge
show={
(watchedStorageType !== type ||
(watchedStorageType === 'gp3' && field.value !== iops)) &&
!formState.errors.provisionedIOPS
}
beforePrice={Number(iopsPrice.oldPrice)}
afterPrice={Number(iopsPrice.newPrice)}
className="mb-2"
/>
)}
</div>
}
labelOptional={
<>
<BillingChangeBadge
show={
(watchedStorageType !== type ||
(watchedStorageType === 'gp3' && field.value !== iops)) &&
!formState.errors.provisionedIOPS
}
beforePrice={Number(iopsPrice.oldPrice)}
afterPrice={Number(iopsPrice.newPrice)}
className="mb-2"
/>
<p>Input/output operations per second.</p>
<p>Higher IOPS for high-throughput apps.</p>
</>
}
>
<InputPostTab label="IOPS">
<FormControl_Shadcn_>
<Input
id="provisionedIOPS"
type="number"
className="flex-grow font-mono rounded-r-none max-w-32"
{...field}
value={
disableIopsInput
? COMPUTE_BASELINE_IOPS[
watchedComputeSize as keyof typeof COMPUTE_BASELINE_IOPS
]
: field.value
}
disabled={disableInput || disableIopsInput}
onChange={(e) => {
setValue('provisionedIOPS', e.target.valueAsNumber, {
shouldDirty: true,
shouldValidate: true,
})
}}
/>
</FormControl_Shadcn_>
</InputPostTab>
</FormItemLayout>
)}
<p>Input/output operations per second.</p>
<p>Higher IOPS for high-throughput apps.</p>
</>
}
>
<InputPostTab label="IOPS">
<FormControl_Shadcn_>
<Input
id="provisionedIOPS"
type="number"
className="flex-grow font-mono rounded-r-none max-w-32"
{...field}
value={
disableIopsInput
? COMPUTE_BASELINE_IOPS[
watchedComputeSize as keyof typeof COMPUTE_BASELINE_IOPS
]
: field.value
}
disabled={disableInput || disableIopsInput}
onChange={(e) => {
setValue('provisionedIOPS', e.target.valueAsNumber, {
shouldDirty: true,
shouldValidate: true,
})
}}
/>
</FormControl_Shadcn_>
</InputPostTab>
</FormItemLayout>
)
}}
/>
<AnimatePresence initial={false}>
@@ -851,14 +881,14 @@ export function DiskManagementForm() {
<div className="flex flex-col gap-y-2">
<div>
<div className="flex items-center gap-x-2">
<span>
{/* <span>
Throughput must be between {minThroughput.toLocaleString()}{' '}
and {maxThroughput?.toLocaleString()} MB/s based on your IOPS.
</span>
<InfoTooltip>
Min throughput is at 125MB/s, while max throughput is at
0.25MB/s * IOPS or 1,000, whichever is lower
</InfoTooltip>
</InfoTooltip> */}
</div>
{!form.formState.errors.throughput &&
field.value !== undefined &&
@@ -1,6 +1,14 @@
import { z } from 'zod'
import { DISK_LIMITS, DiskType } from './DiskManagement.constants'
import { calculateMaxIopsAllowed } from './DiskManagement.utils'
import { DISK_LIMITS, DiskType, THROUGHPUT_RANGE, IOPS_RANGE } from './DiskManagement.constants'
import {
calculateComputeSizeRequiredForIops,
calculateDiskSizeRequiredForIopsWithGp3,
calculateDiskSizeRequiredForIopsWithIo2,
calculateIopsRequiredForThroughput,
calculateMaxIopsAllowedForComputeSize,
calculateMaxIopsAllowedForDiskSizeWithGp3,
calculateMaxIopsAllowedForDiskSizeWithio2,
} from './DiskManagement.utils'
const baseSchema = z.object({
storageType: z.enum(['io2', 'gp3']).describe('Type of storage: io2 or gp3'),
@@ -17,6 +25,8 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => {
const schema = baseSchema.superRefine((data, ctx) => {
const { storageType, totalSize, provisionedIOPS, throughput, computeSize } = data
const computeLabel = computeSize.toUpperCase().replace('CI_', '')
if (totalSize < defaultTotalSize) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -27,7 +37,17 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => {
if (storageType === 'io2') {
// Validation rules for io2
const maxIOPS = calculateMaxIopsAllowed(totalSize)
if (provisionedIOPS > 256000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
'IOPS can not exceed 256,000 for io2 Disk type. Please reach out to support if you need higher IOPS than this.',
path: ['provisionedIOPS'],
})
}
const maxIOPSforDiskSizeWithio2 = calculateMaxIopsAllowedForDiskSizeWithio2(totalSize)
if (provisionedIOPS < 100) {
ctx.addIssue({
@@ -35,11 +55,14 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => {
message: 'Provisioned IOPS must be at least 100',
path: ['provisionedIOPS'],
})
} else if (provisionedIOPS > maxIOPS) {
} else if (provisionedIOPS > maxIOPSforDiskSizeWithio2) {
if (totalSize >= 8) {
const diskSizeRequiredForIopsWithIo2 =
calculateDiskSizeRequiredForIopsWithIo2(provisionedIOPS)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Provisioned IOPS must be at most ${maxIOPS}.`,
message: `Larger Disk size of at least ${diskSizeRequiredForIopsWithIo2}GB required. Current max is ${maxIOPSforDiskSizeWithio2} IOPS.`,
path: ['provisionedIOPS'],
})
} else {
@@ -66,22 +89,34 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => {
path: ['totalSize'],
})
}
} else if (storageType === 'gp3') {
// Validation rules for gp3
const maxIOPS = Math.min(500 * totalSize, 16000)
const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000)
}
if (provisionedIOPS < 3000) {
if (storageType === 'gp3') {
const maxIopsAllowedForDiskSizeWithGp3 = calculateMaxIopsAllowedForDiskSizeWithGp3(totalSize)
if (provisionedIOPS > IOPS_RANGE[DiskType.GP3].max) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
'IOPS can not exceed 16,000 for GP3 Disk. Change the Disk type to io2 for higher IOPS support.',
path: ['provisionedIOPS'],
})
}
if (provisionedIOPS < IOPS_RANGE[DiskType.GP3].min) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Provisioned IOPS must be at least 3000`,
path: ['provisionedIOPS'],
})
} else if (provisionedIOPS > maxIOPS) {
} else if (provisionedIOPS > maxIopsAllowedForDiskSizeWithGp3) {
if (totalSize >= 8) {
const diskSizeRequiredForIopsWithGp3 =
calculateDiskSizeRequiredForIopsWithGp3(provisionedIOPS)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Provisioned IOPS must be at most ${maxIOPS}`,
message: `Larger Disk size of at least ${diskSizeRequiredForIopsWithGp3}GB required. Current max is ${maxIopsAllowedForDiskSizeWithGp3} IOPS.`,
path: ['provisionedIOPS'],
})
} else {
@@ -93,13 +128,26 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => {
}
}
if (throughput !== undefined && (throughput < 125 || throughput > maxThroughput)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Throughput must be set between 125 and ${maxThroughput?.toLocaleString()} MB/s.`,
path: ['throughput'],
})
if (throughput !== undefined) {
const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000)
if (throughput < THROUGHPUT_RANGE[DiskType.GP3].min) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Throughput must be at least 125`,
path: ['throughput'],
})
}
if (throughput > maxThroughput) {
const iopsRequiredForThroughput = calculateIopsRequiredForThroughput(throughput)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Need at least ${iopsRequiredForThroughput}IOPS to support ${throughput}MB/s.`,
path: ['throughput'],
})
}
}
if (totalSize > DISK_LIMITS[DiskType.GP3].maxStorage) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -108,6 +156,20 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => {
})
}
}
// const maxIOPSforComputeSize = calculateMaxIopsAllowedForComputeSize(computeSize)
// // console.log('provisionedIOPS in schame', provisionedIOPS)
// if (provisionedIOPS > maxIOPSforComputeSize) {
// const computeSizeRequiredForIops = calculateComputeSizeRequiredForIops(provisionedIOPS)
// ctx.addIssue({
// code: z.ZodIssueCode.custom,
// message: `${computeSizeRequiredForIops} Compute size required for this amount of IOPS.`,
// path: ['provisionedIOPS'],
// })
// }
})
return schema
@@ -1,8 +1,7 @@
import { AnimatePresence, motion } from 'framer-motion'
import { RotateCcw, X } from 'lucide-react'
import React, { useState } from 'react'
import { DialogSection, TableCell, TableRow } from 'ui'
import { DialogSection } from 'ui'
export const DiskMangementRestartRequiredSection: React.FC = () => {
const [progress, setProgress] = useState(100)