import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { capitalize } from 'lodash' import { Fragment, useEffect } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import toast from 'react-hot-toast' import z from 'zod' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' import { FormActions } from 'components/ui/Forms/FormActions' import Panel from 'components/ui/Panel' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' import { usePoolingConfigurationUpdateMutation } from 'data/database/pooling-configuration-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useDatabaseSettingsStateSnapshot } from 'state/database-settings' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Badge, Button, FormControl_Shadcn_, FormDescription_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, FormLabel_Shadcn_, FormMessage_Shadcn_, Form_Shadcn_, IconExternalLink, Input_Shadcn_, Listbox, Separator, } from 'ui' import { SESSION_MODE_DESCRIPTION, TRANSACTION_MODE_DESCRIPTION } from '../Database.constants' import { POOLING_OPTIMIZATIONS } from './ConnectionPooling.constants' const formId = 'connection-pooling-form' // This validator validates a string to be a positive integer or if it's an empty string, transforms it to a null const StringToPositiveNumber = z.union([ // parse the value if it's a number z.number().positive().int(), // parse the value if it's a non-empty string z.string().min(1).pipe(z.coerce.number().positive().int()), // transform a non-empty string into a null value z .string() .max(0, 'The field accepts only a number') .transform((v) => null), z.null(), ]) const FormSchema = z.object({ default_pool_size: StringToPositiveNumber, pool_mode: z.union([z.literal('transaction'), z.literal('session')]), max_client_conn: StringToPositiveNumber, }) export const ConnectionPooling = () => { const { ref: projectRef } = useParams() const { project } = useProjectContext() const snap = useDatabaseSettingsStateSnapshot() const { data: addons } = useProjectAddonsQuery({ projectRef }) const computeInstance = addons?.selected_addons.find((addon) => addon.type === 'compute_instance') const poolingOptimizations = POOLING_OPTIMIZATIONS[ (computeInstance?.variant.identifier as keyof typeof POOLING_OPTIMIZATIONS) ?? (project?.infra_compute_size === 'nano' ? 'ci_nano' : 'ci_micro') ] const defaultPoolSize = poolingOptimizations.poolSize ?? 15 const defaultMaxClientConn = poolingOptimizations.maxClientConn ?? 200 const computeSize = computeInstance?.variant.name ?? capitalize(project?.infra_compute_size) ?? 'Micro' const { data: poolingInfo, error, isLoading, isError, isSuccess, } = usePoolingConfigurationQuery({ projectRef: projectRef }) const { data: maxConnData } = useMaxConnectionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const poolingConfiguration = poolingInfo?.find((x) => x.database_type === 'PRIMARY') const connectionPoolingUnavailable = poolingConfiguration?.pool_mode === null const canUpdateConnectionPoolingConfiguration = useCheckPermissions( PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, }, } ) const form = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { pool_mode: poolingConfiguration?.pool_mode as 'transaction' | 'session', default_pool_size: poolingConfiguration?.default_pool_size as number | undefined, max_client_conn: poolingConfiguration?.max_client_conn as number | undefined, }, }) const { mutate: updateConfiguration, isLoading: isUpdating } = usePoolingConfigurationUpdateMutation({ onSuccess: (data) => { if (data) { form.reset({ pool_mode: data.pool_mode, default_pool_size: data.default_pool_size, max_client_conn: poolingConfiguration?.max_client_conn, }) } toast.success('Successfully saved settings') }, }) const onSubmit: SubmitHandler> = async (data) => { if (!projectRef) return console.error('Project ref is required') if (!poolingInfo) return console.error('Pooling info required') updateConfiguration({ ref: projectRef, default_pool_size: data.default_pool_size as number | undefined, pool_mode: data.pool_mode, }) } useEffect(() => { if (isSuccess) { form.reset({ pool_mode: poolingConfiguration?.pool_mode as 'transaction' | 'session', default_pool_size: poolingConfiguration?.default_pool_size as number | undefined, max_client_conn: poolingConfiguration?.max_client_conn as number | undefined, }) } }, [isSuccess]) return (

{connectionPoolingUnavailable ? 'Connection Pooling is not available for this project' : 'Connection pooling configuration'}

Supavisor
} footer={ form.reset()} helper={ !canUpdateConnectionPoolingConfiguration ? 'You need additional permissions to update connection pooling settings' : undefined } /> } > {isLoading && ( {Array.from({ length: 4 }).map((_, i) => (
))}
)} {isError && (
)} {isSuccess && ( <> {connectionPoolingUnavailable && (

Please start a new project to enable this feature.

)}
( Pool Mode field.onChange(value)} >

Transaction mode

{TRANSACTION_MODE_DESCRIPTION}

Session mode

{SESSION_MODE_DESCRIPTION}

{poolingConfiguration?.pool_mode === 'transaction' && ( Pool mode is permanently set to Transaction on port 6543 You can use Session mode by connecting to the pooler on port 5432 instead )} {poolingConfiguration?.pool_mode === 'session' && ( <> {field.value === 'transaction' ? ( Pool mode will be set to transaction permanently on port 6543 This will take into effect once saved. You can use session mode by pointing the pooler connection to use port 5432. ) : ( Set to transaction mode to use both pooling modes concurrently Session mode can be used concurrently with transaction mode by using 5432 for session and 6543 for transaction. However, by configuring the pooler mode to session here, you will not be able to use transaction mode at the same time. )} )}

Specify when a connection can be returned to the pool.{' '} snap.setShowPoolingModeHelper(true)} className="cursor-pointer underline underline-offset-2" > Learn more about pool modes .

)} /> ( Pool Size {maxConnData !== undefined && Number(form.getValues('default_pool_size') ?? 15) > maxConnData.maxConnections * 0.8 && (
Pool size is greater than 80% of the max connections ( {maxConnData.maxConnections}) on your database This may result in instability and unreliability with your database connections.
)} The maximum number of connections made to the underlying Postgres cluster, per user+db combination. Pool size has a default of {defaultPoolSize} based on your compute size of {computeSize}. Please refer to our{' '} documentation {' '} to find out more.
)} /> ( Max Client Connections The maximum number of concurrent client connections allowed. This value is fixed at {defaultMaxClientConn} based on your compute size of {computeSize}{' '} and cannot be changed. Please refer to our{' '} documentation {' '} to find out more. )} />
)}
) }