diff --git a/src/client/config.rs b/src/client/config.rs index fff68bc15..36084ee78 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,34 @@ impl Config { self.readonly = readnoly; } + /// Sets whether the client should try every address the server name + /// resolves to concurrently, rather than one at a time. + /// + /// An Always-On availability group listener commonly resolves to one + /// address per subnet, of which only the one in front of the primary + /// replica accepts connections. Attempting them sequentially stalls on each + /// unreachable address until the OS gives up, so a caller with its own + /// connect deadline can time out before the live replica is ever tried. + /// + /// This only records the intent -- opening the connection is the caller's + /// job, and [`Config::get_multi_subnet_failover`] is how it reads the + /// setting back. + /// + /// - Defaults to `false`. + /// + /// [`Config::get_multi_subnet_failover`]: #method.get_multi_subnet_failover + pub fn multi_subnet_failover(&mut self, multi_subnet_failover: bool) { + self.multi_subnet_failover = multi_subnet_failover; + } + + /// Whether `MultiSubnetFailover` was requested, either through the + /// connection string or [`Config::multi_subnet_failover`]. + /// + /// [`Config::multi_subnet_failover`]: #method.multi_subnet_failover + pub fn get_multi_subnet_failover(&self) -> bool { + self.multi_subnet_failover + } + pub(crate) fn get_host(&self) -> &str { self.host .as_deref() @@ -212,7 +242,9 @@ impl Config { /// |`TrustServerCertificateCA`|``|Path to a `pem`, `crt` or `der` certificate file. Cannot be used together with `TrustServerCertificate`| /// |`encrypt`|`true`,`false`,`yes`,`no`,`DANGER_PLAINTEXT`|Specifies whether the driver uses TLS to encrypt communication.| /// |`Application Name`, `ApplicationName`|``|Sets the application name for the connection.| + /// |`MultiSubnetFailover`|`true`,`false`,`yes`,`no`|Records that the client should try every address the server name resolves to concurrently, for an Always-On listener spanning subnets. Read back with [`get_multi_subnet_failover`]; establishing the connection is the caller's job.| /// + /// [`get_multi_subnet_failover`]: struct.Config.html#method.get_multi_subnet_failover /// [ADO.NET connection string]: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings pub fn from_ado_string(s: &str) -> crate::Result { let ado: AdoNetConfig = s.parse()?; @@ -269,6 +301,8 @@ impl Config { builder.readonly(s.readonly()); + builder.multi_subnet_failover(s.multi_subnet_failover()?); + Ok(builder) } } @@ -388,4 +422,79 @@ pub(crate) trait ConfigString { .filter(|val| *val == "ReadOnly") .is_some() } + + /// `MultiSubnetFailover` in an ADO.NET string, `multiSubnetFailover` in a + /// JDBC one -- both parsers lower-case their keys, so one lookup serves + /// both. + fn multi_subnet_failover(&self) -> crate::Result { + self.dict() + .get("multisubnetfailover") + .map(Self::parse_bool) + .unwrap_or(Ok(false)) + } +} + +#[cfg(test)] +mod tests { + use super::Config; + + /// The whole point of the setting: it has to survive the public + /// connection-string entry point, which is the only way a caller that did + /// not build the `Config` by hand can see it. + #[test] + fn multi_subnet_failover_survives_from_ado_string() -> crate::Result<()> { + let config = Config::from_ado_string( + "server=tcp:my-listener.com,1433;MultiSubnetFailover=Yes;TrustServerCertificate=true", + )?; + + assert!(config.get_multi_subnet_failover()); + + Ok(()) + } + + #[test] + fn multi_subnet_failover_survives_from_jdbc_string() -> crate::Result<()> { + let config = Config::from_jdbc_string( + "jdbc:sqlserver://my-listener.com:1433;multiSubnetFailover=true", + )?; + + assert!(config.get_multi_subnet_failover()); + + Ok(()) + } + + /// The regression guard: before this setting existed the key was parsed + /// into the dictionary and dropped, so a caller reading the `Config` saw + /// the same value whether or not it was asked for. + #[test] + fn multi_subnet_failover_defaults_off_and_is_not_implied() -> crate::Result<()> { + let config = Config::from_ado_string("server=tcp:my-server.com,1433")?; + assert!(!config.get_multi_subnet_failover()); + + let config = + Config::from_ado_string("server=tcp:my-server.com,1433;MultiSubnetFailover=No")?; + assert!(!config.get_multi_subnet_failover()); + + assert!(!Config::new().get_multi_subnet_failover()); + + Ok(()) + } + + #[test] + fn multi_subnet_failover_can_be_set_directly() { + let mut config = Config::new(); + config.multi_subnet_failover(true); + + assert!(config.get_multi_subnet_failover()); + } + + /// A non-boolean value fails the whole connection string rather than + /// silently connecting sequentially. + #[test] + fn multi_subnet_failover_rejects_a_non_boolean() { + assert!(Config::from_ado_string( + "server=tcp:my-server.com,1433;MultiSubnetFailover=sometimes" + ) + .is_err()); + } } diff --git a/src/client/config/ado_net.rs b/src/client/config/ado_net.rs index 94df9ca38..187cbea50 100644 --- a/src/client/config/ado_net.rs +++ b/src/client/config/ado_net.rs @@ -484,4 +484,53 @@ mod tests { Ok(()) } + + #[test] + fn multi_subnet_failover_parsing() -> crate::Result<()> { + for (test_str, expected) in [ + ("MultiSubnetFailover=Yes", true), + ("MultiSubnetFailover=true", true), + ("MultiSubnetFailover=No", false), + ("MultiSubnetFailover=false", false), + ] { + let ado: AdoNetConfig = test_str.parse()?; + assert_eq!(expected, ado.multi_subnet_failover()?, "{test_str}"); + } + + Ok(()) + } + + /// The key is matched case-insensitively, like every other key the parser + /// handles. + #[test] + fn multi_subnet_failover_parsing_is_case_insensitive() -> crate::Result<()> { + for test_str in ["multisubnetfailover=yes", "MULTISUBNETFAILOVER=yes"] { + let ado: AdoNetConfig = test_str.parse()?; + assert!(ado.multi_subnet_failover()?, "{test_str}"); + } + + Ok(()) + } + + /// An absent key leaves the sequential connect behaviour untouched. + #[test] + fn multi_subnet_failover_parsing_missing() -> crate::Result<()> { + let test_str = "server=tcp:my-server.com,4200"; + let ado: AdoNetConfig = test_str.parse()?; + + assert!(!ado.multi_subnet_failover()?); + + Ok(()) + } + + /// A value that is not a boolean is a configuration error rather than a + /// silent `false` -- the caller asked for something the driver could not + /// honour. + #[test] + fn multi_subnet_failover_parsing_rejects_a_non_boolean() { + let test_str = "MultiSubnetFailover=sometimes"; + let ado: AdoNetConfig = test_str.parse().expect("parses as a connection string"); + + assert!(ado.multi_subnet_failover().is_err()); + } } diff --git a/src/client/config/jdbc.rs b/src/client/config/jdbc.rs index 4168cf975..bade66265 100644 --- a/src/client/config/jdbc.rs +++ b/src/client/config/jdbc.rs @@ -333,4 +333,31 @@ mod tests { Ok(()) } + + /// JDBC spells the key `multiSubnetFailover`; it reaches the same lookup as + /// the ADO.NET `MultiSubnetFailover`. + #[test] + fn multi_subnet_failover_parsing() -> crate::Result<()> { + let test_str = "jdbc:sqlserver://my-server.com:4200;multiSubnetFailover=true"; + let jdbc: JdbcConfig = test_str.parse()?; + + assert!(jdbc.multi_subnet_failover()?); + + let test_str = "jdbc:sqlserver://my-server.com:4200;multiSubnetFailover=false"; + let jdbc: JdbcConfig = test_str.parse()?; + + assert!(!jdbc.multi_subnet_failover()?); + + Ok(()) + } + + #[test] + fn multi_subnet_failover_parsing_missing() -> crate::Result<()> { + let test_str = "jdbc:sqlserver://my-server.com:4200"; + let jdbc: JdbcConfig = test_str.parse()?; + + assert!(!jdbc.multi_subnet_failover()?); + + Ok(()) + } }