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 { toast } from 'sonner' import z from 'zod' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import { FormActions } from 'components/ui/Forms/FormActions' import Panel from 'components/ui/Panel' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import UpgradeToPro from 'components/ui/UpgradeToPro' 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 } from 'hooks/misc/useCheckPermissions' import { useDatabaseSettingsStateSnapshot } from 'state/database-settings' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Badge, FormControl_Shadcn_, FormDescription_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, FormLabel_Shadcn_, FormMessage_Shadcn_, Form_Shadcn_, Input_Shadcn_, Listbox, Separator, } from 'ui' import { Admonition } from 'ui-patterns' import { SESSION_MODE_DESCRIPTION, TRANSACTION_MODE_DESCRIPTION } from '../Database.constants' import { POOLING_OPTIMIZATIONS } from './ConnectionPooling.constants' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/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')]), max_client_conn: StringToPositiveNumber, DB_MAX_POOL_SIZE: StringToPositiveNumber, }) export const ConnectionPooling = () => { const { ref: projectRef } = useParams() const { project } = useProjectContext() const snap = useDatabaseSettingsStateSnapshot() const organization = useSelectedOrganization() const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug, }) const isTeamsEnterprisePlan = isSuccessSubscription && subscription.plan.id !== 'free' && subscription.plan.id !== 'pro' const promptTeamsEnterpriseUpgrade = IS_PLATFORM && !isTeamsEnterprisePlan 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 }) const { data: authConfig } = useAuthConfigQuery({ 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 canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') const form = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { pool_mode: poolingConfiguration?.pool_mode as 'transaction' | 'session', default_pool_size: poolingConfiguration?.default_pool_size as number | undefined, max_client_conn: poolingConfiguration?.max_client_conn as number | undefined, DB_MAX_POOL_SIZE: authConfig?.DB_MAX_POOL_SIZE ?? null, }, }) const { mutate: 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, DB_MAX_POOL_SIZE: authConfig?.DB_MAX_POOL_SIZE ?? null, }) } toast.success('Successfully saved settings') }, }) const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() const onSubmit: SubmitHandler> = async (data) => { if (!projectRef) return console.error('Project ref is required') if (!poolingInfo) return console.error('Pooling info required') updateConfiguration({ ref: projectRef, default_pool_size: data.default_pool_size as number | undefined, pool_mode: data.pool_mode, }) if (data.DB_MAX_POOL_SIZE !== authConfig?.DB_MAX_POOL_SIZE) { updateAuthConfig( { projectRef, config: { DB_MAX_POOL_SIZE: data.DB_MAX_POOL_SIZE ?? undefined } }, { onError: (error: Error) => { toast.error(`Failed to update auth max direct connections: ${error?.message}`) }, onSuccess: () => { toast.success('Successfully updated auth max direct connections') }, } ) } } useEffect(() => { if (isSuccess) { form.reset({ pool_mode: poolingConfiguration?.pool_mode as 'transaction' | 'session', default_pool_size: poolingConfiguration?.default_pool_size as number | undefined, max_client_conn: poolingConfiguration?.max_client_conn as number | undefined, DB_MAX_POOL_SIZE: authConfig?.DB_MAX_POOL_SIZE ?? null, }) } }, [isSuccess, authConfig]) 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}

{poolingConfiguration?.pool_mode === 'transaction' && ( Pool mode is permanently set to Transaction on port 6543 You can use Session mode by connecting to the pooler on port 5432 instead )} {poolingConfiguration?.pool_mode === 'session' && ( <> {field.value === 'transaction' ? ( ) : ( {/* [Joshen] Can probably remove this after Feb 28 */} )} )}

Specify when a connection can be returned to the pool.{' '} snap.setShowPoolingModeHelper(true)} className="cursor-pointer underline underline-offset-2" > Learn more about pool modes .

)} /> ( 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. )} /> ( Max Direct Auth Connections
{promptTeamsEnterpriseUpgrade && (
)} Auth will take up no more than this number of connections from the total number of available connections to serve requests. These connections are not reserved, so when unused they are released. Defaults to 10 connections.
)} />
)}
) }