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
{isHighAvailability
? 'One pooler runs for each Postgres pod in the cluster.'
: 'Configuration is shared across all connection poolers.'}
Connection poolers
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" >
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.{' '}