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 { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Badge, Button, FormControl_Shadcn_, FormDescription_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, FormLabel_Shadcn_, FormMessage_Shadcn_, Form_Shadcn_, IconAlertTriangle, IconExternalLink, Input_Shadcn_, Listbox, } from 'ui' import z from 'zod' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' import Divider from 'components/ui/Divider' import { FormActions } from 'components/ui/Forms' 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, useStore } from 'hooks' import { useDatabaseSettingsStateSnapshot } from 'state/database-settings' 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'), z.literal('statement')]), max_client_conn: StringToPositiveNumber, }) export const ConnectionPooling = () => { const { ui } = useStore() 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' | 'statement', default_pool_size: poolingConfiguration?.default_pool_size as number | undefined, max_client_conn: poolingConfiguration?.max_client_conn as number | undefined, }, }) const { mutateAsync: 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, }) } ui.setNotification({ category: 'success', message: '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') try { await updateConfiguration({ ref: projectRef, default_pool_size: data.default_pool_size as number | undefined, pool_mode: data.pool_mode, }) } finally { } } useEffect(() => { if (isSuccess) { form.reset({ pool_mode: poolingConfiguration?.pool_mode as 'transaction' | 'session' | 'statement', 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}

Specify when a connection can be returned to the pool.{' '} snap.setShowPoolingModeHelper(true)} className="cursor-pointer underline underline-offset-2" > Unsure which pooling mode to use?

{field.value === 'session' && ( 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 session. However, by configuring the pooler mode to session here, you will not be able to use transaction mode at the same time. )}
)} /> ( 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. )} />
)}
) }