Files
supabase/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.ts
Joshen Lim 0987824674 Clean up + add unit tests for database connections (#48372)
## Context

As per PR title - no functional / visual changes, just some code clean
up / refactor + adding unit tests

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added database connection metrics covering active, blocked, and
idle-in-transaction queries.
* Enhanced insights for the longest-running query and the top blocker
(most queries blocked), including warning indicators based on duration
thresholds.
* **Bug Fixes**
* Improved consistency and accuracy of database-activity calculations in
the connection overview.
* **Refactor**
* Centralized metric derivation so the UI uses the same computed logic
everywhere.
* **Tests**
* Added metric-calculation tests with controlled time to validate
multiple scenarios and warning behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 13:15:51 +08:00

144 lines
5.3 KiB
TypeScript

import dayjs from 'dayjs'
import {
WARN_DURATION_ACTIVE_QUERY,
WARN_DURATION_BLOCKED,
WARN_DURATION_IDLE_TXN,
WARN_TOP_BLOCKER,
} from './DatabaseConnections.constants'
import { type DatabaseActivity } from '@/data/database/activity-query'
const LONG_RUNNING_STATES: (DatabaseActivity['state'] | undefined)[] = [
'active',
'idle in transaction',
'idle in transaction (aborted)',
]
type ActivityWithDuration = { activity: DatabaseActivity; duration: number }
export type ConnectionMetrics = {
activeQueries: DatabaseActivity[]
blockedQueries: DatabaseActivity[]
warnBlockedQueries: boolean
longestBlockedQuery: ActivityWithDuration | null
idleInTransactionQueries: DatabaseActivity[]
longestRunningQuery: ActivityWithDuration | null
warnLongestRunningQuery: boolean
queryBlockingTheMostQueries: { activity: DatabaseActivity; count: number } | null
warnTopBlocker: boolean
}
export const getDuration = (activity: DatabaseActivity) => {
const { state } = activity
if (state === 'active' && activity.query_start) {
return dayjs().utc().diff(dayjs(activity.query_start).utc(), 'second')
}
if (state === 'idle' && activity.state_change) {
return dayjs().utc().diff(dayjs(activity.state_change).utc(), 'second')
}
if (
(state === 'idle in transaction' || state === 'idle in transaction (aborted)') &&
activity.transaction_start
) {
return dayjs().utc().diff(dayjs(activity.transaction_start).utc(), 'second')
}
return null
}
export const getBadgeVariant = (activity: DatabaseActivity) => {
const { state } = activity
if (state === 'active') return 'success'
if (state === 'idle in transaction' || state === 'idle in transaction (aborted)') return 'warning'
return 'default'
}
// Resolves the timestamp that duration should be measured from for a given state -
// query_start while actively running, transaction_start while idle in transaction.
export const getActivityStart = (activity: DatabaseActivity) => {
if (activity.state === 'active') return activity.query_start
if (
activity.state === 'idle in transaction' ||
activity.state === 'idle in transaction (aborted)'
) {
return activity.transaction_start
}
return null
}
// Finds the activity with the largest duration-since-start among the given activities (e.g. the
// longest-blocked query, or the longest-running query).
const findLongestRunning = (
activities: DatabaseActivity[],
now: dayjs.Dayjs
): ActivityWithDuration | null =>
activities.reduce<ActivityWithDuration | null>((longest, activity) => {
const start = getActivityStart(activity)
if (!start) return longest
const duration = Math.max(now.diff(dayjs(start).utc(), 'second'), 0)
return longest === null || duration > longest.duration ? { activity, duration } : longest
}, null)
// Derives the Overview page's connection/activity metrics (and their warning thresholds) from the
// raw pg_stat_activity rows, so the logic can be unit tested independently of the component.
export const getConnectionMetrics = (activities: DatabaseActivity[]): ConnectionMetrics => {
// Computed once so every metric below is measured against the same instant, rather than each
// drifting slightly against the others.
const now = dayjs().utc()
const activeQueries = activities.filter((x) => x.state === 'active')
const blockedQueries = activities.filter((x) => x.blocked_by.length > 0)
const warnBlockedQueries = blockedQueries.some((activity) => {
const start = getActivityStart(activity)
if (!start) return false
return now.diff(dayjs(start).utc(), 'second') >= WARN_DURATION_BLOCKED
})
const longestBlockedQuery = findLongestRunning(blockedQueries, now)
const idleInTransactionQueries = activities.filter((x) => {
const isIdleInTransaction =
x.state === 'idle in transaction' || x.state === 'idle in transaction (aborted)'
if (!isIdleInTransaction || !x.transaction_start) return false
return now.diff(dayjs(x.transaction_start).utc(), 'second') >= WARN_DURATION_IDLE_TXN
})
const longestRunningQuery = findLongestRunning(
activities.filter((x) => LONG_RUNNING_STATES.includes(x.state)),
now
)
const warnLongestRunningQuery =
(longestRunningQuery?.activity.state === 'active' &&
longestRunningQuery.duration >= WARN_DURATION_ACTIVE_QUERY) ||
((longestRunningQuery?.activity.state === 'idle in transaction' ||
longestRunningQuery?.activity.state === 'idle in transaction (aborted)') &&
longestRunningQuery.duration >= WARN_DURATION_IDLE_TXN)
const blockingCounts = activities.reduce<Map<number, number>>((counts, activity) => {
activity.blocked_by.forEach((pid) => counts.set(pid, (counts.get(pid) ?? 0) + 1))
return counts
}, new Map())
const queryBlockingTheMostQueries = [...blockingCounts].reduce<{
activity: DatabaseActivity
count: number
} | null>((mostBlocking, [pid, count]) => {
if (mostBlocking && count <= mostBlocking.count) return mostBlocking
const activity = activities.find((x) => x.pid === pid)
return activity ? { activity, count } : mostBlocking
}, null)
const warnTopBlocker = (queryBlockingTheMostQueries?.count ?? 0) >= WARN_TOP_BLOCKER
return {
activeQueries,
blockedQueries,
warnBlockedQueries,
longestBlockedQuery,
idleInTransactionQueries,
longestRunningQuery,
warnLongestRunningQuery,
queryBlockingTheMostQueries,
warnTopBlocker,
}
}