diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx b/apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx index 24822f519a2..a5a85b0478a 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx @@ -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, }) diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.test.ts b/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.test.ts index dc7994b097f..bef91c2c0ae 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.test.ts +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.test.ts @@ -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, diff --git a/apps/studio/data/database/activity-query.ts b/apps/studio/data/database/activity-query.ts index 447f0f62658..c244c120bfa 100644 --- a/apps/studio/data/database/activity-query.ts +++ b/apps/studio/data/database/activity-query.ts @@ -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 diff --git a/apps/studio/data/sql/cancel-query-mutation.ts b/apps/studio/data/sql/cancel-query-mutation.ts index e0fa00a9a16..ba23e590d41 100644 --- a/apps/studio/data/sql/cancel-query-mutation.ts +++ b/apps/studio/data/sql/cancel-query-mutation.ts @@ -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 } diff --git a/apps/studio/data/sql/terminate-session-mutation.ts b/apps/studio/data/sql/terminate-session-mutation.ts index e90b35fad7a..e5daf093fd1 100644 --- a/apps/studio/data/sql/terminate-session-mutation.ts +++ b/apps/studio/data/sql/terminate-session-mutation.ts @@ -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 } diff --git a/packages/pg-meta/src/sql/studio/database/activity.ts b/packages/pg-meta/src/sql/studio/database/activity.ts index 6059b36d377..56e1071d9a0 100644 --- a/packages/pg-meta/src/sql/studio/database/activity.ts +++ b/packages/pg-meta/src/sql/studio/database/activity.ts @@ -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, diff --git a/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts b/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts index 0ff2dd93154..de2c44c7a3c 100644 --- a/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts +++ b/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts @@ -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``} +` }