diff --git a/apps/studio/components/interfaces/DiskManagement/ComputeSizeReccomendationSection.tsx b/apps/studio/components/interfaces/DiskManagement/ComputeSizeReccomendationSection.tsx new file mode 100644 index 00000000000..cc9d00b3057 --- /dev/null +++ b/apps/studio/components/interfaces/DiskManagement/ComputeSizeReccomendationSection.tsx @@ -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 ( + + {isVisible && ( + + + +
+
+

{compute} Compute size is recommended.

+ + To achieve optimal IOPS performance use{' '} + {compute} compute size. + +
+ {actions &&
{actions}
} +
+
+
+ )} +
+ ) +} diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts index 26704fad509..7ac8190e36d 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts @@ -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') diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index fae521ba17a..3e1564c8939 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -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() { form.resetField('totalSize')} + onClick={() => { + form.resetField('totalSize') + trigger('provisionedIOPS') + }} /> @@ -724,14 +731,16 @@ export function DiskManagementForm() { ( - -
- {watchedStorageType === 'io2' ? ( + render={({ field }) => { + const reccomendedComputeSize = calculateComputeSizeRequiredForIops(watchedIOPS) + return ( + +
+ {/* {watchedStorageType === 'io2' ? ( <> IOPS must be{' '} @@ -761,9 +770,27 @@ export function DiskManagementForm() { lower - )} -
- {!form.formState.errors.provisionedIOPS && + )} */} +
+ { + setValue( + 'computeSize', + `ci_${reccomendedComputeSize.toLocaleLowerCase()}` + ) + trigger('provisionedIOPS') + }} + > + Update to {reccomendedComputeSize} + + } + /> + {/* {!form.formState.errors.provisionedIOPS && field.value > maxIopsBasedOnCompute && (

Note: Final usable IOPS will be at{' '} @@ -772,62 +799,65 @@ export function DiskManagementForm() { {' '} based on your current compute size of {currentCompute?.name}

- )} + )} */} - {!form.formState.errors.provisionedIOPS && ( - + )} + + } + labelOptional={ + <> + - )} - - } - labelOptional={ - <> - -

Input/output operations per second.

-

Higher IOPS for high-throughput apps.

- - } - > - - - { - setValue('provisionedIOPS', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - }} - /> - - -
- )} +

Input/output operations per second.

+

Higher IOPS for high-throughput apps.

+ + } + > + + + { + setValue('provisionedIOPS', e.target.valueAsNumber, { + shouldDirty: true, + shouldValidate: true, + }) + }} + /> + + + + ) + }} /> @@ -851,14 +881,14 @@ export function DiskManagementForm() {
- + {/* Throughput must be between {minThroughput.toLocaleString()}{' '} and {maxThroughput?.toLocaleString()} MB/s based on your IOPS. Min throughput is at 125MB/s, while max throughput is at 0.25MB/s * IOPS or 1,000, whichever is lower - + */}
{!form.formState.errors.throughput && field.value !== undefined && diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts index 91d51cf4194..df3e2a66690 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts @@ -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 diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementRestartRequiredSection.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementRestartRequiredSection.tsx index 04f86c499d2..5f82e54581d 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementRestartRequiredSection.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementRestartRequiredSection.tsx @@ -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)