Skip to content

Commit 8da13c8

Browse files
authored
Fix LoginWithFailover missing parser state check (dotnet#4140)
1 parent 0759bee commit 8da13c8

10 files changed

Lines changed: 409 additions & 7 deletions

File tree

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ This project includes several key products and libraries that facilitate SQL Ser
5050
- **Data Encryption**: Supports data encryption for secure data transmission.
5151
- **Logging and Diagnostics**: Provides event source tracing diagnostic capabilities for troubleshooting.
5252
- **Failover Support**: Handles automatic failover scenarios for high availability.
53+
- Compatibility switch: `Switch.Microsoft.Data.SqlClient.UseLegacyFailoverAlternationOnLoginSqlErrors` (default `false`) can restore legacy alternation behavior in `LoginWithFailover` for login-phase SQL errors.
5354
- **Cross-Platform Support**: Compatible with both .NET Framework and .NET Core, allowing applications to run on Windows, Linux, and macOS.
5455
- **Column Encryption AKV Provider**: Supports Azure Key Vault (AKV) provider for acquiring keys from Azure Key Vault to be used for encryption and decryption.
5556

.github/instructions/features.instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ AppContext switches allow runtime behavior changes without modifying connection
246246
| `Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault` | `false` | Sets `MultiSubnetFailover=true` as the default for all connections |
247247
| `Switch.Microsoft.Data.SqlClient.EnableUserAgent` | varies | Controls sending user agent information to SQL Server |
248248
| `Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner` | `false` | Ignores failover partner information sent by the server |
249+
| `Switch.Microsoft.Data.SqlClient.UseLegacyFailoverAlternationOnLoginSqlErrors` | `false` | Restores legacy `LoginWithFailover` alternation for login-phase SQL errors when parser state is not `Closed` |
249250
| `Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior` | `false` | Restores legacy null handling for rowversion columns |
250251
| `Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour` | `false` | Restores legacy zero-scale behavior for time/datetime2/datetimeoffset |
251252
| `Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking` | `false` | Makes ReadAsync behave synchronously (legacy compat) |

AGENTS.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,11 @@ Do **not** create branches directly under `main`, `dev/`, or any other top-level
7979
### Bug Fix Workflow
8080
1. Understand the issue from the bug report
8181
2. Locate relevant code in `src/Microsoft.Data.SqlClient/src/` (do NOT modify legacy `netcore/src/` or `netfx/src/`)
82-
3. Write a failing test that reproduces the issue
83-
4. Implement the fix
84-
5. Ensure all tests pass
85-
6. Update documentation if behavior changes
82+
3. Check `.github/instructions/features.instructions.md` for existing AppContext switches (including failover compatibility switches) before introducing behavior changes
83+
4. Write a failing test that reproduces the issue
84+
5. Implement the fix
85+
6. Ensure all tests pass
86+
7. Update documentation if behavior changes
8687

8788
### Feature Implementation
8889
1. Review the feature specification

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3635,7 +3635,17 @@ private void LoginWithFailover(
36353635
continue;
36363636
}
36373637

3638-
if (IsDoNotRetryConnectError(sqlex) || timeout.IsExpired)
3638+
bool isLoginPhaseSqlError = _parser?.State is not TdsParserState.Closed;
3639+
3640+
// If state != closed, indicates that the parser encountered an error while
3641+
// processing the login response (e.g. an explicit error token). Transient
3642+
// network errors that impact connectivity will result in parser state being
3643+
// closed. Only network-level errors should trigger failover alternation;
3644+
// login-phase errors (like transient errors) should be thrown so they can
3645+
// be handled by the outer ConnectRetryCount loop.
3646+
if ((isLoginPhaseSqlError && !LocalAppContextSwitches.UseLegacyFailoverAlternationOnLoginSqlErrors) ||
3647+
IsDoNotRetryConnectError(sqlex) ||
3648+
timeout.IsExpired)
36393649
{
36403650
// No more time to try again.
36413651
// Caller will call LoginFailure()

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ internal static class LocalAppContextSwitches
5858
private const string IgnoreServerProvidedFailoverPartnerString =
5959
"Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner";
6060

61+
/// <summary>
62+
/// The name of the app context switch that controls whether failover
63+
/// alternation should use legacy behavior for login-phase SQL errors.
64+
/// </summary>
65+
private const string UseLegacyFailoverAlternationOnLoginSqlErrorsString =
66+
"Switch.Microsoft.Data.SqlClient.UseLegacyFailoverAlternationOnLoginSqlErrors";
67+
6168
/// <summary>
6269
/// The name of the app context switch that controls whether to preserve
6370
/// legacy behavior where Timestamp/RowVersion fields return empty byte
@@ -182,6 +189,11 @@ private enum SwitchValue : byte
182189
/// </summary>
183190
private static SwitchValue s_ignoreServerProvidedFailoverPartner = SwitchValue.None;
184191

192+
/// <summary>
193+
/// The cached value of the UseLegacyFailoverAlternationOnLoginSqlErrors switch.
194+
/// </summary>
195+
private static SwitchValue s_useLegacyFailoverAlternationOnLoginSqlErrors = SwitchValue.None;
196+
185197
/// <summary>
186198
/// The cached value of the LegacyRowVersionNullBehavior switch.
187199
/// </summary>
@@ -409,6 +421,19 @@ public static bool GlobalizationInvariantMode
409421
defaultValue: false,
410422
ref s_ignoreServerProvidedFailoverPartner);
411423

424+
/// <summary>
425+
/// When set to true, LoginWithFailover preserves legacy behavior and may
426+
/// alternate to the failover partner on login-phase SQL errors where the
427+
/// parser state is not Closed.
428+
///
429+
/// The default value of this switch is false.
430+
/// </summary>
431+
public static bool UseLegacyFailoverAlternationOnLoginSqlErrors =>
432+
AcquireAndReturn(
433+
UseLegacyFailoverAlternationOnLoginSqlErrorsString,
434+
defaultValue: false,
435+
ref s_useLegacyFailoverAlternationOnLoginSqlErrors);
436+
412437
/// <summary>
413438
/// In System.Data.SqlClient and Microsoft.Data.SqlClient prior to 3.0.0 a
414439
/// field with type Timestamp/RowVersion would return an empty byte array.

src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable
4747
private readonly bool? _globalizationInvariantModeOriginal;
4848
#endif
4949
private readonly bool? _ignoreServerProvidedFailoverPartnerOriginal;
50+
private readonly bool? _useLegacyFailoverAlternationOnLoginSqlErrorsOriginal;
5051
private readonly bool? _legacyRowVersionNullBehaviorOriginal;
5152
private readonly bool? _legacyVarTimeZeroScaleBehaviourOriginal;
5253
private readonly bool? _makeReadAsyncBlockingOriginal;
@@ -98,6 +99,8 @@ public LocalAppContextSwitchesHelper()
9899
#endif
99100
_ignoreServerProvidedFailoverPartnerOriginal =
100101
GetSwitchValue("s_ignoreServerProvidedFailoverPartner");
102+
_useLegacyFailoverAlternationOnLoginSqlErrorsOriginal =
103+
GetSwitchValue("s_useLegacyFailoverAlternationOnLoginSqlErrors");
101104
_legacyRowVersionNullBehaviorOriginal =
102105
GetSwitchValue("s_legacyRowVersionNullBehavior");
103106
_legacyVarTimeZeroScaleBehaviourOriginal =
@@ -154,6 +157,9 @@ public void Dispose()
154157
SetSwitchValue(
155158
"s_ignoreServerProvidedFailoverPartner",
156159
_ignoreServerProvidedFailoverPartnerOriginal);
160+
SetSwitchValue(
161+
"s_useLegacyFailoverAlternationOnLoginSqlErrors",
162+
_useLegacyFailoverAlternationOnLoginSqlErrorsOriginal);
157163
SetSwitchValue(
158164
"s_legacyRowVersionNullBehavior",
159165
_legacyRowVersionNullBehaviorOriginal);
@@ -242,6 +248,15 @@ public bool? IgnoreServerProvidedFailoverPartner
242248
set => SetSwitchValue("s_ignoreServerProvidedFailoverPartner", value);
243249
}
244250

251+
/// <summary>
252+
/// Get or set the UseLegacyFailoverAlternationOnLoginSqlErrors switch value.
253+
/// </summary>
254+
public bool? UseLegacyFailoverAlternationOnLoginSqlErrors
255+
{
256+
get => GetSwitchValue("s_useLegacyFailoverAlternationOnLoginSqlErrors");
257+
set => SetSwitchValue("s_useLegacyFailoverAlternationOnLoginSqlErrors", value);
258+
}
259+
245260
/// <summary>
246261
/// Get or set the LegacyRowVersionNullBehavior switch value.
247262
/// </summary>

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public void TestDefaultAppContextSwitchValues()
2828
Assert.False(LocalAppContextSwitches.UseConnectionPoolV2);
2929
Assert.False(LocalAppContextSwitches.TruncateScaledDecimal);
3030
Assert.False(LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner);
31+
Assert.False(LocalAppContextSwitches.UseLegacyFailoverAlternationOnLoginSqlErrors);
3132
Assert.False(LocalAppContextSwitches.EnableMultiSubnetFailoverByDefault);
3233
#if NET
3334
Assert.False(LocalAppContextSwitches.GlobalizationInvariantMode);

0 commit comments

Comments
 (0)