mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-22 13:15:31 +00:00
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
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -164,6 +164,10 @@ export async function disconnectDb(connectionId: string): Promise<void> {
|
||||
return post("/api/connection/disconnect", { connectionId });
|
||||
}
|
||||
|
||||
export async function checkConnectionHealth(connectionId: string): Promise<void> {
|
||||
return post("/api/connection/check-health", { connectionId });
|
||||
}
|
||||
|
||||
export async function closeDatabaseConnection(connectionId: string, database: string): Promise<boolean> {
|
||||
return post("/api/connection/close-database", { connectionId, database });
|
||||
}
|
||||
|
||||
@@ -473,6 +473,10 @@ export async function disconnectDb(connectionId: string): Promise<void> {
|
||||
return invoke("disconnect_db", { connectionId });
|
||||
}
|
||||
|
||||
export async function checkConnectionHealth(connectionId: string): Promise<void> {
|
||||
return invoke("check_connection_health", { connectionId });
|
||||
}
|
||||
|
||||
export async function closeDatabaseConnection(connectionId: string, database: string): Promise<boolean> {
|
||||
return invoke("close_database_connection", { connectionId, database });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -110,6 +110,14 @@ pub async fn disconnect_db(
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn check_connection_health(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(body): Json<DisconnectRequest>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
state.app.check_connection_health(&body.connection_id).await.map_err(AppError)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn close_database_connection(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(body): Json<CloseDatabaseConnectionRequest>,
|
||||
|
||||
@@ -1017,7 +1017,7 @@ pub async fn connection_final_proxy_port(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disconnect_db(state: State<'_, Arc<AppState>>, 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<AppState>>) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_connection_health(state: State<'_, Arc<AppState>>, 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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user