mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix. Link string changes only, no content changes. ## What is the current behavior? Nine inbound links in `apps/www` and `apps/studio` point at anchors on the connecting to Postgres guide that don't exist. All nine are already broken on production today: `#connection-pooler`, `#connection-pool`, `#how-connection-pooling-works`, `#serverside-poolers`, and `#connecting-with-drizzle` are all missing from the live page. #49869 moves the pooling content to a child page, so these links need current destinations either way. ## What is the new behavior? Point each link at the page that holds the content now. - **Studio, 3 links.** The Connect sheet's Drizzle link goes to the Drizzle guide. The connection pooling and pooling modes links go to `pooling-and-limits#how-connection-pooling-works`. - **www, 6 links.** Three blog posts, the Heroku comparison page, and the Dedicated poolers feature entry go to `pooling-and-limits`. The feature entry uses `#shared-pooler`. ## Additional context Split out of #49869. These paths belong to `@supabase/marketing` and `@supabase/Dashboard` in CODEOWNERS, and pulling both teams into a docs-only restructure for nine link strings isn't a good trade. Merge after #49928. The destinations don't exist on production until the docs pages land. ## Manual testing 1. Open [Supavisor: Scaling Postgres to 1 Million Connections](https://zone-www-dot-com-git-fix-pooler-docs-links-supabase.vercel.app/blog/supavisor-1-million). The "connection pooling" link in the opening paragraph resolves to `connecting-to-postgres/pooling-and-limits#how-connection-pooling-works`. 2. Open [Dedicated poolers](https://zone-www-dot-com-git-fix-pooler-docs-links-supabase.vercel.app/features/dedicated-poolers). The docs link resolves to `pooling-and-limits#shared-pooler`. 3. Open [Supabase vs Heroku Postgres](https://zone-www-dot-com-git-fix-pooler-docs-links-supabase.vercel.app/alternatives/supabase-vs-heroku-postgres). Both connection pooling links resolve to `pooling-and-limits#how-connection-pooling-works`. 4. Open [Connection pooling and limits](https://docs-git-docs-connecting-to-postgres-technical-supabase.vercel.app/docs/guides/database/connecting-to-postgres/pooling-and-limits) on the #49928 docs preview. The `how-connection-pooling-works` and `shared-pooler` headings both render with those IDs. 5. Open the Connect dialog on any project. Under Drizzle, the docs link opens the Drizzle guide. 6. Open Database settings, then Connection pooling. The pooler link opens Connection pooling and limits. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated connection pooling links across Studio, product pages, blogs, and comparison content to point to the relevant pooling guidance. * Refined links for Drizzle ORM, dedicated poolers, direct connections, and shared poolers. * Improved navigation to specific documentation sections explaining connection pooling modes and behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
427 lines
18 KiB
TypeScript
427 lines
18 KiB
TypeScript
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'
|
|
import { preprocessEmptyNumberInput } from '@/lib/forms/zod-number-input'
|
|
|
|
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: preprocessEmptyNumberInput(z.coerce.number().optional()),
|
|
max_client_conn: preprocessEmptyNumberInput(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<z.infer<typeof PoolingConfigurationFormSchema>>({
|
|
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<z.infer<typeof PoolingConfigurationFormSchema>> = 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 (
|
|
<PageSection id="connection-pooler">
|
|
<PageSectionMeta>
|
|
<PageSectionSummary>
|
|
<PageSectionTitle>Connection pooling</PageSectionTitle>
|
|
</PageSectionSummary>
|
|
<PageSectionAside>
|
|
<DocsButton
|
|
href={`${DOCS_URL}/guides/database/connecting-to-postgres/pooling-and-limits#how-connection-pooling-works`}
|
|
/>
|
|
</PageSectionAside>
|
|
</PageSectionMeta>
|
|
<PageSectionContent className="space-y-4">
|
|
{isHighAvailability && (
|
|
<HighAvailabilityDisabledSectionNotice
|
|
title={HA_DISABLED_TITLE}
|
|
description={
|
|
<>
|
|
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.{' '}
|
|
<InlineLink href="https://multigres.com/blog/pooling-without-choosing-a-mode">
|
|
Learn more
|
|
</InlineLink>
|
|
.
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{isSuccessAddons && !isHighAvailability && !disablePoolModeSelection && !hasIpv4Addon && (
|
|
<Admonition
|
|
type="default"
|
|
layout="responsive"
|
|
title="Dedicated pooler uses IPv6 by default"
|
|
description="Connections from IPv4-only networks require enabling the IPv4 add-on on your project instance."
|
|
actions={
|
|
<Button variant="default" asChild>
|
|
<Link href={`/project/${projectRef}/settings/addons?panel=ipv4`}>
|
|
Enable IPv4 add-on
|
|
</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
<Panel
|
|
noMargin
|
|
footer={
|
|
isHighAvailability ? undefined : (
|
|
<FormActions
|
|
form={formId}
|
|
isSubmitting={isUpdatingPoolerConfig}
|
|
hasChanges={form.formState.isDirty}
|
|
handleReset={() => resetForm()}
|
|
helper={
|
|
!canUpdateConnectionPoolingConfiguration
|
|
? 'You need additional permissions to update connection pooling settings'
|
|
: undefined
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
>
|
|
<Panel.Content>
|
|
{!isHighAvailability && isLoadingPgbouncerConfig && (
|
|
<div className="flex flex-col gap-y-4">
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
<Fragment key={`loader-${i}`}>
|
|
<div className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full">
|
|
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
|
|
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
|
|
</div>
|
|
<Separator />
|
|
</Fragment>
|
|
))}
|
|
|
|
<ShimmeringLoader className="h-8 w-full" />
|
|
</div>
|
|
)}
|
|
{!isHighAvailability && isErrorPgbouncerConfig && (
|
|
<AlertError
|
|
error={pgbouncerConfigError}
|
|
subject="Failed to retrieve connection pooler configuration"
|
|
/>
|
|
)}
|
|
{!isHighAvailability && connectionPoolingUnavailable && (
|
|
<Admonition
|
|
type="default"
|
|
title="Unable to retrieve pooling configuration"
|
|
description="Please start a new project to enable this feature"
|
|
/>
|
|
)}
|
|
{(isHighAvailability ||
|
|
(isSuccessPgbouncerConfig && !connectionPoolingUnavailable)) && (
|
|
<>
|
|
<div className="flex flex-row gap-2 justify-between w-full">
|
|
<div className="flex flex-col text-sm">
|
|
<h5 className="text-foreground font-normal">Connection poolers</h5>
|
|
<p className="text-foreground-lighter">
|
|
{isHighAvailability
|
|
? 'One pooler runs for each Postgres pod in the cluster.'
|
|
: 'Configuration is shared across all connection poolers.'}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-row gap-1 items-center">
|
|
{isHighAvailability ? (
|
|
<Badge>High Availability</Badge>
|
|
) : (
|
|
<>
|
|
<Badge>Shared</Badge>
|
|
{!disablePoolModeSelection && <Badge>Dedicated</Badge>}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Separator className="bg-border -mx-6 w-[calc(100%+3rem)] my-4" />
|
|
<Form {...form}>
|
|
<form
|
|
id={formId}
|
|
className="flex flex-col gap-y-4 w-full"
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name="default_pool_size"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="flex-row-reverse"
|
|
label="Connection pool size"
|
|
description={
|
|
isHighAvailability ? (
|
|
<p>
|
|
Pool size is managed automatically for each Postgres pod and cannot
|
|
be changed.
|
|
</p>
|
|
) : (
|
|
<p>
|
|
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}.
|
|
</p>
|
|
)
|
|
}
|
|
className="[&>div]:md:w-1/2 [&>div]:xl:w-2/5 [&>div>div]:w-full"
|
|
>
|
|
<FormControl>
|
|
<InputGroup>
|
|
<FormInputGroupInput
|
|
{...field}
|
|
disabled={isHighAvailability}
|
|
type="number"
|
|
className="w-full"
|
|
value={isHighAvailability ? '' : (field.value ?? '')}
|
|
placeholder={
|
|
isHighAvailability
|
|
? 'Managed automatically'
|
|
: defaultPoolSize.toString()
|
|
}
|
|
onChange={(event) =>
|
|
field.onChange(
|
|
isNaN(event.target.valueAsNumber)
|
|
? null
|
|
: event.target.valueAsNumber
|
|
)
|
|
}
|
|
/>
|
|
{!isHighAvailability && (
|
|
<InputGroupAddon align="inline-end">
|
|
<InputGroupText>connections</InputGroupText>
|
|
</InputGroupAddon>
|
|
)}
|
|
</InputGroup>
|
|
</FormControl>
|
|
{!isHighAvailability &&
|
|
!!maxConnData &&
|
|
(default_pool_size ?? 15) > maxConnData.maxConnections * 0.8 && (
|
|
<Alert variant="warning" className="mt-2">
|
|
<AlertTitle className="text-foreground">
|
|
Pool size is greater than 80% of the max connections (
|
|
{maxConnData.maxConnections}) on your database
|
|
</AlertTitle>
|
|
<AlertDescription>
|
|
This may result in instability and unreliability with your
|
|
database connections.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<Separator className="bg-border -mx-6 w-[calc(100%+3rem)]" />
|
|
|
|
<FormField
|
|
control={form.control}
|
|
disabled
|
|
name="max_client_conn"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="flex-row-reverse"
|
|
label="Max client connections"
|
|
className="[&>div]:md:w-1/2 [&>div]:xl:w-2/5 [&>div>div]:w-full"
|
|
description={
|
|
isHighAvailability ? (
|
|
<p>
|
|
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.
|
|
</p>
|
|
) : (
|
|
<p>
|
|
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.{' '}
|
|
<InlineLink
|
|
href={`${DOCS_URL}/guides/database/connection-management#configuring-supavisors-pool-size`}
|
|
>
|
|
Learn more
|
|
</InlineLink>
|
|
</p>
|
|
)
|
|
}
|
|
>
|
|
<FormControl>
|
|
<InputGroup>
|
|
<FormInputGroupInput
|
|
{...field}
|
|
type="number"
|
|
className="w-full"
|
|
value={
|
|
isHighAvailability
|
|
? HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS
|
|
: (pgbouncerConfig?.max_client_conn ?? '')
|
|
}
|
|
placeholder={
|
|
isHighAvailability
|
|
? HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS.toString()
|
|
: defaultMaxClientConn.toString()
|
|
}
|
|
onChange={(event) =>
|
|
field.onChange(
|
|
isNaN(event.target.valueAsNumber)
|
|
? null
|
|
: event.target.valueAsNumber
|
|
)
|
|
}
|
|
/>
|
|
<InputGroupAddon align="inline-end">
|
|
<InputGroupText>clients</InputGroupText>
|
|
</InputGroupAddon>
|
|
</InputGroup>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</form>
|
|
</Form>
|
|
</>
|
|
)}
|
|
</Panel.Content>
|
|
</Panel>
|
|
</PageSectionContent>
|
|
</PageSection>
|
|
)
|
|
}
|