fix(process-list): cancel queries without closing sessions
Closes #3967
This commit is contained in:
1 parent
396ebf8393
commit
4106a8ce22
15 files changed
+189
-165
No files matched your search
@@ -42,8 +42,8 @@ const autoRefresh = ref(false);
|
||||
const intervalSeconds = ref(DEFAULT_REFRESH_SECONDS);
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
const killTarget = ref<ProcessRow | null>(null);
|
||||
const killing = ref(false);
|
||||
const cancelTarget = ref<ProcessRow | null>(null);
|
||||
const canceling = ref(false);
|
||||
const fallbackListSql = ref<string | null>(null);
|
||||
|
||||
// Full-text preview for long cells (SQL statement / info), opened by clicking them.
|
||||
@@ -137,20 +137,20 @@ function isOwnSession(row: ProcessRow): boolean {
|
||||
return ownSessionId.value !== null && row.id === ownSessionId.value;
|
||||
}
|
||||
|
||||
function requestKill(row: ProcessRow) {
|
||||
function requestCancel(row: ProcessRow) {
|
||||
if (isOwnSession(row)) return;
|
||||
killTarget.value = row;
|
||||
cancelTarget.value = row;
|
||||
}
|
||||
|
||||
async function confirmKill() {
|
||||
const target = killTarget.value;
|
||||
async function confirmCancel() {
|
||||
const target = cancelTarget.value;
|
||||
const activeDriver = driver.value;
|
||||
if (!target || !activeDriver) return;
|
||||
killing.value = true;
|
||||
canceling.value = true;
|
||||
try {
|
||||
const killSql = activeDriver.buildKillSql(target.id);
|
||||
let usedFallbackKillSql = false;
|
||||
const executeKillSql = async (sql: string) => {
|
||||
const cancelSql = activeDriver.buildCancelQuerySql(target.id);
|
||||
let usedFallbackCancelSql = false;
|
||||
const executeCancelSql = async (sql: string) => {
|
||||
const results = await api.executeMulti(props.connection.id, "", sql, undefined, undefined, { maxRows: 1 });
|
||||
const executionError = processListExecutionError(results);
|
||||
if (executionError) throw new Error(executionError);
|
||||
@@ -159,28 +159,28 @@ async function confirmKill() {
|
||||
const result = await executeWithProductionSqlGuard({
|
||||
connection: props.connection,
|
||||
database: "",
|
||||
sql: killSql,
|
||||
sql: cancelSql,
|
||||
source: t("production.sourceAdmin"),
|
||||
execute: async () => {
|
||||
try {
|
||||
return await executeKillSql(killSql);
|
||||
return await executeCancelSql(cancelSql);
|
||||
} catch (error) {
|
||||
if (!activeDriver.buildFallbackKillSql || !activeDriver.shouldUseFallbackKillSql?.(error)) throw error;
|
||||
usedFallbackKillSql = true;
|
||||
return executeKillSql(activeDriver.buildFallbackKillSql(target.id));
|
||||
if (!activeDriver.buildFallbackCancelQuerySql || !activeDriver.shouldUseFallbackCancelQuerySql?.(error)) throw error;
|
||||
usedFallbackCancelSql = true;
|
||||
return executeCancelSql(activeDriver.buildFallbackCancelQuerySql(target.id));
|
||||
}
|
||||
},
|
||||
});
|
||||
if (result === undefined) return;
|
||||
const killResultError = usedFallbackKillSql ? activeDriver.fallbackKillResultError?.(result) : activeDriver.killResultError?.(result);
|
||||
if (killResultError) throw new Error(killResultError);
|
||||
const cancelResultError = usedFallbackCancelSql ? activeDriver.fallbackCancelQueryResultError?.(result) : activeDriver.cancelQueryResultError?.(result);
|
||||
if (cancelResultError) throw new Error(cancelResultError);
|
||||
toast(t("processList.killSuccess", { id: target.id }), 2500);
|
||||
killTarget.value = null;
|
||||
cancelTarget.value = null;
|
||||
await load({ silent: true });
|
||||
} catch (error: any) {
|
||||
toast(t("processList.killFailed", { message: error?.message || String(error) }), 5000);
|
||||
} finally {
|
||||
killing.value = false;
|
||||
canceling.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ onBeforeUnmount(stopTimer);
|
||||
class="h-6 gap-1 px-1.5 text-[11px] text-destructive hover:bg-destructive/10 hover:text-destructive disabled:opacity-40"
|
||||
:disabled="isOwnSession(row)"
|
||||
:title="isOwnSession(row) ? t('processList.cannotKillSelf') : t('processList.kill')"
|
||||
@click="requestKill(row)"
|
||||
@click="requestCancel(row)"
|
||||
>
|
||||
<Ban class="h-3.5 w-3.5" />
|
||||
{{ t("processList.kill") }}
|
||||
@@ -316,10 +316,10 @@ onBeforeUnmount(stopTimer);
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
:open="killTarget !== null"
|
||||
:open="cancelTarget !== null"
|
||||
@update:open="
|
||||
(open) => {
|
||||
if (!open) killTarget = null;
|
||||
if (!open) cancelTarget = null;
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -330,13 +330,13 @@ onBeforeUnmount(stopTimer);
|
||||
{{ t("processList.killTitle") }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p v-if="killTarget" class="text-sm text-muted-foreground">
|
||||
{{ t("processList.killConfirm", { id: killTarget.id, user: killTarget.user }) }}
|
||||
<p v-if="cancelTarget" class="text-sm text-muted-foreground">
|
||||
{{ t("processList.killConfirm", { id: cancelTarget.id, user: cancelTarget.user }) }}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="killTarget = null">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button variant="destructive" :disabled="killing" @click="confirmKill">
|
||||
<Loader2 v-if="killing" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Button variant="outline" @click="cancelTarget = null">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button variant="destructive" :disabled="canceling" @click="confirmCancel">
|
||||
<Loader2 v-if="canceling" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("processList.kill") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -2956,13 +2956,13 @@ export default {
|
||||
colQuery: "Query",
|
||||
colActions: "Actions",
|
||||
self: "you",
|
||||
kill: "Kill",
|
||||
cannotKillSelf: "You cannot kill your own session",
|
||||
kill: "Cancel query",
|
||||
cannotKillSelf: "You cannot cancel a query on your own session",
|
||||
empty: "No active sessions.",
|
||||
killTitle: "Kill session",
|
||||
killConfirm: "Kill session {id} ({user})? Its current statement will be aborted and the connection closed.",
|
||||
killSuccess: "Session {id} killed",
|
||||
killFailed: "Failed to kill session: {message}",
|
||||
killTitle: "Cancel running query",
|
||||
killConfirm: "Cancel the running query for session {id} ({user})? The session will remain connected.",
|
||||
killSuccess: "Canceled the running query for session {id}",
|
||||
killFailed: "Failed to cancel query: {message}",
|
||||
previewTitle: "Statement",
|
||||
copy: "Copy",
|
||||
copied: "Copied",
|
||||
|
||||
@@ -7279,13 +7279,13 @@ export default withEnglishFallback({
|
||||
colInfo: "SQL",
|
||||
colActions: "Acciones",
|
||||
self: "Actual",
|
||||
kill: "Terminar",
|
||||
cannotKillSelf: "No se puede terminar la sesión actual",
|
||||
kill: "Cancelar consulta",
|
||||
cannotKillSelf: "No se puede cancelar una consulta en la sesión actual",
|
||||
empty: "No hay sesiones activas.",
|
||||
killTitle: "Terminar sesión",
|
||||
killConfirm: "¿Está seguro de que desea terminar la sesión {id} ({user})? La sentencia actual se cancelará y la conexión se cerrará.",
|
||||
killSuccess: "Sesión {id} terminada",
|
||||
killFailed: "Error al terminar la sesión: {message}",
|
||||
killTitle: "Cancelar consulta en ejecución",
|
||||
killConfirm: "¿Cancelar la consulta en ejecución de la sesión {id} ({user})? La sesión permanecerá conectada.",
|
||||
killSuccess: "Consulta en ejecución de la sesión {id} cancelada",
|
||||
killFailed: "Error al cancelar la consulta: {message}",
|
||||
colPid: "PID",
|
||||
colClient: "Cliente",
|
||||
colApp: "Aplicación",
|
||||
|
||||
@@ -7279,13 +7279,13 @@ export default withEnglishFallback({
|
||||
colInfo: "SQL",
|
||||
colActions: "Azioni",
|
||||
self: "Corrente",
|
||||
kill: "Termina",
|
||||
cannotKillSelf: "Impossibile terminare la sessione corrente",
|
||||
kill: "Annulla query",
|
||||
cannotKillSelf: "Impossibile annullare una query nella sessione corrente",
|
||||
empty: "Nessuna sessione attiva.",
|
||||
killTitle: "Termina sessione",
|
||||
killConfirm: "Sei sicuro di voler terminare la sessione {id} ({user})? La dichiarazione corrente verrà interrotta e la connessione verrà chiusa.",
|
||||
killSuccess: "Sessione {id} terminata",
|
||||
killFailed: "Terminazione sessione non riuscita: {message}",
|
||||
killTitle: "Annulla query in esecuzione",
|
||||
killConfirm: "Annullare la query in esecuzione della sessione {id} ({user})? La sessione resterà connessa.",
|
||||
killSuccess: "Query in esecuzione della sessione {id} annullata",
|
||||
killFailed: "Impossibile annullare la query: {message}",
|
||||
colPid: "PID",
|
||||
colClient: "Client",
|
||||
colApp: "Applicazione",
|
||||
|
||||
@@ -7332,13 +7332,13 @@ export default withEnglishFallback({
|
||||
colInfo: "SQL",
|
||||
colActions: "操作",
|
||||
self: "現在",
|
||||
kill: "強制終了",
|
||||
cannotKillSelf: "現在のセッションは強制終了できません",
|
||||
kill: "クエリをキャンセル",
|
||||
cannotKillSelf: "現在のセッションのクエリはキャンセルできません",
|
||||
empty: "アクティブなセッションはありません。",
|
||||
killTitle: "セッションの強制終了",
|
||||
killConfirm: "セッション {id}({user})を強制終了しますか?現在のステートメントは中断され、接続が閉じられます。",
|
||||
killSuccess: "セッション {id} を強制終了しました。",
|
||||
killFailed: "セッションの強制終了に失敗しました:{message}",
|
||||
killTitle: "実行中のクエリをキャンセル",
|
||||
killConfirm: "セッション {id}({user})で実行中のクエリをキャンセルしますか?セッションの接続は維持されます。",
|
||||
killSuccess: "セッション {id} で実行中のクエリをキャンセルしました。",
|
||||
killFailed: "クエリのキャンセルに失敗しました:{message}",
|
||||
colPid: "PID",
|
||||
colClient: "クライアント",
|
||||
colApp: "アプリケーション",
|
||||
|
||||
@@ -2806,13 +2806,13 @@ export default withEnglishFallback({
|
||||
colQuery: "쿼리",
|
||||
colActions: "작업",
|
||||
self: "나",
|
||||
kill: "종료",
|
||||
cannotKillSelf: "자신의 세션은 종료할 수 없습니다",
|
||||
kill: "쿼리 취소",
|
||||
cannotKillSelf: "현재 세션의 쿼리는 취소할 수 없습니다",
|
||||
empty: "활성 세션이 없습니다.",
|
||||
killTitle: "세션 종료",
|
||||
killConfirm: "세션 {id} ({user})을(를) 종료하시겠습니까? 현재 구문이 중단되고 연결이 닫힙니다.",
|
||||
killSuccess: "세션 {id}을(를) 종료했습니다",
|
||||
killFailed: "세션 종료에 실패했습니다: {message}",
|
||||
killTitle: "실행 중인 쿼리 취소",
|
||||
killConfirm: "세션 {id} ({user})에서 실행 중인 쿼리를 취소하시겠습니까? 세션 연결은 유지됩니다.",
|
||||
killSuccess: "세션 {id}에서 실행 중인 쿼리를 취소했습니다",
|
||||
killFailed: "쿼리 취소에 실패했습니다: {message}",
|
||||
previewTitle: "구문",
|
||||
copy: "복사",
|
||||
copied: "복사했습니다",
|
||||
|
||||
@@ -7281,13 +7281,13 @@ export default withEnglishFallback({
|
||||
colInfo: "SQL",
|
||||
colActions: "Ações",
|
||||
self: "Atual",
|
||||
kill: "Encerrar",
|
||||
cannotKillSelf: "Não é possível encerrar a sessão atual",
|
||||
kill: "Cancelar consulta",
|
||||
cannotKillSelf: "Não é possível cancelar uma consulta na sessão atual",
|
||||
empty: "Nenhuma sessão ativa.",
|
||||
killTitle: "Encerrar sessão",
|
||||
killConfirm: "Tem certeza de que deseja encerrar a sessão {id} ({user})? Sua instrução atual será abortada e a conexão será fechada.",
|
||||
killSuccess: "Sessão {id} encerrada.",
|
||||
killFailed: "Falha ao encerrar a sessão: {message}",
|
||||
killTitle: "Cancelar consulta em execução",
|
||||
killConfirm: "Cancelar a consulta em execução da sessão {id} ({user})? A sessão permanecerá conectada.",
|
||||
killSuccess: "Consulta em execução da sessão {id} cancelada.",
|
||||
killFailed: "Falha ao cancelar a consulta: {message}",
|
||||
colPid: "PID",
|
||||
colClient: "Cliente",
|
||||
colApp: "Aplicativo",
|
||||
|
||||
@@ -2881,13 +2881,13 @@ export default withEnglishFallback({
|
||||
colQuery: "查询",
|
||||
colActions: "操作",
|
||||
self: "当前",
|
||||
kill: "终止",
|
||||
cannotKillSelf: "无法终止当前会话",
|
||||
kill: "取消查询",
|
||||
cannotKillSelf: "无法取消当前会话中的查询",
|
||||
empty: "没有活动会话。",
|
||||
killTitle: "终止会话",
|
||||
killConfirm: "确定终止会话 {id}({user})?其当前语句将被中止,连接将被关闭。",
|
||||
killSuccess: "已终止会话 {id}",
|
||||
killFailed: "终止会话失败:{message}",
|
||||
killTitle: "取消正在运行的查询",
|
||||
killConfirm: "确定取消会话 {id}({user})中正在运行的查询?该会话将保持连接。",
|
||||
killSuccess: "已取消会话 {id} 中正在运行的查询",
|
||||
killFailed: "取消查询失败:{message}",
|
||||
previewTitle: "语句",
|
||||
copy: "复制",
|
||||
copied: "已复制",
|
||||
|
||||
@@ -7272,13 +7272,13 @@ export default withEnglishFallback({
|
||||
colInfo: "SQL",
|
||||
colActions: "操作",
|
||||
self: "目前",
|
||||
kill: "終止",
|
||||
cannotKillSelf: "無法終止目前會話",
|
||||
kill: "取消查詢",
|
||||
cannotKillSelf: "無法取消目前會話中的查詢",
|
||||
empty: "沒有活動會話。",
|
||||
killTitle: "終止會話",
|
||||
killConfirm: "確定終止會話 {id}({user})?其目前的陳述式將被中止,連線將被關閉。",
|
||||
killSuccess: "已終止會話 {id}",
|
||||
killFailed: "終止會話失敗:{message}",
|
||||
killTitle: "取消執行中的查詢",
|
||||
killConfirm: "確定取消會話 {id}({user})中執行中的查詢?該會話將保持連線。",
|
||||
killSuccess: "已取消會話 {id} 中執行中的查詢",
|
||||
killFailed: "取消查詢失敗:{message}",
|
||||
colPid: "PID",
|
||||
colClient: "用戶端",
|
||||
colApp: "應用程式",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import { buildKillSql, clampInterval, createProcessListLoadCoordinator, mapProcessRows, processListExecutionError, processListSessionCount, supportsProcessList } from "@/lib/database/mysqlProcessList";
|
||||
import { buildCancelQuerySql, clampInterval, createProcessListLoadCoordinator, mapProcessRows, processListExecutionError, processListSessionCount, supportsProcessList } from "@/lib/database/mysqlProcessList";
|
||||
|
||||
function result(columns: string[], rows: (string | number | boolean | null)[][]): QueryResult {
|
||||
return { columns, rows, affected_rows: 0, execution_time_ms: 0 };
|
||||
@@ -35,15 +35,16 @@ describe("mapProcessRows", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildKillSql", () => {
|
||||
it("builds KILL CONNECTION for a valid id", () => {
|
||||
expect(buildKillSql(8213)).toBe("KILL CONNECTION 8213");
|
||||
describe("buildCancelQuerySql", () => {
|
||||
it("builds KILL QUERY for a valid id without disconnecting the session", () => {
|
||||
expect(buildCancelQuerySql(8213)).toBe("KILL QUERY 8213");
|
||||
});
|
||||
|
||||
it("rejects non-integer or negative ids", () => {
|
||||
expect(() => buildKillSql(1.5)).toThrow();
|
||||
expect(() => buildKillSql(-1)).toThrow();
|
||||
expect(() => buildKillSql(Number.NaN)).toThrow();
|
||||
it("rejects non-integer or nonpositive ids", () => {
|
||||
expect(() => buildCancelQuerySql(1.5)).toThrow();
|
||||
expect(() => buildCancelQuerySql(0)).toThrow();
|
||||
expect(() => buildCancelQuerySql(-1)).toThrow();
|
||||
expect(() => buildCancelQuerySql(Number.NaN)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import {
|
||||
buildKingbaseKillSql,
|
||||
buildKingbasePgKillSql,
|
||||
buildPgKillSql,
|
||||
buildKingbaseCancelQuerySql,
|
||||
buildKingbasePgCancelQuerySql,
|
||||
buildPgCancelQuerySql,
|
||||
isKingbaseOwnSessionCatalogCompatibilityError,
|
||||
isKingbaseProcessListCatalogCompatibilityError,
|
||||
isKingbaseTerminateCatalogCompatibilityError,
|
||||
isKingbaseCancelCatalogCompatibilityError,
|
||||
isPgProcessListCompatibilityError,
|
||||
KINGBASE_OWN_SESSION_SQL,
|
||||
KINGBASE_PG_OWN_SESSION_SQL,
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
mapPgProcessRows,
|
||||
OPENGAUSS_OWN_SESSION_SQL,
|
||||
OPENGAUSS_PROCESS_LIST_SQL,
|
||||
pgKillResultError,
|
||||
kingbaseCancelQueryResultError,
|
||||
kingbasePgCancelQueryResultError,
|
||||
pgCancelQueryResultError,
|
||||
PG_PROCESS_LIST_LEGACY_SQL,
|
||||
PG_PROCESS_LIST_SQL,
|
||||
} from "@/lib/database/postgresProcessList";
|
||||
@@ -56,22 +58,22 @@ describe("mapPgProcessRows", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPgKillSql", () => {
|
||||
it("builds pg_terminate_backend for a valid pid", () => {
|
||||
expect(buildPgKillSql(4211)).toBe("SELECT pg_terminate_backend(4211)");
|
||||
describe("buildPgCancelQuerySql", () => {
|
||||
it("builds pg_cancel_backend for a valid pid without disconnecting the session", () => {
|
||||
expect(buildPgCancelQuerySql(4211)).toBe("SELECT pg_cancel_backend(4211)");
|
||||
});
|
||||
|
||||
it("rejects non-integer, zero, or negative pids", () => {
|
||||
expect(() => buildPgKillSql(1.5)).toThrow();
|
||||
expect(() => buildPgKillSql(0)).toThrow();
|
||||
expect(() => buildPgKillSql(-1)).toThrow();
|
||||
expect(() => buildPgKillSql(Number.NaN)).toThrow();
|
||||
expect(() => buildPgCancelQuerySql(1.5)).toThrow();
|
||||
expect(() => buildPgCancelQuerySql(0)).toThrow();
|
||||
expect(() => buildPgCancelQuerySql(-1)).toThrow();
|
||||
expect(() => buildPgCancelQuerySql(Number.NaN)).toThrow();
|
||||
});
|
||||
|
||||
it("builds the KingbaseES sys_terminate_backend call with the same PID validation", () => {
|
||||
expect(buildKingbaseKillSql(4211)).toBe("SELECT sys_terminate_backend(4211)");
|
||||
expect(() => buildKingbaseKillSql(0)).toThrow();
|
||||
expect(() => buildKingbaseKillSql(1.5)).toThrow();
|
||||
it("builds the KingbaseES sys_cancel_backend call with the same PID validation", () => {
|
||||
expect(buildKingbaseCancelQuerySql(4211)).toBe("SELECT sys_cancel_backend(4211)");
|
||||
expect(() => buildKingbaseCancelQuerySql(0)).toThrow();
|
||||
expect(() => buildKingbaseCancelQuerySql(1.5)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +85,7 @@ describe("PostgreSQL-family process SQL", () => {
|
||||
expect(OPENGAUSS_PROCESS_LIST_SQL).toContain("FROM pg_catalog.pg_stat_activity");
|
||||
expect(OPENGAUSS_PROCESS_LIST_SQL).toContain("CASE WHEN waiting");
|
||||
expect(OPENGAUSS_PROCESS_LIST_SQL).not.toContain("wait_event_type");
|
||||
expect(driver?.buildKillSql(7)).toBe("SELECT pg_terminate_backend(7)");
|
||||
expect(driver?.buildCancelQuerySql(7)).toBe("SELECT pg_cancel_backend(7)");
|
||||
});
|
||||
|
||||
it("uses KingbaseES's sys catalog and session functions", () => {
|
||||
@@ -95,26 +97,26 @@ describe("PostgreSQL-family process SQL", () => {
|
||||
expect(KINGBASE_PROCESS_LIST_SQL).toContain("extract(epoch FROM CAST(CURRENT_TIMESTAMP AS TIMESTAMP))");
|
||||
expect(KINGBASE_PROCESS_LIST_SQL).toContain("extract(epoch FROM CAST(coalesce(query_start, xact_start, backend_start) AS TIMESTAMP))");
|
||||
expect(KINGBASE_PROCESS_LIST_SQL).not.toContain("CURRENT_TIMESTAMP - coalesce(query_start");
|
||||
expect(driver?.buildKillSql(7)).toBe("SELECT sys_terminate_backend(7)");
|
||||
expect(driver?.buildCancelQuerySql(7)).toBe("SELECT sys_cancel_backend(7)");
|
||||
});
|
||||
|
||||
it("provides pg_catalog fallbacks for pg-compatible KingbaseES servers", () => {
|
||||
const driver = resolveProcessListDriver("kingbase");
|
||||
expect(driver?.fallbackListSql).toBe(KINGBASE_PG_PROCESS_LIST_SQL);
|
||||
expect(driver?.fallbackOwnSessionSql).toBe(KINGBASE_PG_OWN_SESSION_SQL);
|
||||
expect(driver?.buildFallbackKillSql?.(7)).toBe("SELECT pg_terminate_backend(7)");
|
||||
expect(driver?.buildFallbackCancelQuerySql?.(7)).toBe("SELECT pg_cancel_backend(7)");
|
||||
expect(KINGBASE_PG_PROCESS_LIST_SQL).toContain("FROM pg_catalog.pg_stat_activity");
|
||||
expect(KINGBASE_PG_PROCESS_LIST_SQL).not.toContain("sys_catalog");
|
||||
expect(buildKingbasePgKillSql(4211)).toBe("SELECT pg_terminate_backend(4211)");
|
||||
expect(buildKingbasePgCancelQuerySql(4211)).toBe("SELECT pg_cancel_backend(4211)");
|
||||
});
|
||||
|
||||
it("retries only missing sys relation/function errors", () => {
|
||||
expect(isKingbaseProcessListCatalogCompatibilityError(new Error('relation "sys_catalog.sys_stat_activity" does not exist (SQLSTATE 42P01)'))).toBe(true);
|
||||
expect(isKingbaseOwnSessionCatalogCompatibilityError(Object.assign(new Error("function sys_backend_pid() does not exist"), { code: "42883" }))).toBe(true);
|
||||
expect(isKingbaseTerminateCatalogCompatibilityError(new Error("function sys_terminate_backend(integer) does not exist (SQLSTATE 42883)"))).toBe(true);
|
||||
expect(isKingbaseCancelCatalogCompatibilityError(new Error("function sys_cancel_backend(integer) does not exist (SQLSTATE 42883)"))).toBe(true);
|
||||
expect(isKingbaseProcessListCatalogCompatibilityError(Object.assign(new Error("permission denied for relation sys_catalog.sys_stat_activity"), { code: "42501" }))).toBe(false);
|
||||
expect(isKingbaseOwnSessionCatalogCompatibilityError(new Error("authentication failed for sys_backend_pid"))).toBe(false);
|
||||
expect(isKingbaseTerminateCatalogCompatibilityError(new Error("connection refused"))).toBe(false);
|
||||
expect(isKingbaseCancelCatalogCompatibilityError(new Error("connection refused"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,11 +130,13 @@ describe("Postgres compatibility", () => {
|
||||
expect(isPgProcessListCompatibilityError(new Error("permission denied for view pg_stat_activity"))).toBe(false);
|
||||
});
|
||||
|
||||
it("requires pg_terminate_backend to confirm termination", () => {
|
||||
expect(pgKillResultError([result(["pg_terminate_backend"], [[true]])])).toBeNull();
|
||||
expect(pgKillResultError([result(["pg_terminate_backend"], [["t"]])])).toBeNull();
|
||||
expect(pgKillResultError([result(["pg_terminate_backend"], [[false]])])).toContain("did not terminate");
|
||||
expect(pgKillResultError([result(["pg_terminate_backend"], [])])).toContain("did not terminate");
|
||||
it("requires cancellation functions to confirm that a running query was canceled", () => {
|
||||
expect(pgCancelQueryResultError([result(["pg_cancel_backend"], [[true]])])).toBeNull();
|
||||
expect(pgCancelQueryResultError([result(["pg_cancel_backend"], [["t"]])])).toBeNull();
|
||||
expect(pgCancelQueryResultError([result(["pg_cancel_backend"], [[false]])])).toContain("did not cancel");
|
||||
expect(pgCancelQueryResultError([result(["pg_cancel_backend"], [])])).toContain("did not cancel");
|
||||
expect(kingbaseCancelQueryResultError([result(["sys_cancel_backend"], [[false]])])).toContain("did not cancel");
|
||||
expect(kingbasePgCancelQueryResultError([result(["pg_cancel_backend"], [[true]])])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,11 +144,11 @@ describe("resolveProcessListDriver", () => {
|
||||
it("routes MySQL and Postgres engines to distinct drivers", () => {
|
||||
const mysql = resolveProcessListDriver("mysql");
|
||||
const postgres = resolveProcessListDriver("postgres");
|
||||
expect(mysql?.buildKillSql(7)).toBe("KILL CONNECTION 7");
|
||||
expect(postgres?.buildKillSql(7)).toBe("SELECT pg_terminate_backend(7)");
|
||||
expect(mysql?.buildCancelQuerySql(7)).toBe("KILL QUERY 7");
|
||||
expect(postgres?.buildCancelQuerySql(7)).toBe("SELECT pg_cancel_backend(7)");
|
||||
expect(postgres?.fallbackListSql).toBe(PG_PROCESS_LIST_LEGACY_SQL);
|
||||
expect(postgres?.shouldUseFallbackListSql?.(new Error('column "wait_event" does not exist'))).toBe(true);
|
||||
expect(postgres?.killResultError?.([result(["pg_terminate_backend"], [[false]])])).toContain("did not terminate");
|
||||
expect(postgres?.cancelQueryResultError?.([result(["pg_cancel_backend"], [[false]])])).toContain("did not cancel");
|
||||
expect(resolveProcessListDriver("sqlite")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -190,7 +194,7 @@ describe("connectionSupportsProcessList", () => {
|
||||
|
||||
describe("resolveProcessListDriverForConnection", () => {
|
||||
it("returns the engine driver for a JDBC MySQL connection and null for lookalikes", () => {
|
||||
expect(resolveProcessListDriverForConnection(conn({ db_type: "jdbc", connection_string: "jdbc:mysql://db/app" }))?.buildKillSql(9)).toBe("KILL CONNECTION 9");
|
||||
expect(resolveProcessListDriverForConnection(conn({ db_type: "jdbc", connection_string: "jdbc:mysql://db/app" }))?.buildCancelQuerySql(9)).toBe("KILL QUERY 9");
|
||||
expect(resolveProcessListDriverForConnection(conn({ db_type: "jdbc", connection_string: "jdbc:hive2://hs2/default" }))).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import type { DatabaseType, QueryResult } from "@/types/database";
|
||||
|
||||
/**
|
||||
* Engines that speak the MySQL protocol and support `SHOW FULL PROCESSLIST` /
|
||||
* `KILL CONNECTION`. MariaDB, TiDB, and OceanBase ride the `mysql` dbType via a
|
||||
* `KILL QUERY`. MariaDB, TiDB, and OceanBase ride the `mysql` dbType via a
|
||||
* driver profile, so they are covered by the `"mysql"` entry.
|
||||
*/
|
||||
const PROCESS_LIST_DB_TYPES = new Set<DatabaseType>(["mysql"]);
|
||||
@@ -127,14 +127,14 @@ export function mapProcessRows(result: QueryResult | null | undefined): ProcessR
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `KILL CONNECTION <id>` statement. `id` must be a finite integer; it
|
||||
* Build the `KILL QUERY <id>` statement. `id` must be a finite positive integer; it
|
||||
* is validated (never interpolated as free text) so there is no injection path.
|
||||
*/
|
||||
export function buildKillSql(id: number): string {
|
||||
if (!Number.isInteger(id) || id < 0) {
|
||||
export function buildCancelQuerySql(id: number): string {
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
throw new Error(`Invalid session id: ${id}`);
|
||||
}
|
||||
return `KILL CONNECTION ${id}`;
|
||||
return `KILL QUERY ${id}`;
|
||||
}
|
||||
|
||||
/** Clamp a user-entered refresh interval to a safe integer range of seconds. */
|
||||
|
||||
@@ -149,7 +149,7 @@ export function mapPgProcessRows(result: QueryResult | null | undefined): PgProc
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `SELECT pg_terminate_backend(<pid>)` statement. `pid` is validated as a
|
||||
* Build a `SELECT pg_cancel_backend(<pid>)` statement. `pid` is validated as a
|
||||
* finite positive integer (never interpolated as free text) so there is no
|
||||
* injection path.
|
||||
*/
|
||||
@@ -159,39 +159,39 @@ function validateBackendPid(pid: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPgKillSql(pid: number): string {
|
||||
export function buildPgCancelQuerySql(pid: number): string {
|
||||
validateBackendPid(pid);
|
||||
return `SELECT pg_terminate_backend(${pid})`;
|
||||
return `SELECT pg_cancel_backend(${pid})`;
|
||||
}
|
||||
|
||||
export function buildKingbaseKillSql(pid: number): string {
|
||||
export function buildKingbaseCancelQuerySql(pid: number): string {
|
||||
validateBackendPid(pid);
|
||||
return `SELECT sys_terminate_backend(${pid})`;
|
||||
return `SELECT sys_cancel_backend(${pid})`;
|
||||
}
|
||||
|
||||
export function buildKingbasePgKillSql(pid: number): string {
|
||||
export function buildKingbasePgCancelQuerySql(pid: number): string {
|
||||
validateBackendPid(pid);
|
||||
return `SELECT pg_terminate_backend(${pid})`;
|
||||
return `SELECT pg_cancel_backend(${pid})`;
|
||||
}
|
||||
|
||||
/** Return an error when PostgreSQL declines to terminate the target backend. */
|
||||
export function pgKillResultError(results: QueryResult[]): string | null {
|
||||
return backendKillResultError(results, "pg_terminate_backend");
|
||||
/** Return an error when PostgreSQL reports that no running query was canceled. */
|
||||
export function pgCancelQueryResultError(results: QueryResult[]): string | null {
|
||||
return backendCancelQueryResultError(results, "pg_cancel_backend");
|
||||
}
|
||||
|
||||
export function kingbaseKillResultError(results: QueryResult[]): string | null {
|
||||
return backendKillResultError(results, "sys_terminate_backend");
|
||||
export function kingbaseCancelQueryResultError(results: QueryResult[]): string | null {
|
||||
return backendCancelQueryResultError(results, "sys_cancel_backend");
|
||||
}
|
||||
|
||||
export function kingbasePgKillResultError(results: QueryResult[]): string | null {
|
||||
return backendKillResultError(results, "pg_terminate_backend");
|
||||
export function kingbasePgCancelQueryResultError(results: QueryResult[]): string | null {
|
||||
return backendCancelQueryResultError(results, "pg_cancel_backend");
|
||||
}
|
||||
|
||||
function backendKillResultError(results: QueryResult[], functionName: string): string | null {
|
||||
function backendCancelQueryResultError(results: QueryResult[], functionName: string): string | null {
|
||||
const result = results.find((item) => item.execution_error !== true);
|
||||
const value = result?.rows?.[0]?.[0];
|
||||
if (value === true || value === 1 || String(value).toLowerCase() === "t" || String(value).toLowerCase() === "true") return null;
|
||||
return `${functionName} did not terminate the backend`;
|
||||
return `${functionName} did not cancel a running query`;
|
||||
}
|
||||
|
||||
/** Detect the undefined-column failure produced by pre-9.6 pg_stat_activity. */
|
||||
@@ -210,6 +210,6 @@ export function isKingbaseOwnSessionCatalogCompatibilityError(error: unknown): b
|
||||
return isMissingKingbaseSysFunction(error, ["sys_backend_pid"]);
|
||||
}
|
||||
|
||||
export function isKingbaseTerminateCatalogCompatibilityError(error: unknown): boolean {
|
||||
return isMissingKingbaseSysFunction(error, ["sys_terminate_backend"]);
|
||||
export function isKingbaseCancelCatalogCompatibilityError(error: unknown): boolean {
|
||||
return isMissingKingbaseSysFunction(error, ["sys_cancel_backend"]);
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { ConnectionConfig, DatabaseType, QueryResult } from "@/types/database";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { buildKillSql as buildMysqlKillSql, mapProcessRows as mapMysqlProcessRows, PROCESS_LIST_SQL as MYSQL_PROCESS_LIST_SQL, supportsProcessList as supportsMysqlProcessList } from "./mysqlProcessList";
|
||||
import { buildCancelQuerySql as buildMysqlCancelQuerySql, mapProcessRows as mapMysqlProcessRows, PROCESS_LIST_SQL as MYSQL_PROCESS_LIST_SQL, supportsProcessList as supportsMysqlProcessList } from "./mysqlProcessList";
|
||||
import {
|
||||
buildKingbaseKillSql,
|
||||
buildKingbasePgKillSql,
|
||||
buildPgKillSql,
|
||||
buildKingbaseCancelQuerySql,
|
||||
buildKingbasePgCancelQuerySql,
|
||||
buildPgCancelQuerySql,
|
||||
isKingbaseOwnSessionCatalogCompatibilityError,
|
||||
isKingbaseProcessListCatalogCompatibilityError,
|
||||
isKingbaseTerminateCatalogCompatibilityError,
|
||||
isKingbaseCancelCatalogCompatibilityError,
|
||||
isPgProcessListCompatibilityError,
|
||||
kingbaseKillResultError,
|
||||
kingbasePgKillResultError,
|
||||
kingbaseCancelQueryResultError,
|
||||
kingbasePgCancelQueryResultError,
|
||||
KINGBASE_OWN_SESSION_SQL,
|
||||
KINGBASE_PG_OWN_SESSION_SQL,
|
||||
KINGBASE_PG_PROCESS_LIST_SQL,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
mapPgProcessRows,
|
||||
OPENGAUSS_OWN_SESSION_SQL,
|
||||
OPENGAUSS_PROCESS_LIST_SQL,
|
||||
pgKillResultError,
|
||||
pgCancelQueryResultError,
|
||||
PG_OWN_SESSION_SQL,
|
||||
PG_PROCESS_LIST_LEGACY_SQL,
|
||||
PG_PROCESS_LIST_SQL,
|
||||
@@ -67,16 +67,16 @@ export interface ProcessListDriver {
|
||||
maxRows: number;
|
||||
/** Map a raw list result into typed rows. */
|
||||
mapRows(result: QueryResult | null | undefined): ProcessRow[];
|
||||
/** Build the validated statement that kills the given session id. */
|
||||
buildKillSql(id: number): string;
|
||||
/** Build the compatibility statement used when the primary kill function is unavailable. */
|
||||
buildFallbackKillSql?(id: number): string;
|
||||
/** Restrict kill fallback attempts to known compatibility failures. */
|
||||
shouldUseFallbackKillSql?(error: unknown): boolean;
|
||||
/** Validate any engine-specific success value returned by the kill statement. */
|
||||
killResultError?(results: QueryResult[]): string | null;
|
||||
/** Validate the success value returned by the compatibility kill statement. */
|
||||
fallbackKillResultError?(results: QueryResult[]): string | null;
|
||||
/** Build the validated statement that cancels the selected session's running query. */
|
||||
buildCancelQuerySql(id: number): string;
|
||||
/** Build the compatibility statement used when the primary cancellation function is unavailable. */
|
||||
buildFallbackCancelQuerySql?(id: number): string;
|
||||
/** Restrict cancellation fallback attempts to known compatibility failures. */
|
||||
shouldUseFallbackCancelQuerySql?(error: unknown): boolean;
|
||||
/** Validate any engine-specific success value returned by the cancellation statement. */
|
||||
cancelQueryResultError?(results: QueryResult[]): string | null;
|
||||
/** Validate the success value returned by the compatibility cancellation statement. */
|
||||
fallbackCancelQueryResultError?(results: QueryResult[]): string | null;
|
||||
}
|
||||
|
||||
const MYSQL_COLUMNS: ProcessColumn[] = [
|
||||
@@ -110,7 +110,7 @@ const MYSQL_DRIVER: ProcessListDriver = {
|
||||
maxRows: 5000,
|
||||
// Typed structs carry no index signature; they are plain string-keyed objects at runtime.
|
||||
mapRows: (result) => mapMysqlProcessRows(result) as unknown as ProcessRow[],
|
||||
buildKillSql: buildMysqlKillSql,
|
||||
buildCancelQuerySql: buildMysqlCancelQuerySql,
|
||||
};
|
||||
|
||||
const POSTGRES_DRIVER: ProcessListDriver = {
|
||||
@@ -122,8 +122,8 @@ const POSTGRES_DRIVER: ProcessListDriver = {
|
||||
defaultSortKey: "time",
|
||||
maxRows: 5000,
|
||||
mapRows: (result) => mapPgProcessRows(result) as unknown as ProcessRow[],
|
||||
buildKillSql: buildPgKillSql,
|
||||
killResultError: pgKillResultError,
|
||||
buildCancelQuerySql: buildPgCancelQuerySql,
|
||||
cancelQueryResultError: pgCancelQueryResultError,
|
||||
};
|
||||
|
||||
const OPENGAUSS_DRIVER: ProcessListDriver = {
|
||||
@@ -133,8 +133,8 @@ const OPENGAUSS_DRIVER: ProcessListDriver = {
|
||||
defaultSortKey: "time",
|
||||
maxRows: 5000,
|
||||
mapRows: (result) => mapPgProcessRows(result) as unknown as ProcessRow[],
|
||||
buildKillSql: buildPgKillSql,
|
||||
killResultError: pgKillResultError,
|
||||
buildCancelQuerySql: buildPgCancelQuerySql,
|
||||
cancelQueryResultError: pgCancelQueryResultError,
|
||||
};
|
||||
|
||||
const KINGBASE_DRIVER: ProcessListDriver = {
|
||||
@@ -148,11 +148,11 @@ const KINGBASE_DRIVER: ProcessListDriver = {
|
||||
defaultSortKey: "time",
|
||||
maxRows: 5000,
|
||||
mapRows: (result) => mapPgProcessRows(result) as unknown as ProcessRow[],
|
||||
buildKillSql: buildKingbaseKillSql,
|
||||
buildFallbackKillSql: buildKingbasePgKillSql,
|
||||
shouldUseFallbackKillSql: isKingbaseTerminateCatalogCompatibilityError,
|
||||
killResultError: kingbaseKillResultError,
|
||||
fallbackKillResultError: kingbasePgKillResultError,
|
||||
buildCancelQuerySql: buildKingbaseCancelQuerySql,
|
||||
buildFallbackCancelQuerySql: buildKingbasePgCancelQuerySql,
|
||||
shouldUseFallbackCancelQuerySql: isKingbaseCancelCatalogCompatibilityError,
|
||||
cancelQueryResultError: kingbaseCancelQueryResultError,
|
||||
fallbackCancelQueryResultError: kingbasePgCancelQueryResultError,
|
||||
};
|
||||
|
||||
/** Resolve the process-list driver for a connection, or null if unsupported. */
|
||||
|
||||
Reference in new issue
Block a user