From fa0dfc93833a7da687bad2aa91f92e2c1b0a30d0 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 13 May 2026 14:18:28 +0800 Subject: [PATCH] perf: race GitHub proxies concurrently for driver downloads and registry fetch Replace sequential proxy fallback (up to 30s worst-case) with concurrent racing via futures::select_ok, returning the first successful response. Add list_installed_agents_local command for instant UI rendering with async registry update. Fix missing useToast import in RedisValueViewer. --- crates/dbx-core/src/lib.rs | 34 +++++ crates/dbx-core/src/update.rs | 32 ++--- src-tauri/src/commands/agents.rs | 139 ++++++++------------ src-tauri/src/commands/plugins.rs | 29 +--- src-tauri/src/lib.rs | 1 + src/components/config/DriverStoreDialog.vue | 12 +- src/components/redis/RedisValueViewer.vue | 2 + 7 files changed, 121 insertions(+), 128 deletions(-) diff --git a/crates/dbx-core/src/lib.rs b/crates/dbx-core/src/lib.rs index 36143057ba..277ce374ab 100644 --- a/crates/dbx-core/src/lib.rs +++ b/crates/dbx-core/src/lib.rs @@ -22,3 +22,37 @@ pub mod types; pub mod update; pub const GITHUB_PROXIES: &[&str] = &["https://update.hwdns.net/", "https://gh-proxy.org/", ""]; + +pub async fn race_github_proxies( + client: &reqwest::Client, + github_url: &str, + user_agent: &str, +) -> Result { + use futures::future::select_ok; + use std::pin::Pin; + + let futs: Vec> + Send>>> = + GITHUB_PROXIES + .iter() + .map(|proxy| { + let url = format!("{proxy}{github_url}"); + let client = client.clone(); + let ua = user_agent.to_string(); + Box::pin(async move { + client + .get(&url) + .header(reqwest::header::USER_AGENT, ua) + .send() + .await + .and_then(|r| r.error_for_status()) + .map_err(|e| format!("{e}")) + }) + as Pin> + Send>> + }) + .collect(); + + match select_ok(futs).await { + Ok((resp, _)) => Ok(resp), + Err(last_err) => Err(last_err), + } +} diff --git a/crates/dbx-core/src/update.rs b/crates/dbx-core/src/update.rs index b70d68a9c8..bba49c4f96 100644 --- a/crates/dbx-core/src/update.rs +++ b/crates/dbx-core/src/update.rs @@ -35,30 +35,16 @@ pub async fn fetch_latest_release() -> Result { .timeout(std::time::Duration::from_secs(10)) .build() .map_err(|e| format!("Failed to create HTTP client: {e}"))?; - let mut last_err = String::new(); - for proxy in crate::GITHUB_PROXIES { - let url = format!("{proxy}{LATEST_JSON_PATH}"); - match client - .get(&url) - .header(reqwest::header::USER_AGENT, "dbx-update-checker") - .send() - .await - .and_then(|r| r.error_for_status()) - { - Ok(resp) => { - let mut release = - resp.json::().await.map_err(|e| format!("Failed to parse update response: {e}"))?; - if let Ok(github) = fetch_github_release_metadata(&client, &release.version).await { - release.github = Some(github); - } - return Ok(release); - } - Err(e) => { - last_err = format!("{e}"); - } - } + + let resp = crate::race_github_proxies(&client, LATEST_JSON_PATH, "dbx-update-checker") + .await + .map_err(|e| format!("Failed to check updates: {e}"))?; + + let mut release = resp.json::().await.map_err(|e| format!("Failed to parse update response: {e}"))?; + if let Ok(github) = fetch_github_release_metadata(&client, &release.version).await { + release.github = Some(github); } - Err(format!("Failed to check updates: {last_err}")) + Ok(release) } async fn fetch_github_release_metadata( diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index f5f2022d5d..56c11680dc 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -11,38 +11,34 @@ const REGISTRY_PATH: &str = "https://github.com/t8y2/dbx-agents/releases/latest/ static REGISTRY_CACHE: std::sync::LazyLock>> = std::sync::LazyLock::new(|| Mutex::new(None)); -#[tauri::command] -pub async fn list_installed_agents(state: State<'_, Arc>) -> Result, String> { - let am = &state.agent_manager; +const AGENT_TYPES: &[(&str, &str)] = &[ + ("dameng", "达梦 DM8"), + ("kingbase", "人大金仓 KingbaseES"), + ("vastbase", "Vastbase"), + ("goldendb", "GoldenDB"), + ("oracle", "Oracle"), + ("h2", "H2"), + ("snowflake", "Snowflake"), + ("trino", "Trino (Presto)"), + ("hive", "Apache Hive"), + ("db2", "IBM DB2"), + ("informix", "IBM Informix"), + ("neo4j", "Neo4j"), + ("cassandra", "Apache Cassandra"), + ("bigquery", "Google BigQuery"), + ("kylin", "Apache Kylin"), + ("sundb", "SunDB"), + ("gaussdb", "GaussDB"), +]; + +fn build_agent_list(am: &AgentManager, registry: Option<&AgentRegistry>) -> Vec { let local_state = am.load_state(); - let registry = fetch_registry().await.ok(); - - let agent_types = [ - ("dameng", "达梦 DM8"), - ("kingbase", "人大金仓 KingbaseES"), - ("vastbase", "Vastbase"), - ("goldendb", "GoldenDB"), - ("oracle", "Oracle"), - ("h2", "H2"), - ("snowflake", "Snowflake"), - ("trino", "Trino (Presto)"), - ("hive", "Apache Hive"), - ("db2", "IBM DB2"), - ("informix", "IBM Informix"), - ("neo4j", "Neo4j"), - ("cassandra", "Apache Cassandra"), - ("bigquery", "Google BigQuery"), - ("kylin", "Apache Kylin"), - ("sundb", "SunDB"), - ("gaussdb", "GaussDB"), - ]; - - Ok(agent_types + AGENT_TYPES .iter() .map(|(key, label)| { let installed = am.is_driver_installed(key); let local = local_state.installed_drivers.get(*key); - let remote = registry.as_ref().and_then(|r| r.drivers.get(*key)); + let remote = registry.and_then(|r| r.drivers.get(*key)); AgentDriverInfo { db_type: key.to_string(), label: label.to_string(), @@ -56,7 +52,18 @@ pub async fn list_installed_agents(state: State<'_, Arc>) -> Result>) -> Result, String> { + Ok(build_agent_list(&state.agent_manager, None)) +} + +#[tauri::command] +pub async fn list_installed_agents(state: State<'_, Arc>) -> Result, String> { + let registry = fetch_registry().await.ok(); + Ok(build_agent_list(&state.agent_manager, registry.as_ref())) } #[tauri::command] @@ -175,27 +182,12 @@ async fn fetch_registry() -> Result { .timeout(std::time::Duration::from_secs(10)) .build() .map_err(|e| format!("Failed to create HTTP client: {e}"))?; - let mut last_err = String::new(); - for proxy in dbx_core::GITHUB_PROXIES { - let url = format!("{proxy}{REGISTRY_PATH}"); - match client - .get(&url) - .header(reqwest::header::USER_AGENT, "dbx-agent-manager") - .send() - .await - .and_then(|r| r.error_for_status()) - { - Ok(resp) => { - let reg: AgentRegistry = resp.json().await.map_err(|e| format!("Failed to parse registry: {e}"))?; - *REGISTRY_CACHE.lock().await = Some((std::time::Instant::now(), reg.clone())); - return Ok(reg); - } - Err(e) => { - last_err = format!("{e}"); - } - } - } - Err(format!("Failed to fetch agent registry: {last_err}")) + let resp = dbx_core::race_github_proxies(&client, REGISTRY_PATH, "dbx-agent-manager") + .await + .map_err(|e| format!("Failed to fetch agent registry: {e}"))?; + let reg: AgentRegistry = resp.json().await.map_err(|e| format!("Failed to parse registry: {e}"))?; + *REGISTRY_CACHE.lock().await = Some((std::time::Instant::now(), reg.clone())); + Ok(reg) } async fn download_with_progress( @@ -212,39 +204,24 @@ async fn download_with_progress( .timeout(std::time::Duration::from_secs(300)) .build() .map_err(|e| format!("Failed to create HTTP client: {e}"))?; - let mut last_err = String::new(); - for proxy in dbx_core::GITHUB_PROXIES { - let full_url = format!("{proxy}{url}"); - log::info!("[agent] downloading from {full_url}"); - match client - .get(&full_url) - .header(reqwest::header::USER_AGENT, "dbx-agent-manager") - .send() - .await - .and_then(|r| r.error_for_status()) - { - Ok(resp) => { - let content_length = resp.content_length().unwrap_or(total_size); - let mut file = std::fs::File::create(dest).map_err(|e| format!("Failed to create file: {e}"))?; - let mut downloaded: u64 = 0; - let mut bytes = resp; - while let Some(chunk) = bytes.chunk().await.map_err(|e| format!("Download stream error: {e}"))? { - std::io::Write::write_all(&mut file, &chunk).map_err(|e| format!("Failed to write chunk: {e}"))?; - downloaded += chunk.len() as u64; - let _ = app.emit( - "agent-install-progress", - serde_json::json!({ "step": step, "downloaded": downloaded, "total": content_length }), - ); - } - return Ok(()); - } - Err(e) => { - last_err = format!("{e}"); - log::warn!("[agent] download failed from {full_url}: {last_err}"); - } - } + + let resp = dbx_core::race_github_proxies(&client, url, "dbx-agent-manager") + .await + .map_err(|e| format!("Failed to download {url}: {e}"))?; + + let content_length = resp.content_length().unwrap_or(total_size); + let mut file = std::fs::File::create(dest).map_err(|e| format!("Failed to create file: {e}"))?; + let mut downloaded: u64 = 0; + let mut bytes = resp; + while let Some(chunk) = bytes.chunk().await.map_err(|e| format!("Download stream error: {e}"))? { + std::io::Write::write_all(&mut file, &chunk).map_err(|e| format!("Failed to write chunk: {e}"))?; + downloaded += chunk.len() as u64; + let _ = app.emit( + "agent-install-progress", + serde_json::json!({ "step": step, "downloaded": downloaded, "total": content_length }), + ); } - Err(format!("Failed to download {url}: {last_err}")) + Ok(()) } fn extract_archive(archive: &std::path::Path, dest: &std::path::Path) -> Result<(), String> { diff --git a/src-tauri/src/commands/plugins.rs b/src-tauri/src/commands/plugins.rs index 97ab9d84b4..df4b950639 100644 --- a/src-tauri/src/commands/plugins.rs +++ b/src-tauri/src/commands/plugins.rs @@ -6,13 +6,7 @@ use serde::Serialize; use super::connection::AppState; -const JDBC_PLUGIN_DOWNLOAD_VERSION: &str = "0.1.0"; - -fn jdbc_plugin_download_urls() -> Vec { - let asset = format!("dbx-jdbc-plugin-{JDBC_PLUGIN_DOWNLOAD_VERSION}.zip"); - let latest_url = format!("https://github.com/t8y2/dbx/releases/latest/download/{asset}"); - vec![format!("https://update.hwdns.net/{latest_url}"), format!("https://gh-proxy.org/{latest_url}"), latest_url] -} +const JDBC_PLUGIN_DOWNLOAD_URL: &str = "https://github.com/t8y2/dbx/releases/latest/download/dbx-jdbc-plugin-0.1.0.zip"; #[tauri::command] pub async fn list_plugins(state: State<'_, Arc>) -> Result, String> { @@ -146,24 +140,13 @@ async fn download_jdbc_plugin_zip() -> Result, String> { .timeout(std::time::Duration::from_secs(60)) .build() .map_err(|err| err.to_string())?; - let mut last_error = String::new(); - for url in jdbc_plugin_download_urls() { - match client.get(&url).header(reqwest::header::USER_AGENT, "dbx-jdbc-plugin-installer").send().await { - Ok(response) if response.status().is_success() => { - let bytes = response.bytes().await.map_err(|err| err.to_string())?; - return Ok(bytes.to_vec()); - } - Ok(response) => { - last_error = format!("{url} returned {}", response.status()); - } - Err(err) => { - last_error = format!("{url}: {err}"); - } - } - } + let resp = dbx_core::race_github_proxies(&client, JDBC_PLUGIN_DOWNLOAD_URL, "dbx-jdbc-plugin-installer") + .await + .map_err(|err| format!("Failed to download JDBC plugin: {err}"))?; - Err(format!("Failed to download JDBC plugin: {last_error}")) + let bytes = resp.bytes().await.map_err(|err| err.to_string())?; + Ok(bytes.to_vec()) } fn install_jdbc_plugin_zip(bytes: &[u8], plugin_dir: &std::path::Path) -> Result<(), String> { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 41722c1ade..fb7f735ec3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -145,6 +145,7 @@ pub fn run() { commands::database_export::export_database_sql, commands::database_export::cancel_database_export, commands::agents::list_installed_agents, + commands::agents::list_installed_agents_local, commands::agents::install_agent, commands::agents::uninstall_agent, commands::agents::check_jre_installed, diff --git a/src/components/config/DriverStoreDialog.vue b/src/components/config/DriverStoreDialog.vue index 00d7724b5a..fa42ae9fe3 100644 --- a/src/components/config/DriverStoreDialog.vue +++ b/src/components/config/DriverStoreDialog.vue @@ -238,7 +238,17 @@ async function deleteJdbcDriver(path: string) { // ──────────── Lifecycle ──────────── onMounted(async () => { - await refreshAgents(); + const [jre, localDrivers] = await Promise.all([ + invoke("check_jre_installed"), + invoke("list_installed_agents_local"), + ]); + jreInstalled.value = jre; + drivers.value = localDrivers; + + invoke("list_installed_agents").then((result) => { + drivers.value = result; + }); + unlisten = await listen("agent-install-progress", (event) => { if (event.payload.step === "done") { progress.value = null; diff --git a/src/components/redis/RedisValueViewer.vue b/src/components/redis/RedisValueViewer.vue index 8342d3e029..a363ba96a3 100644 --- a/src/components/redis/RedisValueViewer.vue +++ b/src/components/redis/RedisValueViewer.vue @@ -9,6 +9,7 @@ import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/com import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import * as api from "@/lib/api"; import type { RedisValue } from "@/lib/api"; +import { useToast } from "@/composables/useToast"; import { canEditRedisMemberDetail, clampRedisMemberDetailSheetWidth, @@ -18,6 +19,7 @@ import { } from "@/lib/redisValuePresentation"; const { t } = useI18n(); +const { toast } = useToast(); const props = defineProps<{ connectionId: string;