diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index f3b26e7899..bf4e9cf1cc 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -3431,7 +3431,7 @@ private void CheckNotificationStateAndAutoEnlist() } Notification.Options = SqlDependency.GetDefaultComposedOptions(_activeConnection.DataSource, - InternalTdsConnection.ServerProvidedFailOverPartner, + InternalTdsConnection.ServerProvidedFailoverPartner, identityUserName, _activeConnection.Database); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 49c0883e3d..d024c96a7d 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -300,7 +300,6 @@ internal SessionData CurrentSessionData // FOR CONNECTION RESET MANAGEMENT private bool _fResetConnection; private string _originalDatabase; - private string _currentFailoverPartner; // only set by ENV change from server private string _originalLanguage; private string _currentLanguage; private int _currentPacketSize; @@ -667,13 +666,7 @@ internal TdsParser Parser } } - internal string ServerProvidedFailOverPartner - { - get - { - return _currentFailoverPartner; - } - } + internal string ServerProvidedFailoverPartner { get; private set; } internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo { @@ -1533,7 +1526,7 @@ private void LoginNoFailover(ServerInfo serverInfo, !connectionOptions.MultiSubnetFailover, // ignore timeout for SniOpen call unless MSF connectionOptions.MultiSubnetFailover ? intervalTimer : timeout); - if (connectionOptions.MultiSubnetFailover && null != ServerProvidedFailOverPartner) + if (connectionOptions.MultiSubnetFailover && ServerProvidedFailoverPartner != null) { // connection succeeded: trigger exception if server sends failover partner and MultiSubnetFailover is used throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); @@ -1561,7 +1554,7 @@ private void LoginNoFailover(ServerInfo serverInfo, _currentPacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; routingAttempts++; @@ -1599,7 +1592,7 @@ private void LoginNoFailover(ServerInfo serverInfo, // We only get here when we failed to connect, but are going to re-try // Switch to failover logic if the server provided a partner - if (null != ServerProvidedFailOverPartner) + if (ServerProvidedFailoverPartner != null) { if (connectionOptions.MultiSubnetFailover) { @@ -1615,7 +1608,7 @@ private void LoginNoFailover(ServerInfo serverInfo, LoginWithFailover( true, // start by using failover partner, since we already failed to connect to the primary serverInfo, - ServerProvidedFailOverPartner, + ServerProvidedFailoverPartner, newPassword, newSecurePassword, redirectedUserInstance, @@ -1637,8 +1630,13 @@ private void LoginNoFailover(ServerInfo serverInfo, { // We must wait for CompleteLogin to finish for to have the // env change from the server to know its designated failover - // partner; save this information in _currentFailoverPartner. - PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, ServerProvidedFailOverPartner); + // partner; save this information in ServerProvidedFailoverPartner. + + // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. + // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? string.Empty : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, actualFailoverPartner); } CurrentDataSource = originalServerInfo.UserServerName; } @@ -1699,7 +1697,7 @@ TimeoutTimer timeout ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN); ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); - if (null == ServerProvidedFailOverPartner) + if (ServerProvidedFailoverPartner == null) { ResolveExtendedServerName(failoverServerInfo, !redirectedUserInstance && failoverHost != primaryServerInfo.UserServerName, connectionOptions); } @@ -1747,12 +1745,21 @@ TimeoutTimer timeout ServerInfo currentServerInfo; if (useFailoverHost) { - // Primary server may give us a different failover partner than the connection string indicates. Update it - if (null != ServerProvidedFailOverPartner && failoverServerInfo.ResolvedServerName != ServerProvidedFailOverPartner) + // Primary server may give us a different failover partner than the connection string indicates. + // Update it only if we are respecting server-provided failover partner values. + if (ServerProvidedFailoverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailoverPartner) { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailOverPartner); - failoverServerInfo.SetDerivedNames(string.Empty, ServerProvidedFailOverPartner); + if (LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Ignoring server provided failover partner '{1}' due to IgnoreServerProvidedFailoverPartner AppContext switch.", ObjectID, ServerProvidedFailoverPartner); + } + else + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailoverPartner); + failoverServerInfo.SetDerivedNames(string.Empty, ServerProvidedFailoverPartner); + } } + currentServerInfo = failoverServerInfo; _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); } @@ -1834,7 +1841,7 @@ TimeoutTimer timeout _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; // if connected to failover host, but said host doesn't have DbMirroring set up, throw an error - if (useFailoverHost && null == ServerProvidedFailOverPartner) + if (useFailoverHost && ServerProvidedFailoverPartner == null) { throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); } @@ -1843,8 +1850,13 @@ TimeoutTimer timeout { // We must wait for CompleteLogin to finish for to have the // env change from the server to know its designated failover - // partner; save this information in _currentFailoverPartner. - PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, ServerProvidedFailOverPartner); + // partner. + + // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. + // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? failoverHost : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, actualFailoverPartner); } CurrentDataSource = (useFailoverHost ? failoverHost : primaryServerInfo.UserServerName); } @@ -2068,7 +2080,8 @@ internal void OnEnvChange(SqlEnvChange rec) { throw SQL.ROR_FailoverNotSupportedServer(this); } - _currentFailoverPartner = rec._newValue; + + ServerProvidedFailoverPartner = rec._newValue; break; case TdsEnums.ENV_PROMOTETRANSACTION: diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index 6922be9735..fae38d0012 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -3814,7 +3814,7 @@ private void CheckNotificationStateAndAutoEnlist() } Notification.Options = SqlDependency.GetDefaultComposedOptions(_activeConnection.DataSource, - InternalTdsConnection.ServerProvidedFailOverPartner, + InternalTdsConnection.ServerProvidedFailoverPartner, identityUserName, _activeConnection.Database); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 748483b49d..d39591fcf9 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -277,7 +277,6 @@ internal SessionData CurrentSessionData // FOR CONNECTION RESET MANAGEMENT private bool _fResetConnection; private string _originalDatabase; - private string _currentFailoverPartner; // only set by ENV change from server private string _originalLanguage; private string _currentLanguage; private int _currentPacketSize; @@ -777,13 +776,7 @@ internal TdsParser Parser } } - internal string ServerProvidedFailOverPartner - { - get - { - return _currentFailoverPartner; - } - } + internal string ServerProvidedFailoverPartner { get; private set; } internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo { @@ -1707,7 +1700,7 @@ private void OpenLoginEnlist(TimeoutTimer timeout, SqlConnectionString connectio throw SQL.ROR_FailoverNotSupportedConnString(); } - if (null != ServerProvidedFailOverPartner) + if (ServerProvidedFailoverPartner != null) { throw SQL.ROR_FailoverNotSupportedServer(this); } @@ -1859,7 +1852,7 @@ private void LoginNoFailover(ServerInfo serverInfo, string newPassword, SecureSt isFirstTransparentAttempt: isFirstTransparentAttempt, disableTnir: disableTnir); - if (connectionOptions.MultiSubnetFailover && null != ServerProvidedFailOverPartner) + if (connectionOptions.MultiSubnetFailover && ServerProvidedFailoverPartner != null) { // connection succeeded: trigger exception if server sends failover partner and MultiSubnetFailover is used. throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); @@ -1888,8 +1881,8 @@ private void LoginNoFailover(ServerInfo serverInfo, string newPassword, SecureSt _currentPacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - _currentFailoverPartner = null; - _instanceName = String.Empty; + ServerProvidedFailoverPartner = null; + _instanceName = string.Empty; routingAttempts++; @@ -1928,7 +1921,7 @@ private void LoginNoFailover(ServerInfo serverInfo, string newPassword, SecureSt // We only get here when we failed to connect, but are going to re-try // Switch to failover logic if the server provided a partner - if (null != ServerProvidedFailOverPartner) + if (ServerProvidedFailoverPartner != null) { if (connectionOptions.MultiSubnetFailover) { @@ -1944,7 +1937,7 @@ private void LoginNoFailover(ServerInfo serverInfo, string newPassword, SecureSt LoginWithFailover( true, // start by using failover partner, since we already failed to connect to the primary serverInfo, - ServerProvidedFailOverPartner, + ServerProvidedFailoverPartner, newPassword, newSecurePassword, redirectedUserInstance, @@ -1967,8 +1960,13 @@ private void LoginNoFailover(ServerInfo serverInfo, string newPassword, SecureSt { // We must wait for CompleteLogin to finish for to have the // env change from the server to know its designated failover - // partner; save this information in _currentFailoverPartner. - PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, ServerProvidedFailOverPartner); + // partner; save this information in ServerProvidedFailoverPartner. + + // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. + // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? string.Empty : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, actualFailoverPartner); } CurrentDataSource = originalServerInfo.UserServerName; } @@ -2052,7 +2050,7 @@ TimeoutTimer timeout ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN); ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); - if (null == ServerProvidedFailOverPartner) + if (ServerProvidedFailoverPartner == null) {// No point in resolving the failover partner when we're going to override it below // Don't resolve aliases if failover == primary // UNDONE: WHY? Previous code in TdsParser.Connect did this, but the reason is not clear ResolveExtendedServerName(failoverServerInfo, !redirectedUserInstance && failoverHost != primaryServerInfo.UserServerName, connectionOptions); @@ -2107,13 +2105,21 @@ TimeoutTimer timeout FailoverPermissionDemand(); failoverDemandDone = true; } - - // Primary server may give us a different failover partner than the connection string indicates. Update it - if (null != ServerProvidedFailOverPartner && failoverServerInfo.ResolvedServerName != ServerProvidedFailOverPartner) + // Primary server may give us a different failover partner than the connection string indicates. + // Update it only if we are respecting server-provided failover partner values. + if (ServerProvidedFailoverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailoverPartner) { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailOverPartner); - failoverServerInfo.SetDerivedNames(protocol, ServerProvidedFailOverPartner); + if (LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Ignoring server provided failover partner '{1}' due to IgnoreServerProvidedFailoverPartner AppContext switch.", ObjectID, ServerProvidedFailoverPartner); + } + else + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailoverPartner); + failoverServerInfo.SetDerivedNames(protocol, ServerProvidedFailoverPartner); + } } + currentServerInfo = failoverServerInfo; timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); } @@ -2161,8 +2167,8 @@ TimeoutTimer timeout _currentPacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - _currentFailoverPartner = null; - _instanceName = String.Empty; + ServerProvidedFailoverPartner = null; + _instanceName = string.Empty; AttemptOneLogin( currentServerInfo, @@ -2226,8 +2232,7 @@ TimeoutTimer timeout // If we get here, connection/login succeeded! Just a few more checks & record-keeping _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; - // if connected to failover host, but said host doesn't have DbMirroring set up, throw an error - if (useFailoverHost && null == ServerProvidedFailOverPartner) + if (useFailoverHost && ServerProvidedFailoverPartner == null) { throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); } @@ -2236,8 +2241,13 @@ TimeoutTimer timeout { // We must wait for CompleteLogin to finish for to have the // env change from the server to know its designated failover - // partner; save this information in _currentFailoverPartner. - PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, ServerProvidedFailOverPartner); + // partner. + + // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. + // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? failoverHost : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, actualFailoverPartner); } CurrentDataSource = (useFailoverHost ? failoverHost : primaryServerInfo.UserServerName); } @@ -2500,7 +2510,7 @@ internal void OnEnvChange(SqlEnvChange rec) break; case TdsEnums.ENV_LOGSHIPNODE: - _currentFailoverPartner = rec._newValue; + ServerProvidedFailoverPartner = rec._newValue; break; case TdsEnums.ENV_PROMOTETRANSACTION: diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs index 1791ad5d52..3270afaae9 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -14,10 +14,12 @@ internal static partial class LocalAppContextSwitches internal const string MakeReadAsyncBlockingString = @"Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking"; internal const string LegacyRowVersionNullString = @"Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior"; internal const string SuppressInsecureTLSWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning"; + private const string IgnoreServerProvidedFailoverPartnerString = @"Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner"; private static bool s_makeReadAsyncBlocking; private static bool? s_LegacyRowVersionNullBehavior; private static bool? s_SuppressInsecureTLSWarning; + private static bool? s_ignoreServerProvidedFailoverPartner; #if !NETFRAMEWORK static LocalAppContextSwitches() @@ -76,5 +78,28 @@ public static bool LegacyRowVersionNullBehavior return s_LegacyRowVersionNullBehavior.Value; } } + + /// + /// When set to true, the failover partner provided by the server during connection + /// will be ignored. This is useful in scenarios where the application wants to + /// control the failover behavior explicitly (e.g. using a custom port). The application + /// must be kept up to date with the failover configuration of the server. + /// The application will not automatically discover a newly configured failover partner. + /// + /// This app context switch defaults to 'false'. + /// + public static bool IgnoreServerProvidedFailoverPartner + { + get + { + if (s_ignoreServerProvidedFailoverPartner is null) + { + bool result; + result = AppContext.TryGetSwitch(IgnoreServerProvidedFailoverPartnerString, out result) ? result : false; + s_ignoreServerProvidedFailoverPartner = result; + } + return s_ignoreServerProvidedFailoverPartner.Value; + } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index e3183ee6a0..accb6f2201 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System; +using System.Configuration; using System.Data; using System.Data.Common; using System.Reflection; using System.Security; using System.Threading.Tasks; +using Microsoft.SqlServer.TDS.EndPoint; using Microsoft.SqlServer.TDS.Servers; using Xunit; @@ -15,6 +17,17 @@ namespace Microsoft.Data.SqlClient.Tests { public class SqlConnectionBasicTests { + // Reflection + public static Assembly systemData = Assembly.GetAssembly(typeof(SqlConnection)); + + public static Type sqlConnection = systemData.GetType("Microsoft.Data.SqlClient.SqlConnection"); + public static PropertyInfo innerConnectionProperty = sqlConnection.GetProperty("InnerConnection", BindingFlags.NonPublic | BindingFlags.Instance); + public static Type sqlInternalConnectionTds = systemData.GetType("Microsoft.Data.SqlClient.SqlInternalConnectionTds"); + public static PropertyInfo serverProvidedFailoverPartnerProperty = sqlInternalConnectionTds.GetProperty("ServerProvidedFailoverPartner", BindingFlags.NonPublic | BindingFlags.Instance); + public static Type localAppContextSwitches = systemData.GetType("Microsoft.Data.SqlClient.LocalAppContextSwitches"); + public static FieldInfo ignoreServerProvidedFailoverPartnerField = localAppContextSwitches.GetField("s_ignoreServerProvidedFailoverPartner", BindingFlags.NonPublic | BindingFlags.Static); + + [Fact] public void ConnectionTest() { @@ -207,5 +220,75 @@ public void ConnectionTestValidCredentialCombination() Assert.Equal(sqlCredential, conn.Credential); } + + [Fact] + public void TransientFault_IgnoreServerProvidedFailoverPartner_ShouldConnectToUserProvidedPartner() + { + // Arrange + ignoreServerProvidedFailoverPartnerField.SetValue(null, true); + + + try + { + using TestTdsServer failoverServer = TestTdsServer.StartTestServer(); + // Doesn't need to point to a real endpoint, just needs a value specified + failoverServer.Arguments.FailoverPartner = "localhost,1234"; + + var failoverBuilder = new SqlConnectionStringBuilder(failoverServer.ConnectionString); + + using TestTdsServer server = TestTdsServer.StartTestServer(); + // Set an invalid failover partner to ensure that the connection fails if the + // server provided failover partner is used. + server.Arguments.FailoverPartner = $"invalidhost"; + + SqlConnectionStringBuilder builder = new(server.ConnectionString) + { + InitialCatalog = "master", + Encrypt = false, + FailoverPartner = failoverBuilder.DataSource, + // Ensure pooling is enabled so that the failover partner information + // is persisted in the pool group. If pooling is disabled, the server + // provided failover partner will never be used. + Pooling = true, + MinPoolSize = 1, + }; + SqlConnection connection = new(builder.ConnectionString); + + // Connect once to the primary to trigger it to send the failover partner + connection.Open(); + + var innerConnection = innerConnectionProperty.GetValue(connection); + var serverProvidedFailoverPartner = serverProvidedFailoverPartnerProperty.GetValue(innerConnection); + Assert.Equal("invalidhost", serverProvidedFailoverPartner); + + // Close the connection to return it to the pool + connection.Close(); + + // Act + // Dispose of the server to trigger a failover + server.Dispose(); + + // Opening a new connection will use the failover partner stored in the pool group. + // This will fail if the server provided failover partner was stored to the pool group. + using SqlConnection failoverConnection = new(builder.ConnectionString); + + // Clear the pool to ensure a new physical connection is created + // Pool group info such as failover partner will still be retained + SqlConnection.ClearPool(connection); + failoverConnection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, failoverConnection.State); + Assert.Equal(failoverBuilder.DataSource, failoverConnection.DataSource); + // 1 for the initial connection + Assert.Equal(1, server.PreLoginCount); + // 1 for the failover connection + Assert.Equal(1, failoverServer.PreLoginCount); + } + finally + { + ignoreServerProvidedFailoverPartnerField.SetValue(null, false); + } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs index 535304964d..09e51558a8 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs @@ -32,16 +32,26 @@ public class GenericTDSServer : ITDSServer /// private int _sessionCount = 0; + /// + /// Counts pre-login requests to the server. + /// + private int _preLoginCount = 0; + /// /// Server configuration /// - protected TDSServerArguments Arguments { get; set; } + public TDSServerArguments Arguments { get; set; } /// /// Query engine instance /// protected QueryEngine Engine { get; set; } + /// + /// Counts pre-login requests to the server. + /// + public int PreLoginCount => _preLoginCount; + /// /// Default constructor /// @@ -104,6 +114,8 @@ public virtual void CloseSession(ITDSServerSession session) /// public virtual TDSMessageCollection OnPreLoginRequest(ITDSServerSession session, TDSMessage request) { + Interlocked.Increment(ref _preLoginCount); + // Inflate pre-login request from the message TDSPreLoginToken preLoginRequest = request[0] as TDSPreLoginToken; @@ -544,6 +556,16 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi responseMessage.Add(featureExtActToken); } + if (!string.IsNullOrEmpty(Arguments.FailoverPartner)) + { + envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.RealTimeLogShipping, Arguments.FailoverPartner); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", envChange); + + responseMessage.Add(envChange); + } + // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDSServerArguments.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDSServerArguments.cs index 1543ebde63..341bc3e46a 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDSServerArguments.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDSServerArguments.cs @@ -69,6 +69,11 @@ public class TDSServerArguments /// public X509Certificate EncryptionCertificate { get; set; } + /// + /// Specifies the failover partner server name and port + /// + public string FailoverPartner { get; set; } = string.Empty; + /// /// Initialization constructor ///