From e2f68de4556c55403790d513f286eb900e83dad4 Mon Sep 17 00:00:00 2001 From: fagao Date: Mon, 22 Jun 2026 20:40:40 +0800 Subject: [PATCH] fix: resolve MySQL connection state desync after disconnect/reconnect - Use synchronous pool removal in desktop disconnect_db to prevent race condition when user quickly disconnects and reconnects - Rename recordMetadataLoadError to recordConnectionLostError and export it for cross-store reuse - Sync query execution errors back to connection state via recordConnectionLostError in queryStore catch blocks - Add backend check_connection_health method (dbx-core) with Tauri command and web route for proactive pool verification - Add frontend checkConnectionHealth API (tauri.ts/http.ts/api.ts) - Enhance ensureConnected to verify backend pool health before assuming connection is valid, auto-reconnect if stale --- apps/desktop/src/lib/api.ts | 1 + apps/desktop/src/lib/http.ts | 4 ++ apps/desktop/src/lib/tauri.ts | 4 ++ apps/desktop/src/stores/connectionStore.ts | 55 +++++++++++++--------- apps/desktop/src/stores/queryStore.ts | 9 +++- crates/dbx-core/src/connection.rs | 26 ++++++++++ crates/dbx-web/src/main.rs | 1 + crates/dbx-web/src/routes/connection.rs | 8 ++++ src-tauri/src/commands/connection.rs | 7 ++- src-tauri/src/lib.rs | 1 + 10 files changed, 92 insertions(+), 24 deletions(-) diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index cc02bf16c9..63a590aafa 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -53,6 +53,7 @@ export const testConnection = forward("testConnection"); export const connectDb = forward("connectDb"); export const connectionFinalProxyPort = forward("connectionFinalProxyPort"); export const disconnectDb = forward("disconnectDb"); +export const checkConnectionHealth = forward("checkConnectionHealth"); export const closeDatabaseConnection = forward("closeDatabaseConnection"); export const refreshConnections = forward("refreshConnections"); export const saveConnections = forward("saveConnections"); diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index bc1e0f2ed5..df1811f030 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -164,6 +164,10 @@ export async function disconnectDb(connectionId: string): Promise { return post("/api/connection/disconnect", { connectionId }); } +export async function checkConnectionHealth(connectionId: string): Promise { + return post("/api/connection/check-health", { connectionId }); +} + export async function closeDatabaseConnection(connectionId: string, database: string): Promise { return post("/api/connection/close-database", { connectionId, database }); } diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 199ebe0f50..3b366196f4 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -473,6 +473,10 @@ export async function disconnectDb(connectionId: string): Promise { return invoke("disconnect_db", { connectionId }); } +export async function checkConnectionHealth(connectionId: string): Promise { + return invoke("check_connection_health", { connectionId }); +} + export async function closeDatabaseConnection(connectionId: string, database: string): Promise { return invoke("close_database_connection", { connectionId, database }); } diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 64baf1d89d..279968c50d 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -260,7 +260,7 @@ export const useConnectionStore = defineStore("connection", () => { return message; } - function recordMetadataLoadError(connectionId: string, error: unknown) { + function recordConnectionLostError(connectionId: string, error: unknown) { if (shouldMarkDisconnected(error)) { connectedIds.value.delete(connectionId); if (activeConnectionId.value === connectionId) activeConnectionId.value = null; @@ -902,7 +902,17 @@ export const useConnectionStore = defineStore("connection", () => { } async function ensureConnected(connectionId: string) { - if (connectedIds.value.has(connectionId)) return; + if (connectedIds.value.has(connectionId)) { + // Optimistic: verify backend pool is actually healthy + try { + await api.checkConnectionHealth(connectionId); + return; + } catch { + // Backend pool is dead — remove from connectedIds and reconnect + connectedIds.value.delete(connectionId); + if (activeConnectionId.value === connectionId) activeConnectionId.value = null; + } + } let config = getConfig(connectionId); if (!config) { await initFromDisk(); @@ -1020,7 +1030,7 @@ export const useConnectionStore = defineStore("connection", () => { } node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1063,7 +1073,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1097,7 +1107,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1127,7 +1137,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1191,7 +1201,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1223,7 +1233,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1255,7 +1265,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1283,7 +1293,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1322,7 +1332,7 @@ export const useConnectionStore = defineStore("connection", () => { await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1358,7 +1368,7 @@ export const useConnectionStore = defineStore("connection", () => { await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1391,7 +1401,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1426,7 +1436,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1468,7 +1478,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(node.connectionId, e); + recordConnectionLostError(node.connectionId, e); throw e; } finally { node.isLoading = false; @@ -1524,7 +1534,7 @@ export const useConnectionStore = defineStore("connection", () => { await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1584,7 +1594,7 @@ export const useConnectionStore = defineStore("connection", () => { } node.isExpanded = true; } catch (e) { - recordMetadataLoadError(node.connectionId, e); + recordConnectionLostError(node.connectionId, e); throw e; } finally { node.isLoading = false; @@ -1623,7 +1633,7 @@ export const useConnectionStore = defineStore("connection", () => { await savePersistedTreeChildren(objectGroupCacheKey(parent), nextChildren); parent.isExpanded = true; } catch (e) { - recordMetadataLoadError(parent.connectionId, e); + recordConnectionLostError(parent.connectionId, e); throw e; } finally { node.isLoading = false; @@ -1755,7 +1765,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1792,7 +1802,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1833,7 +1843,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -1870,7 +1880,7 @@ export const useConnectionStore = defineStore("connection", () => { ); node.isExpanded = true; } catch (e) { - recordMetadataLoadError(connectionId, e); + recordConnectionLostError(connectionId, e); throw e; } finally { node.isLoading = false; @@ -3039,6 +3049,7 @@ export const useConnectionStore = defineStore("connection", () => { setConnectionError, clearConnectionError, recordConnectionError, + recordConnectionLostError, sidebarLayout, getConfig, isTreeNodePinned, diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 8da38dcd25..6f2243bb8d 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1638,6 +1638,8 @@ export const useQueryStore = defineStore("query", () => { } } catch (e: any) { console.error("[DBX][executeTabSql:error]", { traceId, elapsed: elapsed(), error: e }); + // Sync connection state if the error indicates a lost connection + useConnectionStore().recordConnectionLostError(tab.connectionId, e); const current = tabs.value.find((t) => t.id === id); if (current?.executionId === executionId) { current.result = toErrorResult(e); @@ -1787,6 +1789,8 @@ export const useQueryStore = defineStore("query", () => { } return canceled; } catch (e: any) { + // Sync connection state if the error indicates a lost connection + if (tab) useConnectionStore().recordConnectionLostError(tab.connectionId, e); const current = tabs.value.find((t) => t.id === id); if (current && current.executionId === executionId) { current.isCancelling = false; @@ -1833,12 +1837,15 @@ export const useQueryStore = defineStore("query", () => { function notifyConnectionMayBeLost() { const stuck = tabs.value.filter((t) => t.isExecuting); if (stuck.length > 0) { + const connStore = useConnectionStore(); stuck.forEach((tab) => { tab.isExecuting = false; tab.isCancelling = false; tab.queryExecutionStartedAt = undefined; tab.executionId = undefined; - tab.result = toErrorResult(new Error(t("editor.connectionMayBeLost"))); + const error = new Error(t("editor.connectionMayBeLost")); + tab.result = toErrorResult(error); + connStore.recordConnectionLostError(tab.connectionId, error); }); } } diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 317ce8a77b..b22b1f1fc5 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -1281,6 +1281,32 @@ impl AppState { self.proxy_tunnels.stop_tunnel(connection_id).await; } + /// Health-check the base connection pool for a given connection_id. + /// Returns `Ok(())` if the pool exists and is healthy, `Err` otherwise. + /// If the pool is unhealthy it is removed from the map so subsequent + /// `get_or_create_pool` calls will transparently recreate it. + pub async fn check_connection_health(&self, connection_id: &str) -> Result<(), String> { + let db_type = { + let configs = self.configs.read().await; + configs.get(connection_id).map(|c| c.db_type) + }; + let pool_key = base_pool_key_for(db_type, connection_id, None, false); + + // Check if pool exists first + { + let connections = self.connections.read().await; + if !connections.contains_key(&pool_key) { + return Err("No active connection pool found".to_string()); + } + } + + // `remove_stale_connection_pool` returns true if the pool was stale (and removed) + if self.remove_stale_connection_pool(&pool_key).await { + return Err("Connection pool is unhealthy".to_string()); + } + Ok(()) + } + pub async fn refresh_connections(&self) { // Clone pool handles under a short-lived read lock, then release it // before performing I/O-heavy health checks to avoid blocking writers. diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index 794cb10fd1..de9b3e11fc 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -153,6 +153,7 @@ async fn main() { .route("/connection/connect", post(routes::connection::connect_db)) .route("/connection/final-proxy-port", post(routes::connection::connection_final_proxy_port)) .route("/connection/disconnect", post(routes::connection::disconnect_db)) + .route("/connection/check-health", post(routes::connection::check_connection_health)) .route("/connection/close-database", post(routes::connection::close_database_connection)) .route("/connection/save", post(routes::connection::save_connections)) .route("/connection/list", get(routes::connection::load_connections)) diff --git a/crates/dbx-web/src/routes/connection.rs b/crates/dbx-web/src/routes/connection.rs index 34f0c62147..6f35f7d3ce 100644 --- a/crates/dbx-web/src/routes/connection.rs +++ b/crates/dbx-web/src/routes/connection.rs @@ -110,6 +110,14 @@ pub async fn disconnect_db( Ok(Json(())) } +pub async fn check_connection_health( + State(state): State>, + Json(body): Json, +) -> Result, AppError> { + state.app.check_connection_health(&body.connection_id).await.map_err(AppError)?; + Ok(Json(())) +} + pub async fn close_database_connection( State(state): State>, Json(body): Json, diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 381ca6f485..35e5561f61 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -1017,7 +1017,7 @@ pub async fn connection_final_proxy_port( #[tauri::command] pub async fn disconnect_db(state: State<'_, Arc>, connection_id: String) -> Result<(), String> { - state.remove_connection_pools_detached(&connection_id).await; + state.remove_connection_pools(&connection_id).await; drop_mq_adapters_for_connection_ids(state.inner(), std::slice::from_ref(&connection_id)).await; state.reset_connection_transport(&connection_id).await; if connection_id.starts_with("__visible_draft_") { @@ -1043,6 +1043,11 @@ pub async fn refresh_connections(state: State<'_, Arc>) -> Result<(), Ok(()) } +#[tauri::command] +pub async fn check_connection_health(state: State<'_, Arc>, connection_id: String) -> Result<(), String> { + state.check_connection_health(&connection_id).await +} + /// Check whether a connection has read-only protection enabled. /// Returns an error if the connection is read-only, preventing write operations. pub async fn ensure_connection_writable( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b0057b8042..86e28c1cba 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -423,6 +423,7 @@ pub fn run() { commands::connection::disconnect_db, commands::connection::close_database_connection, commands::connection::refresh_connections, + commands::connection::check_connection_health, commands::connection::save_connections, commands::connection::load_connections, commands::connection::save_sidebar_layout,