mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 12:41:39 +00:00
fix(connection): release stuck connection attempts
This commit is contained in:
@@ -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.`;
|
||||
}
|
||||
@@ -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/);
|
||||
});
|
||||
Reference in New Issue
Block a user