From eb2a44e956cb71f0c723328e65257d31fa561fee Mon Sep 17 00:00:00 2001 From: Tyler Clendenin Date: Mon, 30 Sep 2024 11:26:17 -0400 Subject: [PATCH] Add support for MultiSubnetFailover connection property When the MultiSubnetFailover=Yes property is added to the connection string, the TCP connection should be attempted for each resolved IP address in parallel rather than in sequence. This creates a race where the first connection to be established wins and becomes the target server. https://learn.microsoft.com/en-us/sql/relational-databases/native-client/features/sql-server-native-client-support-for-high-availability-disaster-recovery?view=sql-server-ver15#connecting-with-multisubnetfailover --- Cargo.toml | 4 ++ src/client/config.rs | 18 ++++++ src/sql_browser/async_std.rs | 107 ++++++++++++++++++++--------------- 3 files changed, 82 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d8ccf95..98c95874 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,10 @@ default-features = false version = "0.3" optional = true +[dependencies.futures] +version = "0.3" +default-features = false + [dependencies.futures-util] version = "0.3" default-features = false diff --git a/src/client/config.rs b/src/client/config.rs index fff68bc1..595898a0 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -32,6 +32,7 @@ pub struct Config { pub(crate) trust: TrustConfig, pub(crate) auth: AuthMethod, pub(crate) readonly: bool, + pub(crate) multi_subnet_failover: bool, } #[derive(Clone, Debug)] @@ -65,6 +66,7 @@ impl Default for Config { trust: TrustConfig::Default, auth: AuthMethod::None, readonly: false, + multi_subnet_failover: false, } } } @@ -171,6 +173,13 @@ impl Config { self.readonly = readnoly; } + /// Sets multiSubnetFailover flag. + /// + /// - Defaults to `false`. + pub fn multi_subnet_failover(&mut self, multi_subnet_failover: bool) { + self.multi_subnet_failover = multi_subnet_failover; + } + pub(crate) fn get_host(&self) -> &str { self.host .as_deref() @@ -269,6 +278,8 @@ impl Config { builder.readonly(s.readonly()); + builder.multi_subnet_failover(s.multi_subnet_failover()?); + Ok(builder) } } @@ -388,4 +399,11 @@ pub(crate) trait ConfigString { .filter(|val| *val == "ReadOnly") .is_some() } + + fn multi_subnet_failover(&self) -> crate::Result { + self.dict() + .get("multisubnetfailover") + .map(Self::parse_bool) + .unwrap_or(Ok(false)) + } } diff --git a/src/sql_browser/async_std.rs b/src/sql_browser/async_std.rs index 14f55de5..5236c924 100644 --- a/src/sql_browser/async_std.rs +++ b/src/sql_browser/async_std.rs @@ -1,69 +1,82 @@ use super::SqlBrowser; +use std::net::SocketAddr; use async_std::{ io, net::{self, ToSocketAddrs}, }; use async_trait::async_trait; +use futures::{future::select_all, FutureExt}; use futures_util::future::TryFutureExt; use std::time; use tracing::Level; -#[async_trait] -impl SqlBrowser for net::TcpStream { - /// This method can be used to connect to SQL Server named instances - /// when on a Windows platform with the `sql-browser-async-std` feature - /// enabled. Please see the crate examples for more detailed examples. - async fn connect_named(builder: &crate::client::Config) -> crate::Result { - let addrs = builder.get_addr().to_socket_addrs().await?; +async fn connect_addr(builder: &crate::client::Config, mut addr: SocketAddr) -> crate::Result { + if let Some(ref instance_name) = builder.instance_name { + // First resolve the instance to a port via the + // SSRP protocol/MS-SQLR protocol [1] + // [1] https://msdn.microsoft.com/en-us/library/cc219703.aspx - for mut addr in addrs { - if let Some(ref instance_name) = builder.instance_name { - // First resolve the instance to a port via the - // SSRP protocol/MS-SQLR protocol [1] - // [1] https://msdn.microsoft.com/en-us/library/cc219703.aspx + let local_bind: std::net::SocketAddr = if addr.is_ipv4() { + "0.0.0.0:0".parse().unwrap() + } else { + "[::]:0".parse().unwrap() + }; - let local_bind: std::net::SocketAddr = if addr.is_ipv4() { - "0.0.0.0:0".parse().unwrap() - } else { - "[::]:0".parse().unwrap() - }; + tracing::event!( + Level::TRACE, + "Connecting to instance `{}` using SQL Browser in port `{}`", + instance_name, + builder.get_port() + ); - tracing::event!( - Level::TRACE, - "Connecting to instance `{}` using SQL Browser in port `{}`", - instance_name, - builder.get_port() - ); + let msg = [&[4u8], instance_name.as_bytes()].concat(); + let mut buf = vec![0u8; 4096]; - let msg = [&[4u8], instance_name.as_bytes()].concat(); - let mut buf = vec![0u8; 4096]; + let socket = net::UdpSocket::bind(&local_bind).await?; + socket.send_to(&msg, &addr).await?; - let socket = net::UdpSocket::bind(&local_bind).await?; - socket.send_to(&msg, &addr).await?; + let timeout = time::Duration::from_millis(1000); - let timeout = time::Duration::from_millis(1000); + let len = io::timeout(timeout, socket.recv(&mut buf)) + .map_err(|_| { + crate::error::Error::Conversion( + format!( + "SQL browser timeout during resolving instance {}. Please check if browser is running in port {} and does the instance exist.", + instance_name, + builder.get_port(), + ) + .into(), + ) + }) + .await?; - let len = io::timeout(timeout, socket.recv(&mut buf)) - .map_err(|_| { - crate::error::Error::Conversion( - format!( - "SQL browser timeout during resolving instance {}. Please check if browser is running in port {} and does the instance exist.", - instance_name, - builder.get_port(), - ) - .into(), - ) - }) - .await?; + let port = super::get_port_from_sql_browser_reply(buf, len, instance_name)?; + tracing::event!(Level::TRACE, "Found port `{}` from SQL Browser", port); + addr.set_port(port); + }; + + if let Ok(stream) = net::TcpStream::connect(addr).await { + stream.set_nodelay(true)?; + return Ok(stream); + } else { + Err(io::Error::new(io::ErrorKind::NotFound, "Could not resolve server host").into()) + } +} - let port = super::get_port_from_sql_browser_reply(buf, len, instance_name)?; - tracing::event!(Level::TRACE, "Found port `{}` from SQL Browser", port); - addr.set_port(port); - }; +#[async_trait] +impl SqlBrowser for net::TcpStream { + /// This method can be used to connect to SQL Server named instances + /// when on a Windows platform with the `sql-browser-async-std` feature + /// enabled. Please see the crate examples for more detailed examples. + async fn connect_named(builder: &crate::client::Config) -> crate::Result { + let addrs = builder.get_addr().to_socket_addrs().await?; - if let Ok(stream) = net::TcpStream::connect(addr).await { - stream.set_nodelay(true)?; - return Ok(stream); + if builder.multi_subnet_failover { + let futures = addrs.map(|addr| connect_addr(builder, addr).boxed()); + select_all(futures).await; + } else { + for mut addr in addrs { + connect_addr(builder, addr).await?; } }