import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { capitalize } from 'lodash' import { Fragment, useEffect, useMemo } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import z from 'zod' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import { setValueAsNullableNumber } from 'components/ui/Forms/Form.constants' import { FormActions } from 'components/ui/Forms/FormActions' import { InlineLink } from 'components/ui/InlineLink' import Panel from 'components/ui/Panel' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' import { usePgbouncerConfigurationUpdateMutation } from 'data/database/pgbouncer-config-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Badge, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, Input_Shadcn_, Separator, } from 'ui' import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { POOLING_OPTIMIZATIONS } from './ConnectionPooling.constants' const formId = 'pooling-configuration-form' const PoolingConfigurationFormSchema = z.object({ default_pool_size: z.number().nullable(), max_client_conn: z.number().nullable(), }) /** * [Joshen] PgBouncer configuration will be the main endpoint for GET and PATCH of pooling config */ export const ConnectionPooling = () => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const canUpdateConnectionPoolingConfiguration = useCheckPermissions( PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id } } ) const { data: pgbouncerConfig, error: pgbouncerConfigError, isLoading: isLoadingPgbouncerConfig, isError: isErrorPgbouncerConfig, isSuccess: isSuccessPgbouncerConfig, } = usePgbouncerConfigQuery({ projectRef }) const disablePoolModeSelection = useMemo(() => { return org?.plan?.id === 'free' }, [org]) const { data: maxConnData } = useMaxConnectionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { data: addons, isSuccess: isSuccessAddons } = useProjectAddonsQuery({ projectRef }) const { mutate: updatePoolerConfig, isLoading: isUpdatingPoolerConfig } = usePgbouncerConfigurationUpdateMutation() const hasIpv4Addon = !!addons?.selected_addons.find((addon) => addon.type === 'ipv4') const computeInstance = addons?.selected_addons.find((addon) => addon.type === 'compute_instance') const computeSize = computeInstance?.variant.name ?? capitalize(project?.infra_compute_size) ?? 'Nano' 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 form = useForm>({ resolver: zodResolver(PoolingConfigurationFormSchema), defaultValues: { default_pool_size: undefined, max_client_conn: null, }, }) const { default_pool_size } = form.watch() const connectionPoolingUnavailable = pgbouncerConfig?.pool_mode === null const ignoreStartupParameters = pgbouncerConfig?.ignore_startup_parameters const onSubmit: SubmitHandler> = async (data) => { const { default_pool_size } = data if (!projectRef) return console.error('Project ref is required') updatePoolerConfig( { ref: projectRef, default_pool_size: default_pool_size === null ? undefined : default_pool_size, ignore_startup_parameters: ignoreStartupParameters ?? '', }, { onSuccess: (data) => { toast.success(`Successfully updated pooler configuration`) if (data) { form.reset({ default_pool_size: data.default_pool_size, }) } }, } ) } const resetForm = () => { form.reset({ default_pool_size: pgbouncerConfig?.default_pool_size ?? defaultPoolSize, max_client_conn: pgbouncerConfig?.max_client_conn ?? defaultMaxClientConn, }) } useEffect(() => { if (isSuccessPgbouncerConfig) resetForm() }, [isSuccessPgbouncerConfig]) return (

Connection pooling configuration

{disablePoolModeSelection ? ( Shared Pooler ) : ( Shared/Dedicated Pooler )}
} footer={ resetForm()} helper={ !canUpdateConnectionPoolingConfiguration ? 'You need additional permissions to update connection pooling settings' : undefined } /> } > {isSuccessAddons && !disablePoolModeSelection && !hasIpv4Addon && (

If your network only supports IPv4, consider purchasing the{' '} IPv4 add-on

)} {isLoadingPgbouncerConfig && (
{Array.from({ length: 4 }).map((_, i) => (
))}
)} {isErrorPgbouncerConfig && ( )} {connectionPoolingUnavailable && ( )} {isSuccessPgbouncerConfig && (
( 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}.

} > {!!maxConnData && (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 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.

} >
)} />
)}
) }