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 e64d81da40..a3713fcc87 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 @@ -3611,7 +3611,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 1f7cb869b1..933a4a3691 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 @@ -307,7 +307,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; @@ -676,13 +675,7 @@ internal TdsParser Parser } } - internal string ServerProvidedFailOverPartner - { - get - { - return _currentFailoverPartner; - } - } + internal string ServerProvidedFailoverPartner { get; private set; } internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo { @@ -1461,7 +1454,7 @@ private void OpenLoginEnlist(TimeoutTimer timeout, throw SQL.ROR_FailoverNotSupportedConnString(); } - if (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { throw SQL.ROR_FailoverNotSupportedServer(this); } @@ -1574,7 +1567,7 @@ private void LoginNoFailover(ServerInfo serverInfo, newSecurePassword, connectionOptions.MultiSubnetFailover ? intervalTimer : timeout); - if (connectionOptions.MultiSubnetFailover && ServerProvidedFailOverPartner != null) + 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); @@ -1602,7 +1595,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++; @@ -1641,7 +1634,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 (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { if (connectionOptions.MultiSubnetFailover) { @@ -1657,7 +1650,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, @@ -1679,8 +1672,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; } @@ -1741,7 +1739,7 @@ TimeoutTimer timeout ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN); ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); - if (ServerProvidedFailOverPartner == null) + if (ServerProvidedFailoverPartner == null) { ResolveExtendedServerName(failoverServerInfo, !redirectedUserInstance && failoverHost != primaryServerInfo.UserServerName, connectionOptions); } @@ -1791,12 +1789,21 @@ TimeoutTimer timeout ServerInfo currentServerInfo; if (useFailoverHost) { - // Primary server may give us a different failover partner than the connection string indicates. Update it - if (ServerProvidedFailOverPartner != null && 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); } @@ -1845,7 +1852,7 @@ TimeoutTimer timeout _currentPacketSize = connectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; AttemptOneLogin( @@ -1906,7 +1913,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 && ServerProvidedFailOverPartner == null) + if (useFailoverHost && ServerProvidedFailoverPartner == null) { throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); } @@ -1915,8 +1922,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); } @@ -2138,7 +2150,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 f66d9a811d..4425898b8c 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 @@ -3899,7 +3899,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 d0c64d07e3..2d6c18134b 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 @@ -275,7 +275,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; @@ -771,13 +770,7 @@ internal TdsParser Parser } } - internal string ServerProvidedFailOverPartner - { - get - { - return _currentFailoverPartner; - } - } + internal string ServerProvidedFailoverPartner { get; private set; } internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo { @@ -1711,7 +1704,7 @@ private void OpenLoginEnlist(TimeoutTimer timeout, SqlConnectionString connectio throw SQL.ROR_FailoverNotSupportedConnString(); } - if (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { throw SQL.ROR_FailoverNotSupportedServer(this); } @@ -1860,7 +1853,7 @@ private void LoginNoFailover(ServerInfo serverInfo, string newPassword, SecureSt isFirstTransparentAttempt: isFirstTransparentAttempt, disableTnir: disableTnir); - if (connectionOptions.MultiSubnetFailover && ServerProvidedFailOverPartner != null) + 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); @@ -1889,7 +1882,7 @@ private void LoginNoFailover(ServerInfo serverInfo, string newPassword, SecureSt _currentPacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; routingAttempts++; @@ -1929,7 +1922,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 (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { if (connectionOptions.MultiSubnetFailover) { @@ -1945,7 +1938,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, @@ -1968,8 +1961,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; } @@ -2054,9 +2052,10 @@ TimeoutTimer timeout ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN); ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); - 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 + 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); } @@ -2112,12 +2111,21 @@ TimeoutTimer timeout failoverDemandDone = true; } - // Primary server may give us a different failover partner than the connection string indicates. Update it - if (ServerProvidedFailOverPartner != null && 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); } @@ -2166,7 +2174,7 @@ TimeoutTimer timeout _currentPacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; AttemptOneLogin( @@ -2231,7 +2239,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 && ServerProvidedFailOverPartner == null) + if (useFailoverHost && ServerProvidedFailoverPartner == null) { throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); } @@ -2240,8 +2248,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); } @@ -2499,7 +2512,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 9ea289f5b7..afb1a95a8a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -20,6 +20,7 @@ private enum Tristate : byte internal const string SuppressInsecureTLSWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning"; internal const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin"; internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; + private const string IgnoreServerProvidedFailoverPartnerString = @"Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner"; // this field is accessed through reflection in tests and should not be renamed or have the type changed without refactoring NullRow related tests private static Tristate s_legacyRowVersionNullBehavior; @@ -28,6 +29,7 @@ private enum Tristate : byte private static Tristate s_useMinimumLoginTimeout; // this field is accessed through reflection in Microsoft.Data.SqlClient.Tests.SqlParameterTests and should not be renamed or have the type changed without refactoring related tests private static Tristate s_legacyVarTimeZeroScaleBehaviour; + private static Tristate s_ignoreServerProvidedFailoverPartner; #if NET static LocalAppContextSwitches() @@ -185,7 +187,7 @@ public static bool UseMinimumLoginTimeout /// When set to 'true' this will output a scale value of 7 (DEFAULT_VARTIME_SCALE) when the scale /// is explicitly set to zero for VarTime data types ('datetime2', 'datetimeoffset' and 'time') /// If no scale is set explicitly it will continue to output scale of 7 (DEFAULT_VARTIME_SCALE) - /// regardsless of switch value. + /// regardless of switch value. /// This app context switch defaults to 'true'. /// public static bool LegacyVarTimeZeroScaleBehaviour @@ -206,5 +208,33 @@ public static bool LegacyVarTimeZeroScaleBehaviour return s_legacyVarTimeZeroScaleBehaviour == Tristate.True; } } + + /// + /// 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 == Tristate.NotInitialized) + { + if (AppContext.TryGetSwitch(IgnoreServerProvidedFailoverPartnerString, out bool returnedValue) && returnedValue) + { + s_ignoreServerProvidedFailoverPartner = Tristate.True; + } + else + { + s_ignoreServerProvidedFailoverPartner = Tristate.False; + } + } + return s_ignoreServerProvidedFailoverPartner == Tristate.True; + } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 6bbc44f6b1..3ef7287c00 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -3,6 +3,7 @@ // 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.Diagnostics; @@ -12,7 +13,7 @@ using System.Security; using System.Threading; using System.Threading.Tasks; -using Microsoft.SqlServer.TDS.PreLogin; +using Microsoft.SqlServer.TDS.EndPoint; using Microsoft.SqlServer.TDS.Servers; using Xunit; @@ -20,6 +21,20 @@ 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); + public static Type tristateEnum = localAppContextSwitches.GetNestedType("Tristate", BindingFlags.NonPublic); + public static object tristateTrue = Enum.Parse(tristateEnum, "True"); + public static object tristateFalse = Enum.Parse(tristateEnum, "False"); + + [Fact] public void ConnectionTest() { @@ -295,6 +310,76 @@ public void ConnectionTestValidCredentialCombination() Assert.Equal(sqlCredential, conn.Credential); } + [Fact] + public void TransientFault_IgnoreServerProvidedFailoverPartner_ShouldConnectToUserProvidedPartner() + { + // Arrange + ignoreServerProvidedFailoverPartnerField.SetValue(null, tristateTrue); + + + 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, tristateFalse); + } + } + [Theory] [InlineData(60)] 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 a214c51f88..24fbc8c7dc 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 /// @@ -105,6 +115,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; @@ -577,6 +589,16 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi } } + 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 d49e10416a..c92182c7aa 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 @@ -75,6 +75,11 @@ public class TDSServerArguments /// public SslProtocols EncryptionProtocols { get; set; } + /// + /// Specifies the failover partner server name and port + /// + public string FailoverPartner { get; set; } = string.Empty; + /// /// Initialization constructor ///