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 } 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' 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 { 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 { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' const formId = 'pooling-configuration-form' 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 { can: canUpdateConnectionPoolingConfiguration } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id } } ) const { data: pgbouncerConfig, error: pgbouncerConfigError, isPending: isLoadingPgbouncerConfig, isError: isErrorPgbouncerConfig, isSuccess: isSuccessPgbouncerConfig, } = usePgbouncerConfigQuery({ projectRef }) const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler') const disablePoolModeSelection = !hasDedicatedPooler const { data: maxConnData } = useMaxConnectionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) 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 } = 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 {isSuccessAddons && !disablePoolModeSelection && !hasIpv4Addon && ( Enable IPv4 add-on } /> )} resetForm()} helper={ !canUpdateConnectionPoolingConfiguration ? 'You need additional permissions to update connection pooling settings' : undefined } /> } > {isLoadingPgbouncerConfig && (
{Array.from({ length: 4 }).map((_, i) => (
))}
)} {isErrorPgbouncerConfig && ( )} {connectionPoolingUnavailable && ( )} {isSuccessPgbouncerConfig && !connectionPoolingUnavailable && ( <>
Connection poolers

Configuration is shared across all connection poolers.

Shared {!disablePoolModeSelection && Dedicated}
( 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 ) } /> connections {!!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.{' '} Learn more

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