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 47349b492e..0f7d4c164c 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 @@ -3623,7 +3623,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 f4f51f9b56..80466509a6 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 @@ -310,7 +310,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; @@ -716,13 +715,7 @@ internal TdsParser Parser } } - internal string ServerProvidedFailOverPartner - { - get - { - return _currentFailoverPartner; - } - } + internal string ServerProvidedFailoverPartner { get; private set; } internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo { @@ -1515,7 +1508,7 @@ private void OpenLoginEnlist(TimeoutTimer timeout, throw SQL.ROR_FailoverNotSupportedConnString(); } - if (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { throw SQL.ROR_FailoverNotSupportedServer(this); } @@ -1643,7 +1636,7 @@ private void LoginNoFailover(ServerInfo serverInfo, newSecurePassword, attemptOneLoginTimeout); - 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); @@ -1671,7 +1664,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++; @@ -1710,7 +1703,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) { @@ -1726,7 +1719,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, @@ -1748,8 +1741,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; } @@ -1810,7 +1808,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); } @@ -1869,12 +1867,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(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); } @@ -1924,7 +1931,7 @@ TimeoutTimer timeout _currentPacketSize = connectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; AttemptOneLogin( @@ -1986,7 +1993,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); } @@ -1995,8 +2002,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); } @@ -2246,7 +2258,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 97212843f5..96ce941f39 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 @@ -3763,7 +3763,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 6456584fa8..4330e5205b 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 @@ -322,7 +322,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; @@ -724,13 +723,7 @@ internal TdsParser Parser } } - internal string ServerProvidedFailOverPartner - { - get - { - return _currentFailoverPartner; - } - } + internal string ServerProvidedFailoverPartner { get; private set; } internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo { @@ -1521,7 +1514,7 @@ private void OpenLoginEnlist(TimeoutTimer timeout, throw SQL.ROR_FailoverNotSupportedConnString(); } - if (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { throw SQL.ROR_FailoverNotSupportedServer(this); } @@ -1671,7 +1664,7 @@ private void LoginNoFailover(ServerInfo serverInfo, 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); @@ -1699,7 +1692,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++; @@ -1741,7 +1734,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) { @@ -1757,7 +1750,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, @@ -1779,8 +1772,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; } @@ -1864,7 +1862,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); } @@ -1921,12 +1919,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); } @@ -1976,7 +1983,7 @@ TimeoutTimer timeout _currentPacketSize = connectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; AttemptOneLogin( @@ -2041,7 +2048,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); } @@ -2050,8 +2057,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); } @@ -2297,7 +2309,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 28d3510407..702be5f842 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -21,6 +21,7 @@ private enum Tristate : byte internal const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin"; internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; internal const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; + 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; @@ -30,6 +31,7 @@ private enum Tristate : byte // 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_useConnectionPoolV2; + private static Tristate s_ignoreServerProvidedFailoverPartner; #if NET @@ -188,7 +190,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 @@ -233,5 +235,33 @@ public static bool UseConnectionPoolV2 return s_useConnectionPoolV2 == 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/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 2460b9234e..734a4377fa 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -28,6 +28,7 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly PropertyInfo _useMinimumLoginTimeoutProperty; private readonly PropertyInfo _legacyVarTimeZeroScaleBehaviourProperty; private readonly PropertyInfo _useConnectionPoolV2Property; + private readonly PropertyInfo _ignoreServerProvidedFailoverPartner; #if NETFRAMEWORK private readonly PropertyInfo _disableTnirByDefaultProperty; #endif @@ -45,10 +46,12 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly Tristate _legacyVarTimeZeroScaleBehaviourOriginal; private readonly FieldInfo _useConnectionPoolV2Field; private readonly Tristate _useConnectionPoolV2Original; - #if NETFRAMEWORK + private readonly FieldInfo _ignoreServerProvidedFailoverPartnerField; + private readonly Tristate _ignoreServerProvidedFailoverPartnerOriginal; +#if NETFRAMEWORK private readonly FieldInfo _disableTnirByDefaultField; private readonly Tristate _disableTnirByDefaultOriginal; - #endif +#endif #endregion @@ -126,6 +129,10 @@ void InitProperty(string name, out PropertyInfo property) "UseConnectionPoolV2", out _useConnectionPoolV2Property); + InitProperty( + "IgnoreServerProvidedFailoverPartner", + out _ignoreServerProvidedFailoverPartner); + #if NETFRAMEWORK InitProperty( "DisableTnirByDefault", @@ -177,6 +184,11 @@ void InitField(string name, out FieldInfo field, out Tristate value) out _useConnectionPoolV2Field, out _useConnectionPoolV2Original); + InitField( + "s_ignoreServerProvidedFailoverPartner", + out _ignoreServerProvidedFailoverPartnerField, + out _ignoreServerProvidedFailoverPartnerOriginal); + #if NETFRAMEWORK InitField( "s_disableTnirByDefault", @@ -233,6 +245,10 @@ void RestoreField(FieldInfo field, Tristate value) _useConnectionPoolV2Field, _useConnectionPoolV2Original); + RestoreField( + _ignoreServerProvidedFailoverPartnerField, + _ignoreServerProvidedFailoverPartnerOriginal); + #if NETFRAMEWORK RestoreField( _disableTnirByDefaultField, @@ -247,7 +263,7 @@ void RestoreField(FieldInfo field, Tristate value) } } - #endregion +#endregion #region Public Properties @@ -374,6 +390,12 @@ public Tristate UseConnectionPoolV2Field set => SetValue(_useConnectionPoolV2Field, value); } + public Tristate IgnoreServerProvidedFailoverPartnerField + { + get => GetValue(_ignoreServerProvidedFailoverPartnerField); + set => SetValue(_ignoreServerProvidedFailoverPartnerField, value); + } + #if NETFRAMEWORK /// /// Get or set the LocalAppContextSwitches.DisableTnirByDefault switch @@ -386,7 +408,7 @@ public Tristate DisableTnirByDefaultField } #endif - #endregion +#endregion #region Private Helpers diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs index 14bc51d520..52bc386875 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs @@ -5,6 +5,9 @@ namespace Microsoft.Data.SqlClient.UnitTests.Microsoft.Data.SqlClient { + // TODO: We need to keep this in the same collection as SimulatedServerTests because of the AppContext switch manipulation. + // Make AppContext switches testable in isolation and remove this constraint so that these tests can run in parallel with others. + [Collection("SimulatedServerTests")] public class SqlConnectionStringTest : IDisposable { private LocalAppContextSwitchesHelper _appContextSwitchHelper; diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs index 3fa98d1e18..a05efcf879 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs @@ -4,13 +4,15 @@ using System; using System.Data; +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.Tests.Common; using Microsoft.SqlServer.TDS.Servers; using Xunit; namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests { - [Trait("Category", "flaky")] [Collection("SimulatedServerTests")] + [Trait("Category", "flaky")] public class ConnectionFailoverTests { //TODO parameterize for transient errors @@ -345,7 +347,7 @@ public void TransientFault_ShouldConnectToPrimary(uint errorCode) new TdsServerArguments { // Doesn't need to point to a real endpoint, just needs a value specified - FailoverPartner = "localhost:1234", + FailoverPartner = "localhost,1234", }); failoverServer.Start(); @@ -354,7 +356,7 @@ public void TransientFault_ShouldConnectToPrimary(uint errorCode) { IsEnabledTransientError = true, Number = errorCode, - FailoverPartner = $"localhost:{failoverServer.EndPoint.Port}", + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", }); server.Start(); @@ -521,5 +523,74 @@ public void TransientFault_WithUserProvidedPartner_RetryDisabled_ShouldFail(uint Assert.Fail(); } + + [Fact] + public void TransientFault_IgnoreServerProvidedFailoverPartner_ShouldConnectToUserProvidedPartner() + { + // Arrange + using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.IgnoreServerProvidedFailoverPartnerField = LocalAppContextSwitchesHelper.Tristate.True; + + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234", + }); + failoverServer.Start(); + + using TdsServer server = new( + new TdsServerArguments() + { + // Set an invalid failover partner to ensure that the connection fails if the + // server provided failover partner is used. + FailoverPartner = $"invalidhost", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{server.EndPoint.Port}", + InitialCatalog = "master", + Encrypt = false, + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", + // 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); + // Clear the pool to ensure a new physical connection is created + SqlConnection.ClearPool(connection); + + // Connect once to the primary to trigger it to send the failover partner + connection.Open(); + Assert.Equal("invalidhost", (connection.InnerConnection as SqlInternalConnectionTds)!.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($"localhost,{failoverServer.EndPoint.Port}", failoverConnection.DataSource); + // 1 for the initial connection + Assert.Equal(1, server.PreLoginCount); + // 1 for the failover connection + Assert.Equal(1, failoverServer.PreLoginCount); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServer.cs index e5d2e52100..ecd89f5812 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Threading; using Microsoft.SqlServer.TDS.Done; using Microsoft.SqlServer.TDS.EndPoint; using Microsoft.SqlServer.TDS.Error; @@ -59,38 +60,54 @@ public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, // Check if we're still going to raise transient error if (Arguments.IsEnabledTransientError && RequestCounter < Arguments.RepeatCount) { - uint errorNumber = Arguments.Number; - string errorMessage = Arguments.Message ?? GetErrorMessage(errorNumber); + return GenerateErrorMessage(request); + } + + // Return login response from the base class + return base.OnLogin7Request(session, request); + } + + /// + public override TDSMessageCollection OnSQLBatchRequest(ITDSServerSession session, TDSMessage message) + { + if (Arguments.IsEnabledTransientError && RequestCounter < Arguments.RepeatCount) + { + return GenerateErrorMessage(message); + } + + return base.OnSQLBatchRequest(session, message); + } - // Log request to which we're about to send a failure - TDSUtilities.Log(Arguments.Log, "Request", loginRequest); + private TDSMessageCollection GenerateErrorMessage(TDSMessage request) + { + uint errorNumber = Arguments.Number; + string errorMessage = Arguments.Message ?? GetErrorMessage(errorNumber); - // Prepare ERROR token with the denial details - TDSErrorToken errorToken = new TDSErrorToken(errorNumber, 1, 20, errorMessage); + // Log request to which we're about to send a failure + TDSUtilities.Log(Arguments.Log, "Request", request); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); + // Prepare ERROR token with the denial details + TDSErrorToken errorToken = new TDSErrorToken(errorNumber, 1, 20, errorMessage); - // Serialize the error token into the response packet - TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); - // Create DONE token - TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); + // Serialize the error token into the response packet + TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", doneToken); + // Create DONE token + TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); - // Serialize DONE token into the response packet - responseMessage.Add(doneToken); + // Log response + TDSUtilities.Log(Arguments.Log, "Response", doneToken); - RequestCounter++; + // Serialize DONE token into the response packet + responseMessage.Add(doneToken); - // Put a single message into the collection and return it - return new TDSMessageCollection(responseMessage); - } + RequestCounter++; - // Return login response from the base class - return base.OnLogin7Request(session, request); + // Put a single message into the collection and return it + return new TDSMessageCollection(responseMessage); } public override void Dispose() {