Files
supabase/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.test.ts
Joshen Lim 75b90c5de1 Check the session's backend_start for cancelling or terminating sessions (#48929)
## Context

Related to database connections - specifically for cancelling queries or
terminating sessions

PIDs can be re-used, so a more accurate check is to use both PID and
`backend_start` to uniquely identify the session to cancel or terminate

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

* **Bug Fixes**
* Improved query cancellation and session termination reliability by
verifying the active database session before taking action.
* Prevented actions from affecting a different session that reused the
same process ID.
* Added clearer guidance to refresh when a session has changed or is no
longer available.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-12 10:14:59 +07:00

335 lines
11 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getBlockChain, getBlockingChain, getConnectionMetrics } from './DatabaseConnections.utils'
import { type DatabaseActivity } from '@/data/database/activity-query'
const NOW = '2024-01-15T12:00:00Z'
const secondsAgo = (seconds: number) =>
new Date(new Date(NOW).getTime() - seconds * 1000).toISOString()
// `DatabaseActivity` intersects a discriminated `WaitEvent` union, so `Partial<DatabaseActivity>`
// doesn't distribute cleanly over it - scope overrides to the plain fields tests actually vary.
type ActivityOverrides = Partial<
Pick<
DatabaseActivity,
| 'pid'
| 'role_name'
| 'application_name'
| 'blocked_by'
| 'query'
| 'query_start'
| 'transaction_start'
| 'state_change'
| 'state'
>
>
const activity = (overrides: ActivityOverrides = {}): DatabaseActivity => ({
pid: 1,
role_name: 'postgres',
application_name: 'test',
blocked_by: [],
query: 'select 1',
backend_start: NOW,
query_start: null,
transaction_start: null,
state_change: null,
state: 'active',
wait_event_type: null,
wait_event: null,
...overrides,
})
describe('getConnectionMetrics', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date(NOW))
})
afterEach(() => {
vi.useRealTimers()
})
it('returns empty/null metrics for no activity', () => {
const metrics = getConnectionMetrics([])
expect(metrics.activeQueries).toEqual([])
expect(metrics.blockedQueries).toEqual([])
expect(metrics.warnBlockedQueries).toBe(false)
expect(metrics.longestBlockedQuery).toBe(null)
expect(metrics.idleInTransactionQueries).toEqual([])
expect(metrics.longestRunningQuery).toBe(null)
expect(metrics.warnLongestRunningQuery).toBe(false)
expect(metrics.queryBlockingTheMostQueries).toBe(null)
expect(metrics.warnTopBlocker).toBe(false)
})
it('counts active queries', () => {
const activities = [
activity({ pid: 1, state: 'active' }),
activity({ pid: 2, state: 'idle' }),
activity({ pid: 3, state: 'active' }),
]
expect(getConnectionMetrics(activities).activeQueries).toHaveLength(2)
})
describe('blocked queries', () => {
it('collects queries that are blocked by another pid', () => {
const activities = [
activity({ pid: 1, blocked_by: [2] }),
activity({ pid: 2, blocked_by: [] }),
]
const { blockedQueries } = getConnectionMetrics(activities)
expect(blockedQueries.map((a) => a.pid)).toEqual([1])
})
it('does not warn when blocked under the threshold (10s)', () => {
const activities = [
activity({ pid: 1, state: 'active', blocked_by: [2], query_start: secondsAgo(5) }),
]
expect(getConnectionMetrics(activities).warnBlockedQueries).toBe(false)
})
it('warns once a blocked query crosses the threshold (10s)', () => {
const activities = [
activity({ pid: 1, state: 'active', blocked_by: [2], query_start: secondsAgo(11) }),
]
expect(getConnectionMetrics(activities).warnBlockedQueries).toBe(true)
})
it('picks the longest-blocked query', () => {
const activities = [
activity({ pid: 1, state: 'active', blocked_by: [3], query_start: secondsAgo(5) }),
activity({ pid: 2, state: 'active', blocked_by: [3], query_start: secondsAgo(20) }),
]
const { longestBlockedQuery } = getConnectionMetrics(activities)
expect(longestBlockedQuery?.activity.pid).toBe(2)
expect(longestBlockedQuery?.duration).toBe(20)
})
})
describe('idle in transaction', () => {
it('does not flag idle-in-transaction queries under the threshold (10s)', () => {
const activities = [
activity({ pid: 1, state: 'idle in transaction', transaction_start: secondsAgo(5) }),
]
expect(getConnectionMetrics(activities).idleInTransactionQueries).toEqual([])
})
it('flags idle-in-transaction (and aborted) queries over the threshold (10s)', () => {
const activities = [
activity({ pid: 1, state: 'idle in transaction', transaction_start: secondsAgo(11) }),
activity({
pid: 2,
state: 'idle in transaction (aborted)',
transaction_start: secondsAgo(11),
}),
activity({ pid: 3, state: 'active', query_start: secondsAgo(11) }),
]
const { idleInTransactionQueries } = getConnectionMetrics(activities)
expect(idleInTransactionQueries.map((a) => a.pid)).toEqual([1, 2])
})
})
describe('longest running query', () => {
it('ignores states outside active/idle-in-transaction', () => {
const activities = [activity({ pid: 1, state: 'idle', state_change: secondsAgo(1000) })]
expect(getConnectionMetrics(activities).longestRunningQuery).toBe(null)
})
it('picks the longest-running query among active/idle-in-transaction states', () => {
const activities = [
activity({ pid: 1, state: 'active', query_start: secondsAgo(5) }),
activity({
pid: 2,
state: 'idle in transaction',
transaction_start: secondsAgo(15),
}),
]
const { longestRunningQuery } = getConnectionMetrics(activities)
expect(longestRunningQuery?.activity.pid).toBe(2)
expect(longestRunningQuery?.duration).toBe(15)
})
it('warns for an active query past the active threshold (30s)', () => {
const activities = [activity({ pid: 1, state: 'active', query_start: secondsAgo(31) })]
expect(getConnectionMetrics(activities).warnLongestRunningQuery).toBe(true)
})
it('does not warn for an active query under the active threshold (30s)', () => {
const activities = [activity({ pid: 1, state: 'active', query_start: secondsAgo(29) })]
expect(getConnectionMetrics(activities).warnLongestRunningQuery).toBe(false)
})
it('warns for an idle-in-transaction query past the shorter idle threshold (10s)', () => {
const activities = [
activity({ pid: 1, state: 'idle in transaction', transaction_start: secondsAgo(11) }),
]
expect(getConnectionMetrics(activities).warnLongestRunningQuery).toBe(true)
})
})
describe('top blocker', () => {
it('picks the pid blocking the most other queries', () => {
const activities = [
activity({ pid: 1, blocked_by: [] }),
activity({ pid: 2, blocked_by: [1] }),
activity({ pid: 3, blocked_by: [1] }),
activity({ pid: 4, blocked_by: [1] }),
activity({ pid: 5, blocked_by: [2] }),
]
const { queryBlockingTheMostQueries } = getConnectionMetrics(activities)
expect(queryBlockingTheMostQueries?.activity.pid).toBe(1)
expect(queryBlockingTheMostQueries?.count).toBe(4)
})
it('counts transitively - a longer block chain outweighs several short ones', () => {
const activities = [
activity({ pid: 1, blocked_by: [2] }),
activity({ pid: 2, blocked_by: [3] }),
activity({ pid: 3, blocked_by: [] }),
activity({ pid: 4, blocked_by: [5] }),
activity({ pid: 5, blocked_by: [] }),
]
const { queryBlockingTheMostQueries } = getConnectionMetrics(activities)
expect(queryBlockingTheMostQueries?.activity.pid).toBe(3)
expect(queryBlockingTheMostQueries?.count).toBe(2)
})
it('counts a diamond-shaped block pattern once, not per incoming path', () => {
// root blocks both p1 and p2 directly, and both p1 and p2 block w - w must only count once
const activities = [
activity({ pid: 0, blocked_by: [] }),
activity({ pid: 1, blocked_by: [0] }),
activity({ pid: 2, blocked_by: [0] }),
activity({ pid: 3, blocked_by: [1, 2] }),
]
const { queryBlockingTheMostQueries } = getConnectionMetrics(activities)
expect(queryBlockingTheMostQueries?.activity.pid).toBe(0)
expect(queryBlockingTheMostQueries?.count).toBe(3)
})
it('does not warn when the top blocker is under the threshold (3)', () => {
const activities = [
activity({ pid: 1, blocked_by: [] }),
activity({ pid: 2, blocked_by: [1] }),
]
expect(getConnectionMetrics(activities).warnTopBlocker).toBe(false)
})
it('warns once the top blocker meets the threshold (3)', () => {
const activities = [
activity({ pid: 1, blocked_by: [] }),
activity({ pid: 2, blocked_by: [1] }),
activity({ pid: 3, blocked_by: [1] }),
activity({ pid: 4, blocked_by: [1] }),
]
expect(getConnectionMetrics(activities).warnTopBlocker).toBe(true)
})
})
})
describe('getBlockChain', () => {
it('returns just the pid when it is not blocked', () => {
const activities = [activity({ pid: 1, blocked_by: [] })]
expect(getBlockChain(1, activities)).toEqual([1])
})
it('walks blocked_by up to the root, nearest first', () => {
const activities = [
activity({ pid: 1, blocked_by: [2] }),
activity({ pid: 2, blocked_by: [3] }),
activity({ pid: 3, blocked_by: [] }),
]
expect(getBlockChain(1, activities)).toEqual([1, 2, 3])
})
it('only follows the first blocker when blocked by multiple pids', () => {
const activities = [
activity({ pid: 1, blocked_by: [2, 3] }),
activity({ pid: 2, blocked_by: [] }),
activity({ pid: 3, blocked_by: [] }),
]
expect(getBlockChain(1, activities)).toEqual([1, 2])
})
it('stops rather than looping on a cycle', () => {
const activities = [
activity({ pid: 1, blocked_by: [2] }),
activity({ pid: 2, blocked_by: [1] }),
]
expect(getBlockChain(1, activities)).toEqual([1, 2])
})
it('includes a blocker pid even if its own activity record is missing', () => {
const activities = [activity({ pid: 1, blocked_by: [99] })]
expect(getBlockChain(1, activities)).toEqual([1, 99])
})
})
describe('getBlockingChain', () => {
it('returns an empty chain when nothing is blocked by the root', () => {
const activities = [activity({ pid: 1, blocked_by: [] })]
expect(getBlockingChain(1, activities)).toEqual([])
})
it('walks forward from the root, nearest waiter first', () => {
const activities = [
activity({ pid: 1, blocked_by: [2] }),
activity({ pid: 2, blocked_by: [3] }),
activity({ pid: 3, blocked_by: [] }),
]
expect(getBlockingChain(3, activities)).toEqual([2, 1])
})
it('does not require the root pid to have its own activity record', () => {
const activities = [activity({ pid: 2, blocked_by: [1] })]
expect(getBlockingChain(1, activities)).toEqual([2])
})
it('stops rather than looping on a cycle', () => {
const activities = [
activity({ pid: 2, blocked_by: [1] }),
activity({ pid: 3, blocked_by: [2] }),
activity({ pid: 1, blocked_by: [3] }), // would cycle back to the root
]
expect(getBlockingChain(1, activities)).toEqual([2, 3])
})
it('only follows one branch when the root has multiple direct waiters', () => {
const activities = [
activity({ pid: 2, blocked_by: [1] }),
activity({ pid: 3, blocked_by: [1] }),
]
expect(getBlockingChain(1, activities)).toEqual([2])
})
})