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 -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-08-12 10:14:59 +07:00
1 parent cdfb5b310f
commit 75b90c5de1
7 files changed
+56 -9

No files matched your search

@@ -158,6 +158,7 @@ export const ActivityRow = ({
try {
await cancelQuery({
pid: activity.pid,
backendStart: activity.backend_start,
projectRef: project?.ref,
connectionString: project?.connectionString,
})
@@ -183,6 +184,7 @@ export const ActivityRow = ({
try {
await terminateSession({
pid: activity.pid,
backendStart: activity.backend_start,
projectRef: project?.ref,
connectionString: project?.connectionString,
})
@@ -31,6 +31,7 @@ const activity = (overrides: ActivityOverrides = {}): DatabaseActivity => ({
application_name: 'test',
blocked_by: [],
query: 'select 1',
backend_start: NOW,
query_start: null,
transaction_start: null,
state_change: null,
@@ -67,6 +67,7 @@ export type DatabaseActivity = {
application_name: string
blocked_by: number[]
query: string | null
backend_start: string
query_start: string | null
transaction_start: string | null
state_change: string | null
+15 -3
View File
@@ -4,22 +4,34 @@ import { toast } from 'sonner'
import { sqlKeys } from './keys'
import { executeSql } from '@/data/sql/execute-sql-mutation'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
import { ResponseError, type UseCustomMutationOptions } from '@/types'
type QueryCancelVariables = {
pid: number
/** Pass the pid's last-known backend_start to guard against a reused pid matching an unrelated session */
backendStart?: string
projectRef?: string
connectionString?: string | null
}
async function cancelQuery({ pid, projectRef, connectionString }: QueryCancelVariables) {
const sql = getCancelQuerySQL({ pid })
async function cancelQuery({
pid,
backendStart,
projectRef,
connectionString,
}: QueryCancelVariables) {
const sql = getCancelQuerySQL({ pid, backendStart })
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['cancel-query'],
})
if (backendStart !== undefined && result.length === 0) {
throw new ResponseError(
`Session (PID: ${pid}) has already changed since this list was loaded. Refresh and try again.`
)
}
return result
}
@@ -4,26 +4,34 @@ import { toast } from 'sonner'
import { sqlKeys } from './keys'
import { executeSql } from '@/data/sql/execute-sql-mutation'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
import { ResponseError, type UseCustomMutationOptions } from '@/types'
type SessionTerminateVariables = {
pid: number
/** Pass the pid's last-known backend_start to guard against a reused pid matching an unrelated session */
backendStart?: string
projectRef?: string
connectionString?: string | null
}
export async function terminateSession({
pid,
backendStart,
projectRef,
connectionString,
}: SessionTerminateVariables) {
const sql = getTerminateSessionSQL({ pid })
const sql = getTerminateSessionSQL({ pid, backendStart })
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['terminate-session'],
})
if (backendStart !== undefined && result.length === 0) {
throw new ResponseError(
`Session (PID: ${pid}) has already changed since this list was loaded. Refresh and try again.`
)
}
return result
}
@@ -10,6 +10,7 @@ select
a.query,
a.wait_event_type,
a.wait_event,
a.backend_start,
a.xact_start as transaction_start,
a.query_start,
a.state_change,
@@ -1,9 +1,31 @@
import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format'
export const getCancelQuerySQL = ({ pid }: { pid: number }): SafeSqlFragment => {
return safeSql`select pg_cancel_backend(${literal(pid)})`
export const getCancelQuerySQL = ({
pid,
backendStart,
}: {
pid: number
backendStart?: string
}): SafeSqlFragment => {
return safeSql`
select pg_cancel_backend(pid) as cancelled
from pg_stat_activity
where pid = ${literal(pid)}
${backendStart ? safeSql`and backend_start = ${literal(backendStart)}::timestamptz` : safeSql``}
`
}
export const getTerminateSessionSQL = ({ pid }: { pid: number }): SafeSqlFragment => {
return safeSql`select pg_terminate_backend(${literal(pid)})`
export const getTerminateSessionSQL = ({
pid,
backendStart,
}: {
pid: number
backendStart?: string
}): SafeSqlFragment => {
return safeSql`
select pg_terminate_backend(pid) as terminated
from pg_stat_activity
where pid = ${literal(pid)}
${backendStart ? safeSql`and backend_start = ${literal(backendStart)}::timestamptz` : safeSql``}
`
}