Skip to content

Commit 0a4166a

Browse files
authored
Support for Dedicated Pooler in Connection Pooling Part 2 (supabase#33829)
* Init * Initial set up for hooking up supavisor and pgbouncer * Hook up pgbouncer status check after swapping pooler type * Add check for nano compute for switching to pg bouncer * Add check for ipv4 addon * Remove expect error tag * Update copy in IPv4SidePanel * Add badge to select options for pooler types * Hook up pgbouncer config for connect UI * Refactor pooling-configuration react queries to supavisor-configuration * Update Ipv4 compatability UI indicators in Connect UI when on pgbouncer * Remove statement mode * Resolve undefined problem with react hook form * Fix * Update UI texts from PgBouncer to Dedicated Pooler * Feature flag changes * Add pooler settings link in Connect UI * Smol update * Update session pooler description for pgbouncer
1 parent 5508d2c commit 0a4166a

15 files changed

+314
-256
lines changed

apps/studio/components/interfaces/Connect/Connect.constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,8 @@ export const CONNECTION_TYPES = [
348348
{ key: 'mobiles', label: 'Mobile Frameworks', obj: MOBILES },
349349
{ key: 'orms', label: 'ORMs', obj: ORMS },
350350
]
351+
352+
// [Joshen] Consolidating reusable copy here
353+
export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT =
354+
'Purchase IPv4 add-on or use Supavisor if on a IPv4 network'
355+
export const IPV4_ADDON_TEXT = 'Connections are IPv4 proxied with IPv4 add-on'

apps/studio/components/interfaces/Connect/ConnectTabContent.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { forwardRef, HTMLAttributes, useMemo } from 'react'
44
import { useParams } from 'common'
55
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
66
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
7-
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
7+
import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query'
8+
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
9+
import { useFlag } from 'hooks/ui/useFlag'
810
import { pluckObjectFields } from 'lib/helpers'
911
import { cn } from 'ui'
1012
import type { projectKeys } from './Connect.types'
@@ -23,18 +25,36 @@ interface ConnectContentTabProps extends HTMLAttributes<HTMLDivElement> {
2325
const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
2426
({ projectKeys, filePath, ...props }, ref) => {
2527
const { ref: projectRef } = useParams()
28+
const allowPgBouncerSelection = useFlag('dualPoolerSupport')
2629

2730
const { data: settings } = useProjectSettingsV2Query({ projectRef })
28-
const { data: poolingInfo } = usePoolingConfigurationQuery({ projectRef })
31+
const { data: pgbouncerConfig } = usePgbouncerConfigQuery(
32+
{ projectRef },
33+
{ enabled: allowPgBouncerSelection }
34+
)
35+
const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
2936

37+
const isPgBouncerEnabled = allowPgBouncerSelection && !!pgbouncerConfig?.pgbouncer_enabled
3038
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
3139
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
3240
const connectionInfo = pluckObjectFields(settings || emptyState, DB_FIELDS)
33-
const poolingConfiguration = poolingInfo?.find((x) => x.database_type === 'PRIMARY')
41+
const poolingConfiguration = isPgBouncerEnabled
42+
? pgbouncerConfig
43+
: supavisorConfig?.find((x) => x.database_type === 'PRIMARY')
3444

3545
const connectionStrings =
3646
poolingConfiguration !== undefined
37-
? getConnectionStringsV2(connectionInfo, poolingConfiguration, { projectRef })
47+
? getConnectionStringsV2({
48+
connectionInfo,
49+
poolingInfo: {
50+
connectionString: poolingConfiguration.connectionString,
51+
db_host: poolingConfiguration.db_host,
52+
db_name: poolingConfiguration.db_name,
53+
db_port: poolingConfiguration.db_port,
54+
db_user: poolingConfiguration.db_user,
55+
},
56+
metadata: { projectRef },
57+
})
3858
: { direct: { uri: '' }, pooler: { uri: '' } }
3959
const connectionStringsPooler = connectionStrings.pooler
4060
const connectionStringsDirect = connectionStrings.direct

apps/studio/components/interfaces/Connect/ConnectionPanel.tsx

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import Link from 'next/link'
33

44
import { useParams } from 'common'
55
import Panel from 'components/ui/Panel'
6-
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
6+
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
77
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
88
import {
9+
Badge,
910
Button,
1011
cn,
1112
CodeBlock,
@@ -21,14 +22,15 @@ import { DirectConnectionIcon, TransactionIcon } from './PoolerIcons'
2122

2223
interface ConnectionPanelProps {
2324
type?: 'direct' | 'transaction' | 'session'
25+
badge?: string
2426
title: string
2527
description: string
2628
connectionString: string
2729
ipv4Status: {
2830
type: 'error' | 'success'
2931
title: string
3032
description?: string
31-
link?: { text: string; url: string }
33+
links?: { text: string; url: string }[]
3234
}
3335
notice?: string[]
3436
parameters?: Array<{
@@ -100,6 +102,7 @@ export const CodeBlockFileHeader = ({ title }: { title: string }) => {
100102

101103
export const ConnectionPanel = ({
102104
type = 'direct',
105+
badge,
103106
title,
104107
description,
105108
connectionString,
@@ -113,14 +116,19 @@ export const ConnectionPanel = ({
113116
const { ref: projectRef } = useParams()
114117
const state = useDatabaseSelectorStateSnapshot()
115118

116-
const { data: poolingInfo } = usePoolingConfigurationQuery({ projectRef })
119+
const { data: poolingInfo } = useSupavisorConfigurationQuery({ projectRef })
117120
const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId)
118121
const isSessionMode = poolingConfiguration?.pool_mode === 'session'
119122

123+
const links = ipv4Status.links ?? []
124+
120125
return (
121126
<div className="flex flex-col gap-5 lg:grid lg:grid-cols-2 lg:gap-20 w-full">
122127
<div className="flex flex-col">
123-
<h1 className="text-sm mb-2">{title}</h1>
128+
<div className="flex items-center gap-x-2 mb-2">
129+
<h1 className="text-sm">{title}</h1>
130+
{!!badge && <Badge>{badge}</Badge>}
131+
</div>
124132
<p className="text-sm text-foreground-light mb-4">{description}</p>
125133
<div className="flex flex-col -space-y-px">
126134
{fileTitle && <CodeBlockFileHeader title={fileTitle} />}
@@ -216,21 +224,15 @@ export const ConnectionPanel = ({
216224
{ipv4Status.description && (
217225
<span className="text-xs text-foreground-lighter">{ipv4Status.description}</span>
218226
)}
219-
{ipv4Status.type === 'error' && (
220-
<span className="text-xs text-foreground-lighter">
221-
Use Session Pooler if on a IPv4 network or purchase IPv4 addon
222-
</span>
223-
)}
224-
{ipv4Status.link && (
225-
<div className="mt-2">
226-
<Button asChild type="default" size="tiny">
227-
<Link
228-
href={ipv4Status.link.url}
229-
className="text-xs text-light hover:text-foreground"
230-
>
231-
{ipv4Status.link.text}
232-
</Link>
233-
</Button>
227+
{links.length > 0 && (
228+
<div className="flex items-center gap-x-2 mt-2">
229+
{links.map((link) => (
230+
<Button key={link.text} asChild type="default" size="tiny">
231+
<Link href={link.url} className="text-xs text-light hover:text-foreground">
232+
{link.text}
233+
</Link>
234+
</Button>
235+
))}
234236
</div>
235237
)}
236238
</div>
@@ -285,7 +287,7 @@ export const ConnectionPanel = ({
285287
<p className="text-xs text-foreground-lighter max-w-xs">
286288
If you wish to use a Direct Connection with these, please purchase{' '}
287289
<Link
288-
href={ipv4Status.link?.url ?? '/'}
290+
href={`/project/${projectRef}/settings/addons?panel=ipv4`}
289291
className="text-xs text-light hover:text-foreground"
290292
>
291293
IPv4 support

apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx

Lines changed: 95 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { getAddons } from 'components/interfaces/Billing/Subscription/Subscripti
66
import AlertError from 'components/ui/AlertError'
77
import DatabaseSelector from 'components/ui/DatabaseSelector'
88
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
9-
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
9+
import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query'
10+
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
1011
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
1112
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
1213
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
1314
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
15+
import { useFlag } from 'hooks/ui/useFlag'
1416
import { pluckObjectFields } from 'lib/helpers'
1517
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
1618
import {
@@ -32,6 +34,8 @@ import {
3234
CONNECTION_PARAMETERS,
3335
DATABASE_CONNECTION_TYPES,
3436
DatabaseConnectionType,
37+
IPV4_ADDON_TEXT,
38+
PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT,
3539
} from './Connect.constants'
3640
import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel'
3741
import { getConnectionStrings } from './DatabaseSettings.utils'
@@ -54,19 +58,28 @@ export const DatabaseConnectionString = () => {
5458
const { ref: projectRef } = useParams()
5559
const org = useSelectedOrganization()
5660
const state = useDatabaseSelectorStateSnapshot()
61+
const allowPgBouncerSelection = useFlag('dualPoolerSupport')
5762

5863
const [selectedTab, setSelectedTab] = useState<DatabaseConnectionType>('uri')
5964

6065
const {
61-
data: poolingInfo,
62-
error: poolingInfoError,
63-
isLoading: isLoadingPoolingInfo,
64-
isError: isErrorPoolingInfo,
65-
isSuccess: isSuccessPoolingInfo,
66-
} = usePoolingConfigurationQuery({
67-
projectRef,
68-
})
69-
const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId)
66+
data: pgbouncerConfig,
67+
error: pgbouncerError,
68+
isLoading: isLoadingPgbouncerConfig,
69+
isError: isErrorPgbouncerConfig,
70+
isSuccess: isSuccessPgBouncerConfig,
71+
} = usePgbouncerConfigQuery({ projectRef }, { enabled: allowPgBouncerSelection })
72+
const {
73+
data: supavisorConfig,
74+
error: supavisorConfigError,
75+
isLoading: isLoadingSupavisorConfig,
76+
isError: isErrorSupavisorConfig,
77+
isSuccess: isSuccessSupavisorConfig,
78+
} = useSupavisorConfigurationQuery({ projectRef })
79+
const isPgBouncerEnabled = allowPgBouncerSelection && !!pgbouncerConfig?.pgbouncer_enabled
80+
const poolingConfiguration = isPgBouncerEnabled
81+
? pgbouncerConfig
82+
: supavisorConfig?.find((x) => x.identifier === state.selectedDatabaseId)
7083

7184
const {
7285
data: databases,
@@ -76,10 +89,19 @@ export const DatabaseConnectionString = () => {
7689
isSuccess: isSuccessReadReplicas,
7790
} = useReadReplicasQuery({ projectRef })
7891

79-
const error = poolingInfoError || readReplicasError
80-
const isLoading = isLoadingPoolingInfo || isLoadingReadReplicas
81-
const isError = isErrorPoolingInfo || isErrorReadReplicas
82-
const isSuccess = isSuccessPoolingInfo && isSuccessReadReplicas
92+
const poolerError = isPgBouncerEnabled ? pgbouncerError : supavisorConfigError
93+
const isLoadingPoolerConfig = isPgBouncerEnabled
94+
? isLoadingPgbouncerConfig
95+
: isLoadingSupavisorConfig
96+
const isErrorPoolerConfig = isPgBouncerEnabled ? isErrorPgbouncerConfig : isErrorSupavisorConfig
97+
const isSuccessPoolerConfig = isPgBouncerEnabled
98+
? isSuccessPgBouncerConfig
99+
: isSuccessSupavisorConfig
100+
101+
const error = poolerError || readReplicasError
102+
const isLoading = isLoadingPoolerConfig || isLoadingReadReplicas
103+
const isError = isErrorPoolerConfig || isErrorReadReplicas
104+
const isSuccess = isSuccessPoolerConfig && isSuccessReadReplicas
83105

84106
const selectedDatabase = (databases ?? []).find(
85107
(db) => db.identifier === state.selectedDatabaseId
@@ -109,9 +131,17 @@ export const DatabaseConnectionString = () => {
109131
}
110132

111133
const connectionStrings =
112-
isSuccessPoolingInfo && poolingConfiguration !== undefined
113-
? getConnectionStrings(connectionInfo, poolingConfiguration, {
114-
projectRef,
134+
isSuccessSupavisorConfig && poolingConfiguration !== undefined
135+
? getConnectionStrings({
136+
connectionInfo,
137+
poolingInfo: {
138+
connectionString: poolingConfiguration.connectionString,
139+
db_host: poolingConfiguration.db_host,
140+
db_name: poolingConfiguration.db_name,
141+
db_port: poolingConfiguration.db_port,
142+
db_user: poolingConfiguration.db_user,
143+
},
144+
metadata: { projectRef },
115145
})
116146
: {
117147
direct: {
@@ -173,6 +203,22 @@ export const DatabaseConnectionString = () => {
173203
// [Refactor] See if we can do this in an immutable way, technically not a good practice to do this
174204
let stepNumber = 0
175205

206+
const ipv4AddOnUrl = {
207+
text: 'IPv4 add-on',
208+
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
209+
}
210+
const ipv4SettingsUrl = {
211+
text: 'IPv4 settings',
212+
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
213+
}
214+
const poolerSettingsUrl = {
215+
text: 'Pooler settings',
216+
url: `/project/${projectRef}/settings/database#connection-pooling`,
217+
}
218+
const buttonLinks = !ipv4Addon
219+
? [ipv4AddOnUrl, ...(isPgBouncerEnabled ? [poolerSettingsUrl] : [])]
220+
: [ipv4SettingsUrl, ...(isPgBouncerEnabled ? [poolerSettingsUrl] : [])]
221+
176222
return (
177223
<div className="flex flex-col">
178224
<div
@@ -279,16 +325,13 @@ export const DatabaseConnectionString = () => {
279325
ipv4Status={{
280326
type: !ipv4Addon ? 'error' : 'success',
281327
title: !ipv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
282-
description: ipv4Addon && 'Connections are IPv4 proxied with IPv4 addon.',
283-
link: !ipv4Addon
284-
? {
285-
text: 'IPv4 addon',
286-
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
287-
}
288-
: {
289-
text: 'IPv4 settings',
290-
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
291-
},
328+
description:
329+
isPgBouncerEnabled && !ipv4Addon
330+
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
331+
: !isPgBouncerEnabled
332+
? 'Use Session Pooler if on a IPv4 network or purchase IPv4 add-on'
333+
: IPV4_ADDON_TEXT,
334+
links: buttonLinks,
292335
}}
293336
parameters={[
294337
{ ...CONNECTION_PARAMETERS.host, value: connectionInfo.db_host },
@@ -303,13 +346,20 @@ export const DatabaseConnectionString = () => {
303346
lang={lang}
304347
type="transaction"
305348
title="Transaction pooler"
349+
badge={isPgBouncerEnabled ? 'Dedicated Pooler' : 'Supavisor'}
306350
fileTitle={fileTitle}
307351
description="Ideal for stateless applications like serverless functions where each interaction with Postgres is brief and isolated."
308352
connectionString={connectionStrings['pooler'][selectedTab]}
309353
ipv4Status={{
310-
type: 'success',
311-
title: 'IPv4 compatible',
312-
description: 'Transaction pooler connections are IPv4 proxied for free.',
354+
type: isPgBouncerEnabled && !ipv4Addon ? 'error' : 'success',
355+
title: isPgBouncerEnabled && !ipv4Addon ? 'IPv4 incompatible' : 'IPv4 compatible',
356+
description:
357+
isPgBouncerEnabled && !ipv4Addon
358+
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
359+
: !isPgBouncerEnabled
360+
? 'Transaction pooler connections are IPv4 proxied for free.'
361+
: IPV4_ADDON_TEXT,
362+
links: isPgBouncerEnabled ? buttonLinks : undefined,
313363
}}
314364
notice={['Does not support PREPARE statements']}
315365
parameters={[
@@ -341,13 +391,24 @@ export const DatabaseConnectionString = () => {
341391
lang={lang}
342392
type="session"
343393
title="Session pooler"
394+
badge={isPgBouncerEnabled ? 'Dedicated Pooler' : 'Supavisor'}
344395
fileTitle={fileTitle}
345-
description="Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network."
396+
description={
397+
isPgBouncerEnabled
398+
? 'Recommended if you need to use prepared statements, or other features that are only available in Session mode.'
399+
: 'Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network.'
400+
}
346401
connectionString={connectionStrings['pooler'][selectedTab].replace('6543', '5432')}
347402
ipv4Status={{
348-
type: 'success',
349-
title: 'IPv4 compatible',
350-
description: 'Session pooler connections are IPv4 proxied for free.',
403+
type: isPgBouncerEnabled && !ipv4Addon ? 'error' : 'success',
404+
title: isPgBouncerEnabled && !ipv4Addon ? 'IPv4 incompatible' : 'IPv4 compatible',
405+
description:
406+
isPgBouncerEnabled && !ipv4Addon
407+
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
408+
: !isPgBouncerEnabled
409+
? 'Session pooler connections are IPv4 proxied for free'
410+
: IPV4_ADDON_TEXT,
411+
links: isPgBouncerEnabled ? buttonLinks : undefined,
351412
}}
352413
parameters={[
353414
{ ...CONNECTION_PARAMETERS.host, value: poolingConfiguration?.db_host ?? '' },

0 commit comments

Comments
 (0)