fix(db): update Oracle and OceanBase agent URLs

This commit is contained in:
t8y2
2026-06-12 11:52:38 +08:00
parent a4bd62005b
commit bb2768a62f
2 changed files with 217 additions and 47 deletions
+171 -24
View File
@@ -15,8 +15,10 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da
};
let connection_string = if config.db_type == DatabaseType::MongoDb {
config.connection_url_with_host(host, port)
} else if matches!(config.db_type, DatabaseType::Oracle | DatabaseType::OceanbaseOracle) {
} else if config.db_type == DatabaseType::Oracle {
oracle_jdbc_connection_string(config, host, port, database)
} else if config.db_type == DatabaseType::OceanbaseOracle {
oceanbase_oracle_jdbc_connection_string(config, host, port, database)
} else if matches!(config.db_type, DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Vastbase) {
postgres_like_agent_jdbc_connection_string(config, host, port, database)
} else if config.db_type == DatabaseType::SapHana {
@@ -205,6 +207,64 @@ pub fn oracle_error_with_driver_hint(config: &ConnectionConfig, err: &str) -> St
)
}
pub fn oracle_alternate_connect_configs(config: &ConnectionConfig, err: &str) -> Vec<ConnectionConfig> {
if config.db_type != DatabaseType::Oracle {
return Vec::new();
}
if config.driver_profile.as_deref() == Some("oracle-10g") {
return Vec::new();
}
if config.connection_string.as_deref().is_some_and(|value| !value.trim().is_empty()) {
return Vec::new();
}
if !oracle_listener_error_can_retry(err) {
return Vec::new();
}
let database = config.effective_database().unwrap_or("").trim();
if database.is_empty() {
return Vec::new();
}
let host = config.host.trim();
let port = config.port;
let current_url = oracle_jdbc_connection_string(config, host, port, database);
let service_url = oracle_service_jdbc_url(host, port, database);
let sid_url = oracle_sid_jdbc_url(host, port, database);
let legacy_service_url = oracle_legacy_service_jdbc_url(host, port, database);
let descriptor_service_url = oracle_descriptor_jdbc_url(host, port, database, "SERVICE_NAME");
let descriptor_sid_url = oracle_descriptor_jdbc_url(host, port, database, "SID");
let normalized = err.to_lowercase();
let candidates = if normalized.contains("ora-12505") {
vec![service_url, descriptor_service_url, legacy_service_url]
} else if normalized.contains("ora-12514") {
vec![sid_url, descriptor_sid_url, legacy_service_url]
} else {
match config.oracle_connection_type.as_deref() {
Some("sid") => vec![service_url, legacy_service_url, descriptor_sid_url, descriptor_service_url],
_ => vec![sid_url, legacy_service_url, descriptor_service_url, descriptor_sid_url],
}
};
let mut urls = Vec::new();
for url in candidates {
if url == current_url || urls.iter().any(|seen| seen == &url) {
continue;
}
urls.push(url);
}
urls.into_iter()
.map(|url| {
let mut retry = config.clone();
retry.oracle_connection_type = None;
retry.connection_string = Some(url);
retry
})
.collect()
}
fn oracle_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String {
if let Some(connection_string) = config.connection_string.as_deref().filter(|value| !value.trim().is_empty()) {
let connection_string = connection_string.trim();
@@ -220,12 +280,54 @@ fn oracle_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u1
}
if config.oracle_connection_type.as_deref() == Some("sid") {
format!("jdbc:oracle:thin:@{host}:{port}:{database}")
oracle_sid_jdbc_url(host, port, database)
} else {
format!("jdbc:oracle:thin:@//{host}:{port}/{database}")
oracle_service_jdbc_url(host, port, database)
}
}
fn oracle_listener_error_can_retry(err: &str) -> bool {
let normalized = err.to_lowercase();
normalized.contains("ora-12505")
|| normalized.contains("ora-12514")
|| normalized.contains("ora-12541")
|| normalized.contains("no listener")
|| err.contains("没有监听程序")
}
fn oracle_service_jdbc_url(host: &str, port: u16, database: &str) -> String {
format!("jdbc:oracle:thin:@//{host}:{port}/{database}")
}
fn oracle_sid_jdbc_url(host: &str, port: u16, database: &str) -> String {
format!("jdbc:oracle:thin:@{host}:{port}:{database}")
}
fn oracle_legacy_service_jdbc_url(host: &str, port: u16, database: &str) -> String {
format!("jdbc:oracle:thin:@{host}:{port}/{database}")
}
fn oracle_descriptor_jdbc_url(host: &str, port: u16, database: &str, key: &str) -> String {
format!("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={host})(PORT={port}))(CONNECT_DATA=({key}={database})))")
}
fn oceanbase_oracle_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String {
if let Some(connection_string) = config.connection_string.as_deref().filter(|value| !value.trim().is_empty()) {
let connection_string = connection_string.trim();
if host == config.host && port == config.port {
return connection_string.to_string();
}
return crate::models::connection::rewrite_jdbc_url_host(connection_string, host, port);
}
let database = database.trim();
if database.is_empty() {
return String::new();
}
append_agent_url_params(format!("jdbc:oceanbase://{host}:{port}/{database}"), config.url_params.as_deref())
}
fn postgres_like_agent_jdbc_connection_string(
config: &ConnectionConfig,
host: &str,
@@ -262,24 +364,35 @@ pub fn oracle_auth_fallback_profiles(config: &ConnectionConfig, err: &str) -> Ve
}
pub fn oracle_alternate_connect_config(config: &ConnectionConfig, err: &str) -> Option<ConnectionConfig> {
if config.db_type != DatabaseType::Oracle {
return None;
}
if config.driver_profile.as_deref() == Some("oracle-10g") {
return None;
}
if config.connection_string.as_deref().is_some_and(|value| !value.trim().is_empty()) {
return None;
}
let normalized = err.to_lowercase();
if !normalized.contains("ora-12505") && !normalized.contains("ora-12514") {
return None;
}
oracle_alternate_connect_configs(config, err).into_iter().next()
}
let mut retry = config.clone();
retry.oracle_connection_type =
Some(if config.oracle_connection_type.as_deref() == Some("sid") { "service_name" } else { "sid" }.to_string());
Some(retry)
pub fn oracle_alternate_connect_config_labels(configs: &[ConnectionConfig]) -> Vec<String> {
configs
.iter()
.map(|config| {
config
.connection_string
.as_deref()
.map(oracle_connection_string_label)
.unwrap_or_else(|| config.oracle_connection_type.as_deref().unwrap_or("service_name").to_string())
})
.collect()
}
fn oracle_connection_string_label(connection_string: &str) -> String {
let upper = connection_string.to_ascii_uppercase();
if upper.contains("(SERVICE_NAME=") {
"descriptor service name".to_string()
} else if upper.contains("(SID=") {
"descriptor SID".to_string()
} else if connection_string.starts_with("jdbc:oracle:thin:@//") {
"service name".to_string()
} else if connection_string.contains(':') && !connection_string.contains('/') {
"SID".to_string()
} else {
"legacy service name".to_string()
}
}
fn sap_hana_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String {
@@ -575,7 +688,7 @@ mod tests {
}
#[test]
fn oceanbase_oracle_uses_oracle_jdbc_connection_string_for_agent_protocol() {
fn oceanbase_oracle_uses_oceanbase_jdbc_connection_string_for_agent_protocol() {
let mut cfg = config(DatabaseType::OceanbaseOracle, Some("sys"));
cfg.host = "oceanbase.example.com".to_string();
cfg.port = 2881;
@@ -584,7 +697,19 @@ mod tests {
assert_eq!(params["database"], "sys");
assert_eq!(params["sysdba"], false);
assert_eq!(params["connection_string"], "jdbc:oracle:thin:@//oceanbase.example.com:2881/sys");
assert_eq!(params["connection_string"], "jdbc:oceanbase://oceanbase.example.com:2881/sys");
}
#[test]
fn oceanbase_oracle_jdbc_url_appends_params_and_rewrites_forwarded_host() {
let mut cfg = config(DatabaseType::OceanbaseOracle, Some("sys"));
cfg.host = "oceanbase.example.com".to_string();
cfg.port = 2881;
cfg.url_params = Some("useSSL=false".to_string());
let params = agent_connect_params(&cfg, "127.0.0.1", 12881, "sys");
assert_eq!(params["connection_string"], "jdbc:oceanbase://127.0.0.1:12881/sys?useSSL=false");
}
#[test]
@@ -631,9 +756,9 @@ mod tests {
let retry = oracle_alternate_connect_config(&cfg, "ORA-12514: listener does not know service").unwrap();
assert_eq!(retry.oracle_connection_type.as_deref(), Some("sid"));
assert_eq!(retry.connection_string.as_deref(), Some("jdbc:oracle:thin:@127.0.0.1:3306:ORCL"));
assert!(oracle_alternate_connect_config(&retry, "ORA-01017: invalid username/password").is_none());
assert!(oracle_alternate_connect_config(&cfg, "ORA-12541: TNS:no listener").is_none());
assert!(oracle_alternate_connect_config(&cfg, "ORA-12541: TNS:no listener").is_some());
}
#[test]
@@ -666,6 +791,28 @@ mod tests {
assert!(oracle_alternate_connect_config(&cfg, "ORA-12514: listener does not know service").is_none());
}
#[test]
fn oracle_no_listener_errors_try_common_jdbc_url_variants() {
let mut cfg = config(DatabaseType::Oracle, Some("ORCL"));
cfg.host = "oracle.example.com".to_string();
cfg.port = 1521;
cfg.driver_profile = Some("oracle".to_string());
cfg.oracle_connection_type = Some("service_name".to_string());
let retries = oracle_alternate_connect_configs(&cfg, "ORA-12541: TNS:no listener");
let urls: Vec<_> = retries.iter().filter_map(|retry| retry.connection_string.as_deref()).collect();
assert_eq!(
urls,
vec![
"jdbc:oracle:thin:@oracle.example.com:1521:ORCL",
"jdbc:oracle:thin:@oracle.example.com:1521/ORCL",
"jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=oracle.example.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))",
"jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=oracle.example.com)(PORT=1521))(CONNECT_DATA=(SID=ORCL)))",
]
);
}
#[test]
fn sap_hana_url_includes_selected_database_and_params() {
let mut cfg = config(DatabaseType::SapHana, Some("TENANT1"));
+46 -23
View File
@@ -8,8 +8,8 @@ use mysql_async::Row as MysqlRow;
use crate::agent_connection::{
agent_connect_params, h2_file_path_from_jdbc_url, is_h2_file_connection, mongo_legacy_error_with_auth_hint,
oracle_alternate_connect_config, oracle_auth_fallback_profiles, oracle_error_with_driver_hint,
should_retry_oracle_with_10g_driver,
oracle_alternate_connect_config_labels, oracle_alternate_connect_configs, oracle_auth_fallback_profiles,
oracle_error_with_driver_hint, should_retry_oracle_with_10g_driver,
};
use crate::agent_manager::{JavaRuntimeMode, DEFAULT_JRE_KEY};
use crate::database_capabilities;
@@ -553,27 +553,48 @@ impl AppState {
let connect_result =
client.call_method::<serde_json::Value>(AgentMethod::Connect, connect_params.clone()).await;
if let Err(err) = connect_result {
if let Some(alternate_config) = oracle_alternate_connect_config(&db_config, &err) {
let alternate_configs = oracle_alternate_connect_configs(&db_config, &err);
if !alternate_configs.is_empty() {
log::warn!(
"Oracle connect failed with {:?} descriptor: {}. Retrying with {:?} descriptor.",
"Oracle connect failed with {:?} descriptor: {}. Retrying with Oracle JDBC URL variants: {:?}.",
db_config.oracle_connection_type,
err,
alternate_config.oracle_connection_type
oracle_alternate_connect_config_labels(&alternate_configs)
);
client
.call_method::<serde_json::Value>(
AgentMethod::Connect,
agent_connect_params(
&alternate_config,
&host,
port,
alternate_config.effective_database().unwrap_or(""),
),
)
.await
.map_err(|alternate_err| {
format!("{err}\n\nFallback with alternate Oracle descriptor failed: {alternate_err}")
})?;
let mut fallback_errors = Vec::new();
let mut connected = false;
for alternate_config in alternate_configs {
let label = oracle_alternate_connect_config_labels(std::slice::from_ref(&alternate_config))
.into_iter()
.next()
.unwrap_or_else(|| "alternate".to_string());
match client
.call_method::<serde_json::Value>(
AgentMethod::Connect,
agent_connect_params(
&alternate_config,
&host,
port,
alternate_config.effective_database().unwrap_or(""),
),
)
.await
{
Ok(_) => {
connected = true;
break;
}
Err(alternate_err) => {
fallback_errors.push(format!("{label}: {alternate_err}"));
}
}
}
if !connected {
return Err(format!(
"{err}\n\nFallback with alternate Oracle JDBC URLs failed: {}",
fallback_errors.join("\n")
));
}
} else if should_retry_oracle_with_10g_driver(&db_config, &err) {
log::warn!(
"Oracle connect failed with profile {:?}: {}. Retrying with legacy Oracle profiles.",
@@ -1480,16 +1501,18 @@ mod tests {
)
.expect("listener errors should allow alternate descriptor retry");
assert_eq!(retry.driver_profile.as_deref(), Some("oracle"));
assert_eq!(retry.oracle_connection_type.as_deref(), Some("sid"));
assert_eq!(retry.connection_string.as_deref(), Some("jdbc:oracle:thin:@127.0.0.1:3306:ORCL"));
let mut sid_config = config.clone();
sid_config.oracle_connection_type = Some("sid".to_string());
let service_retry = oracle_alternate_connect_config(
&retry,
&sid_config,
"Agent RPC error (-1): ORA-12505: listener does not currently know of SID given",
)
.expect("SID listener errors should allow service-name retry");
assert_eq!(service_retry.oracle_connection_type.as_deref(), Some("service_name"));
assert_eq!(service_retry.connection_string.as_deref(), Some("jdbc:oracle:thin:@//127.0.0.1:3306/ORCL"));
assert!(oracle_alternate_connect_config(&config, "ORA-12541: TNS:no listener").is_none());
assert!(oracle_alternate_connect_config(&config, "ORA-12541: TNS:no listener").is_some());
}
#[test]