fix(connection): release stuck connection attempts

This commit is contained in:
t8y2
2026-06-02 22:20:26 +08:00
parent 21cfd1b0f7
commit 413261a476
3 changed files with 76 additions and 2 deletions
@@ -0,0 +1,25 @@
import type { ConnectionConfig } from "@/types/database";
export const CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS = 2_000;
const DEFAULT_CONNECT_TIMEOUT_SECS = 5;
function positiveSeconds(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
}
export function connectionAttemptTimeoutMs(
config: Pick<ConnectionConfig, "connect_timeout_secs" | "ssh_enabled" | "ssh_connect_timeout_secs" | "ssh_tunnels">,
): number {
const timeouts = [positiveSeconds(config.connect_timeout_secs, DEFAULT_CONNECT_TIMEOUT_SECS)];
if (config.ssh_enabled) {
timeouts.push(positiveSeconds(config.ssh_connect_timeout_secs, DEFAULT_CONNECT_TIMEOUT_SECS));
for (const tunnel of config.ssh_tunnels ?? []) {
timeouts.push(positiveSeconds(tunnel.connect_timeout_secs, DEFAULT_CONNECT_TIMEOUT_SECS));
}
}
return Math.ceil(Math.max(...timeouts) * 1000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS);
}
export function connectionAttemptTimeoutMessage(timeoutMs: number): string {
return `Connection attempt timed out after ${Math.ceil(timeoutMs / 1000)}s. Please check the network or VPN and try again.`;
}
+18 -2
View File
@@ -25,6 +25,7 @@ import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes } from "@/lib/da
import { buildSqlServerDatabaseTreeNodes, SQLSERVER_DEFAULT_SCHEMA } from "@/lib/sqlServerTree";
import { findDatabaseTreeNode } from "@/lib/treeRefreshTarget";
import { shouldMarkDisconnected } from "@/lib/connectionHealth";
import { connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connectionAttemptTimeout";
import {
filterDatabaseNamesForConnection,
filterVisibleDatabaseNames,
@@ -195,6 +196,21 @@ export const useConnectionStore = defineStore("connection", () => {
recordConnectionError(connectionId, error);
}
async function withConnectionAttemptTimeout<T>(promise: Promise<T>, config: ConnectionConfig): Promise<T> {
const timeoutMs = connectionAttemptTimeoutMs(config);
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(connectionAttemptTimeoutMessage(timeoutMs))), timeoutMs);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
function normalizeConnection(config: ConnectionConfig): ConnectionConfig {
const labelMap: Record<string, string> = {
mysql: "MySQL",
@@ -642,7 +658,7 @@ export const useConnectionStore = defineStore("connection", () => {
if (pendingNode) pendingNode.isLoading = true;
try {
await beforeConnectHandler?.(config);
const id = await api.connectDb(config);
const id = await withConnectionAttemptTimeout(api.connectDb(config), config);
activeConnectionId.value = id;
connectedIds.value.add(id);
clearConnectionError(config.id);
@@ -720,7 +736,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
try {
await beforeConnectHandler?.(config);
await api.connectDb(config);
await withConnectionAttemptTimeout(api.connectDb(config), config);
connectedIds.value.add(connectionId);
activeConnectionId.value = connectionId;
clearConnectionError(connectionId);
@@ -0,0 +1,33 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import {
CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS,
connectionAttemptTimeoutMessage,
connectionAttemptTimeoutMs,
} from "../../apps/desktop/src/lib/connectionAttemptTimeout.ts";
test("uses connection timeout with a small UI buffer", () => {
assert.equal(connectionAttemptTimeoutMs({ connect_timeout_secs: 8 }), 8_000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS);
});
test("falls back to the default connection timeout", () => {
assert.equal(connectionAttemptTimeoutMs({}), 5_000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS);
assert.equal(connectionAttemptTimeoutMs({ connect_timeout_secs: 0 }), 5_000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS);
});
test("honors slower SSH tunnel connection timeouts", () => {
assert.equal(
connectionAttemptTimeoutMs({
connect_timeout_secs: 5,
ssh_enabled: true,
ssh_connect_timeout_secs: 12,
ssh_tunnels: [{ id: "hop-1", host: "bastion", port: 22, user: "dbx", connect_timeout_secs: 20 }],
}),
20_000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS,
);
});
test("formats connection attempt timeout messages", () => {
assert.match(connectionAttemptTimeoutMessage(7_001), /timed out after 8s/);
assert.match(connectionAttemptTimeoutMessage(7_001), /VPN/);
});