import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' 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 { 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' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' 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, ignore_startup_parameters: z.string(), 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 defaultPoolSize = POOLING_OPTIMIZATIONS?.[ computeInstance?.variant.identifier as keyof typeof POOLING_OPTIMIZATIONS ]?.poolSize ?? 15 // [TODO] 15 is for micro, need to change to 10 for nano const defaultMaxClientConn = POOLING_OPTIMIZATIONS?.[ computeInstance?.variant.identifier as keyof typeof POOLING_OPTIMIZATIONS ]?.maxClientConn ?? 200 const { data: poolingInfo, error, isLoading, isError, isSuccess, } = usePoolingConfigurationQuery({ projectRef: projectRef }) const { data: maxConnData } = useMaxConnectionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const connectionPoolingUnavailable = !poolingInfo?.pgbouncer_enabled && poolingInfo?.pool_mode === null const canUpdateConnectionPoolingConfiguration = useCheckPermissions( PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, }, } ) const form = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { ignore_startup_parameters: poolingInfo?.ignore_startup_parameters, pool_mode: poolingInfo?.pool_mode as 'transaction' | 'session' | 'statement', default_pool_size: poolingInfo?.default_pool_size as number | undefined, max_client_conn: poolingInfo?.max_client_conn as number | undefined, }, }) const { mutateAsync: updateConfiguration, isLoading: isUpdating } = usePoolingConfigurationUpdateMutation({ onSuccess: (data) => { if (data) { form.reset({ ignore_startup_parameters: data.ignore_startup_parameters, pool_mode: data.pool_mode, default_pool_size: data.default_pool_size, max_client_conn: data.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, // pgbouncer can't be disabled in the UI, so just pass it along pgbouncer_enabled: poolingInfo.pgbouncer_enabled, default_pool_size: data.default_pool_size as number | undefined, ignore_startup_parameters: data.ignore_startup_parameters, pool_mode: data.pool_mode, max_client_conn: data.max_client_conn, }) } finally { } } useEffect(() => { if (isSuccess) { form.reset({ ignore_startup_parameters: poolingInfo?.ignore_startup_parameters, pool_mode: poolingInfo?.pool_mode as 'transaction' | 'session' | 'statement', default_pool_size: poolingInfo?.default_pool_size as number | undefined, max_client_conn: poolingInfo?.max_client_conn as number | undefined, }) } }, [isSuccess]) return (

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

{isSuccess && (
{poolingInfo?.supavisor_enabled ? 'Supavisor' : 'PGBouncer'}
)}
} footer={ (poolingInfo?.pgbouncer_enabled ?? false) && ( 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.

)} {poolingInfo.pgbouncer_enabled && ( <>
( 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?

)} /> ( 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{' '} {computeInstance?.variant.name ?? 'Micro'}. Please refer to our{' '} documentation {' '} to find out more.
)} /> {!poolingInfo?.supavisor_enabled && ( ( Ignore Startup Parameters Defaults are either blank or "extra_float_digits" )} /> )} ( Max Client Connections The maximum number of concurrent client connections allowed.{' '} {poolingInfo.supavisor_enabled ? `This value is fixed at ${defaultMaxClientConn} based on your compute size of ${ computeInstance?.variant.name ?? 'Micro' } and cannot be changed.` : 'Overrides default optimizations. '} Please refer to our{' '} documentation {' '} to find out more. )} />
)} )}
) }