|
7 | 7 | using System.Linq;
|
8 | 8 | using System.Text;
|
9 | 9 | using System.Text.RegularExpressions;
|
| 10 | +using Xunit; |
10 | 11 |
|
11 | 12 | namespace Microsoft.Data.SqlClient.ManualTesting.Tests
|
12 | 13 | {
|
13 |
| - /// Define the SQL command type by filtering purpose. |
14 |
| - [Flags] |
15 |
| - public enum FilterSqlStatements |
16 |
| - { |
17 |
| - /// Don't filter any SQL commands |
18 |
| - None = 0, |
19 |
| - /// Filter INSERT or INSERT INTO |
20 |
| - Insert = 1, |
21 |
| - /// Filter UPDATE |
22 |
| - Update = 2, |
23 |
| - /// Filter DELETE |
24 |
| - Delete = 1 << 2, |
25 |
| - /// Filter EXECUTE or EXEC |
26 |
| - Execute = 1 << 3, |
27 |
| - /// Filter ALTER |
28 |
| - Alter = 1 << 4, |
29 |
| - /// Filter CREATE |
30 |
| - Create = 1 << 5, |
31 |
| - /// Filter DROP |
32 |
| - Drop = 1 << 6, |
33 |
| - /// Filter TRUNCATE |
34 |
| - Truncate = 1 << 7, |
35 |
| - /// Filter SELECT |
36 |
| - Select = 1 << 8, |
37 |
| - /// Filter data manipulation commands consist of INSERT, INSERT INTO, UPDATE, and DELETE |
38 |
| - DML = Insert | Update | Delete | Truncate, |
39 |
| - /// Filter data definition commands consist of ALTER, CREATE, and DROP |
40 |
| - DDL = Alter | Create | Drop, |
41 |
| - /// Filter any SQL command types |
42 |
| - All = DML | DDL | Execute | Select |
43 |
| - } |
44 |
| - |
45 | 14 | public class RetryLogicTestHelper
|
46 | 15 | {
|
47 | 16 | private static readonly HashSet<int> s_defaultTransientErrors
|
@@ -75,152 +44,87 @@ private static readonly HashSet<int> s_defaultTransientErrors
|
75 | 44 | 18456 // Using managed identity in Azure Sql Server throws 18456 for non-existent database instead of 4060.
|
76 | 45 | };
|
77 | 46 |
|
| 47 | + public static readonly Regex FilterDmlStatements = new Regex( |
| 48 | + @"\b(INSERT( +INTO)|UPDATE|DELETE|TRUNCATE)\b", |
| 49 | + RegexOptions.Compiled | RegexOptions.IgnoreCase); |
| 50 | + |
78 | 51 | internal static readonly string s_exceedErrMsgPattern = SystemDataResourceManager.Instance.SqlRetryLogic_RetryExceeded;
|
79 | 52 | internal static readonly string s_cancelErrMsgPattern = SystemDataResourceManager.Instance.SqlRetryLogic_RetryCanceled;
|
80 | 53 |
|
81 |
| - public static IEnumerable<object[]> GetConnectionStrings() |
| 54 | + public static TheoryData<string, SqlRetryLogicBaseProvider> GetConnectionStringAndRetryProviders( |
| 55 | + int numberOfRetries, |
| 56 | + TimeSpan maxInterval, |
| 57 | + TimeSpan? deltaTime = null, |
| 58 | + IEnumerable<int> transientErrorCodes = null, |
| 59 | + Regex unauthorizedStatementRegex = null) |
82 | 60 | {
|
83 |
| - var builder = new SqlConnectionStringBuilder(); |
84 |
| - |
85 |
| - foreach (var cnnString in DataTestUtility.GetConnectionStrings(withEnclave: false)) |
86 |
| - { |
87 |
| - builder.Clear(); |
88 |
| - builder.ConnectionString = cnnString; |
89 |
| - builder.ConnectTimeout = 5; |
90 |
| - builder.Pooling = false; |
91 |
| - yield return new object[] { builder.ConnectionString }; |
92 |
| - |
93 |
| - builder.Pooling = true; |
94 |
| - yield return new object[] { builder.ConnectionString }; |
95 |
| - } |
96 |
| - } |
97 |
| - |
98 |
| - public static IEnumerable<object[]> GetConnectionAndRetryStrategy(int numberOfRetries, |
99 |
| - TimeSpan maxInterval, |
100 |
| - FilterSqlStatements unauthorizedStatemets, |
101 |
| - IEnumerable<int> transientErrors, |
102 |
| - int deltaTimeMillisecond = 10, |
103 |
| - bool custom = true) |
104 |
| - { |
105 |
| - var option = new SqlRetryLogicOption() |
| 61 | + var option = new SqlRetryLogicOption |
106 | 62 | {
|
107 | 63 | NumberOfTries = numberOfRetries,
|
108 |
| - DeltaTime = TimeSpan.FromMilliseconds(deltaTimeMillisecond), |
| 64 | + DeltaTime = deltaTime ?? TimeSpan.FromMilliseconds(10), |
109 | 65 | MaxTimeInterval = maxInterval,
|
110 |
| - TransientErrors = transientErrors ?? (custom ? s_defaultTransientErrors : null), |
111 |
| - AuthorizedSqlCondition = custom ? RetryPreConditon(unauthorizedStatemets) : null |
| 66 | + TransientErrors = transientErrorCodes ?? s_defaultTransientErrors, |
| 67 | + AuthorizedSqlCondition = RetryPreCondition(unauthorizedStatementRegex) |
112 | 68 | };
|
113 | 69 |
|
114 |
| - foreach (var item in GetRetryStrategies(option)) |
115 |
| - foreach (var cnn in GetConnectionStrings()) |
116 |
| - yield return new object[] { cnn[0], item[0] }; |
117 |
| - } |
118 |
| - |
119 |
| - public static IEnumerable<object[]> GetConnectionAndRetryStrategyInvalidCatalog(int numberOfRetries) |
120 |
| - { |
121 |
| - return GetConnectionAndRetryStrategy(numberOfRetries, TimeSpan.FromSeconds(1), FilterSqlStatements.None, null, 250, true); |
122 |
| - } |
| 70 | + var result = new TheoryData<string, SqlRetryLogicBaseProvider>(); |
| 71 | + foreach (var connectionString in GetConnectionStringsTyped()) |
| 72 | + { |
| 73 | + foreach (var retryProvider in GetRetryStrategiesTyped(option)) |
| 74 | + { |
| 75 | + result.Add(connectionString, retryProvider); |
| 76 | + } |
| 77 | + } |
123 | 78 |
|
124 |
| - public static IEnumerable<object[]> GetConnectionAndRetryStrategyInvalidCommand(int numberOfRetries) |
125 |
| - { |
126 |
| - return GetConnectionAndRetryStrategy(numberOfRetries, TimeSpan.FromMilliseconds(100), FilterSqlStatements.None, null); |
| 79 | + return result; |
127 | 80 | }
|
128 | 81 |
|
129 |
| - public static IEnumerable<object[]> GetConnectionAndRetryStrategyFilterDMLStatements(int numberOfRetries) |
130 |
| - { |
131 |
| - return GetConnectionAndRetryStrategy(numberOfRetries, TimeSpan.FromMilliseconds(100), FilterSqlStatements.DML, new int[] { 207, 102, 2812 }); |
132 |
| - } |
| 82 | + public static TheoryData<string, SqlRetryLogicBaseProvider> GetNonRetriableCases() => |
| 83 | + new TheoryData<string, SqlRetryLogicBaseProvider> |
| 84 | + { |
| 85 | + { DataTestUtility.TCPConnectionString, null }, |
| 86 | + { DataTestUtility.TCPConnectionString, SqlConfigurableRetryFactory.CreateNoneRetryProvider() } |
| 87 | + }; |
133 | 88 |
|
134 |
| - //40613: Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. If the problem persists, contact customer support, and provide them the session tracing ID of '%.*ls'. |
135 |
| - public static IEnumerable<object[]> GetConnectionAndRetryStrategyLongRunner(int numberOfRetries) |
| 89 | + private static IEnumerable<string> GetConnectionStringsTyped() |
136 | 90 | {
|
137 |
| - return GetConnectionAndRetryStrategy(numberOfRetries, TimeSpan.FromSeconds(120), FilterSqlStatements.None, null, 20 * 1000); |
138 |
| - } |
| 91 | + var builder = new SqlConnectionStringBuilder(); |
| 92 | + foreach (var connectionString in DataTestUtility.GetConnectionStrings(withEnclave: false)) |
| 93 | + { |
| 94 | + builder.Clear(); |
| 95 | + builder.ConnectionString = connectionString; |
| 96 | + builder.ConnectTimeout = 5; |
| 97 | + builder.Pooling = false; |
| 98 | + yield return builder.ConnectionString; |
139 | 99 |
|
140 |
| - public static IEnumerable<object[]> GetConnectionAndRetryStrategyDropDB(int numberOfRetries) |
141 |
| - { |
142 |
| - List<int> faults = s_defaultTransientErrors.ToList(); |
143 |
| - faults.Add(3702); // Cannot drop database because it is currently in use. |
144 |
| - return GetConnectionAndRetryStrategy(numberOfRetries, TimeSpan.FromMilliseconds(2000), FilterSqlStatements.None, faults, 500); |
| 100 | + builder.Pooling = true; |
| 101 | + yield return builder.ConnectionString; |
| 102 | + } |
145 | 103 | }
|
146 | 104 |
|
147 |
| - public static IEnumerable<object[]> GetConnectionAndRetryStrategyLockedTable(int numberOfRetries) |
| 105 | + private static IEnumerable<SqlRetryLogicBaseProvider> GetRetryStrategiesTyped(SqlRetryLogicOption option) |
148 | 106 | {
|
149 |
| - return GetConnectionAndRetryStrategy(numberOfRetries, TimeSpan.FromMilliseconds(100), FilterSqlStatements.None, null); |
| 107 | + yield return SqlConfigurableRetryFactory.CreateExponentialRetryProvider(option); |
| 108 | + yield return SqlConfigurableRetryFactory.CreateIncrementalRetryProvider(option); |
| 109 | + yield return SqlConfigurableRetryFactory.CreateFixedRetryProvider(option); |
150 | 110 | }
|
151 | 111 |
|
152 |
| - public static IEnumerable<object[]> GetNoneRetriableCondition() |
| 112 | + public static IEnumerable<int> GetDefaultTransientErrorCodes(params int[] additionalCodes) |
153 | 113 | {
|
154 |
| - yield return new object[] { DataTestUtility.TCPConnectionString, null }; |
155 |
| - yield return new object[] { DataTestUtility.TCPConnectionString, SqlConfigurableRetryFactory.CreateNoneRetryProvider() }; |
156 |
| - } |
| 114 | + var transientErrorCodes = new HashSet<int>(s_defaultTransientErrors); |
| 115 | + foreach (int additionalCode in additionalCodes) |
| 116 | + { |
| 117 | + transientErrorCodes.Add(additionalCode); |
| 118 | + } |
157 | 119 |
|
158 |
| - private static IEnumerable<object[]> GetRetryStrategies(SqlRetryLogicOption retryLogicOption) |
159 |
| - { |
160 |
| - yield return new object[] { SqlConfigurableRetryFactory.CreateExponentialRetryProvider(retryLogicOption) }; |
161 |
| - yield return new object[] { SqlConfigurableRetryFactory.CreateIncrementalRetryProvider(retryLogicOption) }; |
162 |
| - yield return new object[] { SqlConfigurableRetryFactory.CreateFixedRetryProvider(retryLogicOption) }; |
| 120 | + return transientErrorCodes; |
163 | 121 | }
|
164 | 122 |
|
165 | 123 | /// Generate a predicate function to skip unauthorized SQL commands.
|
166 |
| - private static Predicate<string> RetryPreConditon(FilterSqlStatements unauthorizedSqlStatements) |
167 |
| - { |
168 |
| - var pattern = GetRegexPattern(unauthorizedSqlStatements); |
169 |
| - return (commandText) => string.IsNullOrEmpty(pattern) |
170 |
| - || !Regex.IsMatch(commandText, pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); |
171 |
| - } |
172 |
| - |
173 |
| - /// Provide a regex pattern regarding to the SQL statement. |
174 |
| - private static string GetRegexPattern(FilterSqlStatements sqlStatements) |
| 124 | + private static Predicate<string> RetryPreCondition(Regex unauthorizedStatementRegex) |
175 | 125 | {
|
176 |
| - if (sqlStatements == FilterSqlStatements.None) |
177 |
| - { |
178 |
| - return string.Empty; |
179 |
| - } |
180 |
| - |
181 |
| - var pattern = new StringBuilder(); |
182 |
| - |
183 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Insert)) |
184 |
| - { |
185 |
| - pattern.Append(@"INSERT( +INTO){0,1}|"); |
186 |
| - } |
187 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Update)) |
188 |
| - { |
189 |
| - pattern.Append(@"UPDATE|"); |
190 |
| - } |
191 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Delete)) |
192 |
| - { |
193 |
| - pattern.Append(@"DELETE|"); |
194 |
| - } |
195 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Execute)) |
196 |
| - { |
197 |
| - pattern.Append(@"EXEC(UTE){0,1}|"); |
198 |
| - } |
199 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Alter)) |
200 |
| - { |
201 |
| - pattern.Append(@"ALTER|"); |
202 |
| - } |
203 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Create)) |
204 |
| - { |
205 |
| - pattern.Append(@"CREATE|"); |
206 |
| - } |
207 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Drop)) |
208 |
| - { |
209 |
| - pattern.Append(@"DROP|"); |
210 |
| - } |
211 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Truncate)) |
212 |
| - { |
213 |
| - pattern.Append(@"TRUNCATE|"); |
214 |
| - } |
215 |
| - if (sqlStatements.HasFlag(FilterSqlStatements.Select)) |
216 |
| - { |
217 |
| - pattern.Append(@"SELECT|"); |
218 |
| - } |
219 |
| - if (pattern.Length > 0) |
220 |
| - { |
221 |
| - pattern.Remove(pattern.Length - 1, 1); |
222 |
| - } |
223 |
| - return string.Format(@"\b({0})\b", pattern.ToString()); |
| 126 | + return commandText => unauthorizedStatementRegex is null || |
| 127 | + !unauthorizedStatementRegex.IsMatch(commandText); |
224 | 128 | }
|
225 | 129 | }
|
226 | 130 | }
|
0 commit comments