import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { capitalize } from 'lodash' import Link from 'next/link' import { Fragment, useEffect } from 'react' import { SubmitHandler, useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Alert, AlertDescription, AlertTitle, Badge, Button, Form, FormControl, FormField, FormInputGroupInput, InputGroup, InputGroupAddon, InputGroupText, Separator, } from 'ui' import { Admonition } from 'ui-patterns/Admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageSection, PageSectionAside, PageSectionContent, PageSectionMeta, PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import z from 'zod' import { POOLING_OPTIMIZATIONS } from './ConnectionPooling.constants' import { AlertError } from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' import { FormActions } from '@/components/ui/Forms/FormActions' import { HighAvailabilityDisabledSectionNotice } from '@/components/ui/HighAvailability/HighAvailabilityDisabledSectionNotice' 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 { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useHighAvailability } from '@/hooks/misc/useHighAvailability' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' const formId = 'pooling-configuration-form' const HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS = 100_000 const HA_DISABLED_TITLE = 'Connection pooling settings are managed automatically on High Availability projects' const PoolingConfigurationFormSchema = z.object({ default_pool_size: z.preprocess( (val) => (val === '' || val === null || val === undefined ? undefined : val), z.coerce.number().optional() ), max_client_conn: z.preprocess( (val) => (val === '' || val === null || val === undefined ? undefined : val), z.coerce.number().optional() ), }) /** * [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 { isHighAvailability, isPending: isHighAvailabilityPending } = useHighAvailability() const canLoadPoolingConfig = !isHighAvailability && !isHighAvailabilityPending const { can: canUpdateConnectionPoolingConfiguration } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id } } ) const { data: pgbouncerConfig, error: pgbouncerConfigError, isPending: isLoadingPgbouncerConfig, isError: isErrorPgbouncerConfig, isSuccess: isSuccessPgbouncerConfig, } = usePgbouncerConfigQuery({ projectRef }, { enabled: canLoadPoolingConfig }) const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler') const disablePoolModeSelection = !hasDedicatedPooler const { data: maxConnData } = useMaxConnectionsQuery( { projectRef: project?.ref, connectionString: project?.connectionString, }, { enabled: canLoadPoolingConfig } ) const { data: addons, isSuccess: isSuccessAddons } = useProjectAddonsQuery({ projectRef }) const { mutate: updatePoolerConfig, isPending: 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: undefined, }, }) const default_pool_size = useWatch({ control: form.control, name: 'default_pool_size' }) const connectionPoolingUnavailable = pgbouncerConfig?.pool_mode === null const ignoreStartupParameters = pgbouncerConfig?.ignore_startup_parameters const onSubmit: SubmitHandler> = async (data) => { const { default_pool_size } = data if (!projectRef || isHighAvailability) return 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 {isHighAvailability && ( High Availability projects run one pooler per Postgres pod, each supporting up to{' '} {HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS.toLocaleString()} active or passive connections, with pooling behavior selected automatically.{' '} Learn more . } /> )} {isSuccessAddons && !isHighAvailability && !disablePoolModeSelection && !hasIpv4Addon && ( Enable IPv4 add-on } /> )} resetForm()} helper={ !canUpdateConnectionPoolingConfiguration ? 'You need additional permissions to update connection pooling settings' : undefined } /> ) } > {!isHighAvailability && isLoadingPgbouncerConfig && (
{Array.from({ length: 4 }).map((_, i) => (
))}
)} {!isHighAvailability && isErrorPgbouncerConfig && ( )} {!isHighAvailability && connectionPoolingUnavailable && ( )} {(isHighAvailability || (isSuccessPgbouncerConfig && !connectionPoolingUnavailable)) && ( <>
Connection poolers

{isHighAvailability ? 'One pooler runs for each Postgres pod in the cluster.' : 'Configuration is shared across all connection poolers.'}

{isHighAvailability ? ( High Availability ) : ( <> Shared {!disablePoolModeSelection && Dedicated} )}
( Pool size is managed automatically for each Postgres pod and cannot be changed.

) : (

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}.

) } className="[&>div]:md:w-1/2 [&>div]:xl:w-2/5 [&>div>div]:w-full" > field.onChange( isNaN(event.target.valueAsNumber) ? null : event.target.valueAsNumber ) } /> {!isHighAvailability && ( connections )} {!isHighAvailability && !!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. )}
)} /> ( Each pooler can support up to{' '} {HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS.toLocaleString()} active or passive client connections. This value is managed automatically and cannot be changed.

) : (

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.{' '} Learn more

) } > field.onChange( isNaN(event.target.valueAsNumber) ? null : event.target.valueAsNumber ) } /> clients
)} /> )}
) }